@dn-inc/openclaw-seahorse 0.2.1 → 0.2.2

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.
package/README.md CHANGED
@@ -3,17 +3,23 @@
3
3
  Drop-in replacement for OpenClaw's built-in memory search, powered by [Seahorse](https://seahorse.dnotitia.ai).
4
4
 
5
5
  - Uploads local memory files (`MEMORY.md`, `memory/**/*.md`) to Seahorse Storage
6
- - Optionally syncs the entire workspace for full semantic search over all files
6
+ - Syncs the entire workspace for full semantic search (`syncWorkspace: true`)
7
7
  - Routes `memory_search` to Seahorse's vector search API
8
8
  - Keeps `memory_get` reading from local files (same as native)
9
9
 
10
+ ## Why Seahorse?
11
+
12
+ OpenClaw's built-in memory only indexes markdown notes the agent explicitly writes (`MEMORY.md` and `memory/**/*.md`). But agents produce far more than markdown — JSON data, YAML configs, PDF reports, images, spreadsheets, and other artifacts accumulate in the workspace over time, invisible to memory search.
13
+
14
+ With `syncWorkspace: true`, Seahorse indexes every supported file in the workspace automatically. The agent can semantically search across all of its own output — not just what it remembered to write down. Documents, data files, images, and markup (37 supported extensions) all become searchable through the same `memory_search` tool, with no API or workflow changes.
15
+
10
16
  ## Requirements
11
17
 
12
18
  - Node.js 20+
13
19
  - OpenClaw >= 2026.1.26
14
20
  - Seahorse account (Storage + Table tenants)
15
21
 
16
- ## Quick Start (Zero Config)
22
+ ## Quick Start
17
23
 
18
24
  1. Install the plugin:
19
25
 
@@ -35,9 +41,76 @@ openclaw plugins install @dn-inc/openclaw-seahorse
35
41
  }
36
42
  ```
37
43
 
38
- If you prefer not to store the key in config, you can set the `SEAHORSE_API_KEY` environment variable instead. The `apiKey` field also supports `${ENV_VAR}` syntax (e.g., `"${SEAHORSE_API_KEY}"`).
44
+ The `apiKey` field supports `${ENV_VAR}` syntax (e.g., `"${SEAHORSE_API_KEY}"`), or you can omit it entirely and set the `SEAHORSE_API_KEY` environment variable.
45
+
46
+ That's it. The plugin auto-syncs your files on startup and watches for changes in real time.
47
+
48
+ ## File Sync
49
+
50
+ The plugin runs a background service (`seahorse-sync`) that keeps Seahorse Storage in sync:
51
+
52
+ - **Initial sync**: scans files on startup, uploads only changed files
53
+ - **Live watch**: detects file create/modify/delete via `fs.watch` (1.5s debounce)
54
+ - **Dedup**: mtime-based state tracking prevents redundant uploads
55
+ - **Delete sync**: local file deletion triggers Seahorse Storage deletion
56
+
57
+ By default, only memory files (`MEMORY.md` + `memory/**/*.md`) are synced. Set `syncWorkspace: true` to sync the entire workspace — all supported files become searchable via `memory_search` without any tool or API changes.
58
+
59
+ Supported file extensions (workspace sync):
39
60
 
40
- That's it. The plugin auto-syncs your files on startup and watches for changes in real time. With zero additional config, search behaves identically to OpenClaw's built-in memory search.
61
+ | Category | Extensions |
62
+ |----------|-----------|
63
+ | Documents | `.doc`, `.docx`, `.hwp`, `.hwpx`, `.odg`, `.odp`, `.odt`, `.pdf`, `.ppt`, `.pptx`, `.rtf` |
64
+ | Data | `.csv`, `.json`, `.ods`, `.toml`, `.tsv`, `.xls`, `.xlsx`, `.yaml`, `.yml` |
65
+ | Images | `.bmp`, `.gif`, `.jpeg`, `.jpg`, `.png`, `.tif`, `.tiff`, `.webp` |
66
+ | Text / Markup | `.epub`, `.htm`, `.html`, `.markdown`, `.md`, `.text`, `.txt`, `.xhtml`, `.xml` |
67
+
68
+ When workspace sync is enabled, dotfiles/dotdirs (`.*`) are always excluded. Add further patterns via `syncBlacklist`:
69
+
70
+ ```json
71
+ {
72
+ "storageUrl": "...", "tableUrl": "...", "apiKey": "...",
73
+ "syncWorkspace": true,
74
+ "syncBlacklist": ["node_modules/", "dist/", "*.log", "*.tmp"]
75
+ }
76
+ ```
77
+
78
+ Supported blacklist patterns:
79
+
80
+ | Pattern | Example | Matches |
81
+ |---------|---------|---------|
82
+ | `"dir/"` | `"node_modules/"` | Directory with that exact name |
83
+ | `"*.ext"` | `"*.tmp"` | Any segment ending with `.tmp` |
84
+ | `"prefix*"` | `".*"` | Any segment starting with `.` (built-in) |
85
+ | `"name"` | `"Thumbs.db"` | Exact filename in any directory |
86
+
87
+ ## Storage Layout
88
+
89
+ All synced files are stored under the `openclaw/` prefix in Seahorse Storage (e.g., `MEMORY.md` → `openclaw/MEMORY.md`). This isolates plugin data from other data in the same Seahorse tenant. Search results are mapped back to workspace-relative paths automatically.
90
+
91
+ ## Tools
92
+
93
+ ### `memory_search`
94
+
95
+ Semantic vector search over synced files via Seahorse.
96
+
97
+ | Parameter | Type | Required | Description |
98
+ |-----------|------|----------|-------------|
99
+ | `query` | string | Yes | Search query |
100
+ | `maxResults` | number | No | Max results (default: `topK` config) |
101
+ | `minScore` | number | No | Min relevance score 0-1 (default: `minScore` config) |
102
+
103
+ ### `memory_get`
104
+
105
+ Read a local file by path.
106
+
107
+ | Parameter | Type | Required | Description |
108
+ |-----------|------|----------|-------------|
109
+ | `path` | string | Yes | Relative path (e.g., `MEMORY.md`, `memory/notes.md`) |
110
+ | `from` | number | No | Start line (1-based) |
111
+ | `lines` | number | No | Number of lines to read |
112
+
113
+ By default, allowed paths are `MEMORY.md` and files under `memory/` only. With `syncWorkspace: true`, any workspace file is accessible (except blacklisted paths).
41
114
 
42
115
  ## Config Reference
43
116
 
@@ -47,7 +120,7 @@ That's it. The plugin auto-syncs your files on startup and watches for changes i
47
120
  |-------|------|----------|-------------|
48
121
  | `storageUrl` | string | Yes | Seahorse Storage tenant URL |
49
122
  | `tableUrl` | string | Yes | Seahorse Table tenant URL |
50
- | `apiKey` | string | No | API key. Auto-reads from `SEAHORSE_API_KEY` env var if omitted. Also supports `${ENV_VAR}` syntax. |
123
+ | `apiKey` | string | No | API key. Auto-reads from `SEAHORSE_API_KEY` env var if omitted. Supports `${ENV_VAR}` syntax. |
51
124
 
52
125
  ### Layer 1: Search & Sync Behavior (optional)
53
126
 
@@ -125,7 +198,7 @@ Example — hybrid with equal dense/sparse weighting (Seahorse native behavior):
125
198
 
126
199
  ## Defaults Comparison: OpenClaw vs Seahorse
127
200
 
128
- The plugin ships with OpenClaw-compatible defaults so it works as a drop-in replacement. The table below shows where these differ from Seahorse server defaults, so you can tune toward Seahorse-native behavior when desired.
201
+ The plugin ships with OpenClaw-compatible defaults. The table below shows where these differ from Seahorse server defaults, so you can tune toward Seahorse-native behavior when desired.
129
202
 
130
203
  | Parameter | Plugin Default | OpenClaw Native | Seahorse Server | Notes |
131
204
  |-----------|---------------|-----------------|-----------------|-------|
@@ -142,61 +215,3 @@ The plugin ships with OpenClaw-compatible defaults so it works as a drop-in repl
142
215
  | Projection | `"id, text, metadata, distance, score"` | N/A | *(must specify)* | Requests both `distance` (dense) and `score` (hybrid) for proper normalization |
143
216
 
144
217
  > **Hybrid (RRF) Score Normalization**: In hybrid mode, Seahorse returns raw RRF scores (~0.01-0.03). The plugin normalizes these to a 0-1 scale by dividing by the theoretical maximum (`numRankers / (k + 1)`), making them compatible with the `minScore` threshold. With default k=60 and 2 rankers, a raw score of 0.016 becomes ~0.49 after normalization.
145
-
146
- ## Tools
147
-
148
- ### `memory_search`
149
-
150
- Semantic vector search over synced files via Seahorse.
151
-
152
- | Parameter | Type | Required | Description |
153
- |-----------|------|----------|-------------|
154
- | `query` | string | Yes | Search query |
155
- | `maxResults` | number | No | Max results (default: `topK` config) |
156
- | `minScore` | number | No | Min relevance score 0-1 (default: `minScore` config) |
157
-
158
- ### `memory_get`
159
-
160
- Read a local memory file by path.
161
-
162
- | Parameter | Type | Required | Description |
163
- |-----------|------|----------|-------------|
164
- | `path` | string | Yes | Relative path (e.g., `MEMORY.md`, `memory/notes.md`) |
165
- | `from` | number | No | Start line (1-based) |
166
- | `lines` | number | No | Number of lines to read |
167
-
168
- Allowed paths: `MEMORY.md` (workspace root) and files under `memory/` only.
169
-
170
- ## Storage Layout
171
-
172
- All memory files are stored under the `openclaw/` prefix in Seahorse Storage (e.g., `MEMORY.md` → `openclaw/MEMORY.md`). This isolates OpenClaw memory files from other data in the same tenant. Search results are mapped back to workspace-relative paths automatically.
173
-
174
- ## File Sync
175
-
176
- The plugin runs a background service (`seahorse-sync`) that keeps Seahorse Storage in sync:
177
-
178
- - **Initial sync**: scans files on startup, uploads only changed files
179
- - **Live watch**: detects file create/modify/delete via `fs.watch` (1.5s debounce)
180
- - **Dedup**: mtime-based state tracking prevents redundant uploads
181
- - **Delete sync**: local file deletion triggers Seahorse Storage deletion
182
-
183
- By default, only memory files (`MEMORY.md` + `memory/**/*.md`) are synced. Set `syncWorkspace: true` to sync the entire workspace — all files become searchable via `memory_search` without any tool or API changes.
184
-
185
- When workspace sync is enabled, dotfiles/dotdirs (`.*`) are always excluded. Add further patterns via `syncBlacklist`:
186
-
187
- ```json
188
- {
189
- "storageUrl": "...", "tableUrl": "...", "apiKey": "...",
190
- "syncWorkspace": true,
191
- "syncBlacklist": ["node_modules/", "dist/", "*.log", "*.tmp"]
192
- }
193
- ```
194
-
195
- Supported blacklist patterns:
196
-
197
- | Pattern | Example | Matches |
198
- |---------|---------|---------|
199
- | `"dir/"` | `"node_modules/"` | Directory with that exact name |
200
- | `"*.ext"` | `"*.tmp"` | Any segment ending with `.tmp` |
201
- | `"prefix*"` | `".*"` | Any segment starting with `.` (built-in) |
202
- | `"name"` | `"Thumbs.db"` | Exact filename in any directory |
package/index.ts CHANGED
@@ -94,7 +94,7 @@ const plugin: OpenClawPluginDefinition = {
94
94
  const client = new SeahorseClient(config, api.logger);
95
95
 
96
96
  registerMemorySearchTool(api, config, client);
97
- registerMemoryGetTool(api);
97
+ registerMemoryGetTool(api, config);
98
98
  registerSyncService(api, client, config);
99
99
 
100
100
  api.logger.info("[openclaw-seahorse] Plugin registered (memory_search, memory_get, sync service).");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dn-inc/openclaw-seahorse",
3
3
  "type": "module",
4
- "version": "0.2.1",
4
+ "version": "0.2.2",
5
5
  "description": "Seahorse memory plugin for OpenClaw",
6
6
  "license": "MIT",
7
7
  "main": "./index.ts",
package/src/helpers.ts CHANGED
@@ -187,17 +187,20 @@ async function collectAllFiles(
187
187
  }
188
188
  }
189
189
 
190
- /** Check whether a resolved absolute path is within the allowed memory directories. */
191
- export function isAllowedMemoryPath(resolvedPath: string, workspaceDir: string): boolean {
190
+ /** Check whether a resolved absolute path is within the workspace boundary. */
191
+ export function isWithinWorkspace(resolvedPath: string, workspaceDir: string): boolean {
192
192
  const normalizedWorkspace = path.resolve(workspaceDir);
193
193
  const normalizedTarget = path.resolve(resolvedPath);
194
+ return normalizedTarget.startsWith(normalizedWorkspace + path.sep) || normalizedTarget === normalizedWorkspace;
195
+ }
194
196
 
195
- // Must be within the workspace directory
196
- if (!normalizedTarget.startsWith(normalizedWorkspace + path.sep) && normalizedTarget !== normalizedWorkspace) {
197
+ /** Check whether a resolved absolute path is within the allowed memory directories. */
198
+ export function isAllowedMemoryPath(resolvedPath: string, workspaceDir: string): boolean {
199
+ if (!isWithinWorkspace(resolvedPath, workspaceDir)) {
197
200
  return false;
198
201
  }
199
202
 
200
- const relative = path.relative(normalizedWorkspace, normalizedTarget);
203
+ const relative = path.relative(path.resolve(workspaceDir), path.resolve(resolvedPath));
201
204
 
202
205
  // Allow MEMORY.md at root
203
206
  if (relative === "MEMORY.md") return true;
package/src/tools.ts CHANGED
@@ -5,7 +5,8 @@ import type { SeahorseConfig } from "./types.js";
5
5
  import type { OpenClawPluginApi, OpenClawPluginToolContext, AgentToolResult } from "openclaw/plugin-sdk";
6
6
  import { jsonResult } from "openclaw/plugin-sdk";
7
7
  import { SeahorseSearchError, type SeahorseClient } from "./client.js";
8
- import { sanitizeError, isAllowedMemoryPath, toPosixPath } from "./helpers.js";
8
+ import { sanitizeError, isAllowedMemoryPath, isWithinWorkspace, isBlacklisted, toPosixPath } from "./helpers.js";
9
+ import { DEFAULT_BLACKLIST } from "./types.js";
9
10
 
10
11
  type ToolResult = AgentToolResult;
11
12
 
@@ -82,8 +83,9 @@ export function registerMemorySearchTool(
82
83
  (_ctx: OpenClawPluginToolContext) => ({
83
84
  name: "memory_search",
84
85
  label: "Memory Search",
85
- description:
86
- "Search memories stored in MEMORY.md and memory/*.md files using semantic vector search powered by Seahorse.",
86
+ description: config.syncWorkspace
87
+ ? "Search across all workspace files using semantic vector search powered by Seahorse."
88
+ : "Search memories stored in MEMORY.md and memory/*.md files using semantic vector search powered by Seahorse.",
87
89
  parameters: Type.Object({
88
90
  query: Type.String({ description: "Search query" }),
89
91
  maxResults: Type.Optional(
@@ -171,14 +173,15 @@ export function registerMemorySearchTool(
171
173
  /**
172
174
  * Register the `memory_get` tool on the given plugin API.
173
175
  */
174
- export function registerMemoryGetTool(api: OpenClawPluginApi): void {
176
+ export function registerMemoryGetTool(api: OpenClawPluginApi, config: SeahorseConfig): void {
175
177
  api.registerTool(
176
178
  (ctx: OpenClawPluginToolContext) => {
177
179
  return {
178
180
  name: "memory_get",
179
181
  label: "Memory Get",
180
- description:
181
- "Read a memory file by path. Returns the file content with optional line range.",
182
+ description: config.syncWorkspace
183
+ ? "Read a workspace file by path. Returns the file content with optional line range."
184
+ : "Read a memory file by path. Returns the file content with optional line range.",
182
185
  parameters: Type.Object({
183
186
  path: Type.String({ description: "Relative path to memory file" }),
184
187
  from: Type.Optional(
@@ -212,13 +215,33 @@ export function registerMemoryGetTool(api: OpenClawPluginApi): void {
212
215
 
213
216
  const resolvedPath = path.resolve(workspaceDir, relativePath);
214
217
 
215
- if (!isAllowedMemoryPath(resolvedPath, workspaceDir)) {
216
- api.logger.warn(`[openclaw-seahorse] memory_get: access denied for "${relativePath}"`);
217
- return jsonResult({
218
- path: relativePath,
219
- text: "",
220
- error: `Access denied. Path "${relativePath}" is outside the allowed memory directories (MEMORY.md, memory/).`,
221
- });
218
+ if (config.syncWorkspace) {
219
+ if (!isWithinWorkspace(resolvedPath, workspaceDir)) {
220
+ api.logger.warn(`[openclaw-seahorse] memory_get: access denied for "${relativePath}"`);
221
+ return jsonResult({
222
+ path: relativePath,
223
+ text: "",
224
+ error: `Access denied. Path "${relativePath}" is outside the workspace.`,
225
+ });
226
+ }
227
+ const mergedBlacklist = [...DEFAULT_BLACKLIST, ...config.syncBlacklist];
228
+ if (isBlacklisted(toPosixPath(relativePath), mergedBlacklist)) {
229
+ api.logger.warn(`[openclaw-seahorse] memory_get: blacklisted path "${relativePath}"`);
230
+ return jsonResult({
231
+ path: relativePath,
232
+ text: "",
233
+ error: `Access denied. Path "${relativePath}" is blacklisted.`,
234
+ });
235
+ }
236
+ } else {
237
+ if (!isAllowedMemoryPath(resolvedPath, workspaceDir)) {
238
+ api.logger.warn(`[openclaw-seahorse] memory_get: access denied for "${relativePath}"`);
239
+ return jsonResult({
240
+ path: relativePath,
241
+ text: "",
242
+ error: `Access denied. Path "${relativePath}" is outside the allowed memory directories (MEMORY.md, memory/).`,
243
+ });
244
+ }
222
245
  }
223
246
 
224
247
  // Guard against symlink traversal: verify the real path stays within workspace