Commit de6e32e39033

Vincent Demeester <vincent@sbr.pm>
2026-02-16 15:28:46
Phase 8+10: Cleanup old code, update NixOS module
**Removed old implementation (17 files):** - src/agent/ (prompts, runner, session, types) - src/llm/ (anthropic, google, ollama, openai, index, types) - src/tools/ (index, orgmode, research, status, types, websearch) - src/config.ts, src/main.ts - src/pi/main-test.ts - Old docs (TESTING.md, TESTING-PI.md, QUICKSTART.md) - Old scripts (test-config.sh, test-run.sh, explore-pi.ts, test-pi-config.ts) **Removed old dependencies:** - @anthropic-ai/sdk, @anthropic-ai/vertex-sdk - @google/generative-ai, google-auth-library - openai, js-yaml, @types/js-yaml → Removed 37 npm packages **Simplified XMPP client:** - XmppClient now takes simple {jid, password, ownerJid} config - No dependency on old Config type **Updated NixOS module:** - geminiApiKeyFile (was googleApiKeyFile) - matches Pi's GEMINI_API_KEY - Added searxngUrl option - Added assertion: at least one API key required - Removed ollamaBaseUrl, defaultModel (Pi handles model selection) - Removed MemoryDenyWriteExecute (Node.js needs JIT) - ddgr included in PATH via makeWrapper **Updated package.json:** - Version 0.2.0 - main: dist/main-pi.js - Removed start:old, start:test scripts - Only Pi dependencies remain **Updated README.md:** - Clean architecture docs - NixOS module example - Tool and prefix reference 47 tests still passing, clean build.
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
-```