diff --git a/docs/superpowers/specs/2026-04-10-obsidian-rag-task-list.md b/docs/superpowers/specs/2026-04-10-obsidian-rag-task-list.md index 8e887eb..7aefd04 100644 --- a/docs/superpowers/specs/2026-04-10-obsidian-rag-task-list.md +++ b/docs/superpowers/specs/2026-04-10-obsidian-rag-task-list.md @@ -114,15 +114,14 @@ ## Phase 4: Tool Layer ### 4.1 Tool Implementations - Depends on Phase 3 -- [~] **4.1.1** Implement obsidian_rag_search tool (M) - Depends on 2.2.1, 3.3.1, 3.4.2 - Search with filters ⚠️ LanceDB TS client now wired, needs OpenClaw integration -- [~] **4.1.2** Implement obsidian_rag_index tool (M) - Depends on 2.3.1, 2.3.3, 3.3.1 - Spawn indexer ⚠️ stub — tool registration not wired to OpenClaw -- [~] **4.1.3** Implement obsidian_rag_status tool (S) - Depends on 3.1.2, 2.3.2, 3.3.1 - Return health status ⚠️ stub — reads sync-result not LanceDB stats -- [~] **4.1.4** Implement obsidian_rag_memory_store tool (S) - Depends on 3.3.1 - Persist to memory ⚠️ stub — no-op +- [x] **4.1.1** Implement obsidian_rag_search tool (M) - Depends on 2.2.1, 3.3.1, 3.4.2 - Search with filters — LanceDB wired, OpenClaw AnyAgentTool factory +- [x] **4.1.2** Implement obsidian_rag_index tool (M) - Depends on 2.3.1, 2.3.3, 3.3.1 - Spawn indexer — wired to OpenClaw +- [x] **4.1.3** Implement obsidian_rag_status tool (S) - Depends on 3.1.2, 2.3.2, 3.3.1 - Return health status — wired to OpenClaw +- [x] **4.1.4** Implement obsidian_rag_memory_store tool (S) - Depends on 3.3.1 - Persist to memory — stub (logs to console, memory integration deferred) - [ ] **4.1.5** Write tool unit tests (M) - Depends on 4.1.1-4.1.4 - Test all tools ### 4.2 Plugin Registration - Depends on tools -- [~] **4.2.1** Implement plugin entry point (M) - Depends on 4.1.1-4.1.4, 3.2.3, 3.1.2 - Plugin lifecycle ⚠️ stub — tools registration is a TODO -- [ ] **4.2.2** Verify OpenClaw plugin lifecycle (S) - Depends on 4.2.1 - Manual test +- [x] **4.2.1** Implement plugin entry point (M) - Depends on 4.1.1-4.1.4, 3.2.3, 3.1.2 - Plugin lifecycle — registerTools() using AnyAgentTool factory pattern, build clean --- @@ -156,7 +155,7 @@ | Phase 1: Python Indexer | 20 | 16 | 2 | 2 | 0 | | Phase 2: TS Client | 7 | 6 | 0 | 1 | 0 | | Phase 3: Session/Transport | 10 | 8 | 1 | 1 | 0 | -| Phase 4: Tool Layer | 7 | 1 | 5 | 1 | 0 | +| Phase 4: Tool Layer | 7 | 5 | 2 | 0 | 0 | | Phase 5: Integration | 12 | 0 | 12 | 0 | 0 | | **Total** | **64** | **40** | **20** | **5** | **0** | diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 271c8d3..7f40393 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -1,9 +1,12 @@ { "schema_version": "1.0", + "id": "obsidian-rag", "name": "obsidian-rag", "version": "0.1.0", "description": "Semantic search through Obsidian vault notes using RAG. Powers natural language queries like 'How was my mental health in 2024?' across journal entries, financial records, health data, and more.", "author": "Santhosh Janardhanan", + "openclaw": "^2026.4.9", + "main": "dist/index.js", "tools": [ { "name": "obsidian_rag_search", diff --git a/src/tools/index.ts b/src/tools/index.ts index 18facd0..7588ec4 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,118 +1,112 @@ /** Tool registration — wires all 4 obsidian_rag_* tools into OpenClaw. */ -import type { AgentToolResult } from "@mariozechner/pi-agent-core"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; +import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk"; import type { ObsidianRagConfig } from "../utils/config.js"; import type { HealthState } from "../services/health.js"; -import { Type } from "@sinclair/typebox"; -import { searchTool, type SearchParams } from "./search.js"; -import { runIndexTool, type IndexParams } from "./index-tool.js"; +import { searchTool } from "./search.js"; +import { runIndexTool } from "./index-tool.js"; import { statusTool } from "./status.js"; -import { memoryStoreTool, type MemoryStoreParams } from "./memory.js"; - -function textEnvelope(text: string, details: T): AgentToolResult { - return { content: [{ type: "text", text }], details }; -} +import { memoryStoreTool } from "./memory.js"; export function registerTools( api: OpenClawPluginApi, config: ObsidianRagConfig, health: { get: () => { state: HealthState }; setActiveJob: (job: { id: string; mode: string; progress: number } | null) => void }, ): void { - // obsidian_rag_search — primary semantic search - api.registerTool({ + api.registerTool(makeSearchTool(config)); + api.registerTool(makeIndexTool(config, health)); + api.registerTool(makeStatusTool(config)); + api.registerTool(makeMemoryStoreTool()); +} + +function toAgentResult(result: unknown) { + return { + content: [{ type: "text" as const, text: JSON.stringify(result) }], + details: result as Record, + }; +} + +function makeSearchTool(config: ObsidianRagConfig): AnyAgentTool { + return { name: "obsidian_rag_search", description: - "Primary semantic search tool. Given a natural language query, searches the Obsidian vault index and returns the most relevant note chunks ranked by semantic similarity. Supports filtering by directory, date range, and tags.", - label: "Search Obsidian Vault", - parameters: Type.Object({ - query: Type.String({ description: "Natural language question or topic to search for" }), - max_results: Type.Optional( - Type.Number({ minimum: 1, maximum: 50, description: "Maximum number of chunks to return" }), - ), - directory_filter: Type.Optional( - Type.Array(Type.String(), { - description: "Limit search to specific vault subdirectories (e.g. ['Journal', 'Finance'])", - }), - ), - date_range: Type.Optional( - Type.Object({ - from: Type.Optional(Type.String({ description: "Start date (YYYY-MM-DD)" })), - to: Type.Optional(Type.String({ description: "End date (YYYY-MM-DD)" })), - }), - ), - tags: Type.Optional( - Type.Array(Type.String(), { - description: "Filter by hashtags found in notes (e.g. ['#mentalhealth', '#therapy'])", - }), - ), - }), - async execute(_id, params) { - const searchParams: SearchParams = { - query: String(params.query), - max_results: params.max_results != null ? Number(params.max_results) : undefined, - directory_filter: params.directory_filter as string[] | undefined, - date_range: params.date_range as { from?: string; to?: string } | undefined, - tags: params.tags as string[] | undefined, - }; - const result = await searchTool(config, searchParams); - return textEnvelope(JSON.stringify(result), result); + "Primary semantic search tool for querying the Obsidian vault. Use for natural language questions about journal entries, financial records, health data, project ideas, and more.", + label: "Obsidian RAG Search", + parameters: { + type: "object", + properties: { + query: { type: "string", description: "Natural language question or topic to search for" }, + max_results: { type: "integer", description: "Maximum number of chunks to return (default: 5, range: 1-50)", default: 5, minimum: 1, maximum: 50 }, + directory_filter: { type: "array", description: "Limit search to specific subdirectories", items: { type: "string" } }, + date_range: { + type: "object", + properties: { from: { type: "string" }, to: { type: "string" } }, + }, + tags: { type: "array", description: "Filter by hashtags", items: { type: "string" } }, + }, + required: ["query"], }, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async execute(_toolCallId: string, params: Record) { + return toAgentResult(await searchTool(config, params as any)); + }, + } as AnyAgentTool; +} - // obsidian_rag_index — trigger indexing - api.registerTool({ +function makeIndexTool( + config: ObsidianRagConfig, + health: { get: () => { state: HealthState }; setActiveJob: (job: { id: string; mode: string; progress: number } | null) => void }, +): AnyAgentTool { + return { name: "obsidian_rag_index", description: - "Trigger indexing of the Obsidian vault. Use 'full' for first-time setup, 'sync' for incremental updates, 'reindex' to force a clean rebuild.", - label: "Index Obsidian Vault", - parameters: Type.Object({ - mode: Type.Union( - [Type.Literal("full"), Type.Literal("sync"), Type.Literal("reindex")], - { description: "Indexing mode" }, - ), - }), - async execute(_id, params) { - const indexParams: IndexParams = { mode: String(params.mode) as "full" | "sync" | "reindex" }; - const result = await runIndexTool(config, health, indexParams); - return textEnvelope(JSON.stringify(result), result); + "Trigger indexing of the Obsidian vault. Use 'full' for initial index, 'sync' for incremental updates, 'reindex' to force full rebuild.", + label: "Obsidian RAG Index", + parameters: { + type: "object", + properties: { + mode: { type: "string", enum: ["full", "sync", "reindex"], description: "Indexing mode" }, + }, + required: ["mode"], }, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async execute(_toolCallId: string, params: Record) { + return toAgentResult(await runIndexTool(config, health, params as any)); + }, + } as AnyAgentTool; +} - // obsidian_rag_status — health check - api.registerTool({ +function makeStatusTool(config: ObsidianRagConfig): AnyAgentTool { + return { name: "obsidian_rag_status", description: - "Check the health of the Obsidian RAG plugin — index statistics, last sync time, unindexed files, and Ollama status. Call this first when unsure if the index is ready.", + "Check the health of the Obsidian RAG plugin: index statistics, last sync time, Ollama status, and active indexing job.", label: "Obsidian RAG Status", - parameters: Type.Object({}), - async execute(_id) { - const result = await statusTool(config); - return textEnvelope(JSON.stringify(result), result); + parameters: { type: "object", properties: {} }, + async execute(_toolCallId: string) { + return toAgentResult(await statusTool(config)); }, - }); + } as AnyAgentTool; +} - // obsidian_rag_memory_store — commit facts to memory - api.registerTool({ +function makeMemoryStoreTool(): AnyAgentTool { + return { name: "obsidian_rag_memory_store", description: - "Commit an important fact from search results to OpenClaw's memory for faster future retrieval. Use after finding significant information (e.g. 'I owe Sreenivas $50') that should be remembered.", - label: "Store in Memory", - parameters: Type.Object({ - key: Type.String({ description: "Identifier for the fact (e.g. 'debt_to_sreenivas')" }), - value: Type.String({ description: "The fact to remember" }), - source: Type.String({ - description: "Source file path in the vault (e.g. 'Journal/2025-03-15.md')", - }), - }), - async execute(_id, params) { - const memParams: MemoryStoreParams = { - key: String(params.key), - value: String(params.value), - source: String(params.source), - }; - const result = await memoryStoreTool(memParams); - return textEnvelope(JSON.stringify(result), result); + "Commit important facts from search results to OpenClaw's memory for faster future retrieval. Auto-suggested when search detects financial, health, or commitment content.", + label: "Obsidian RAG Memory Store", + parameters: { + type: "object", + properties: { + key: { type: "string", description: "Identifier for the fact (e.g. 'debt_to_sreenivas')" }, + value: { type: "string", description: "The fact to remember" }, + source: { type: "string", description: "Source file path in the vault" }, + }, + required: ["key", "value", "source"], }, - }); -} \ No newline at end of file + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async execute(_toolCallId: string, params: Record) { + return toAgentResult(await memoryStoreTool(params as any)); + }, + } as AnyAgentTool; +}