@dn-inc/openclaw-seahorse 0.2.0 → 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.0",
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
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
+ import { SUPPORTED_EXTENSIONS } from "./types.js";
3
4
 
4
5
  /** Resolve ${ENV_VAR} patterns in a string to process.env values. */
5
6
  export function resolveEnvVars(value: string): string {
@@ -103,6 +104,12 @@ export function parseUnitInterval(raw: unknown, name: string): number | undefine
103
104
  return n;
104
105
  }
105
106
 
107
+ /** Check whether a file path has a Seahorse-supported extension. */
108
+ export function isSupportedExtension(filePath: string): boolean {
109
+ const ext = path.extname(filePath).toLowerCase();
110
+ return SUPPORTED_EXTENSIONS.has(ext);
111
+ }
112
+
106
113
  /**
107
114
  * Check whether a POSIX relative path matches any blacklist pattern.
108
115
  * Each pattern is tested against every segment (directory or file name) of the path:
@@ -173,24 +180,27 @@ async function collectAllFiles(
173
180
  if (isBlacklisted(relPath + "/_", blacklist)) continue;
174
181
  await collectAllFiles(path.join(absDir, entry.name), relPath, blacklist, out);
175
182
  } else if (entry.isFile()) {
176
- if (!isBlacklisted(relPath, blacklist)) {
183
+ if (!isBlacklisted(relPath, blacklist) && isSupportedExtension(entry.name)) {
177
184
  out.push(relPath);
178
185
  }
179
186
  }
180
187
  }
181
188
  }
182
189
 
183
- /** Check whether a resolved absolute path is within the allowed memory directories. */
184
- 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 {
185
192
  const normalizedWorkspace = path.resolve(workspaceDir);
186
193
  const normalizedTarget = path.resolve(resolvedPath);
194
+ return normalizedTarget.startsWith(normalizedWorkspace + path.sep) || normalizedTarget === normalizedWorkspace;
195
+ }
187
196
 
188
- // Must be within the workspace directory
189
- 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)) {
190
200
  return false;
191
201
  }
192
202
 
193
- const relative = path.relative(normalizedWorkspace, normalizedTarget);
203
+ const relative = path.relative(path.resolve(workspaceDir), path.resolve(resolvedPath));
194
204
 
195
205
  // Allow MEMORY.md at root
196
206
  if (relative === "MEMORY.md") return true;
package/src/service.ts CHANGED
@@ -6,7 +6,7 @@ import type { SeahorseClient } from "./client.js";
6
6
  import type { SeahorseConfig } from "./types.js";
7
7
  import { DEFAULT_BLACKLIST } from "./types.js";
8
8
  import { SyncState } from "./sync-state.js";
9
- import { sanitizeError, findMemoryFiles, findWorkspaceFiles, isAllowedMemoryPath, isBlacklisted, toPosixPath } from "./helpers.js";
9
+ import { sanitizeError, findMemoryFiles, findWorkspaceFiles, isAllowedMemoryPath, isBlacklisted, isSupportedExtension, toPosixPath } from "./helpers.js";
10
10
 
11
11
  const DEBOUNCE_MS = 1500;
12
12
 
@@ -96,6 +96,7 @@ export function registerSyncService(
96
96
  const processFileEvent = (relPath: string) => {
97
97
  if (config.syncWorkspace) {
98
98
  if (isBlacklisted(relPath, mergedBlacklist)) return;
99
+ if (!isSupportedExtension(relPath)) return;
99
100
  } else {
100
101
  const absCheck = path.resolve(workspaceDir, relPath);
101
102
  if (!isAllowedMemoryPath(absCheck, workspaceDir)) return;
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
package/src/types.ts CHANGED
@@ -42,6 +42,21 @@ export const DEFAULTS = {
42
42
  /** Default blacklist patterns applied when syncWorkspace is enabled. */
43
43
  export const DEFAULT_BLACKLIST: readonly string[] = [".*"] as const;
44
44
 
45
+ /**
46
+ * File extensions supported by Seahorse Storage for processing.
47
+ * Unsupported extensions are filtered out before upload to avoid unnecessary API calls.
48
+ */
49
+ export const SUPPORTED_EXTENSIONS: ReadonlySet<string> = new Set([
50
+ // 문서 (11)
51
+ ".doc", ".docx", ".hwp", ".hwpx", ".odg", ".odp", ".odt", ".pdf", ".ppt", ".pptx", ".rtf",
52
+ // 데이터 (9)
53
+ ".csv", ".json", ".ods", ".toml", ".tsv", ".xls", ".xlsx", ".yaml", ".yml",
54
+ // 이미지 (8)
55
+ ".bmp", ".gif", ".jpeg", ".jpg", ".png", ".tif", ".tiff", ".webp",
56
+ // 텍스트/마크업 (9)
57
+ ".epub", ".htm", ".html", ".markdown", ".md", ".text", ".txt", ".xhtml", ".xml",
58
+ ]);
59
+
45
60
  /** Seahorse RRF default k parameter (server default). */
46
61
  export const RRF_DEFAULT_K = 60;
47
62