Compare commits

..

2 Commits

Author SHA1 Message Date
0510df067d fix: update claude settings 2026-04-11 14:14:13 -04:00
da1cf8bb60 feat: wire all 4 obsidian_rag tools to OpenClaw via AnyAgentTool factory
- Replace TypeBox + AgentToolResult with native OpenClaw AnyAgentTool pattern
- Add id, openclaw, main fields to openclaw.plugin.json manifest
- registerTools() now uses factory helpers returning typed AnyAgentTool objects
- toAgentResult() adapter bridges search/index/status/memory results to AgentToolResult shape
- Build clean — pi-agent-core peer dep not needed, openclaw exports all types
- Task list updated: Phase 4 tools + plugin registration marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 14:12:22 -04:00
4 changed files with 94 additions and 151 deletions

View File

@@ -1,53 +0,0 @@
{
"permissions": {
"allow": [
"Bash(bash:*)",
"Bash(cat /c/dev/obsidian-claw/.superpowers/brainstorm/*/state/server-info)",
"Bash(ls -d /c/dev/obsidian-claw/KnowledgeVault/Default/*/)",
"Bash(git init:*)",
"Bash(git add:*)",
"Bash(git commit -m ':*)",
"WebFetch(domain:www.ollama.com)",
"mcp__web-reader__webReader",
"Bash(ollama list:*)",
"Bash(python3:*)",
"Bash(pip install:*)",
"Bash(npm install:*)",
"Bash(obsidian-rag --help)",
"Bash(obsidian-rag status:*)",
"Bash(npm run:*)",
"Bash(obsidian-rag index:*)",
"Bash(curl -s http://localhost:11434/api/tags)",
"Bash(curl -s -X POST http://localhost:11434/api/embeddings -d '{\"model\":\"mxbai-embed-large\",\"prompt\":\"hello world\"}')",
"Bash(curl -s -X POST http://localhost:11434/api/embeddings -d '{\"model\":\"mxbai-embed-large:335m\",\"prompt\":\"hello world\"}')",
"Bash(curl:*)",
"Bash(find /Users/santhoshj/dev/obsidian-rag/python -name \"*.pyc\" -delete)",
"Bash(find /Users/santhoshj/dev/obsidian-rag/python -name \"__pycache__\" -exec rm -rf {} +)",
"Bash(npm test:*)",
"Bash(python -m pytest --collect-only)",
"Bash(python -m pytest tests/unit/test_chunker.py tests/unit/test_security.py -v)",
"Bash(python -m pytest tests/unit/test_chunker.py -v --tb=short)",
"mcp__plugin_ecc_context7__resolve-library-id",
"mcp__plugin_ecc_context7__query-docs",
"Bash(python -m pytest tests/unit/test_vector_store.py -v)",
"Bash(python -m pytest tests/unit/test_vector_store.py::test_search_chunks_with_tags_filter -v)",
"Bash(python:*)",
"Bash(npx tsx:*)",
"Bash(node test_lancedb_client.mjs)",
"Bash(node -e ':*)",
"Bash(node:*)",
"Bash(ls /Users/santhoshj/dev/obsidian-rag/*.config.*)",
"Bash(npx vitest:*)",
"Bash(git commit:*)",
"mcp__plugin_ecc_memory__add_observations",
"WebSearch",
"WebFetch(domain:docs.openclaw.ai)",
"Bash(ls node_modules/openclaw/dist/plugin-sdk/zod*)",
"Bash(ls:*)",
"Bash(npx ts-node:*)",
"Bash(pkill -f \"ollama serve\")"
]
},
"outputStyle": "default",
"spinnerTipsEnabled": false
}

View File

@@ -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** |

View File

@@ -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",

View File

@@ -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<T>(text: string, details: T): AgentToolResult<T> {
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<string, unknown>,
};
}
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<string, unknown>) {
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<string, unknown>) {
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"],
},
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async execute(_toolCallId: string, params: Record<string, unknown>) {
return toAgentResult(await memoryStoreTool(params as any));
},
} as AnyAgentTool;
}