Compare commits
2 Commits
208531d28d
...
0510df067d
| Author | SHA1 | Date | |
|---|---|---|---|
| 0510df067d | |||
| da1cf8bb60 |
@@ -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
|
||||
}
|
||||
@@ -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** |
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user