@hydra_db/openclaw 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ publish:
10
+ name: Publish package to npm
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Checkout code
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Set up Node.js
18
+ uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '20'
21
+ registry-url: 'https://registry.npmjs.org'
22
+
23
+ - name: Install dependencies
24
+ run: npm ci
25
+
26
+ - name: Build package
27
+ # If you have a build step, otherwise remove this
28
+ run: npm run build
29
+ continue-on-error: true
30
+
31
+ - name: Publish to npm
32
+ env:
33
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
34
+ run: |
35
+ # Only publish if this is not a pre-release and version has changed
36
+ if [ "$(npm view . version)" != "$(node -p "require('./package.json').version")" ]; then
37
+ npm publish --access public
38
+ else
39
+ echo "Version already published, skipping."
40
+ fi
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # Hydra DB — OpenClaw Plugin
2
+
3
+ State-of-the-art agentic memory for OpenClaw powered by [Hydra DB](https://hydradb.com). Automatically captures conversations, recalls relevant context with knowledge-graph connections, and injects them before every AI turn.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ openclaw plugins install @hydra_db/openclaw-hydra-db
9
+ ```
10
+
11
+ Restart OpenClaw after installing.
12
+
13
+ If you run OpenClaw via the local gateway, restart it too:
14
+
15
+ ```bash
16
+ openclaw gateway restart
17
+ ```
18
+
19
+ ## Get Your Credentials
20
+ 1. Get your Hydra API Key from [Hydra DB](https://app.hydradb.com)
21
+ 2. Get your Tenant ID from the Hydra dashboard
22
+
23
+ ## Interactive Onboarding
24
+
25
+ Run the interactive CLI wizard (recommended):
26
+
27
+ ```bash
28
+ # Basic onboarding (API key, tenant ID, sub-tenant, ignore term)
29
+ openclaw hydra onboard
30
+
31
+ # Advanced onboarding (all options including recall mode, graph context, etc.)
32
+ openclaw hydra onboard --advanced
33
+ ```
34
+
35
+ The wizard guides you through configuration with colored prompts and **writes your config to** `plugins.entries.openclaw-hydra-db.config` inside OpenClaw's settings file.
36
+
37
+ The path is resolved in the same order OpenClaw itself uses:
38
+
39
+ 1. `$OPENCLAW_CONFIG_PATH` — if set, used directly
40
+ 2. `$OPENCLAW_STATE_DIR/openclaw.json` — if `OPENCLAW_STATE_DIR` is set
41
+ 3. `$OPENCLAW_HOME/.openclaw/openclaw.json` — if `OPENCLAW_HOME` is set
42
+ 4. Default: `~/.openclaw/openclaw.json` (macOS/Linux) or `%USERPROFILE%\.openclaw\openclaw.json` (Windows)
43
+
44
+ No manual adjustment needed — the wizard auto-detects the correct path.
45
+
46
+ After onboarding, restart the gateway:
47
+
48
+ ```bash
49
+ openclaw gateway restart
50
+ ```
51
+
52
+ ## Manual Configuration
53
+
54
+ If you prefer, you can configure credentials manually.
55
+
56
+ Two required values:
57
+
58
+ - **API key**
59
+ - **Tenant ID**
60
+
61
+ Environment variables (recommended for secrets):
62
+
63
+ ```bash
64
+ export HYDRA_OPENCLAW_API_KEY="your-api-key"
65
+ export HYDRA_OPENCLAW_TENANT_ID="your-tenant-id"
66
+ ```
67
+
68
+ Or configure directly in OpenClaw's settings file:
69
+
70
+ - **macOS / Linux:** `~/.openclaw/openclaw.json`
71
+ - **Windows:** `%USERPROFILE%\.openclaw\openclaw.json`
72
+
73
+ ```json5
74
+ {
75
+ "plugins": {
76
+ "entries": {
77
+ "openclaw-hydra-db": {
78
+ "enabled": true,
79
+ "config": {
80
+ "apiKey": "${HYDRA_OPENCLAW_API_KEY}",
81
+ "tenantId": "${HYDRA_OPENCLAW_TENANT_ID}"
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ After changing config, restart the gateway so the plugin reloads:
90
+
91
+ ```bash
92
+ openclaw gateway restart
93
+ ```
94
+
95
+ ### Options
96
+
97
+ | Key | Type | Default | Description |
98
+ | -------------------- | ----------- | --------------------- | ------------------------------------------------------------------------------ |
99
+ | `subTenantId` | `string` | `"hydra-openclaw-plugin"` | Sub-tenant for data partitioning within your tenant |
100
+ | `autoRecall` | `boolean` | `true` | Inject relevant memories before every AI turn |
101
+ | `autoCapture` | `boolean` | `true` | Store conversation exchanges after every AI turn |
102
+ | `maxRecallResults` | `number` | `10` | Max memory chunks injected into context per turn |
103
+ | `recallMode` | `string` | `"fast"` | `"fast"` or `"thinking"` (deeper personalised recall with graph traversal) |
104
+ | `graphContext` | `boolean` | `true` | Include knowledge graph relations in recalled context |
105
+ | `ignoreTerm` | `string` | `"hydra-ignore"` | Messages containing this term are excluded from recall & capture |
106
+ | `debug` | `boolean` | `false` | Verbose debug logs |
107
+
108
+ ## How It Works
109
+
110
+ - **Auto-Recall** — Before every AI turn, queries Hydra (`/recall/recall_preferences`) for relevant memories and injects graph-enriched context (entity paths, chunk relations, extra context).
111
+ - **Auto-Capture** — After every AI turn, the last user/assistant exchange is sent to Hydra (`/memories/add_memory`) as conversation pairs with `infer: true` and `upsert: true`. The session ID is used as `source_id` so Hydra groups exchanges per session and builds a knowledge graph automatically.
112
+
113
+ ## Slash Commands
114
+
115
+ | Command | Description |
116
+ | --------------------------- | ------------------------------------- |
117
+ | `/hydra-onboard` | Show current configuration status |
118
+ | `/hydra-remember <text>` | Save something to Hydra memory |
119
+ | `/hydra-recall <query>` | Search memories with relevance scores |
120
+ | `/hydra-list` | List all stored user memories |
121
+ | `/hydra-delete <id>` | Delete a specific memory by its ID |
122
+ | `/hydra-get <source_id>` | Fetch the full content of a source |
123
+
124
+ ## AI Tools
125
+
126
+ | Tool | Description |
127
+ | ---------------------- | ----------- |
128
+ | `hydra_store` | Save the recent conversation history to Hydra as memory |
129
+ | `hydra_search` | Search Hydra memories (returns graph-enriched context) |
130
+ | `hydra_list_memories` | List all stored user memories (IDs + summaries) |
131
+ | `hydra_get_content` | Fetch full content for a specific `source_id` |
132
+ | `hydra_delete_memory` | Delete a memory by `memory_id` (use only when user explicitly asks) |
133
+
134
+ ## CLI
135
+
136
+ ```bash
137
+ openclaw hydra onboard # Interactive onboarding wizard
138
+ openclaw hydra onboard --advanced # Advanced onboarding wizard
139
+ openclaw hydra search <query> # Search memories
140
+ openclaw hydra list # List all user memories
141
+ openclaw hydra delete <id> # Delete a memory
142
+ openclaw hydra get <source_id> # Fetch source content
143
+ openclaw hydra status # Show plugin configuration
144
+ ```
145
+
146
+ ## Troubleshooting
147
+
148
+ ### `Not configured. Run openclaw hydra onboard`
149
+
150
+ This means the plugin is enabled, but credentials are missing.
151
+
152
+ Run:
153
+
154
+ ```bash
155
+ openclaw hydra onboard
156
+ openclaw gateway restart
157
+ ```
158
+
159
+ ### CLI says a command is unknown
160
+
161
+ Update/restart the gateway so it reloads the plugin:
162
+
163
+ ```bash
164
+ openclaw gateway restart
165
+ ```
166
+
167
+ ## Context Injection
168
+
169
+ Recalled context is injected inside `<hydra-context>` tags containing:
170
+
171
+ - **Entity Paths** — Knowledge graph paths connecting entities relevant to the query
172
+ - **Context Chunks** — Retrieved memory chunks with source titles, graph relations, and linked extra context
package/client.ts ADDED
@@ -0,0 +1,214 @@
1
+ import { log } from "./log.ts"
2
+ import type {
3
+ AddMemoryRequest,
4
+ AddMemoryResponse,
5
+ ConversationTurn,
6
+ DeleteMemoryResponse,
7
+ FetchContentRequest,
8
+ FetchContentResponse,
9
+ ListDataRequest,
10
+ ListMemoriesResponse,
11
+ ListSourcesResponse,
12
+ RecallRequest,
13
+ RecallResponse,
14
+ } from "./types/hydra.ts"
15
+
16
+ const API_BASE = "https://api.hydradb.com"
17
+
18
+ const INGEST_INSTRUCTIONS =
19
+ "Focus on extracting user preferences, habits, opinions, likes, dislikes, " +
20
+ "goals, and recurring themes. Capture any stated or implied personal context " +
21
+ "that would help personalise future interactions. Capture important personal details like " +
22
+ "name, age, email ids, phone numbers, etc. along with the original name and context " +
23
+ "so that it can be used to personalise future interactions."
24
+
25
+ export class HydraClient {
26
+ private apiKey: string
27
+ private tenantId: string
28
+ private subTenantId: string
29
+
30
+ constructor(apiKey: string, tenantId: string, subTenantId: string) {
31
+ this.apiKey = apiKey
32
+ this.tenantId = tenantId
33
+ this.subTenantId = subTenantId
34
+ log.info(`connected (tenant=${tenantId}, sub=${subTenantId})`)
35
+ }
36
+
37
+ private headers(): Record<string, string> {
38
+ return {
39
+ Authorization: `Bearer ${this.apiKey}`,
40
+ "Content-Type": "application/json",
41
+ }
42
+ }
43
+
44
+ private async post<T>(path: string, body: unknown): Promise<T> {
45
+ const url = `${API_BASE}${path}`
46
+ log.debug("POST", path, body)
47
+ const res = await fetch(url, {
48
+ method: "POST",
49
+ headers: this.headers(),
50
+ body: JSON.stringify(body),
51
+ })
52
+ if (!res.ok) {
53
+ const text = await res.text().catch(() => "")
54
+ throw new Error(`Hydra ${path} → ${res.status}: ${text}`)
55
+ }
56
+ return res.json() as Promise<T>
57
+ }
58
+
59
+ private async del<T>(path: string, params: Record<string, string>): Promise<T> {
60
+ const qs = new URLSearchParams(params).toString()
61
+ const url = `${API_BASE}${path}?${qs}`
62
+ log.debug("DELETE", path, params)
63
+ const res = await fetch(url, {
64
+ method: "DELETE",
65
+ headers: this.headers(),
66
+ })
67
+ if (!res.ok) {
68
+ const text = await res.text().catch(() => "")
69
+ throw new Error(`Hydra ${path} → ${res.status}: ${text}`)
70
+ }
71
+ return res.json() as Promise<T>
72
+ }
73
+
74
+ // --- Ingest ---
75
+
76
+ async ingestConversation(
77
+ turns: ConversationTurn[],
78
+ sourceId: string,
79
+ opts?: {
80
+ userName?: string
81
+ metadata?: Record<string, unknown>
82
+ },
83
+ ): Promise<AddMemoryResponse> {
84
+ const payload: AddMemoryRequest = {
85
+ memories: [
86
+ {
87
+ user_assistant_pairs: turns,
88
+ infer: true,
89
+ source_id: sourceId,
90
+ user_name: opts?.userName ?? "User",
91
+ custom_instructions: INGEST_INSTRUCTIONS,
92
+ ...(opts?.metadata && {
93
+ document_metadata: JSON.stringify(opts.metadata),
94
+ }),
95
+ },
96
+ ],
97
+ tenant_id: this.tenantId,
98
+ sub_tenant_id: this.subTenantId,
99
+ upsert: true,
100
+ }
101
+ return this.post<AddMemoryResponse>("/memories/add_memory", payload)
102
+ }
103
+
104
+ async ingestText(
105
+ text: string,
106
+ opts?: {
107
+ sourceId?: string
108
+ title?: string
109
+ infer?: boolean
110
+ isMarkdown?: boolean
111
+ customInstructions?: string
112
+ },
113
+ ): Promise<AddMemoryResponse> {
114
+ const shouldInfer = opts?.infer ?? true
115
+ const payload: AddMemoryRequest = {
116
+ memories: [
117
+ {
118
+ text,
119
+ infer: shouldInfer,
120
+ is_markdown: opts?.isMarkdown ?? false,
121
+ ...(shouldInfer && {
122
+ custom_instructions: opts?.customInstructions ?? INGEST_INSTRUCTIONS,
123
+ }),
124
+ ...(opts?.sourceId && { source_id: opts.sourceId }),
125
+ ...(opts?.title && { title: opts.title }),
126
+ },
127
+ ],
128
+ tenant_id: this.tenantId,
129
+ sub_tenant_id: this.subTenantId,
130
+ upsert: true,
131
+ }
132
+ return this.post<AddMemoryResponse>("/memories/add_memory", payload)
133
+ }
134
+
135
+ // --- Recall ---
136
+
137
+ async recall(
138
+ query: string,
139
+ opts?: {
140
+ maxResults?: number
141
+ mode?: "fast" | "thinking"
142
+ graphContext?: boolean
143
+ recencyBias?: number
144
+ },
145
+ ): Promise<RecallResponse> {
146
+ const payload: RecallRequest = {
147
+ tenant_id: this.tenantId,
148
+ sub_tenant_id: this.subTenantId,
149
+ query,
150
+ max_results: opts?.maxResults ?? 10,
151
+ mode: opts?.mode ?? "thinking",
152
+ alpha: 0.8,
153
+ recency_bias: opts?.recencyBias ?? 0,
154
+ graph_context: opts?.graphContext ?? true,
155
+ }
156
+ return this.post<RecallResponse>("/recall/recall_preferences", payload)
157
+ }
158
+
159
+ // --- List ---
160
+
161
+ async listMemories(): Promise<ListMemoriesResponse> {
162
+ const payload: ListDataRequest = {
163
+ tenant_id: this.tenantId,
164
+ sub_tenant_id: this.subTenantId,
165
+ kind: "memories",
166
+ }
167
+ return this.post<ListMemoriesResponse>("/list/data", payload)
168
+ }
169
+
170
+ async listSources(sourceIds?: string[]): Promise<ListSourcesResponse> {
171
+ const payload: ListDataRequest = {
172
+ tenant_id: this.tenantId,
173
+ sub_tenant_id: this.subTenantId,
174
+ kind: "memories",
175
+ ...(sourceIds && { source_ids: sourceIds }),
176
+ }
177
+ return this.post<ListSourcesResponse>("/list/data", payload)
178
+ }
179
+
180
+ // --- Delete ---
181
+
182
+ async deleteMemory(memoryId: string): Promise<DeleteMemoryResponse> {
183
+ return this.del<DeleteMemoryResponse>("/memories/delete_memory", {
184
+ tenant_id: this.tenantId,
185
+ memory_id: memoryId,
186
+ sub_tenant_id: this.subTenantId,
187
+ })
188
+ }
189
+
190
+ // --- Fetch Content ---
191
+
192
+ async fetchContent(
193
+ sourceId: string,
194
+ mode: "content" | "url" | "both" = "content",
195
+ ): Promise<FetchContentResponse> {
196
+ const payload: FetchContentRequest = {
197
+ tenant_id: this.tenantId,
198
+ sub_tenant_id: this.subTenantId,
199
+ source_id: sourceId,
200
+ mode,
201
+ }
202
+ return this.post<FetchContentResponse>("/fetch/content", payload)
203
+ }
204
+
205
+ // --- Accessors ---
206
+
207
+ getTenantId(): string {
208
+ return this.tenantId
209
+ }
210
+
211
+ getSubTenantId(): string {
212
+ return this.subTenantId
213
+ }
214
+ }
@@ -0,0 +1,97 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
+ import type { HydraClient } from "../client.ts"
3
+ import type { HydraPluginConfig } from "../config.ts"
4
+
5
+ export function registerCliCommands(
6
+ api: OpenClawPluginApi,
7
+ client: HydraClient,
8
+ cfg: HydraPluginConfig,
9
+ onboardingRegistrar?: (root: any) => void,
10
+ ): void {
11
+ api.registerCli(
12
+ ({ program }: { program: any }) => {
13
+ const root = program
14
+ .command("hydra")
15
+ .description("Hydra DB memory commands")
16
+
17
+ root
18
+ .command("search")
19
+ .argument("<query>", "Search query")
20
+ .option("--limit <n>", "Max results", "10")
21
+ .action(async (query: string, opts: { limit: string }) => {
22
+ const limit = Number.parseInt(opts.limit, 10) || 10
23
+ const res = await client.recall(query, {
24
+ maxResults: limit,
25
+ mode: cfg.recallMode,
26
+ graphContext: cfg.graphContext,
27
+ })
28
+
29
+ if (!res.chunks || res.chunks.length === 0) {
30
+ console.log("No memories found.")
31
+ return
32
+ }
33
+
34
+ for (const chunk of res.chunks) {
35
+ const score = chunk.relevancy_score != null
36
+ ? ` (${(chunk.relevancy_score * 100).toFixed(0)}%)`
37
+ : ""
38
+ const title = chunk.source_title ? `[${chunk.source_title}] ` : ""
39
+ console.log(`- ${title}${chunk.chunk_content.slice(0, 200)}${score}`)
40
+ }
41
+ })
42
+
43
+ root
44
+ .command("list")
45
+ .description("List all user memories")
46
+ .action(async () => {
47
+ const res = await client.listMemories()
48
+ const memories = res.user_memories ?? []
49
+ if (memories.length === 0) {
50
+ console.log("No memories stored.")
51
+ return
52
+ }
53
+ for (const m of memories) {
54
+ console.log(`[${m.memory_id}] ${m.memory_content.slice(0, 150)}`)
55
+ }
56
+ console.log(`\nTotal: ${memories.length}`)
57
+ })
58
+
59
+ root
60
+ .command("delete")
61
+ .argument("<memory_id>", "Memory ID to delete")
62
+ .action(async (memoryId: string) => {
63
+ const res = await client.deleteMemory(memoryId)
64
+ console.log(res.user_memory_deleted ? `Deleted: ${memoryId}` : `Not found: ${memoryId}`)
65
+ })
66
+
67
+ root
68
+ .command("get")
69
+ .argument("<source_id>", "Source ID to fetch")
70
+ .action(async (sourceId: string) => {
71
+ const res = await client.fetchContent(sourceId)
72
+ if (!res.success || res.error) {
73
+ console.error(`Error: ${res.error ?? "unknown"}`)
74
+ return
75
+ }
76
+ console.log(res.content ?? res.content_base64 ?? "(no text content)")
77
+ })
78
+
79
+ root
80
+ .command("status")
81
+ .description("Show plugin configuration")
82
+ .action(() => {
83
+ console.log(`Tenant: ${client.getTenantId()}`)
84
+ console.log(`Sub-Tenant: ${client.getSubTenantId()}`)
85
+ console.log(`Auto-Recall: ${cfg.autoRecall}`)
86
+ console.log(`Auto-Capture: ${cfg.autoCapture}`)
87
+ console.log(`Recall Mode: ${cfg.recallMode}`)
88
+ console.log(`Graph: ${cfg.graphContext}`)
89
+ console.log(`Max Results: ${cfg.maxRecallResults}`)
90
+ console.log(`Ignore Term: ${cfg.ignoreTerm}`)
91
+ })
92
+
93
+ if (onboardingRegistrar) onboardingRegistrar(root)
94
+ },
95
+ { commands: ["hydra"] },
96
+ )
97
+ }