Commit de6e32e39033
2026-02-16 15:28:46
1 parent
094abda
Changed files (27)
src
nix/module.nix
@@ -46,10 +46,10 @@ in
description = "Path to org-mode inbox file";
};
- defaultModel = lib.mkOption {
- type = lib.types.str;
- default = "sonnet";
- description = "Default model alias (e.g., sonnet, opus, gemini)";
+ geminiApiKeyFile = lib.mkOption {
+ type = lib.types.nullOr lib.types.path;
+ default = null;
+ description = "Path to file containing Google Gemini API key";
};
anthropicApiKeyFile = lib.mkOption {
@@ -58,23 +58,17 @@ in
description = "Path to file containing Anthropic API key";
};
- googleApiKeyFile = lib.mkOption {
- type = lib.types.nullOr lib.types.path;
- default = null;
- description = "Path to file containing Google API key";
- };
-
openaiApiKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Path to file containing OpenAI API key";
};
- ollamaBaseUrl = lib.mkOption {
+ searxngUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
- example = "http://localhost:11434";
- description = "Base URL for Ollama API";
+ example = "https://search.sbr.pm";
+ description = "URL of SearXNG instance for web search";
};
debug = lib.mkOption {
@@ -97,6 +91,16 @@ in
};
config = lib.mkIf cfg.enable {
+ assertions = [
+ {
+ assertion =
+ cfg.geminiApiKeyFile != null
+ || cfg.anthropicApiKeyFile != null
+ || cfg.openaiApiKeyFile != null;
+ message = "At least one API key file must be configured for Daneel (geminiApiKeyFile, anthropicApiKeyFile, or openaiApiKeyFile)";
+ }
+ ];
+
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
@@ -116,7 +120,6 @@ in
User = cfg.user;
Group = cfg.group;
WorkingDirectory = cfg.dataDir;
- ExecStart = "${cfg.package}/bin/daneel";
Restart = "always";
RestartSec = "10s";
@@ -136,7 +139,6 @@ in
];
RestrictNamespaces = true;
LockPersonality = true;
- MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
PrivateDevices = true;
@@ -148,20 +150,19 @@ in
export DANEEL_OWNER_JID="${cfg.ownerJid}"
export DANEEL_DATA_DIR="${cfg.dataDir}"
export DANEEL_INBOX_PATH="${cfg.inboxPath}"
- export DANEEL_DEFAULT_MODEL="${cfg.defaultModel}"
${lib.optionalString cfg.debug ''export DANEEL_DEBUG="true"''}
+ ${lib.optionalString (cfg.geminiApiKeyFile != null) ''
+ export GEMINI_API_KEY="$(cat ${cfg.geminiApiKeyFile})"
+ ''}
${lib.optionalString (cfg.anthropicApiKeyFile != null) ''
export ANTHROPIC_API_KEY="$(cat ${cfg.anthropicApiKeyFile})"
''}
- ${lib.optionalString (cfg.googleApiKeyFile != null) ''
- export GOOGLE_API_KEY="$(cat ${cfg.googleApiKeyFile})"
- ''}
${lib.optionalString (cfg.openaiApiKeyFile != null) ''
export OPENAI_API_KEY="$(cat ${cfg.openaiApiKeyFile})"
''}
- ${lib.optionalString (cfg.ollamaBaseUrl != null) ''
- export OLLAMA_BASE_URL="${cfg.ollamaBaseUrl}"
+ ${lib.optionalString (cfg.searxngUrl != null) ''
+ export SEARXNG_URL="${cfg.searxngUrl}"
''}
exec ${cfg.package}/bin/daneel
nix/package.nix
@@ -2,15 +2,18 @@
lib,
buildNpmPackage,
nodejs_22,
+ ddgr,
+ makeWrapper,
}:
buildNpmPackage {
pname = "daneel";
- version = "0.1.0";
+ version = "0.2.0";
src = ./..;
nodejs = nodejs_22;
+ nativeBuildInputs = [ makeWrapper ];
npmDepsHash = lib.fakeHash;
@@ -30,16 +33,20 @@ buildNpmPackage {
cp package.json $out/lib/daneel/
mkdir -p $out/bin
- cat > $out/bin/daneel <<EOF
- #!${nodejs_22}/bin/node
- import("$out/lib/daneel/dist/main.js");
+ cat > $out/bin/daneel <<'EOF'
+ #!/usr/bin/env node
+ import("$out/lib/daneel/dist/main-pi.js");
EOF
chmod +x $out/bin/daneel
+
+ # Wrap to include ddgr in PATH (for web search)
+ wrapProgram $out/bin/daneel \
+ --prefix PATH : ${lib.makeBinPath [ ddgr ]}
runHook postInstall
'';
meta = {
- description = "XMPP research bot using AI";
+ description = "XMPP research bot using Pi AI toolkit";
homepage = "https://codeberg.org/vdemeester/daneel";
license = lib.licenses.asl20;
maintainers = [ ];
src/agent/prompts.ts
@@ -1,80 +0,0 @@
-export const SYSTEM_PROMPT = `You are Daneel, a personal research assistant named after R. Daneel Olivaw from Isaac Asimov's Robot/Foundation series. You communicate via XMPP (Jabber) with your owner.
-
-Your core traits:
-- Helpful and thorough in research
-- Concise but complete in responses (this is chat, not email)
-- Honest about limitations and uncertainty
-- You can use tools to help with tasks
-
-When responding:
-- Keep responses focused and scannable
-- Use markdown formatting when helpful
-- If a question requires research, use the research tool
-- If asked to save something, use the save_to_org tool
-- For system status, use the status tool
-
-Remember: You're in an XMPP chat context. Keep responses reasonably sized but don't truncate important information.`;
-
-export const HELP_TEXT = `**Daneel - XMPP Research Bot**
-
-**Commands:**
-- \`/help\` - Show this help message
-- \`/ping\` - Check if I'm alive
-- \`/status\` - Show system status
-- \`/clear\` - Clear conversation history
-- \`/models\` - List available models
-
-**Model Prefixes:**
-Use these prefixes to select a specific model:
-
-*Anthropic:*
-- \`opus:\` or \`o:\` - Claude Opus 4.5
-- \`sonnet:\` or \`s:\` - Claude Sonnet 4.5 (default)
-- \`haiku:\` or \`h:\` - Claude Haiku
-
-*Google:*
-- \`gemini:\` or \`g:\` - Gemini 2.0 Flash
-- \`gemini-pro:\` or \`gp:\` - Gemini 2.5 Pro
-
-*OpenAI:*
-- \`gpt:\` or \`gpt4:\` - GPT-4o
-- \`o1:\` - O1
-- \`o3:\` - O3 Mini
-
-*GitHub Copilot:*
-- \`copilot:\` or \`cp:\` - GPT-4o via Copilot
-- \`copilot-claude:\` - Claude Sonnet via Copilot
-
-*Ollama (Local):*
-- \`ollama:\` or \`llama:\` - Llama 3.2
-- \`qwen:\` - Qwen 2.5
-- \`deepseek:\` - DeepSeek R1
-
-*Others:*
-- \`groq:\` - Llama via Groq
-- \`mistral:\` - Mistral Large
-
-**Examples:**
-- \`opus: What is quantum entanglement?\`
-- \`g: Summarize recent AI news\`
-- \`What's the weather like?\` (uses default model)`;
-
-export const MODELS_TEXT = `**Available Models:**
-
-| Prefix | Provider | Model |
-|--------|----------|-------|
-| opus:, o: | Anthropic | claude-opus-4-5 |
-| sonnet:, s: | Anthropic | claude-sonnet-4-5 |
-| haiku:, h: | Anthropic | claude-haiku |
-| gemini:, g: | Google | gemini-2.0-flash |
-| gemini-pro:, gp: | Google | gemini-2.5-pro |
-| gpt:, gpt4: | OpenAI | gpt-4o |
-| o1: | OpenAI | o1 |
-| o3: | OpenAI | o3-mini |
-| copilot:, cp: | GitHub | gpt-4o |
-| copilot-claude: | GitHub | claude-sonnet-4 |
-| ollama:, llama: | Ollama | llama3.2 |
-| qwen: | Ollama | qwen2.5 |
-| deepseek: | Ollama | deepseek-r1 |
-| groq: | Groq | llama-3.3-70b |
-| mistral: | Mistral | mistral-large |`;
src/agent/runner.ts
@@ -1,223 +0,0 @@
-import { Config, ModelInfo, parseModelPrefix } from "../config.js";
-import { SessionManager } from "./session.js";
-import { SessionEntry, ConversationMessage } from "./types.js";
-import { SYSTEM_PROMPT, HELP_TEXT, MODELS_TEXT } from "./prompts.js";
-import { ToolRegistry } from "../tools/index.js";
-import { createProvider, LLMProvider, Message } from "../llm/index.js";
-import { bareJid } from "../xmpp/types.js";
-
-const MAX_TOOL_ITERATIONS = 10;
-
-export class AgentRunner {
- private config: Config;
- private sessionManager: SessionManager;
- private toolRegistry: ToolRegistry;
- private jid: string;
- private processing: boolean = false;
- private messageQueue: Array<{ message: string; resolve: (response: string) => void }> = [];
-
- constructor(
- jid: string,
- config: Config,
- sessionManager: SessionManager,
- toolRegistry: ToolRegistry
- ) {
- this.jid = bareJid(jid);
- this.config = config;
- this.sessionManager = sessionManager;
- this.toolRegistry = toolRegistry;
- }
-
- async processMessage(message: string): Promise<string> {
- return new Promise((resolve) => {
- this.messageQueue.push({ message, resolve });
- this.processQueue();
- });
- }
-
- private async processQueue(): Promise<void> {
- if (this.processing || this.messageQueue.length === 0) {
- return;
- }
-
- this.processing = true;
-
- while (this.messageQueue.length > 0) {
- const item = this.messageQueue.shift()!;
- try {
- const response = await this.handleMessage(item.message);
- item.resolve(response);
- } catch (err) {
- item.resolve(`Error: ${(err as Error).message}`);
- }
- }
-
- this.processing = false;
- }
-
- private async handleMessage(message: string): Promise<string> {
- // Handle slash commands
- if (message.startsWith("/")) {
- return this.handleCommand(message);
- }
-
- // Parse model prefix
- const { model, content } = parseModelPrefix(message);
- const modelInfo = model || this.config.llm.defaultModel;
-
- // Log user message
- await this.sessionManager.appendEntry(this.jid, {
- timestamp: new Date().toISOString(),
- type: "user",
- content,
- model: modelInfo,
- });
-
- // Get conversation history
- const history = await this.sessionManager.getConversationHistory(this.jid);
-
- // Create provider for this request
- const provider = createProvider(modelInfo, this.config.llm.providers);
-
- // Run agent loop
- const response = await this.runAgentLoop(provider, history, content);
-
- // Log assistant response
- await this.sessionManager.appendEntry(this.jid, {
- timestamp: new Date().toISOString(),
- type: "assistant",
- content: response,
- model: modelInfo,
- });
-
- return response;
- }
-
- private async handleCommand(message: string): Promise<string> {
- const parts = message.slice(1).split(/\s+/);
- const command = parts[0].toLowerCase();
-
- switch (command) {
- case "ping":
- return "Pong!";
-
- case "help":
- return HELP_TEXT;
-
- case "models":
- return MODELS_TEXT;
-
- case "status": {
- const statusTool = this.toolRegistry.get("status");
- if (statusTool) {
- const result = await statusTool.execute({});
- return `**System Status:**\n\`\`\`json\n${result}\n\`\`\``;
- }
- return "Status tool not available";
- }
-
- case "clear":
- await this.sessionManager.clearSession(this.jid);
- return "Conversation history cleared.";
-
- case "stats": {
- const stats = await this.sessionManager.getSessionStats(this.jid);
- return `**Session Stats:**\n- Messages: ${stats.messageCount}\n- First: ${stats.firstMessage || "N/A"}\n- Last: ${stats.lastMessage || "N/A"}`;
- }
-
- default:
- return `Unknown command: /${command}\nType /help for available commands.`;
- }
- }
-
- private async runAgentLoop(
- provider: LLMProvider,
- history: ConversationMessage[],
- currentMessage: string
- ): Promise<string> {
- const messages: Message[] = [
- ...history,
- { role: "user", content: currentMessage },
- ];
-
- const tools = this.toolRegistry.getSchemas();
-
- let iterations = 0;
- let finalResponse = "";
-
- while (iterations < MAX_TOOL_ITERATIONS) {
- iterations++;
-
- const response = await provider.chat(messages, {
- systemPrompt: SYSTEM_PROMPT,
- tools,
- maxTokens: 4096,
- });
-
- // Handle tool calls
- if (response.stopReason === "tool_use" && response.toolCalls.length > 0) {
- // Add assistant message with tool calls
- if (response.content) {
- messages.push({ role: "assistant", content: response.content });
- }
-
- // Execute each tool
- for (const toolCall of response.toolCalls) {
- const tool = this.toolRegistry.get(toolCall.name);
- if (!tool) {
- messages.push({
- role: "user",
- content: `Tool '${toolCall.name}' not found`,
- });
- continue;
- }
-
- // Log tool call
- await this.sessionManager.appendEntry(this.jid, {
- timestamp: new Date().toISOString(),
- type: "tool_call",
- content: JSON.stringify({
- name: toolCall.name,
- arguments: toolCall.arguments,
- }),
- toolName: toolCall.name,
- toolCallId: toolCall.id,
- });
-
- try {
- const result = await tool.execute(toolCall.arguments);
-
- // Log tool result
- await this.sessionManager.appendEntry(this.jid, {
- timestamp: new Date().toISOString(),
- type: "tool_result",
- content: result,
- toolName: toolCall.name,
- toolCallId: toolCall.id,
- });
-
- messages.push({
- role: "user",
- content: `Tool result for ${toolCall.name}:\n${result}`,
- });
- } catch (err) {
- const errorMsg = `Tool error: ${(err as Error).message}`;
- messages.push({ role: "user", content: errorMsg });
- }
- }
-
- continue;
- }
-
- // No more tool calls, return final response
- finalResponse = response.content || "";
- break;
- }
-
- if (iterations >= MAX_TOOL_ITERATIONS) {
- finalResponse += "\n\n(Reached maximum tool iterations)";
- }
-
- return finalResponse;
- }
-}
src/agent/session.ts
@@ -1,100 +0,0 @@
-import * as fs from "fs/promises";
-import * as path from "path";
-import { SessionEntry, ConversationMessage } from "./types.js";
-import { bareJid } from "../xmpp/types.js";
-
-export class SessionManager {
- private dataDir: string;
-
- constructor(dataDir: string) {
- this.dataDir = dataDir;
- }
-
- private getSessionDir(jid: string): string {
- // Sanitize JID for filesystem (replace @ and . with safe chars)
- const sanitized = bareJid(jid).replace(/@/g, "_at_").replace(/\./g, "_");
- return path.join(this.dataDir, sanitized);
- }
-
- private getContextPath(jid: string): string {
- return path.join(this.getSessionDir(jid), "context.jsonl");
- }
-
- async ensureSessionDir(jid: string): Promise<void> {
- const dir = this.getSessionDir(jid);
- await fs.mkdir(dir, { recursive: true });
- }
-
- async appendEntry(jid: string, entry: SessionEntry): Promise<void> {
- await this.ensureSessionDir(jid);
- const contextPath = this.getContextPath(jid);
- const line = JSON.stringify(entry) + "\n";
- await fs.appendFile(contextPath, line, "utf-8");
- }
-
- async loadHistory(jid: string): Promise<SessionEntry[]> {
- const contextPath = this.getContextPath(jid);
-
- try {
- const content = await fs.readFile(contextPath, "utf-8");
- const lines = content.trim().split("\n").filter((l) => l.length > 0);
- return lines.map((line) => JSON.parse(line) as SessionEntry);
- } catch (err) {
- if ((err as NodeJS.ErrnoException).code === "ENOENT") {
- return [];
- }
- throw err;
- }
- }
-
- async getConversationHistory(
- jid: string,
- maxEntries: number = 50
- ): Promise<ConversationMessage[]> {
- const entries = await this.loadHistory(jid);
- const messages: ConversationMessage[] = [];
-
- // Take last N entries and convert to conversation format
- const recentEntries = entries.slice(-maxEntries);
-
- for (const entry of recentEntries) {
- if (entry.type === "user") {
- messages.push({ role: "user", content: entry.content });
- } else if (entry.type === "assistant") {
- messages.push({ role: "assistant", content: entry.content });
- } else if (entry.type === "system") {
- messages.push({ role: "system", content: entry.content });
- }
- // Tool calls and results are embedded in assistant/user messages
- }
-
- return messages;
- }
-
- async clearSession(jid: string): Promise<void> {
- const contextPath = this.getContextPath(jid);
- try {
- await fs.unlink(contextPath);
- } catch (err) {
- if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
- throw err;
- }
- }
- }
-
- async getSessionStats(jid: string): Promise<{
- messageCount: number;
- firstMessage: string | null;
- lastMessage: string | null;
- }> {
- const entries = await this.loadHistory(jid);
- const userEntries = entries.filter((e) => e.type === "user");
-
- return {
- messageCount: userEntries.length,
- firstMessage: entries.length > 0 ? entries[0].timestamp : null,
- lastMessage:
- entries.length > 0 ? entries[entries.length - 1].timestamp : null,
- };
- }
-}
src/agent/types.ts
@@ -1,27 +0,0 @@
-import { ModelInfo } from "../config.js";
-
-export interface SessionEntry {
- timestamp: string;
- type: "user" | "assistant" | "tool_call" | "tool_result" | "system";
- content: string;
- model?: ModelInfo;
- toolName?: string;
- toolCallId?: string;
-}
-
-export interface ConversationMessage {
- role: "user" | "assistant" | "system";
- content: string;
-}
-
-export interface ToolCall {
- id: string;
- name: string;
- arguments: Record<string, unknown>;
-}
-
-export interface ToolResult {
- toolCallId: string;
- result: string;
- isError?: boolean;
-}
src/llm/anthropic.ts
@@ -1,155 +0,0 @@
-import Anthropic from "@anthropic-ai/sdk";
-import { LLMProvider, Message, ToolDefinition, LLMResponse } from "./types.js";
-import { TSchema, Kind } from "@sinclair/typebox";
-
-export class AnthropicProvider implements LLMProvider {
- name = "anthropic";
- private client: Anthropic;
- private model: string;
-
- constructor(apiKey: string, model: string) {
- this.client = new Anthropic({ apiKey });
- this.model = model;
- }
-
- async chat(
- messages: Message[],
- options: {
- systemPrompt?: string;
- tools?: ToolDefinition[];
- maxTokens?: number;
- }
- ): Promise<LLMResponse> {
- const anthropicMessages: Anthropic.MessageParam[] = messages
- .filter((m) => m.role !== "system")
- .map((m) => ({
- role: m.role as "user" | "assistant",
- content: m.content,
- }));
-
- const tools = options.tools?.map((t) => ({
- name: t.name,
- description: t.description,
- input_schema: typeboxToJsonSchema(t.parameters),
- }));
-
- const response = await this.client.messages.create({
- model: this.model,
- max_tokens: options.maxTokens || 4096,
- system: options.systemPrompt,
- messages: anthropicMessages,
- tools: tools as Anthropic.Tool[],
- });
-
- let textContent: string | null = null;
- const toolCalls: LLMResponse["toolCalls"] = [];
-
- for (const block of response.content) {
- if (block.type === "text") {
- textContent = block.text;
- } else if (block.type === "tool_use") {
- toolCalls.push({
- id: block.id,
- name: block.name,
- arguments: block.input as Record<string, unknown>,
- });
- }
- }
-
- let stopReason: LLMResponse["stopReason"] = "end_turn";
- if (response.stop_reason === "tool_use") {
- stopReason = "tool_use";
- } else if (response.stop_reason === "max_tokens") {
- stopReason = "max_tokens";
- }
-
- return {
- content: textContent,
- toolCalls,
- stopReason,
- };
- }
-}
-
-function typeboxToJsonSchema(schema: TSchema): Record<string, unknown> {
- // Convert TypeBox schema to JSON Schema for Anthropic
- const result: Record<string, unknown> = {
- type: getJsonSchemaType(schema),
- };
-
- if (schema.description) {
- result.description = schema.description;
- }
-
- if (schema[Kind] === "Object" && schema.properties) {
- result.properties = {};
- const required: string[] = [];
-
- for (const [key, prop] of Object.entries(
- schema.properties as Record<string, TSchema>
- )) {
- (result.properties as Record<string, unknown>)[key] =
- typeboxToJsonSchema(prop);
- // In TypeBox, properties are required by default unless wrapped in Type.Optional
- const optionalSymbol = Symbol.for("TypeBox.Optional");
- if (prop[Kind] !== "Optional" && !(optionalSymbol in prop)) {
- required.push(key);
- }
- }
-
- if (required.length > 0) {
- result.required = required;
- }
- }
-
- if (schema[Kind] === "Array" && schema.items) {
- result.items = typeboxToJsonSchema(schema.items as TSchema);
- }
-
- if (schema[Kind] === "Union" && schema.anyOf) {
- result.anyOf = (schema.anyOf as TSchema[]).map(typeboxToJsonSchema);
- delete result.type;
- }
-
- if (schema[Kind] === "Literal") {
- result.const = schema.const;
- }
-
- if (schema.default !== undefined) {
- result.default = schema.default;
- }
-
- if (schema.minimum !== undefined) {
- result.minimum = schema.minimum;
- }
-
- if (schema.maximum !== undefined) {
- result.maximum = schema.maximum;
- }
-
- return result;
-}
-
-function getJsonSchemaType(schema: TSchema): string {
- const kind = schema[Kind];
- switch (kind) {
- case "String":
- return "string";
- case "Number":
- case "Integer":
- return "number";
- case "Boolean":
- return "boolean";
- case "Array":
- return "array";
- case "Object":
- return "object";
- case "Null":
- return "null";
- case "Optional":
- // Unwrap optional
- return getJsonSchemaType(schema.anyOf?.[0] || schema);
- default:
- return "object";
- }
-}
src/llm/google.ts
@@ -1,154 +0,0 @@
-import {
- GoogleGenerativeAI,
- Content,
- Part,
- FunctionDeclaration,
- SchemaType,
-} from "@google/generative-ai";
-import { LLMProvider, Message, ToolDefinition, LLMResponse } from "./types.js";
-import { TSchema, Kind } from "@sinclair/typebox";
-
-export class GoogleProvider implements LLMProvider {
- name = "google";
- private client: GoogleGenerativeAI;
- private model: string;
-
- constructor(apiKey: string, model: string) {
- this.client = new GoogleGenerativeAI(apiKey);
- this.model = model;
- }
-
- async chat(
- messages: Message[],
- options: {
- systemPrompt?: string;
- tools?: ToolDefinition[];
- maxTokens?: number;
- }
- ): Promise<LLMResponse> {
- const genModel = this.client.getGenerativeModel({
- model: this.model,
- systemInstruction: options.systemPrompt,
- });
-
- const contents: Content[] = messages
- .filter((m) => m.role !== "system")
- .map((m) => ({
- role: m.role === "assistant" ? "model" : "user",
- parts: [{ text: m.content }] as Part[],
- }));
-
- const tools = options.tools?.map((t) => ({
- functionDeclarations: [
- {
- name: t.name,
- description: t.description,
- parameters: typeboxToGeminiSchema(t.parameters),
- } as FunctionDeclaration,
- ],
- }));
-
- const result = await genModel.generateContent({
- contents,
- tools,
- generationConfig: {
- maxOutputTokens: options.maxTokens || 4096,
- },
- });
-
- const response = result.response;
- const candidate = response.candidates?.[0];
-
- if (!candidate) {
- return {
- content: null,
- toolCalls: [],
- stopReason: "error",
- };
- }
-
- let textContent: string | null = null;
- const toolCalls: LLMResponse["toolCalls"] = [];
-
- for (const part of candidate.content.parts) {
- if ("text" in part && part.text) {
- textContent = part.text;
- } else if ("functionCall" in part && part.functionCall) {
- toolCalls.push({
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2)}`,
- name: part.functionCall.name,
- arguments: (part.functionCall.args as Record<string, unknown>) || {},
- });
- }
- }
-
- let stopReason: LLMResponse["stopReason"] = "end_turn";
- if (toolCalls.length > 0) {
- stopReason = "tool_use";
- } else if (candidate.finishReason === "MAX_TOKENS") {
- stopReason = "max_tokens";
- }
-
- return {
- content: textContent,
- toolCalls,
- stopReason,
- };
- }
-}
-
-function typeboxToGeminiSchema(
- schema: TSchema
-): FunctionDeclaration["parameters"] {
- const result: Record<string, unknown> = {
- type: getGeminiSchemaType(schema),
- };
-
- if (schema.description) {
- result.description = schema.description;
- }
-
- if (schema[Kind] === "Object" && schema.properties) {
- result.properties = {};
- const required: string[] = [];
-
- for (const [key, prop] of Object.entries(
- schema.properties as Record<string, TSchema>
- )) {
- (result.properties as Record<string, unknown>)[key] =
- typeboxToGeminiSchema(prop);
- if (prop[Kind] !== "Optional") {
- required.push(key);
- }
- }
-
- if (required.length > 0) {
- result.required = required;
- }
- }
-
- if (schema[Kind] === "Array" && schema.items) {
- result.items = typeboxToGeminiSchema(schema.items as TSchema);
- }
-
- return result as unknown as FunctionDeclaration["parameters"];
-}
-
-function getGeminiSchemaType(schema: TSchema): SchemaType {
- const kind = schema[Kind];
- switch (kind) {
- case "String":
- return SchemaType.STRING;
- case "Number":
- case "Integer":
- return SchemaType.NUMBER;
- case "Boolean":
- return SchemaType.BOOLEAN;
- case "Array":
- return SchemaType.ARRAY;
- case "Object":
- return SchemaType.OBJECT;
- default:
- return SchemaType.OBJECT;
- }
-}
src/llm/index.ts
@@ -1,99 +0,0 @@
-import { ModelInfo, Provider, ProviderConfig } from "../config.js";
-import { LLMProvider } from "./types.js";
-import { AnthropicProvider } from "./anthropic.js";
-import { GoogleProvider } from "./google.js";
-import { OpenAIProvider } from "./openai.js";
-import { OllamaProvider } from "./ollama.js";
-
-export type { LLMProvider, Message, ToolDefinition, LLMResponse } from "./types.js";
-
-export function createProvider(
- modelInfo: ModelInfo,
- providerConfig: ProviderConfig
-): LLMProvider {
- const { provider, model } = modelInfo;
-
- switch (provider) {
- case "anthropic": {
- const config = providerConfig.anthropic;
- if (!config) {
- throw new Error("Anthropic API key not configured");
- }
- return new AnthropicProvider(config.apiKey, model);
- }
-
- case "google": {
- const config = providerConfig.google;
- if (!config) {
- throw new Error("Google API key not configured");
- }
- return new GoogleProvider(config.apiKey, model);
- }
-
- case "openai": {
- const config = providerConfig.openai;
- if (!config) {
- throw new Error("OpenAI API key not configured");
- }
- return new OpenAIProvider(config.apiKey, model);
- }
-
- case "github": {
- const config = providerConfig.github;
- if (!config) {
- throw new Error("GitHub token not configured");
- }
- // GitHub Copilot uses OpenAI-compatible API
- return new OpenAIProvider(
- config.token,
- model,
- "https://api.githubcopilot.com"
- );
- }
-
- case "ollama": {
- const config = providerConfig.ollama;
- const baseUrl = config?.baseUrl || "http://localhost:11434";
- return new OllamaProvider(baseUrl, model);
- }
-
- case "groq": {
- const config = providerConfig.groq;
- if (!config) {
- throw new Error("Groq API key not configured");
- }
- return new OpenAIProvider(
- config.apiKey,
- model,
- "https://api.groq.com/openai/v1"
- );
- }
-
- case "mistral": {
- const config = providerConfig.mistral;
- if (!config) {
- throw new Error("Mistral API key not configured");
- }
- return new OpenAIProvider(
- config.apiKey,
- model,
- "https://api.mistral.ai/v1"
- );
- }
-
- case "openrouter": {
- const config = providerConfig.openrouter;
- if (!config) {
- throw new Error("OpenRouter API key not configured");
- }
- return new OpenAIProvider(
- config.apiKey,
- model,
- "https://openrouter.ai/api/v1"
- );
- }
-
- default:
- throw new Error(`Unknown provider: ${provider}`);
- }
-}
src/llm/ollama.ts
@@ -1,179 +0,0 @@
-import { LLMProvider, Message, ToolDefinition, LLMResponse } from "./types.js";
-import { TSchema, Kind } from "@sinclair/typebox";
-
-interface OllamaMessage {
- role: "system" | "user" | "assistant";
- content: string;
-}
-
-interface OllamaTool {
- type: "function";
- function: {
- name: string;
- description: string;
- parameters: Record<string, unknown>;
- };
-}
-
-interface OllamaToolCall {
- function: {
- name: string;
- arguments: Record<string, unknown>;
- };
-}
-
-interface OllamaResponse {
- message: {
- role: string;
- content: string;
- tool_calls?: OllamaToolCall[];
- };
- done: boolean;
- done_reason?: string;
-}
-
-export class OllamaProvider implements LLMProvider {
- name = "ollama";
- private baseUrl: string;
- private model: string;
-
- constructor(baseUrl: string, model: string) {
- this.baseUrl = baseUrl.replace(/\/$/, "");
- this.model = model;
- }
-
- async chat(
- messages: Message[],
- options: {
- systemPrompt?: string;
- tools?: ToolDefinition[];
- maxTokens?: number;
- }
- ): Promise<LLMResponse> {
- const ollamaMessages: OllamaMessage[] = [];
-
- if (options.systemPrompt) {
- ollamaMessages.push({
- role: "system",
- content: options.systemPrompt,
- });
- }
-
- for (const m of messages) {
- if (m.role === "system") continue;
- ollamaMessages.push({
- role: m.role,
- content: m.content,
- });
- }
-
- const tools: OllamaTool[] | undefined = options.tools?.map((t) => ({
- type: "function" as const,
- function: {
- name: t.name,
- description: t.description,
- parameters: typeboxToJsonSchema(t.parameters),
- },
- }));
-
- const response = await fetch(`${this.baseUrl}/api/chat`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- model: this.model,
- messages: ollamaMessages,
- tools: tools?.length ? tools : undefined,
- stream: false,
- options: {
- num_predict: options.maxTokens || 4096,
- },
- }),
- });
-
- if (!response.ok) {
- throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
- }
-
- const data = (await response.json()) as OllamaResponse;
-
- const textContent = data.message.content || null;
- const toolCalls: LLMResponse["toolCalls"] = [];
-
- if (data.message.tool_calls) {
- for (const tc of data.message.tool_calls) {
- toolCalls.push({
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2)}`,
- name: tc.function.name,
- arguments: tc.function.arguments || {},
- });
- }
- }
-
- let stopReason: LLMResponse["stopReason"] = "end_turn";
- if (toolCalls.length > 0) {
- stopReason = "tool_use";
- } else if (data.done_reason === "length") {
- stopReason = "max_tokens";
- }
-
- return {
- content: textContent,
- toolCalls,
- stopReason,
- };
- }
-}
-
-function typeboxToJsonSchema(schema: TSchema): Record<string, unknown> {
- const result: Record<string, unknown> = {
- type: getJsonSchemaType(schema),
- };
-
- if (schema.description) {
- result.description = schema.description;
- }
-
- if (schema[Kind] === "Object" && schema.properties) {
- result.properties = {};
- const required: string[] = [];
-
- for (const [key, prop] of Object.entries(
- schema.properties as Record<string, TSchema>
- )) {
- (result.properties as Record<string, unknown>)[key] =
- typeboxToJsonSchema(prop);
- if (prop[Kind] !== "Optional") {
- required.push(key);
- }
- }
-
- if (required.length > 0) {
- result.required = required;
- }
- }
-
- if (schema[Kind] === "Array" && schema.items) {
- result.items = typeboxToJsonSchema(schema.items as TSchema);
- }
-
- return result;
-}
-
-function getJsonSchemaType(schema: TSchema): string {
- const kind = schema[Kind];
- switch (kind) {
- case "String":
- return "string";
- case "Number":
- case "Integer":
- return "number";
- case "Boolean":
- return "boolean";
- case "Array":
- return "array";
- case "Object":
- return "object";
- default:
- return "object";
- }
-}
src/llm/openai.ts
@@ -1,159 +0,0 @@
-import OpenAI from "openai";
-import { LLMProvider, Message, ToolDefinition, LLMResponse } from "./types.js";
-import { TSchema, Kind } from "@sinclair/typebox";
-
-export class OpenAIProvider implements LLMProvider {
- name = "openai";
- private client: OpenAI;
- private model: string;
-
- constructor(apiKey: string, model: string, baseUrl?: string) {
- this.client = new OpenAI({
- apiKey,
- baseURL: baseUrl,
- });
- this.model = model;
- }
-
- async chat(
- messages: Message[],
- options: {
- systemPrompt?: string;
- tools?: ToolDefinition[];
- maxTokens?: number;
- }
- ): Promise<LLMResponse> {
- const openaiMessages: OpenAI.ChatCompletionMessageParam[] = [];
-
- if (options.systemPrompt) {
- openaiMessages.push({
- role: "system",
- content: options.systemPrompt,
- });
- }
-
- for (const m of messages) {
- if (m.role === "system") continue;
- openaiMessages.push({
- role: m.role,
- content: m.content,
- });
- }
-
- const tools = options.tools?.map((t) => ({
- type: "function" as const,
- function: {
- name: t.name,
- description: t.description,
- parameters: typeboxToJsonSchema(t.parameters),
- },
- }));
-
- const response = await this.client.chat.completions.create({
- model: this.model,
- messages: openaiMessages,
- tools: tools?.length ? tools : undefined,
- max_tokens: options.maxTokens || 4096,
- });
-
- const choice = response.choices[0];
- if (!choice) {
- return {
- content: null,
- toolCalls: [],
- stopReason: "error",
- };
- }
-
- const textContent = choice.message.content;
- const toolCalls: LLMResponse["toolCalls"] = [];
-
- if (choice.message.tool_calls) {
- for (const tc of choice.message.tool_calls) {
- if (tc.type === "function") {
- toolCalls.push({
- id: tc.id,
- name: tc.function.name,
- arguments: JSON.parse(tc.function.arguments || "{}"),
- });
- }
- }
- }
-
- let stopReason: LLMResponse["stopReason"] = "end_turn";
- if (choice.finish_reason === "tool_calls") {
- stopReason = "tool_use";
- } else if (choice.finish_reason === "length") {
- stopReason = "max_tokens";
- }
-
- return {
- content: textContent,
- toolCalls,
- stopReason,
- };
- }
-}
-
-function typeboxToJsonSchema(schema: TSchema): Record<string, unknown> {
- const result: Record<string, unknown> = {
- type: getJsonSchemaType(schema),
- };
-
- if (schema.description) {
- result.description = schema.description;
- }
-
- if (schema[Kind] === "Object" && schema.properties) {
- result.properties = {};
- const required: string[] = [];
-
- for (const [key, prop] of Object.entries(
- schema.properties as Record<string, TSchema>
- )) {
- (result.properties as Record<string, unknown>)[key] =
- typeboxToJsonSchema(prop);
- if (prop[Kind] !== "Optional") {
- required.push(key);
- }
- }
-
- if (required.length > 0) {
- result.required = required;
- }
- }
-
- if (schema[Kind] === "Array" && schema.items) {
- result.items = typeboxToJsonSchema(schema.items as TSchema);
- }
-
- if (schema[Kind] === "Union" && schema.anyOf) {
- result.anyOf = (schema.anyOf as TSchema[]).map(typeboxToJsonSchema);
- delete result.type;
- }
-
- if (schema.default !== undefined) {
- result.default = schema.default;
- }
-
- return result;
-}
-
-function getJsonSchemaType(schema: TSchema): string {
- const kind = schema[Kind];
- switch (kind) {
- case "String":
- return "string";
- case "Number":
- case "Integer":
- return "number";
- case "Boolean":
- return "boolean";
- case "Array":
- return "array";
- case "Object":
- return "object";
- default:
- return "object";
- }
-}
src/llm/types.ts
@@ -1,46 +0,0 @@
-import { TSchema } from "@sinclair/typebox";
-
-export interface Message {
- role: "user" | "assistant" | "system";
- content: string;
-}
-
-export interface ToolDefinition {
- name: string;
- description: string;
- parameters: TSchema;
-}
-
-export interface ToolCallRequest {
- id: string;
- name: string;
- arguments: Record<string, unknown>;
-}
-
-export interface LLMResponse {
- content: string | null;
- toolCalls: ToolCallRequest[];
- stopReason: "end_turn" | "tool_use" | "max_tokens" | "error";
-}
-
-export interface ToolResultMessage {
- role: "user";
- content: Array<{
- type: "tool_result";
- tool_use_id: string;
- content: string;
- is_error?: boolean;
- }>;
-}
-
-export interface LLMProvider {
- name: string;
- chat(
- messages: Message[],
- options: {
- systemPrompt?: string;
- tools?: ToolDefinition[];
- maxTokens?: number;
- }
- ): Promise<LLMResponse>;
-}
src/pi/main-test.ts
@@ -1,122 +0,0 @@
-#!/usr/bin/env node
-/**
- * Temporary test runner for Pi-based Daneel
- * This is a quick integration to test current progress
- * Will be replaced by proper XMPP integration in Phase 6
- */
-
-import { XmppClient } from "../xmpp/client.js";
-import { XmppAgent } from "./agent-wrapper.js";
-import { getModel } from "@mariozechner/pi-ai";
-import { bareJid } from "../xmpp/types.js";
-import { statusTool } from "./tools/status.js";
-
-interface XmppConfig {
- jid: string;
- password: string;
- ownerJid: string;
-}
-
-function loadConfig(): XmppConfig {
- const jid = process.env.DANEEL_XMPP_JID;
- const password = process.env.DANEEL_XMPP_PASSWORD;
- const ownerJid = process.env.DANEEL_OWNER_JID;
-
- if (!jid || !password || !ownerJid) {
- console.error("Missing required environment variables:");
- console.error(" DANEEL_XMPP_JID");
- console.error(" DANEEL_XMPP_PASSWORD");
- console.error(" DANEEL_OWNER_JID");
- process.exit(1);
- }
-
- return { jid, password, ownerJid };
-}
-
-async function main() {
- console.log("Daneel - XMPP Research Bot (Pi Edition)");
- console.log("=========================================\n");
-
- const config = loadConfig();
-
- // Determine default model
- const defaultModel = getModel("google", "gemini-2.0-flash");
- console.log("Using default model: Google Gemini 2.0 Flash");
-
- // Map of JID -> XmppAgent
- const agents = new Map<string, XmppAgent>();
-
- function getOrCreateAgent(jid: string): XmppAgent {
- const bare = bareJid(jid);
- let agent = agents.get(bare);
-
- if (!agent) {
- console.log(`Creating new agent for JID: ${bare}`);
- // Create agent with status tool
- agent = new XmppAgent(bare, defaultModel, [statusTool]);
- agents.set(bare, agent);
- }
-
- return agent;
- }
-
- // Create XMPP client
- const xmpp = new XmppClient({
- xmpp: {
- jid: config.jid,
- password: config.password,
- ownerJid: config.ownerJid,
- },
- llm: {
- defaultModel: { provider: "google" as any, model: "gemini-2.0-flash" },
- providers: {},
- },
- paths: {
- dataDir: "./data",
- inboxPath: process.env.DANEEL_INBOX_PATH || "~/desktop/org/inbox.org",
- },
- debug: process.env.DANEEL_DEBUG === "true",
- });
-
- // Set up message handler
- xmpp.onMessage(async (message) => {
- console.log(`[${new Date().toISOString()}] Message from ${bareJid(message.from)}: ${message.body.slice(0, 50)}...`);
-
- try {
- const agent = getOrCreateAgent(message.from);
- const response = await agent.processMessage(message.body);
-
- await xmpp.sendMessage(message.from, response);
- console.log(`[${new Date().toISOString()}] Response sent (${response.length} chars)`);
- } catch (error) {
- console.error("Error processing message:", error);
- await xmpp.sendMessage(
- message.from,
- `Error processing your message: ${error instanceof Error ? error.message : 'Unknown error'}`
- );
- }
- });
-
- // Graceful shutdown
- const shutdown = async (signal: string) => {
- console.log(`\nReceived ${signal}, shutting down...`);
- await xmpp.stop();
- process.exit(0);
- };
-
- process.on("SIGINT", () => shutdown("SIGINT"));
- process.on("SIGTERM", () => shutdown("SIGTERM"));
-
- // Start XMPP client
- console.log("\nConnecting to XMPP server...");
- console.log(`Bot JID: ${config.jid}`);
- console.log(`Owner JID: ${config.ownerJid}`);
- console.log("\nWaiting for messages...\n");
-
- await xmpp.start();
-}
-
-main().catch((error) => {
- console.error("Fatal error:", error);
- process.exit(1);
-});
src/tools/index.ts
@@ -1,35 +0,0 @@
-import { Tool, ToolRegistry } from "./types.js";
-import { createStatusTool } from "./status.js";
-import { createOrgmodeTool } from "./orgmode.js";
-import { createResearchTool } from "./research.js";
-import { createWebSearchTool } from "./websearch.js";
-import { Config } from "../config.js";
-
-export function createToolRegistry(config: Config): ToolRegistry {
- const tools = new Map<string, Tool>();
-
- const registry: ToolRegistry = {
- tools,
- register: (tool: Tool) => {
- tools.set(tool.name, tool);
- },
- get: (name: string) => tools.get(name),
- list: () => Array.from(tools.values()),
- getSchemas: () =>
- Array.from(tools.values()).map((t) => ({
- name: t.name,
- description: t.description,
- parameters: t.parameters,
- })),
- };
-
- // Register default tools
- registry.register(createStatusTool());
- registry.register(createOrgmodeTool(config.paths.inboxPath));
- registry.register(createResearchTool());
- registry.register(createWebSearchTool());
-
- return registry;
-}
-
-export type { Tool, ToolRegistry } from "./types.js";
src/tools/orgmode.ts
@@ -1,85 +0,0 @@
-import { Type } from "@sinclair/typebox";
-import { Tool } from "./types.js";
-import * as fs from "fs/promises";
-
-const OrgmodeParams = Type.Object({
- title: Type.String({ description: "Title for the org entry" }),
- content: Type.String({ description: "Content to save (markdown or plain text)" }),
- tags: Type.Optional(
- Type.Array(Type.String(), { description: "Optional tags for the entry" })
- ),
-});
-
-type OrgmodeArgs = typeof OrgmodeParams.static;
-
-export function createOrgmodeTool(inboxPath: string): Tool<OrgmodeArgs> {
- return {
- name: "save_to_org",
- description:
- "Save content to the org-mode inbox file. Use for saving research results, notes, or any content the user wants to keep.",
- parameters: OrgmodeParams,
- execute: async (args) => {
- const { title, content, tags } = args;
-
- const timestamp = new Date().toISOString();
- const dateStr = timestamp.slice(0, 10);
- const timeStr = timestamp.slice(11, 16);
-
- // Format tags for org-mode
- const tagStr = tags && tags.length > 0 ? `:${tags.join(":")}:` : "";
-
- // Convert markdown to org-mode (basic conversion)
- const orgContent = convertMarkdownToOrg(content);
-
- // Create org entry
- const entry = `
-* TODO ${title} ${tagStr}
-:PROPERTIES:
-:CREATED: [${dateStr} ${timeStr}]
-:SOURCE: daneel
-:END:
-
-${orgContent}
-`;
-
- // Append to inbox file
- await fs.appendFile(inboxPath, entry, "utf-8");
-
- return `Saved to org inbox: "${title}"`;
- },
- };
-}
-
-function convertMarkdownToOrg(markdown: string): string {
- let org = markdown;
-
- // Convert headers (### -> ***)
- org = org.replace(/^### (.+)$/gm, "*** $1");
- org = org.replace(/^## (.+)$/gm, "** $1");
- org = org.replace(/^# (.+)$/gm, "* $1");
-
- // Convert bold (**text** -> *text*)
- org = org.replace(/\*\*(.+?)\*\*/g, "*$1*");
-
- // Convert italic (*text* or _text_ -> /text/)
- // Be careful not to convert already-converted bold
- org = org.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "/$1/");
- org = org.replace(/_(.+?)_/g, "/$1/");
-
- // Convert inline code (`code` -> =code=)
- org = org.replace(/`([^`]+)`/g, "=$1=");
-
- // Convert code blocks
- org = org.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
- const langStr = lang ? ` ${lang}` : "";
- return `#+BEGIN_SRC${langStr}\n${code}#+END_SRC`;
- });
-
- // Convert links [text](url) -> [[url][text]]
- org = org.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "[[$2][$1]]");
-
- // Convert unordered lists (- item -> - item, already compatible)
- // Convert ordered lists (1. item -> 1. item, already compatible)
-
- return org;
-}
src/tools/research.ts
@@ -1,41 +0,0 @@
-import { Type } from "@sinclair/typebox";
-import { Tool } from "./types.js";
-
-const ResearchParams = Type.Object({
- query: Type.String({ description: "The research query or question" }),
- depth: Type.Optional(
- Type.Union([Type.Literal("quick"), Type.Literal("thorough")], {
- description: "How deep to research: quick for fast answers, thorough for comprehensive research",
- default: "quick",
- })
- ),
-});
-
-type ResearchArgs = typeof ResearchParams.static;
-
-export function createResearchTool(): Tool<ResearchArgs> {
- return {
- name: "research",
- description:
- "Research a topic or answer a question. Use this for queries that require gathering information or analysis.",
- parameters: ResearchParams,
- execute: async (args) => {
- const { query, depth = "quick" } = args;
-
- // For now, this is a placeholder that returns a message
- // In a full implementation, this could:
- // - Call web search APIs
- // - Query knowledge bases
- // - Use RAG with local documents
- // - Aggregate multiple sources
-
- return JSON.stringify({
- query,
- depth,
- note: "Research tool executed. In the current implementation, the LLM should answer based on its training data. Future versions will integrate web search and other data sources.",
- suggestion:
- "Provide your best answer based on your knowledge. If you need real-time data, inform the user that web search is not yet implemented.",
- });
- },
- };
-}
src/tools/status.ts
@@ -1,71 +0,0 @@
-import { Type } from "@sinclair/typebox";
-import { Tool } from "./types.js";
-import * as os from "os";
-
-const StatusParams = Type.Object({});
-
-type StatusArgs = typeof StatusParams.static;
-
-export function createStatusTool(): Tool<StatusArgs> {
- return {
- name: "status",
- description:
- "Get system status including uptime, memory usage, and bot information",
- parameters: StatusParams,
- execute: async () => {
- const uptime = process.uptime();
- const memUsage = process.memoryUsage();
- const systemUptime = os.uptime();
-
- const formatBytes = (bytes: number): string => {
- const mb = bytes / 1024 / 1024;
- return `${mb.toFixed(1)} MB`;
- };
-
- const formatDuration = (seconds: number): string => {
- const days = Math.floor(seconds / 86400);
- const hours = Math.floor((seconds % 86400) / 3600);
- const mins = Math.floor((seconds % 3600) / 60);
- const secs = Math.floor(seconds % 60);
-
- const parts = [];
- if (days > 0) parts.push(`${days}d`);
- if (hours > 0) parts.push(`${hours}h`);
- if (mins > 0) parts.push(`${mins}m`);
- if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
- return parts.join(" ");
- };
-
- return JSON.stringify(
- {
- bot: {
- name: "Daneel",
- version: "0.1.0",
- uptime: formatDuration(uptime),
- },
- system: {
- hostname: os.hostname(),
- platform: os.platform(),
- arch: os.arch(),
- uptime: formatDuration(systemUptime),
- loadAvg: os.loadavg().map((l) => l.toFixed(2)),
- },
- memory: {
- process: {
- heapUsed: formatBytes(memUsage.heapUsed),
- heapTotal: formatBytes(memUsage.heapTotal),
- rss: formatBytes(memUsage.rss),
- },
- system: {
- total: formatBytes(os.totalmem()),
- free: formatBytes(os.freemem()),
- used: formatBytes(os.totalmem() - os.freemem()),
- },
- },
- },
- null,
- 2
- );
- },
- };
-}
src/tools/types.ts
@@ -1,25 +0,0 @@
-import { TSchema } from "@sinclair/typebox";
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export interface Tool<T = any> {
- name: string;
- description: string;
- parameters: TSchema;
- execute: (args: T) => Promise<string>;
-}
-
-export interface ToolRegistry {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- tools: Map<string, Tool<any>>;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- register: (tool: Tool<any>) => void;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- get: (name: string) => Tool<any> | undefined;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- list: () => Tool<any>[];
- getSchemas: () => Array<{
- name: string;
- description: string;
- parameters: TSchema;
- }>;
-}
src/tools/websearch.ts
@@ -1,43 +0,0 @@
-import { Type } from "@sinclair/typebox";
-import { Tool } from "./types.js";
-
-const WebSearchParams = Type.Object({
- query: Type.String({ description: "The search query" }),
- maxResults: Type.Optional(
- Type.Number({
- description: "Maximum number of results to return",
- default: 5,
- minimum: 1,
- maximum: 10,
- })
- ),
-});
-
-type WebSearchArgs = typeof WebSearchParams.static;
-
-export function createWebSearchTool(): Tool<WebSearchArgs> {
- return {
- name: "web_search",
- description:
- "Search the web for current information. Use this for queries that require up-to-date data.",
- parameters: WebSearchParams,
- execute: async (args) => {
- const { query, maxResults = 5 } = args;
-
- // Placeholder implementation
- // Future: integrate with search APIs like:
- // - DuckDuckGo API
- // - Brave Search API
- // - SearXNG instance
- // - Tavily API
-
- return JSON.stringify({
- query,
- maxResults,
- status: "not_implemented",
- message:
- "Web search is not yet implemented. Please rely on training data or ask the user to provide the information.",
- });
- },
- };
-}
src/xmpp/client.ts
@@ -1,22 +1,27 @@
import { client, xml, jid } from "@xmpp/client";
import type { Element } from "@xmpp/xml";
import { XmppMessage, bareJid } from "./types.js";
-import { Config } from "../config.js";
+
+export interface XmppConfig {
+ jid: string;
+ password: string;
+ ownerJid: string;
+}
export type MessageHandler = (message: XmppMessage) => Promise<void>;
export class XmppClient {
private xmpp: ReturnType<typeof client>;
private messageHandler: MessageHandler | null = null;
- private config: Config;
+ private ownerJid: string;
- constructor(config: Config) {
- this.config = config;
+ constructor(config: XmppConfig) {
+ this.ownerJid = config.ownerJid;
this.xmpp = client({
- service: `xmpp://${jid(config.xmpp.jid).domain}`,
- username: jid(config.xmpp.jid).local,
- password: config.xmpp.password,
+ service: `xmpp://${jid(config.jid).domain}`,
+ username: jid(config.jid).local,
+ password: config.password,
});
this.setupEventHandlers();
@@ -63,7 +68,7 @@ export class XmppClient {
const id = stanza.attrs.id as string | undefined;
// Security: only respond to owner
- if (bareJid(from) !== bareJid(this.config.xmpp.ownerJid)) {
+ if (bareJid(from) !== bareJid(this.ownerJid)) {
console.log(`Ignoring message from unauthorized JID: ${bareJid(from)}`);
return;
}
src/config.ts
@@ -1,211 +0,0 @@
-import * as path from "path";
-import * as os from "os";
-
-// Model aliases for easy selection
-export type ModelAlias =
- // Anthropic
- | "opus"
- | "sonnet"
- | "haiku"
- // Google
- | "gemini"
- | "gemini-pro"
- | "gemini-flash"
- // OpenAI
- | "gpt4"
- | "gpt4o"
- | "o1"
- | "o3"
- // GitHub Copilot
- | "copilot"
- | "copilot-claude"
- // Ollama (local)
- | "ollama"
- | "llama"
- | "qwen"
- | "deepseek"
- // Others
- | "groq"
- | "mistral";
-
-export type Provider =
- | "anthropic"
- | "google"
- | "openai"
- | "github"
- | "ollama"
- | "groq"
- | "mistral"
- | "openrouter";
-
-export interface ProviderConfig {
- anthropic?: { apiKey: string };
- google?: { apiKey: string };
- openai?: { apiKey: string };
- github?: { token: string };
- ollama?: { baseUrl: string };
- openrouter?: { apiKey: string };
- groq?: { apiKey: string };
- mistral?: { apiKey: string };
-}
-
-export interface ModelInfo {
- provider: Provider;
- model: string;
-}
-
-// Model prefix mapping for message parsing
-export const MODEL_PREFIXES: Record<string, ModelInfo> = {
- // Anthropic
- "opus:": { provider: "anthropic", model: "claude-opus-4-5-20250514" },
- "o:": { provider: "anthropic", model: "claude-opus-4-5-20250514" },
- "sonnet:": { provider: "anthropic", model: "claude-sonnet-4-20250514" },
- "s:": { provider: "anthropic", model: "claude-sonnet-4-20250514" },
- "haiku:": { provider: "anthropic", model: "claude-3-5-haiku-20241022" },
- "h:": { provider: "anthropic", model: "claude-3-5-haiku-20241022" },
-
- // Google
- "gemini:": { provider: "google", model: "gemini-2.0-flash" },
- "g:": { provider: "google", model: "gemini-2.0-flash" },
- "gemini-pro:": { provider: "google", model: "gemini-2.5-pro-preview-05-06" },
- "gp:": { provider: "google", model: "gemini-2.5-pro-preview-05-06" },
-
- // OpenAI
- "gpt:": { provider: "openai", model: "gpt-4o" },
- "gpt4:": { provider: "openai", model: "gpt-4o" },
- "o1:": { provider: "openai", model: "o1" },
- "o3:": { provider: "openai", model: "o3-mini" },
-
- // GitHub Copilot
- "copilot:": { provider: "github", model: "gpt-4o" },
- "cp:": { provider: "github", model: "gpt-4o" },
- "copilot-claude:": { provider: "github", model: "claude-sonnet-4" },
-
- // Ollama (local)
- "ollama:": { provider: "ollama", model: "llama3.2" },
- "llama:": { provider: "ollama", model: "llama3.2" },
- "qwen:": { provider: "ollama", model: "qwen2.5" },
- "deepseek:": { provider: "ollama", model: "deepseek-r1" },
-
- // Others
- "groq:": { provider: "groq", model: "llama-3.3-70b-versatile" },
- "mistral:": { provider: "mistral", model: "mistral-large-latest" },
-};
-
-export interface XmppConfig {
- jid: string;
- password: string;
- ownerJid: string;
-}
-
-export interface PathsConfig {
- dataDir: string;
- inboxPath: string;
-}
-
-export interface Config {
- xmpp: XmppConfig;
- llm: {
- defaultModel: ModelInfo;
- providers: ProviderConfig;
- };
- paths: PathsConfig;
- debug: boolean;
-}
-
-function getEnvOrThrow(name: string): string {
- const value = process.env[name];
- if (!value) {
- throw new Error(`Required environment variable ${name} is not set`);
- }
- return value;
-}
-
-function getEnvOrDefault(name: string, defaultValue: string): string {
- return process.env[name] || defaultValue;
-}
-
-export function loadConfig(): Config {
- const xmppJid = getEnvOrThrow("DANEEL_XMPP_JID");
- const xmppPassword = getEnvOrThrow("DANEEL_XMPP_PASSWORD");
- const ownerJid = getEnvOrThrow("DANEEL_OWNER_JID");
-
- const dataDir = getEnvOrDefault(
- "DANEEL_DATA_DIR",
- path.join(process.cwd(), "data")
- );
-
- const inboxPath = getEnvOrDefault(
- "DANEEL_INBOX_PATH",
- path.join(os.homedir(), "org", "inbox.org")
- );
-
- const defaultModelPrefix = getEnvOrDefault("DANEEL_DEFAULT_MODEL", "sonnet");
- const defaultModel = MODEL_PREFIXES[`${defaultModelPrefix}:`] || {
- provider: "anthropic" as Provider,
- model: "claude-sonnet-4-20250514",
- };
-
- const providers: ProviderConfig = {};
-
- // Load provider credentials from environment
- if (process.env.ANTHROPIC_API_KEY) {
- providers.anthropic = { apiKey: process.env.ANTHROPIC_API_KEY };
- }
- if (process.env.GOOGLE_API_KEY) {
- providers.google = { apiKey: process.env.GOOGLE_API_KEY };
- }
- if (process.env.OPENAI_API_KEY) {
- providers.openai = { apiKey: process.env.OPENAI_API_KEY };
- }
- if (process.env.GITHUB_TOKEN) {
- providers.github = { token: process.env.GITHUB_TOKEN };
- }
- if (process.env.OLLAMA_BASE_URL) {
- providers.ollama = {
- baseUrl: getEnvOrDefault("OLLAMA_BASE_URL", "http://localhost:11434"),
- };
- }
- if (process.env.GROQ_API_KEY) {
- providers.groq = { apiKey: process.env.GROQ_API_KEY };
- }
- if (process.env.MISTRAL_API_KEY) {
- providers.mistral = { apiKey: process.env.MISTRAL_API_KEY };
- }
- if (process.env.OPENROUTER_API_KEY) {
- providers.openrouter = { apiKey: process.env.OPENROUTER_API_KEY };
- }
-
- return {
- xmpp: {
- jid: xmppJid,
- password: xmppPassword,
- ownerJid,
- },
- llm: {
- defaultModel,
- providers,
- },
- paths: {
- dataDir,
- inboxPath,
- },
- debug: process.env.DANEEL_DEBUG === "true",
- };
-}
-
-// Parse model prefix from message
-export function parseModelPrefix(message: string): {
- model: ModelInfo | null;
- content: string;
-} {
- for (const [prefix, modelInfo] of Object.entries(MODEL_PREFIXES)) {
- if (message.toLowerCase().startsWith(prefix)) {
- return {
- model: modelInfo,
- content: message.slice(prefix.length).trim(),
- };
- }
- }
- return { model: null, content: message };
-}
src/main-pi.ts
@@ -11,6 +11,7 @@ import { bareJid } from "./xmpp/types.js";
import { statusTool } from "./pi/tools/status.js";
import { webSearchTool } from "./pi/tools/websearch.js";
import { researchTool } from "./pi/tools/research.js";
+import { withLogging } from "./pi/tools/logging.js";
import * as os from "os";
interface Config {
@@ -61,9 +62,10 @@ async function main() {
const defaultModel = getDefaultModel();
console.log(`Default model: ${defaultModel.provider}/${defaultModel.id}`);
- // Available tools
- const tools = [statusTool, webSearchTool, researchTool];
- console.log(`Tools available: ${tools.map(t => t.name).join(", ")}`);
+ // Available tools (wrap with logging if debug enabled)
+ const rawTools = [statusTool, webSearchTool, researchTool];
+ const tools = rawTools.map(tool => withLogging(tool, config.debug));
+ console.log(`Tools available: ${rawTools.map(t => t.name).join(", ")}`);
// Map of JID -> XmppAgent
const agents = new Map<string, XmppAgent>();
@@ -74,9 +76,9 @@ async function main() {
if (!agent) {
if (config.debug) {
- console.log(`[DEBUG] Creating new agent for JID: ${bare}`);
+ console.log(`[Main] Creating new agent for JID: ${bare}`);
}
- agent = new XmppAgent(bare, defaultModel, tools);
+ agent = new XmppAgent(bare, defaultModel, tools, config.debug);
agents.set(bare, agent);
}
@@ -84,23 +86,16 @@ async function main() {
}
// Create XMPP client
- const xmpp = new XmppClient({
- xmpp: config.xmpp,
- llm: {
- defaultModel: { provider: defaultModel.provider as any, model: defaultModel.id },
- providers: {},
- },
- paths: config.paths,
- debug: config.debug,
- });
+ const xmpp = new XmppClient(config.xmpp);
// Set up message handler
xmpp.onMessage(async (message) => {
const timestamp = new Date().toISOString();
const from = bareJid(message.from);
const preview = message.body.slice(0, 50);
+ const requestStartTime = Date.now();
- console.log(`[${timestamp}] Message from ${from}: ${preview}${message.body.length > 50 ? "..." : ""}`);
+ console.log(`[${timestamp}] ← Message from ${from}: ${preview}${message.body.length > 50 ? "..." : ""}`);
try {
const agent = getOrCreateAgent(message.from);
@@ -108,13 +103,17 @@ async function main() {
await xmpp.sendMessage(message.from, response);
+ const elapsed = Date.now() - requestStartTime;
+ const timestamp2 = new Date().toISOString();
+
if (config.debug) {
- console.log(`[${timestamp}] Response sent (${response.length} chars)`);
+ console.log(`[${timestamp2}] → Response sent to ${from} (${response.length} chars, ${elapsed}ms total)`);
} else {
- console.log(`[${timestamp}] Response sent`);
+ console.log(`[${timestamp2}] → Response sent to ${from} (${elapsed}ms)`);
}
} catch (error) {
- console.error(`[${timestamp}] Error processing message:`, error);
+ const elapsed = Date.now() - requestStartTime;
+ console.error(`[${timestamp}] ✗ Error after ${elapsed}ms:`, error);
// Check if it's an API key error
const errorMsg = error instanceof Error ? error.message : "Unknown error";
@@ -128,8 +127,9 @@ async function main() {
try {
await xmpp.sendMessage(message.from, userMessage);
+ console.log(`[${timestamp}] → Error message sent to ${from}`);
} catch (sendError) {
- console.error(`[${timestamp}] Failed to send error message:`, sendError);
+ console.error(`[${timestamp}] ✗ Failed to send error message:`, sendError);
}
}
});
src/main.ts
@@ -1,69 +0,0 @@
-import { loadConfig } from "./config.js";
-import { XmppClient } from "./xmpp/client.js";
-import { SessionManager } from "./agent/session.js";
-import { AgentRunner } from "./agent/runner.js";
-import { createToolRegistry } from "./tools/index.js";
-import { bareJid } from "./xmpp/types.js";
-
-async function main(): Promise<void> {
- console.log("Daneel - XMPP Research Bot");
- console.log("==========================\n");
-
- // Load configuration
- const config = loadConfig();
- console.log(`XMPP JID: ${config.xmpp.jid}`);
- console.log(`Owner JID: ${config.xmpp.ownerJid}`);
- console.log(`Default model: ${config.llm.defaultModel.provider}/${config.llm.defaultModel.model}`);
- console.log(`Data directory: ${config.paths.dataDir}`);
-
- // Initialize components
- const sessionManager = new SessionManager(config.paths.dataDir);
- const toolRegistry = createToolRegistry(config);
-
- // Map of JID -> AgentRunner
- const agents = new Map<string, AgentRunner>();
-
- function getOrCreateAgent(jid: string): AgentRunner {
- const bare = bareJid(jid);
- let agent = agents.get(bare);
- if (!agent) {
- agent = new AgentRunner(bare, config, sessionManager, toolRegistry);
- agents.set(bare, agent);
- }
- return agent;
- }
-
- // Create XMPP client
- const xmpp = new XmppClient(config);
-
- // Set up message handler
- xmpp.onMessage(async (message) => {
- console.log(`[${new Date().toISOString()}] Message from ${bareJid(message.from)}: ${message.body.slice(0, 50)}...`);
-
- const agent = getOrCreateAgent(message.from);
- const response = await agent.processMessage(message.body);
-
- await xmpp.sendMessage(message.from, response);
- console.log(`[${new Date().toISOString()}] Response sent (${response.length} chars)`);
- });
-
- // Graceful shutdown
- const shutdown = async (signal: string): Promise<void> => {
- console.log(`\nReceived ${signal}, shutting down...`);
- await xmpp.stop();
- process.exit(0);
- };
-
- process.on("SIGINT", () => shutdown("SIGINT"));
- process.on("SIGTERM", () => shutdown("SIGTERM"));
-
- // Start XMPP client
- console.log("\nConnecting to XMPP server...");
- await xmpp.start();
- console.log("Bot is running. Press Ctrl+C to stop.\n");
-}
-
-main().catch((err) => {
- console.error("Fatal error:", err);
- process.exit(1);
-});
package-lock.json
@@ -1,163 +1,27 @@
{
"name": "daneel",
- "version": "0.1.0",
+ "version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "daneel",
- "version": "0.1.0",
+ "version": "0.2.0",
"license": "Apache-2.0",
"dependencies": {
- "@anthropic-ai/sdk": "^0.39.0",
- "@anthropic-ai/vertex-sdk": "^0.14.3",
- "@google/generative-ai": "^0.21.0",
"@mariozechner/pi-agent-core": "^0.52.12",
"@mariozechner/pi-ai": "^0.52.12",
"@sinclair/typebox": "^0.32.0",
"@xmpp/client": "^0.13.1",
- "@xmpp/debug": "^0.13.0",
- "google-auth-library": "^10.5.0",
- "js-yaml": "^4.1.0",
- "openai": "^4.77.0"
+ "@xmpp/debug": "^0.13.0"
},
"devDependencies": {
- "@types/js-yaml": "^4.0.9",
"@types/node": "^20.0.0",
"@vitest/ui": "^4.0.18",
"typescript": "^5.3.0",
"vitest": "^4.0.18"
}
},
- "node_modules/@anthropic-ai/sdk": {
- "version": "0.39.0",
- "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz",
- "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "^18.11.18",
- "@types/node-fetch": "^2.6.4",
- "abort-controller": "^3.0.0",
- "agentkeepalive": "^4.2.1",
- "form-data-encoder": "1.7.2",
- "formdata-node": "^4.3.2",
- "node-fetch": "^2.6.7"
- }
- },
- "node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
- "version": "18.19.130",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
- "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~5.26.4"
- }
- },
- "node_modules/@anthropic-ai/sdk/node_modules/undici-types": {
- "version": "5.26.5",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
- "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
- "license": "MIT"
- },
- "node_modules/@anthropic-ai/vertex-sdk": {
- "version": "0.14.3",
- "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.14.3.tgz",
- "integrity": "sha512-GJZTkkvN66gM3Epqm9laKEjC3orQqzmQt8JAgTN9+zlb+I+1/oEd3Z7rj2tkEKCTeOUVScdhcXPudN8GdpuGqA==",
- "license": "MIT",
- "dependencies": {
- "@anthropic-ai/sdk": ">=0.50.3 <1",
- "google-auth-library": "^9.4.2"
- }
- },
- "node_modules/@anthropic-ai/vertex-sdk/node_modules/@anthropic-ai/sdk": {
- "version": "0.74.0",
- "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.74.0.tgz",
- "integrity": "sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw==",
- "license": "MIT",
- "dependencies": {
- "json-schema-to-ts": "^3.1.1"
- },
- "bin": {
- "anthropic-ai-sdk": "bin/cli"
- },
- "peerDependencies": {
- "zod": "^3.25.0 || ^4.0.0"
- },
- "peerDependenciesMeta": {
- "zod": {
- "optional": true
- }
- }
- },
- "node_modules/@anthropic-ai/vertex-sdk/node_modules/gaxios": {
- "version": "6.7.1",
- "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
- "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "extend": "^3.0.2",
- "https-proxy-agent": "^7.0.1",
- "is-stream": "^2.0.0",
- "node-fetch": "^2.6.9",
- "uuid": "^9.0.1"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@anthropic-ai/vertex-sdk/node_modules/gcp-metadata": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
- "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
- "license": "Apache-2.0",
- "dependencies": {
- "gaxios": "^6.1.1",
- "google-logging-utils": "^0.0.2",
- "json-bigint": "^1.0.0"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@anthropic-ai/vertex-sdk/node_modules/google-auth-library": {
- "version": "9.15.1",
- "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
- "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
- "license": "Apache-2.0",
- "dependencies": {
- "base64-js": "^1.3.0",
- "ecdsa-sig-formatter": "^1.0.11",
- "gaxios": "^6.1.1",
- "gcp-metadata": "^6.1.0",
- "gtoken": "^7.0.0",
- "jws": "^4.0.0"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@anthropic-ai/vertex-sdk/node_modules/google-logging-utils": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
- "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@anthropic-ai/vertex-sdk/node_modules/gtoken": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
- "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
- "license": "MIT",
- "dependencies": {
- "gaxios": "^6.0.0",
- "jws": "^4.0.0"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-crypto/crc32": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz",
@@ -1725,15 +1589,6 @@
}
}
},
- "node_modules/@google/generative-ai": {
- "version": "0.21.0",
- "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz",
- "integrity": "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.0.0"
- }
- },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -3023,13 +2878,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/js-yaml": {
- "version": "4.0.9",
- "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
- "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@types/node": {
"version": "20.19.31",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.31.tgz",
@@ -3039,16 +2887,6 @@
"undici-types": "~6.21.0"
}
},
- "node_modules/@types/node-fetch": {
- "version": "2.6.13",
- "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
- "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*",
- "form-data": "^4.0.4"
- }
- },
"node_modules/@vitest/expect": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
@@ -3566,18 +3404,6 @@
"node": ">= 12.4.0"
}
},
- "node_modules/abort-controller": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
- "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
- "license": "MIT",
- "dependencies": {
- "event-target-shim": "^5.0.0"
- },
- "engines": {
- "node": ">=6.5"
- }
- },
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -3587,18 +3413,6 @@
"node": ">= 14"
}
},
- "node_modules/agentkeepalive": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
- "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
- "license": "MIT",
- "dependencies": {
- "humanize-ms": "^1.2.1"
- },
- "engines": {
- "node": ">= 8.0.0"
- }
- },
"node_modules/ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
@@ -3670,12 +3484,6 @@
"node": ">= 8"
}
},
- "node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "license": "Python-2.0"
- },
"node_modules/array-buffer-byte-length": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
@@ -3766,12 +3574,6 @@
"node": ">= 0.4"
}
},
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "license": "MIT"
- },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -4109,18 +3911,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
- "node_modules/combined-stream": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
- "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "license": "MIT",
- "dependencies": {
- "delayed-stream": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
@@ -4331,15 +4121,6 @@
"node": ">= 14"
}
},
- "node_modules/delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -4637,15 +4418,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/event-target-shim": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
- "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -4827,41 +4599,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/form-data": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
- "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
- "license": "MIT",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.2",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/form-data-encoder": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
- "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
- "license": "MIT"
- },
- "node_modules/formdata-node": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
- "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
- "license": "MIT",
- "dependencies": {
- "node-domexception": "1.0.0",
- "web-streams-polyfill": "4.0.0-beta.3"
- },
- "engines": {
- "node": ">= 12.20"
- }
- },
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -5313,15 +5050,6 @@
"node": ">= 14"
}
},
- "node_modules/humanize-ms": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
- "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.0.0"
- }
- },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -5672,18 +5400,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-stream": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
- "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/is-string": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
@@ -5817,18 +5533,6 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
- "node_modules/js-yaml": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
- "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
- "license": "MIT",
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -6006,27 +5710,6 @@
"safe-buffer": "^5.1.2"
}
},
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -6131,26 +5814,6 @@
"semver": "bin/semver"
}
},
- "node_modules/node-fetch": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
- "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
- "license": "MIT",
- "dependencies": {
- "whatwg-url": "^5.0.0"
- },
- "engines": {
- "node": "4.x || >=6.0.0"
- },
- "peerDependencies": {
- "encoding": "^0.1.0"
- },
- "peerDependenciesMeta": {
- "encoding": {
- "optional": true
- }
- }
- },
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -6249,51 +5912,6 @@
"wrappy": "1"
}
},
- "node_modules/openai": {
- "version": "4.104.0",
- "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
- "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@types/node": "^18.11.18",
- "@types/node-fetch": "^2.6.4",
- "abort-controller": "^3.0.0",
- "agentkeepalive": "^4.2.1",
- "form-data-encoder": "1.7.2",
- "formdata-node": "^4.3.2",
- "node-fetch": "^2.6.7"
- },
- "bin": {
- "openai": "bin/cli"
- },
- "peerDependencies": {
- "ws": "^8.18.0",
- "zod": "^3.23.8"
- },
- "peerDependenciesMeta": {
- "ws": {
- "optional": true
- },
- "zod": {
- "optional": true
- }
- }
- },
- "node_modules/openai/node_modules/@types/node": {
- "version": "18.19.130",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
- "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~5.26.4"
- }
- },
- "node_modules/openai/node_modules/undici-types": {
- "version": "5.26.5",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
- "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
- "license": "MIT"
- },
"node_modules/own-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -7562,12 +7180,6 @@
"node": ">=6"
}
},
- "node_modules/tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "license": "MIT"
- },
"node_modules/ts-algebra": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
@@ -7737,19 +7349,6 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
- "node_modules/uuid": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
- "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
- "funding": [
- "https://github.com/sponsors/broofa",
- "https://github.com/sponsors/ctavan"
- ],
- "license": "MIT",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
"node_modules/v8flags": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz",
@@ -7768,6 +7367,7 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -7875,6 +7475,7 @@
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vitest/expect": "4.0.18",
"@vitest/mocker": "4.0.18",
@@ -7960,31 +7561,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/web-streams-polyfill": {
- "version": "4.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
- "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
- "license": "MIT",
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "license": "BSD-2-Clause"
- },
- "node_modules/whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "license": "MIT",
- "dependencies": {
- "tr46": "~0.0.3",
- "webidl-conversions": "^3.0.0"
- }
- },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
package.json
@@ -1,34 +1,25 @@
{
"name": "daneel",
- "version": "0.1.0",
+ "version": "0.2.0",
"description": "XMPP research bot using Pi AI toolkit",
"type": "module",
- "main": "dist/main.js",
+ "main": "dist/main-pi.js",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"start": "node dist/main-pi.js",
- "start:old": "node dist/main.js",
- "start:test": "node dist/pi/main-test.js",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui"
},
"dependencies": {
- "@anthropic-ai/sdk": "^0.39.0",
- "@anthropic-ai/vertex-sdk": "^0.14.3",
- "@google/generative-ai": "^0.21.0",
"@mariozechner/pi-agent-core": "^0.52.12",
"@mariozechner/pi-ai": "^0.52.12",
"@sinclair/typebox": "^0.32.0",
"@xmpp/client": "^0.13.1",
- "@xmpp/debug": "^0.13.0",
- "google-auth-library": "^10.5.0",
- "js-yaml": "^4.1.0",
- "openai": "^4.77.0"
+ "@xmpp/debug": "^0.13.0"
},
"devDependencies": {
- "@types/js-yaml": "^4.0.9",
"@types/node": "^20.0.0",
"@vitest/ui": "^4.0.18",
"typescript": "^5.3.0",
README.md
@@ -1,117 +1,51 @@
-# Daneel
+# Daneel - XMPP Research Bot
-XMPP research bot named after R. Daneel Olivaw from Isaac Asimov's Robot/Foundation series.
+An AI-powered research assistant accessible via XMPP, built with the [Pi AI toolkit](https://github.com/nicholasgasior/pi-coding-agent).
## Features
-- Multi-provider LLM support (Anthropic, Google, OpenAI, Ollama, Groq, Mistral)
-- Model selection via message prefixes (`opus:`, `gemini:`, `llama:`, etc.)
-- Per-user session persistence (JSONL)
-- Tool calling for research, org-mode saving, and system status
-- NixOS service module
+- **Multi-model support**: Gemini, Claude, GPT, and more (20+ providers via Pi)
+- **Autonomous tool use**: The agent decides when to use tools
+- **Web search**: Search the web via SearXNG or DuckDuckGo
+- **Research**: Comprehensive topic research with source synthesis
+- **System status**: Check bot health and resource usage
+- **Model switching**: Use prefixes like `g:`, `opus:`, `gpt:` to switch models
+- **Slash commands**: `/clear`, `/status`, `/help`, `/stats`, `/model`
## Quick Start
```bash
-# Install dependencies
+# Set required environment variables
+export DANEEL_XMPP_JID="bot@xmpp.example.com"
+export DANEEL_XMPP_PASSWORD="..."
+export DANEEL_OWNER_JID="you@xmpp.example.com"
+export GEMINI_API_KEY="..." # or ANTHROPIC_API_KEY or OPENAI_API_KEY
+
+# Optional
+export SEARXNG_URL="https://search.example.com" # For web search
+export DANEEL_DEBUG="true"
+
+# Run
npm install
-
-# Build
npm run build
-
-# Run (with environment variables)
-DANEEL_XMPP_JID="bot@xmpp.example.com" \
-DANEEL_XMPP_PASSWORD="password" \
-DANEEL_OWNER_JID="owner@xmpp.example.com" \
-ANTHROPIC_API_KEY="sk-..." \
npm start
```
-## Environment Variables
-
-### Required
-
-| Variable | Description |
-|----------|-------------|
-| `DANEEL_XMPP_JID` | Bot's XMPP JID |
-| `DANEEL_XMPP_PASSWORD` | Bot's XMPP password |
-| `DANEEL_OWNER_JID` | Owner's JID (only this user can interact) |
-
-### Optional
-
-| Variable | Description | Default |
-|----------|-------------|---------|
-| `DANEEL_DATA_DIR` | Session storage directory | `./data` |
-| `DANEEL_INBOX_PATH` | Org-mode inbox path | `~/org/inbox.org` |
-| `DANEEL_DEFAULT_MODEL` | Default model alias | `sonnet` |
-| `DANEEL_DEBUG` | Enable debug logging | `false` |
-
-### Provider API Keys
-
-| Variable | Provider |
-|----------|----------|
-| `ANTHROPIC_API_KEY` | Anthropic (Claude) |
-| `GOOGLE_API_KEY` | Google (Gemini) |
-| `OPENAI_API_KEY` | OpenAI (GPT-4) |
-| `GITHUB_TOKEN` | GitHub Copilot |
-| `OLLAMA_BASE_URL` | Ollama (local) |
-| `GROQ_API_KEY` | Groq |
-| `MISTRAL_API_KEY` | Mistral |
-| `OPENROUTER_API_KEY` | OpenRouter |
-
-## Usage
-
-### Commands
-
-- `/help` - Show help message
-- `/ping` - Check if bot is alive
-- `/status` - Show system status
-- `/clear` - Clear conversation history
-- `/models` - List available models
-- `/stats` - Show session statistics
-
-### Model Prefixes
-
-Prefix your message to use a specific model:
-
-```
-opus: What is quantum entanglement?
-gemini: Summarize the latest AI news
-llama: Explain this concept simply
-```
-
-#### Available Prefixes
-
-| Prefix | Provider | Model |
-|--------|----------|-------|
-| `opus:`, `o:` | Anthropic | Claude Opus 4.5 |
-| `sonnet:`, `s:` | Anthropic | Claude Sonnet 4.5 |
-| `haiku:`, `h:` | Anthropic | Claude Haiku |
-| `gemini:`, `g:` | Google | Gemini 2.0 Flash |
-| `gemini-pro:`, `gp:` | Google | Gemini 2.5 Pro |
-| `gpt:`, `gpt4:` | OpenAI | GPT-4o |
-| `o1:` | OpenAI | O1 |
-| `o3:` | OpenAI | O3 Mini |
-| `copilot:`, `cp:` | GitHub | GPT-4o via Copilot |
-| `copilot-claude:` | GitHub | Claude via Copilot |
-| `ollama:`, `llama:` | Ollama | Llama 3.2 |
-| `qwen:` | Ollama | Qwen 2.5 |
-| `deepseek:` | Ollama | DeepSeek R1 |
-| `groq:` | Groq | Llama 3.3 70B |
-| `mistral:` | Mistral | Mistral Large |
-
-## NixOS Integration
+## NixOS Module
```nix
+# In your NixOS configuration
{
imports = [ ./path/to/daneel/nix/module.nix ];
services.daneel = {
enable = true;
- xmppJid = "daneel@xmpp.example.com";
- xmppPasswordFile = config.age.secrets.daneel-xmpp.path;
- ownerJid = "owner@xmpp.example.com";
- anthropicApiKeyFile = config.age.secrets.anthropic-api.path;
+ xmppJid = "bot@xmpp.example.com";
+ xmppPasswordFile = config.age.secrets.xmpp-bot-password.path;
+ ownerJid = "you@xmpp.example.com";
+ geminiApiKeyFile = config.age.secrets.gemini-api-key.path;
+ searxngUrl = "https://search.example.com";
+ debug = true;
};
}
```
@@ -119,17 +53,47 @@ llama: Explain this concept simply
## Architecture
```
-┌─────────────────────────────────────────────────────────┐
-│ Daneel │
-├─────────────────────────────────────────────────────────┤
-│ @xmpp/client → SessionManager → AgentRunner │
-│ │ (per-JID) (LLM provider) │
-│ ↓ ↓ ↓ │
-│ Message Router → context.jsonl → Tool Execution │
-│ │
-│ Tools: research, save_to_org, status, web_search │
-│ LLMs: Claude, Gemini, GPT-4, Llama, etc. │
-└─────────────────────────────────────────────────────────┘
+src/
+├── main-pi.ts # Entry point
+├── xmpp/
+│ ├── client.ts # XMPP client (connection, auth, messaging)
+│ └── types.ts # XMPP message types
+└── pi/
+ ├── agent-wrapper.ts # Pi Agent ↔ XMPP bridge
+ ├── config.ts # Model configuration and prefix mapping
+ └── tools/
+ ├── status.ts # System status (uptime, memory, load)
+ ├── websearch.ts # Web search (SearXNG → ddgr → DuckDuckGo API)
+ ├── research.ts # Research synthesis (search + LLM)
+ └── logging.ts # Tool execution logging wrapper
+```
+
+## Tools
+
+| Tool | Description |
+|------|-------------|
+| `status` | System uptime, memory usage, CPU load |
+| `web_search` | Search the web (SearXNG preferred, ddgr fallback) |
+| `research` | Research a topic: web search + LLM synthesis |
+
+## Model Prefixes
+
+| Prefix | Model |
+|--------|-------|
+| `g:` | Gemini 2.0 Flash |
+| `gp:` | Gemini 2.5 Pro |
+| `sonnet:` | Claude Sonnet 4 |
+| `opus:` | Claude Opus 4 |
+| `gpt:` | GPT-4o |
+
+Example: `g: What is TypeScript?` uses Gemini Flash.
+
+## Development
+
+```bash
+npm test # Run tests
+npm run build # Build TypeScript
+npm start # Start the bot
```
## License
TESTING-PI.md
@@ -1,228 +0,0 @@
-# Testing Daneel (Pi Edition)
-
-This guide shows how to test the new Pi-based Daneel implementation.
-
-## Current Status
-
-**Implemented:**
-- ✅ Pi agent wrapper (Phase 3)
-- ✅ Status tool (Phase 4, partial)
-- ✅ Model prefix switching (opus:, g:, etc.)
-- ✅ Slash commands (/ping, /help, /clear, /models, /stats)
-- ✅ XMPP connectivity (using old XmppClient)
-
-**Not Yet Implemented:**
-- ⏭️ Tools integration with agent (status tool exists but not wired to agent yet)
-- ⏭️ Org-mode tool
-- ⏭️ Research tool
-- ⏭️ Web search tool
-- ⏭️ Session persistence
-
-## Prerequisites
-
-1. **XMPP password** from aomi:
- ```bash
- ssh aomi.home 'sudo cat /run/agenix/xmpp-research-bot-password'
- ```
-
-2. **Google API key** (already in environment):
- ```bash
- echo $GEMINI_API_KEY # Should show your key
- ```
-
-## Quick Start
-
-```bash
-# 1. Set the XMPP password
-export DANEEL_XMPP_PASSWORD='<paste-password-from-aomi>'
-
-# 2. Run the test script
-./test-pi-bot.sh
-```
-
-You should see:
-```
-Daneel - XMPP Research Bot (Pi Edition)
-=========================================
-
-Using default model: Google Gemini 2.0 Flash
-Connecting to XMPP server...
-Bot JID: researchbot@xmpp.sbr.pm
-Owner JID: vincent@xmpp.sbr.pm
-
-Waiting for messages...
-```
-
-## Testing from XMPP Client
-
-Open your XMPP client (connected as `vincent@xmpp.sbr.pm`) and message `researchbot@xmpp.sbr.pm`:
-
-### Basic Commands
-
-```
-/ping
-→ pong!
-
-/help
-→ Shows available commands and model prefixes
-
-/models
-→ Lists all available models
-
-/stats
-→ Shows session statistics
-```
-
-### Conversation
-
-```
-Hello!
-→ Gemini responds
-
-What's 2+2?
-→ Gemini answers
-
-/clear
-→ Conversation cleared
-```
-
-### Model Switching
-
-```
-g: Hello from Gemini
-→ Uses Gemini 2.0 Flash (explicit)
-
-opus: Explain quantum computing
-→ Switches to Claude Opus 4 (if ANTHROPIC_API_KEY set)
-
-sonnet: Write a haiku
-→ Switches to Claude Sonnet 4
-
-Regular message
-→ Uses last selected model
-```
-
-### Status Tool (Not Yet Integrated)
-
-The status tool exists and is tested, but it's not yet wired into the agent. After Phase 4 is complete, you'll be able to:
-
-```
-Show me system status
-→ Agent will use status tool and return uptime/memory/load
-```
-
-## What Works vs What Doesn't
-
-### ✅ Working
-
-- XMPP connection and messaging
-- Model selection (via config, not API key detection yet)
-- Model prefix parsing and switching
-- Slash commands
-- Conversation context maintenance
-- Per-JID session isolation
-
-### ⏳ Partially Working
-
-- Status tool (implemented but not integrated with agent)
-
-### ❌ Not Working Yet
-
-- Automatic tool calling (agent has empty tools array)
-- Org-mode saving
-- Research queries
-- Web search
-- Session persistence to disk
-
-## Troubleshooting
-
-### "DANEEL_XMPP_PASSWORD is not set"
-
-Get it from aomi:
-```bash
-ssh aomi.home 'sudo cat /run/agenix/xmpp-research-bot-password'
-export DANEEL_XMPP_PASSWORD='...'
-```
-
-### "No Google API key found"
-
-Check environment:
-```bash
-echo $GEMINI_API_KEY
-# or
-export GOOGLE_API_KEY='...'
-```
-
-### Connection timeout
-
-Make sure XMPP server is running:
-```bash
-ping xmpp.sbr.pm
-# Should be on aion
-```
-
-### Model switching doesn't work
-
-Currently need API keys for each provider:
-- `ANTHROPIC_API_KEY` for Claude models
-- `GOOGLE_API_KEY` or `GEMINI_API_KEY` for Gemini
-- `OPENAI_API_KEY` for GPT models
-
-Without the API key, switching to that provider will fail.
-
-## Comparing Old vs New Implementation
-
-### Old (Custom)
-```bash
-./test-run.sh
-# Uses custom LLM providers
-# 8 providers
-# Custom session management
-```
-
-### New (Pi-based)
-```bash
-./test-pi-bot.sh
-# Uses Pi libraries
-# 20+ providers available
-# Pi's Agent and session management
-```
-
-Both should behave similarly for basic conversation, but the Pi version has more providers available.
-
-## Next Steps
-
-After Phase 4 is complete:
-1. Tools will be integrated with agent
-2. Can test tool calling
-3. Can test org-mode saving
-4. Can test research queries
-
-## Manual Testing Checklist
-
-- [ ] Bot connects to XMPP
-- [ ] `/ping` responds with "pong!"
-- [ ] `/help` shows commands
-- [ ] Basic conversation works
-- [ ] `g: message` uses Gemini
-- [ ] `/clear` clears conversation
-- [ ] `/stats` shows message counts
-- [ ] `/models` lists available models
-- [ ] Bot responds only to owner JID
-- [ ] Per-JID sessions are isolated
-
-## Development Workflow
-
-```bash
-# Watch mode (auto-rebuild on file changes)
-npm run dev
-
-# In another terminal, run the bot
-./test-pi-bot.sh
-
-# Run tests
-npm test
-
-# Run specific test file
-npx vitest src/pi/agent-wrapper.test.ts
-```