@dn-inc/openclaw-seahorse 0.1.0 → 0.2.0
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 +38 -16
- package/index.ts +9 -1
- package/package.json +1 -1
- package/src/helpers.ts +77 -0
- package/src/service.ts +104 -73
- package/src/types.ts +7 -0
package/README.md
CHANGED
|
@@ -3,9 +3,16 @@
|
|
|
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
7
|
- Routes `memory_search` to Seahorse's vector search API
|
|
7
8
|
- Keeps `memory_get` reading from local files (same as native)
|
|
8
9
|
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Node.js 20+
|
|
13
|
+
- OpenClaw >= 2026.1.26
|
|
14
|
+
- Seahorse account (Storage + Table tenants)
|
|
15
|
+
|
|
9
16
|
## Quick Start (Zero Config)
|
|
10
17
|
|
|
11
18
|
1. Install the plugin:
|
|
@@ -16,29 +23,21 @@ openclaw plugins install @dn-inc/openclaw-seahorse
|
|
|
16
23
|
|
|
17
24
|
2. Get your Storage URL, Table URL, and API key from the [Seahorse console](https://seahorse.dnotitia.ai), then add to `plugins.entries` in `openclaw.json`:
|
|
18
25
|
|
|
19
|
-
```
|
|
26
|
+
```jsonc
|
|
20
27
|
"openclaw-seahorse": {
|
|
21
28
|
"config": {
|
|
22
29
|
"storageUrl": "https://<your-storage-id>.api.seahorse.dnotitia.ai",
|
|
23
30
|
"tableUrl": "https://<your-table-id>.api.seahorse.dnotitia.ai",
|
|
24
|
-
"apiKey": "your-key-here"
|
|
31
|
+
"apiKey": "your-key-here",
|
|
32
|
+
// Optional: search your entire workspace, not just memory files
|
|
33
|
+
"syncWorkspace": true
|
|
25
34
|
}
|
|
26
35
|
}
|
|
27
36
|
```
|
|
28
37
|
|
|
29
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}"`).
|
|
30
39
|
|
|
31
|
-
That's it. With zero additional config,
|
|
32
|
-
|
|
33
|
-
- **Hybrid search** (dense 70% + sparse 30%)
|
|
34
|
-
- **6 results** per query
|
|
35
|
-
- **0.35 minimum score** threshold
|
|
36
|
-
|
|
37
|
-
## Requirements
|
|
38
|
-
|
|
39
|
-
- Node.js 20+
|
|
40
|
-
- OpenClaw >= 2026.1.26
|
|
41
|
-
- Seahorse account (Storage + Table tenants)
|
|
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.
|
|
42
41
|
|
|
43
42
|
## Config Reference
|
|
44
43
|
|
|
@@ -50,7 +49,7 @@ That's it. With zero additional config, the plugin behaves identically to OpenCl
|
|
|
50
49
|
| `tableUrl` | string | Yes | Seahorse Table tenant URL |
|
|
51
50
|
| `apiKey` | string | No | API key. Auto-reads from `SEAHORSE_API_KEY` env var if omitted. Also supports `${ENV_VAR}` syntax. |
|
|
52
51
|
|
|
53
|
-
### Layer 1: Search Behavior (optional)
|
|
52
|
+
### Layer 1: Search & Sync Behavior (optional)
|
|
54
53
|
|
|
55
54
|
These override the OpenClaw-compatible defaults.
|
|
56
55
|
|
|
@@ -59,6 +58,8 @@ These override the OpenClaw-compatible defaults.
|
|
|
59
58
|
| `topK` | number | `6` | Number of results to return |
|
|
60
59
|
| `minScore` | number | `0.35` | Minimum relevance score (0-1) |
|
|
61
60
|
| `searchMode` | string | `"hybrid"` | `"dense"`, `"sparse"`, or `"hybrid"` |
|
|
61
|
+
| `syncWorkspace` | boolean | `false` | Sync entire workspace (not just memory files) |
|
|
62
|
+
| `syncBlacklist` | string[] | `[]` | Additional blacklist patterns for workspace sync (see [File Sync](#file-sync)) |
|
|
62
63
|
|
|
63
64
|
Example — return more results with a lower score threshold:
|
|
64
65
|
|
|
@@ -146,7 +147,7 @@ The plugin ships with OpenClaw-compatible defaults so it works as a drop-in repl
|
|
|
146
147
|
|
|
147
148
|
### `memory_search`
|
|
148
149
|
|
|
149
|
-
Semantic vector search over
|
|
150
|
+
Semantic vector search over synced files via Seahorse.
|
|
150
151
|
|
|
151
152
|
| Parameter | Type | Required | Description |
|
|
152
153
|
|-----------|------|----------|-------------|
|
|
@@ -174,7 +175,28 @@ All memory files are stored under the `openclaw/` prefix in Seahorse Storage (e.
|
|
|
174
175
|
|
|
175
176
|
The plugin runs a background service (`seahorse-sync`) that keeps Seahorse Storage in sync:
|
|
176
177
|
|
|
177
|
-
- **Initial sync**: scans
|
|
178
|
+
- **Initial sync**: scans files on startup, uploads only changed files
|
|
178
179
|
- **Live watch**: detects file create/modify/delete via `fs.watch` (1.5s debounce)
|
|
179
180
|
- **Dedup**: mtime-based state tracking prevents redundant uploads
|
|
180
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
|
@@ -37,6 +37,12 @@ const plugin: OpenClawPluginDefinition = {
|
|
|
37
37
|
const rawSparse = (rawConfig.sparse ?? {}) as Record<string, unknown>;
|
|
38
38
|
const rawFusion = (rawConfig.fusion ?? {}) as Record<string, unknown>;
|
|
39
39
|
|
|
40
|
+
const syncWorkspace = rawConfig.syncWorkspace === true;
|
|
41
|
+
const rawBlacklist = rawConfig.syncBlacklist;
|
|
42
|
+
const syncBlacklist: string[] = Array.isArray(rawBlacklist)
|
|
43
|
+
? rawBlacklist.filter((item): item is string => typeof item === "string")
|
|
44
|
+
: [];
|
|
45
|
+
|
|
40
46
|
config = {
|
|
41
47
|
storageUrl: resolveEnvVars(String(rawConfig.storageUrl ?? "")),
|
|
42
48
|
tableUrl: resolveEnvVars(String(rawConfig.tableUrl ?? "")),
|
|
@@ -59,6 +65,8 @@ const plugin: OpenClawPluginDefinition = {
|
|
|
59
65
|
...(rawFusion.k != null && { k: parsePositiveNumber(rawFusion.k, "fusion.k") }),
|
|
60
66
|
},
|
|
61
67
|
projection: String(rawConfig.projection ?? DEFAULTS.projection),
|
|
68
|
+
syncWorkspace,
|
|
69
|
+
syncBlacklist,
|
|
62
70
|
};
|
|
63
71
|
} catch (err: unknown) {
|
|
64
72
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -87,7 +95,7 @@ const plugin: OpenClawPluginDefinition = {
|
|
|
87
95
|
|
|
88
96
|
registerMemorySearchTool(api, config, client);
|
|
89
97
|
registerMemoryGetTool(api);
|
|
90
|
-
registerSyncService(api, client);
|
|
98
|
+
registerSyncService(api, client, config);
|
|
91
99
|
|
|
92
100
|
api.logger.info("[openclaw-seahorse] Plugin registered (memory_search, memory_get, sync service).");
|
|
93
101
|
},
|
package/package.json
CHANGED
package/src/helpers.ts
CHANGED
|
@@ -103,6 +103,83 @@ export function parseUnitInterval(raw: unknown, name: string): number | undefine
|
|
|
103
103
|
return n;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Check whether a POSIX relative path matches any blacklist pattern.
|
|
108
|
+
* Each pattern is tested against every segment (directory or file name) of the path:
|
|
109
|
+
* - `"dir/"` — directory exact match (segment equals name without trailing slash)
|
|
110
|
+
* - `"*.ext"` — suffix wildcard (segment ends with ext portion)
|
|
111
|
+
* - `"prefix*"` — prefix wildcard (segment starts with prefix portion)
|
|
112
|
+
* - `"name"` — exact match (segment equals pattern exactly)
|
|
113
|
+
*/
|
|
114
|
+
export function isBlacklisted(relPath: string, blacklist: readonly string[]): boolean {
|
|
115
|
+
if (blacklist.length === 0) return false;
|
|
116
|
+
const segments = relPath.split("/");
|
|
117
|
+
for (const pattern of blacklist) {
|
|
118
|
+
if (pattern.endsWith("/")) {
|
|
119
|
+
// Directory exact match — match any directory segment
|
|
120
|
+
const dirName = pattern.slice(0, -1);
|
|
121
|
+
// All segments except the last are directories
|
|
122
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
123
|
+
if (segments[i] === dirName) return true;
|
|
124
|
+
}
|
|
125
|
+
} else if (pattern.startsWith("*")) {
|
|
126
|
+
// Suffix wildcard — match any segment ending with the suffix
|
|
127
|
+
const suffix = pattern.slice(1);
|
|
128
|
+
for (const seg of segments) {
|
|
129
|
+
if (seg.endsWith(suffix)) return true;
|
|
130
|
+
}
|
|
131
|
+
} else if (pattern.endsWith("*")) {
|
|
132
|
+
// Prefix wildcard — match any segment starting with the prefix
|
|
133
|
+
const prefix = pattern.slice(0, -1);
|
|
134
|
+
for (const seg of segments) {
|
|
135
|
+
if (seg.startsWith(prefix)) return true;
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
// Exact name match — match any segment
|
|
139
|
+
for (const seg of segments) {
|
|
140
|
+
if (seg === pattern) return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Recursively find all files in a workspace directory, excluding blacklisted paths.
|
|
149
|
+
* Blacklisted directories are not entered (efficient pruning).
|
|
150
|
+
*/
|
|
151
|
+
export async function findWorkspaceFiles(
|
|
152
|
+
workspaceDir: string,
|
|
153
|
+
blacklist: readonly string[],
|
|
154
|
+
): Promise<string[]> {
|
|
155
|
+
const files: string[] = [];
|
|
156
|
+
await collectAllFiles(workspaceDir, "", blacklist, files);
|
|
157
|
+
return files;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Recursively collect files, skipping blacklisted entries. */
|
|
161
|
+
async function collectAllFiles(
|
|
162
|
+
absDir: string,
|
|
163
|
+
relDir: string,
|
|
164
|
+
blacklist: readonly string[],
|
|
165
|
+
out: string[],
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
const entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
const relPath = relDir ? toPosixPath(path.join(relDir, entry.name)) : entry.name;
|
|
170
|
+
if (entry.isDirectory()) {
|
|
171
|
+
// Construct a fake child path so directory patterns ("dir/") are checked
|
|
172
|
+
// against this directory name appearing as a non-last segment.
|
|
173
|
+
if (isBlacklisted(relPath + "/_", blacklist)) continue;
|
|
174
|
+
await collectAllFiles(path.join(absDir, entry.name), relPath, blacklist, out);
|
|
175
|
+
} else if (entry.isFile()) {
|
|
176
|
+
if (!isBlacklisted(relPath, blacklist)) {
|
|
177
|
+
out.push(relPath);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
106
183
|
/** Check whether a resolved absolute path is within the allowed memory directories. */
|
|
107
184
|
export function isAllowedMemoryPath(resolvedPath: string, workspaceDir: string): boolean {
|
|
108
185
|
const normalizedWorkspace = path.resolve(workspaceDir);
|
package/src/service.ts
CHANGED
|
@@ -3,8 +3,10 @@ import { watch, type FSWatcher } from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { OpenClawPluginApi, OpenClawPluginServiceContext } from "openclaw/plugin-sdk";
|
|
5
5
|
import type { SeahorseClient } from "./client.js";
|
|
6
|
+
import type { SeahorseConfig } from "./types.js";
|
|
7
|
+
import { DEFAULT_BLACKLIST } from "./types.js";
|
|
6
8
|
import { SyncState } from "./sync-state.js";
|
|
7
|
-
import { sanitizeError, findMemoryFiles, isAllowedMemoryPath, toPosixPath } from "./helpers.js";
|
|
9
|
+
import { sanitizeError, findMemoryFiles, findWorkspaceFiles, isAllowedMemoryPath, isBlacklisted, toPosixPath } from "./helpers.js";
|
|
8
10
|
|
|
9
11
|
const DEBOUNCE_MS = 1500;
|
|
10
12
|
|
|
@@ -14,6 +16,7 @@ const DEBOUNCE_MS = 1500;
|
|
|
14
16
|
export function registerSyncService(
|
|
15
17
|
api: OpenClawPluginApi,
|
|
16
18
|
client: SeahorseClient,
|
|
19
|
+
config: SeahorseConfig,
|
|
17
20
|
): void {
|
|
18
21
|
let watchers: FSWatcher[] = [];
|
|
19
22
|
let syncState: SyncState;
|
|
@@ -35,15 +38,19 @@ export function registerSyncService(
|
|
|
35
38
|
await syncState.load();
|
|
36
39
|
|
|
37
40
|
const memoryDir = path.join(workspaceDir, "memory");
|
|
41
|
+
const mergedBlacklist = [...DEFAULT_BLACKLIST, ...config.syncBlacklist];
|
|
42
|
+
const findFiles = config.syncWorkspace
|
|
43
|
+
? () => findWorkspaceFiles(workspaceDir, mergedBlacklist)
|
|
44
|
+
: () => findMemoryFiles(workspaceDir);
|
|
38
45
|
|
|
39
|
-
// Scan and upload
|
|
40
|
-
const
|
|
46
|
+
// Scan and upload files that need syncing
|
|
47
|
+
const syncFiles = async (label: string): Promise<number> => {
|
|
41
48
|
let files: string[];
|
|
42
49
|
try {
|
|
43
|
-
files = await
|
|
50
|
+
files = await findFiles();
|
|
44
51
|
} catch (err: unknown) {
|
|
45
52
|
logger.warn(
|
|
46
|
-
`[openclaw-seahorse] Error scanning
|
|
53
|
+
`[openclaw-seahorse] Error scanning files (${label}): ${sanitizeError(err)}`,
|
|
47
54
|
);
|
|
48
55
|
return 0;
|
|
49
56
|
}
|
|
@@ -79,7 +86,7 @@ export function registerSyncService(
|
|
|
79
86
|
};
|
|
80
87
|
|
|
81
88
|
// Initial sync
|
|
82
|
-
const fileCount = await
|
|
89
|
+
const fileCount = await syncFiles("initial");
|
|
83
90
|
logger.info(
|
|
84
91
|
`[openclaw-seahorse] Initial sync complete. ${fileCount} file(s) checked.`,
|
|
85
92
|
);
|
|
@@ -87,9 +94,11 @@ export function registerSyncService(
|
|
|
87
94
|
abortController = new AbortController();
|
|
88
95
|
|
|
89
96
|
const processFileEvent = (relPath: string) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
if (config.syncWorkspace) {
|
|
98
|
+
if (isBlacklisted(relPath, mergedBlacklist)) return;
|
|
99
|
+
} else {
|
|
100
|
+
const absCheck = path.resolve(workspaceDir, relPath);
|
|
101
|
+
if (!isAllowedMemoryPath(absCheck, workspaceDir)) return;
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
logger.debug?.(`[openclaw-seahorse] File event: ${relPath} (debounce ${DEBOUNCE_MS}ms)`);
|
|
@@ -141,89 +150,111 @@ export function registerSyncService(
|
|
|
141
150
|
);
|
|
142
151
|
};
|
|
143
152
|
|
|
144
|
-
|
|
153
|
+
if (config.syncWorkspace) {
|
|
154
|
+
// Workspace mode: single recursive watcher on the workspace root
|
|
145
155
|
try {
|
|
146
|
-
const
|
|
147
|
-
|
|
156
|
+
const wsWatcher = watch(
|
|
157
|
+
workspaceDir,
|
|
148
158
|
{ recursive: true, signal: abortController.signal },
|
|
149
159
|
(_eventType, filename) => {
|
|
150
160
|
if (!filename) return;
|
|
151
|
-
const
|
|
152
|
-
if (!normalized.endsWith(".md")) return;
|
|
153
|
-
const relPath = toPosixPath(path.join("memory", normalized));
|
|
161
|
+
const relPath = toPosixPath(filename);
|
|
154
162
|
processFileEvent(relPath);
|
|
155
163
|
},
|
|
156
164
|
);
|
|
157
|
-
watchers.push(
|
|
158
|
-
logger.info("[openclaw-seahorse] Watching
|
|
165
|
+
watchers.push(wsWatcher);
|
|
166
|
+
logger.info("[openclaw-seahorse] Watching workspace directory (recursive).");
|
|
159
167
|
} catch (err: unknown) {
|
|
160
168
|
logger.warn(
|
|
161
|
-
`[openclaw-seahorse] Could not watch
|
|
169
|
+
`[openclaw-seahorse] Could not watch workspace directory: ${sanitizeError(err)}`,
|
|
162
170
|
);
|
|
163
171
|
}
|
|
164
|
-
}
|
|
172
|
+
} else {
|
|
173
|
+
// Memory-only mode: root watcher + lazy memory/ watcher
|
|
174
|
+
const setupMemoryDirWatcher = (): void => {
|
|
175
|
+
try {
|
|
176
|
+
const memWatcher = watch(
|
|
177
|
+
memoryDir,
|
|
178
|
+
{ recursive: true, signal: abortController.signal },
|
|
179
|
+
(_eventType, filename) => {
|
|
180
|
+
if (!filename) return;
|
|
181
|
+
const normalized = filename.replace(/\\/g, "/");
|
|
182
|
+
if (!normalized.endsWith(".md")) return;
|
|
183
|
+
const relPath = toPosixPath(path.join("memory", normalized));
|
|
184
|
+
processFileEvent(relPath);
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
watchers.push(memWatcher);
|
|
188
|
+
logger.info("[openclaw-seahorse] Watching memory/ directory.");
|
|
189
|
+
} catch (err: unknown) {
|
|
190
|
+
logger.warn(
|
|
191
|
+
`[openclaw-seahorse] Could not watch memory/ directory: ${sanitizeError(err)}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
165
195
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
196
|
+
// Watch workspace root for MEMORY.md changes and memory/ directory creation
|
|
197
|
+
let memoryDirWatcherSet = false;
|
|
198
|
+
try {
|
|
199
|
+
const rootWatcher = watch(
|
|
200
|
+
workspaceDir,
|
|
201
|
+
{ signal: abortController.signal },
|
|
202
|
+
(eventType, filename) => {
|
|
203
|
+
if (filename === "MEMORY.md") {
|
|
204
|
+
processFileEvent("MEMORY.md");
|
|
205
|
+
}
|
|
206
|
+
// Detect memory/ directory creation
|
|
207
|
+
if (!memoryDirWatcherSet && filename === "memory" && eventType === "rename") {
|
|
208
|
+
memoryDirWatcherSet = true;
|
|
209
|
+
const op = (async () => {
|
|
210
|
+
try {
|
|
211
|
+
const dirStat = await fs.stat(memoryDir);
|
|
212
|
+
if (!dirStat.isDirectory()) {
|
|
213
|
+
memoryDirWatcherSet = false;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
setupMemoryDirWatcher();
|
|
217
|
+
// Catch-up sync: upload files written before/during watcher setup
|
|
218
|
+
const count = await syncFiles("catch-up");
|
|
219
|
+
if (count > 0) {
|
|
220
|
+
logger.info(
|
|
221
|
+
`[openclaw-seahorse] Catch-up sync complete. ${count} file(s) checked.`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
} catch (err: unknown) {
|
|
183
225
|
memoryDirWatcherSet = false;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
setupMemoryDirWatcher();
|
|
187
|
-
// Catch-up sync: upload files written before/during watcher setup
|
|
188
|
-
const count = await syncMemoryFiles("catch-up");
|
|
189
|
-
if (count > 0) {
|
|
190
|
-
logger.info(
|
|
191
|
-
`[openclaw-seahorse] Catch-up sync complete. ${count} file(s) checked.`,
|
|
226
|
+
logger.warn(
|
|
227
|
+
`[openclaw-seahorse] Catch-up sync error: ${sanitizeError(err)}`,
|
|
192
228
|
);
|
|
193
229
|
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
})();
|
|
201
|
-
trackOp(op);
|
|
202
|
-
}
|
|
203
|
-
},
|
|
204
|
-
);
|
|
205
|
-
watchers.push(rootWatcher);
|
|
206
|
-
} catch (err: unknown) {
|
|
207
|
-
logger.warn(
|
|
208
|
-
`[openclaw-seahorse] Could not watch workspace root: ${sanitizeError(err)}`,
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Watch memory/ directory if it already exists
|
|
213
|
-
try {
|
|
214
|
-
await fs.access(memoryDir);
|
|
215
|
-
memoryDirWatcherSet = true;
|
|
216
|
-
setupMemoryDirWatcher();
|
|
217
|
-
} catch (err: unknown) {
|
|
218
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
219
|
-
logger.info(
|
|
220
|
-
"[openclaw-seahorse] memory/ directory does not exist yet. Will start watching when created.",
|
|
230
|
+
})();
|
|
231
|
+
trackOp(op);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
221
234
|
);
|
|
222
|
-
|
|
235
|
+
watchers.push(rootWatcher);
|
|
236
|
+
} catch (err: unknown) {
|
|
223
237
|
logger.warn(
|
|
224
|
-
`[openclaw-seahorse] Could not
|
|
238
|
+
`[openclaw-seahorse] Could not watch workspace root: ${sanitizeError(err)}`,
|
|
225
239
|
);
|
|
226
240
|
}
|
|
241
|
+
|
|
242
|
+
// Watch memory/ directory if it already exists
|
|
243
|
+
try {
|
|
244
|
+
await fs.access(memoryDir);
|
|
245
|
+
memoryDirWatcherSet = true;
|
|
246
|
+
setupMemoryDirWatcher();
|
|
247
|
+
} catch (err: unknown) {
|
|
248
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
249
|
+
logger.info(
|
|
250
|
+
"[openclaw-seahorse] memory/ directory does not exist yet. Will start watching when created.",
|
|
251
|
+
);
|
|
252
|
+
} else {
|
|
253
|
+
logger.warn(
|
|
254
|
+
`[openclaw-seahorse] Could not access memory/ directory: ${sanitizeError(err)}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
227
258
|
}
|
|
228
259
|
},
|
|
229
260
|
|
package/src/types.ts
CHANGED
|
@@ -35,8 +35,13 @@ export const DEFAULTS = {
|
|
|
35
35
|
sparse: { column: "sparse_vector" },
|
|
36
36
|
fusion: { type: "rrf", alpha: 0.7 },
|
|
37
37
|
projection: "id, text, metadata, distance, score",
|
|
38
|
+
syncWorkspace: false,
|
|
39
|
+
syncBlacklist: [] as string[],
|
|
38
40
|
} as const;
|
|
39
41
|
|
|
42
|
+
/** Default blacklist patterns applied when syncWorkspace is enabled. */
|
|
43
|
+
export const DEFAULT_BLACKLIST: readonly string[] = [".*"] as const;
|
|
44
|
+
|
|
40
45
|
/** Seahorse RRF default k parameter (server default). */
|
|
41
46
|
export const RRF_DEFAULT_K = 60;
|
|
42
47
|
|
|
@@ -55,6 +60,8 @@ export interface SeahorseConfig {
|
|
|
55
60
|
sparse: SparseConfig;
|
|
56
61
|
fusion: FusionConfig;
|
|
57
62
|
projection: string;
|
|
63
|
+
syncWorkspace: boolean;
|
|
64
|
+
syncBlacklist: string[];
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/** Parsed search result from Seahorse */
|