@karmaniverous/jeeves-watcher-openclaw 0.6.2 → 0.8.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 +20 -6
- package/dist/cli.js +46 -27
- package/dist/index.js +201 -135
- package/dist/skills/jeeves-watcher/SKILL.md +98 -24
- package/dist/src/constants.d.ts +18 -0
- package/dist/src/helpers.d.ts +9 -13
- package/dist/src/index.d.ts +2 -1
- package/dist/src/promptInjection.d.ts +0 -6
- package/dist/src/watcherComponent.d.ts +26 -0
- package/dist/src/watcherComponent.test.d.ts +5 -0
- package/dist/src/writerIntegration.test.d.ts +5 -0
- package/openclaw.plugin.json +10 -1
- package/package.json +8 -4
- package/dist/src/toolsWriter.d.ts +0 -20
package/README.md
CHANGED
|
@@ -34,17 +34,30 @@ After install or uninstall, restart the OpenClaw gateway to apply changes.
|
|
|
34
34
|
|
|
35
35
|
## Configuration
|
|
36
36
|
|
|
37
|
-
Set the
|
|
37
|
+
Set the plugin config in `openclaw.json` under `plugins.entries.jeeves-watcher-openclaw.config`:
|
|
38
38
|
|
|
39
39
|
```json
|
|
40
40
|
{
|
|
41
|
-
"apiUrl": "http://127.0.0.1:1936"
|
|
41
|
+
"apiUrl": "http://127.0.0.1:1936",
|
|
42
|
+
"configRoot": "j:/config"
|
|
42
43
|
}
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
- **`apiUrl`** — jeeves-watcher API base URL (default: `http://127.0.0.1:1936`)
|
|
47
|
+
- **`configRoot`** — platform config root path, used by `@karmaniverous/jeeves` core to derive `{configRoot}/jeeves-watcher/` for component config (default: `j:/config`)
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
## Architecture
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
## Jeeves Platform Integration
|
|
54
|
+
|
|
55
|
+
This plugin integrates with [`@karmaniverous/jeeves`](https://www.npmjs.com/package/@karmaniverous/jeeves) to manage workspace content:
|
|
56
|
+
|
|
57
|
+
- **TOOLS.md** — writes a `## Watcher` section with a live menu of indexed content, score thresholds, and escalation rules (refreshes every 71 seconds)
|
|
58
|
+
- **SOUL.md / AGENTS.md** — maintains shared platform content via managed sections
|
|
59
|
+
- **Service commands** — exposes `stop`, `uninstall`, and `status` for the watcher service
|
|
60
|
+
- **Plugin commands** — exposes `uninstall` for the plugin itself
|
|
48
61
|
|
|
49
62
|
## Tools
|
|
50
63
|
|
|
@@ -53,10 +66,11 @@ On startup, the plugin writes a `## Watcher` section to `TOOLS.md` in the agent'
|
|
|
53
66
|
| `watcher_status` | Service health, uptime, and collection stats |
|
|
54
67
|
| `watcher_search` | Semantic search across indexed documents |
|
|
55
68
|
| `watcher_enrich` | Enrich document metadata via rules engine |
|
|
56
|
-
| `
|
|
69
|
+
| `watcher_config` | Query the effective runtime config via JSONPath |
|
|
70
|
+
| `watcher_walk` | Walk watched filesystem paths with glob intersection |
|
|
57
71
|
| `watcher_validate` | Validate a watcher configuration |
|
|
58
72
|
| `watcher_config_apply` | Apply a new configuration |
|
|
59
|
-
| `watcher_reindex` | Trigger a
|
|
73
|
+
| `watcher_reindex` | Trigger a scoped reindex with blast area plan |
|
|
60
74
|
| `watcher_scan` | Filter-only point query with cursor pagination |
|
|
61
75
|
| `watcher_issues` | List indexing issues and errors |
|
|
62
76
|
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { parseManaged, TOOLS_MARKERS, updateManagedSection } from '@karmaniverous/jeeves';
|
|
2
3
|
import { existsSync, rmSync, mkdirSync, cpSync, readFileSync, writeFileSync } from 'fs';
|
|
3
4
|
import { homedir } from 'os';
|
|
4
5
|
import { join, dirname, resolve } from 'path';
|
|
@@ -174,7 +175,7 @@ function install() {
|
|
|
174
175
|
console.log(' Restart the OpenClaw gateway to load the plugin.');
|
|
175
176
|
}
|
|
176
177
|
/** Uninstall the plugin from OpenClaw's extensions directory. */
|
|
177
|
-
function uninstall() {
|
|
178
|
+
async function uninstall() {
|
|
178
179
|
const home = resolveOpenClawHome();
|
|
179
180
|
const configPath = resolveConfigPath(home);
|
|
180
181
|
const extDir = join(home, 'extensions', PLUGIN_ID);
|
|
@@ -200,7 +201,7 @@ function uninstall() {
|
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
203
|
// Clean up TOOLS.md watcher section
|
|
203
|
-
cleanupToolsMd(home, configPath);
|
|
204
|
+
await cleanupToolsMd(home, configPath);
|
|
204
205
|
console.log();
|
|
205
206
|
console.log('✅ Plugin uninstalled successfully.');
|
|
206
207
|
console.log(' Restart the OpenClaw gateway to complete removal.');
|
|
@@ -220,39 +221,57 @@ function resolveWorkspaceDir(home, configPath) {
|
|
|
220
221
|
// Default workspace location
|
|
221
222
|
return join(home, 'workspace');
|
|
222
223
|
}
|
|
223
|
-
/**
|
|
224
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Remove the Watcher section from TOOLS.md on uninstall.
|
|
226
|
+
*
|
|
227
|
+
* @remarks
|
|
228
|
+
* Uses core's `parseManaged` to locate the managed block and its sections,
|
|
229
|
+
* then rewrites without the Watcher section via `updateManagedSection`.
|
|
230
|
+
* If the Watcher section is the only one, removes the entire managed block.
|
|
231
|
+
*/
|
|
232
|
+
async function cleanupToolsMd(home, configPath) {
|
|
225
233
|
const workspaceDir = resolveWorkspaceDir(home, configPath);
|
|
226
234
|
if (!workspaceDir)
|
|
227
235
|
return;
|
|
228
236
|
const toolsPath = join(workspaceDir, 'TOOLS.md');
|
|
229
237
|
if (!existsSync(toolsPath))
|
|
230
238
|
return;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (!watcherRe.test(content))
|
|
239
|
+
const content = readFileSync(toolsPath, 'utf8');
|
|
240
|
+
const parsed = parseManaged(content, TOOLS_MARKERS);
|
|
241
|
+
if (!parsed.found)
|
|
235
242
|
return;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
content =
|
|
250
|
-
content.slice(0, h1Idx) + content.slice(h1Idx + platformH1.length);
|
|
251
|
-
content = content.replace(/^\n{2,}/, '').replace(/\n{3,}/g, '\n\n');
|
|
243
|
+
const watcherSection = parsed.sections.find((s) => s.id === 'Watcher');
|
|
244
|
+
if (!watcherSection)
|
|
245
|
+
return;
|
|
246
|
+
const remaining = parsed.sections.filter((s) => s.id !== 'Watcher');
|
|
247
|
+
if (remaining.length === 0) {
|
|
248
|
+
// No sections left — remove the entire managed block.
|
|
249
|
+
const parts = [];
|
|
250
|
+
if (parsed.beforeContent)
|
|
251
|
+
parts.push(parsed.beforeContent);
|
|
252
|
+
if (parsed.userContent) {
|
|
253
|
+
if (parts.length > 0)
|
|
254
|
+
parts.push('');
|
|
255
|
+
parts.push(parsed.userContent);
|
|
252
256
|
}
|
|
257
|
+
const newContent = parts.join('\n').trim() + '\n';
|
|
258
|
+
writeFileSync(toolsPath, newContent);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
// Rewrite the managed block without the Watcher section.
|
|
262
|
+
// We write an empty string for the Watcher section; core's
|
|
263
|
+
// updateManagedSection will rebuild from the remaining sections.
|
|
264
|
+
// Actually, the cleanest approach is to rebuild the section text
|
|
265
|
+
// from the remaining sections and write the entire block.
|
|
266
|
+
const sectionText = remaining
|
|
267
|
+
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
268
|
+
.join('\n\n');
|
|
269
|
+
const body = `# ${TOOLS_MARKERS.title}\n\n${sectionText}`;
|
|
270
|
+
await updateManagedSection(toolsPath, body, {
|
|
271
|
+
mode: 'block',
|
|
272
|
+
markers: TOOLS_MARKERS,
|
|
273
|
+
});
|
|
253
274
|
}
|
|
254
|
-
content = content.trim() + '\n';
|
|
255
|
-
writeFileSync(toolsPath, content);
|
|
256
275
|
console.log('\u2713 Cleaned up TOOLS.md (removed Watcher section)');
|
|
257
276
|
}
|
|
258
277
|
// Main
|
|
@@ -262,7 +281,7 @@ switch (command) {
|
|
|
262
281
|
install();
|
|
263
282
|
break;
|
|
264
283
|
case 'uninstall':
|
|
265
|
-
uninstall();
|
|
284
|
+
void uninstall();
|
|
266
285
|
break;
|
|
267
286
|
default:
|
|
268
287
|
console.log(`@karmaniverous/jeeves-watcher-openclaw — OpenClaw plugin installer`);
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { createAsyncContentCache, init, createComponentWriter } from '@karmaniverous/jeeves';
|
|
3
|
+
import { execFile as execFile$1 } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module plugin/constants
|
|
8
|
+
* Shared constants for the OpenClaw plugin package.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* Imported by both the plugin bundle (`index.ts`) and the CLI bundle
|
|
12
|
+
* (`cli.ts`). Rollup inlines these into each output independently.
|
|
13
|
+
*/
|
|
14
|
+
/** Plugin identifier used in OpenClaw config and extensions directory. */
|
|
15
|
+
const PLUGIN_ID = 'jeeves-watcher-openclaw';
|
|
16
|
+
/** Default watcher API base URL. */
|
|
17
|
+
const DEFAULT_API_URL = 'http://127.0.0.1:1936';
|
|
18
|
+
/** Default platform config root path. */
|
|
19
|
+
const DEFAULT_CONFIG_ROOT = 'j:/config';
|
|
20
|
+
/** Default Qdrant health check URL (used in diagnostic output). */
|
|
21
|
+
const DEFAULT_QDRANT_URL = 'http://127.0.0.1:6333';
|
|
22
|
+
/** Timeout in milliseconds for service health probes. */
|
|
23
|
+
const PROBE_TIMEOUT_MS = 1500;
|
|
3
24
|
|
|
4
25
|
/**
|
|
5
26
|
* @module plugin/helpers
|
|
6
27
|
* Shared types and utility functions for the OpenClaw plugin tool registrations.
|
|
7
28
|
*/
|
|
8
|
-
|
|
9
|
-
/** Extract plugin config from the API object */
|
|
29
|
+
/** Extract plugin config from the API object. */
|
|
10
30
|
function getPluginConfig(api) {
|
|
11
|
-
return api.config?.plugins?.entries?.[
|
|
31
|
+
return api.config?.plugins?.entries?.[PLUGIN_ID]?.config;
|
|
12
32
|
}
|
|
13
33
|
/** Resolve the watcher API base URL from plugin config. */
|
|
14
34
|
function getApiUrl(api) {
|
|
15
35
|
const url = getPluginConfig(api)?.apiUrl;
|
|
16
36
|
return typeof url === 'string' ? url : DEFAULT_API_URL;
|
|
17
37
|
}
|
|
38
|
+
/** Resolve the platform config root path from plugin config. */
|
|
39
|
+
function getConfigRoot(api) {
|
|
40
|
+
const root = getPluginConfig(api)?.configRoot;
|
|
41
|
+
return typeof root === 'string' ? root : DEFAULT_CONFIG_ROOT;
|
|
42
|
+
}
|
|
18
43
|
/** Format a successful tool result. */
|
|
19
44
|
function ok(data) {
|
|
20
45
|
return {
|
|
@@ -79,24 +104,14 @@ async function generateWatcherMenu(apiUrl) {
|
|
|
79
104
|
const activeRules = [];
|
|
80
105
|
const watchPaths = [];
|
|
81
106
|
const ignoredPaths = [];
|
|
107
|
+
const scoreThresholds = { strong: 0.75, relevant: 0.5, noise: 0.25 };
|
|
82
108
|
try {
|
|
83
|
-
const [statusRes, rulesRes, pathsRes, ignoredRes] = (await Promise.all([
|
|
109
|
+
const [statusRes, rulesRes, pathsRes, thresholdsRes, ignoredRes] = (await Promise.all([
|
|
84
110
|
fetchJson(`${apiUrl}/status`),
|
|
85
|
-
fetchJson(`${apiUrl}/config
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}),
|
|
90
|
-
fetchJson(`${apiUrl}/config/query`, {
|
|
91
|
-
method: 'POST',
|
|
92
|
-
headers: { 'Content-Type': 'application/json' },
|
|
93
|
-
body: JSON.stringify({ path: '$.watch.paths[*]' }),
|
|
94
|
-
}),
|
|
95
|
-
fetchJson(`${apiUrl}/config/query`, {
|
|
96
|
-
method: 'POST',
|
|
97
|
-
headers: { 'Content-Type': 'application/json' },
|
|
98
|
-
body: JSON.stringify({ path: '$.watch.ignored[*]' }),
|
|
99
|
-
}),
|
|
111
|
+
fetchJson(`${apiUrl}/config?path=${encodeURIComponent('$.inferenceRules[*]')}`),
|
|
112
|
+
fetchJson(`${apiUrl}/config?path=${encodeURIComponent('$.watch.paths[*]')}`),
|
|
113
|
+
fetchJson(`${apiUrl}/config?path=${encodeURIComponent('$.search.scoreThresholds')}`),
|
|
114
|
+
fetchJson(`${apiUrl}/config?path=${encodeURIComponent('$.watch.ignored[*]')}`),
|
|
100
115
|
]));
|
|
101
116
|
pointCount = statusRes.collection?.pointCount ?? 0;
|
|
102
117
|
if (Array.isArray(rulesRes.result)) {
|
|
@@ -119,12 +134,22 @@ async function generateWatcherMenu(apiUrl) {
|
|
|
119
134
|
ignoredPaths.push(p);
|
|
120
135
|
}
|
|
121
136
|
}
|
|
137
|
+
if (Array.isArray(thresholdsRes.result) &&
|
|
138
|
+
thresholdsRes.result.length > 0) {
|
|
139
|
+
const t = thresholdsRes.result[0];
|
|
140
|
+
if (typeof t.strong === 'number')
|
|
141
|
+
scoreThresholds.strong = t.strong;
|
|
142
|
+
if (typeof t.relevant === 'number')
|
|
143
|
+
scoreThresholds.relevant = t.relevant;
|
|
144
|
+
if (typeof t.noise === 'number')
|
|
145
|
+
scoreThresholds.noise = t.noise;
|
|
146
|
+
}
|
|
122
147
|
}
|
|
123
148
|
catch {
|
|
124
149
|
let qdrantStatus = '*Unknown*';
|
|
125
150
|
try {
|
|
126
151
|
// Assuming Qdrant runs locally on the default port
|
|
127
|
-
const res = await fetch(
|
|
152
|
+
const res = await fetch(`${DEFAULT_QDRANT_URL}/healthz`, {
|
|
128
153
|
signal: AbortSignal.timeout(1000),
|
|
129
154
|
});
|
|
130
155
|
qdrantStatus = res.ok ? 'Running' : 'Error';
|
|
@@ -136,7 +161,7 @@ async function generateWatcherMenu(apiUrl) {
|
|
|
136
161
|
> The jeeves-watcher service API at ${apiUrl} is down or not installed.
|
|
137
162
|
> **Diagnostic Info:**
|
|
138
163
|
> - Watcher API (${apiUrl}): Unreachable
|
|
139
|
-
> - Qdrant (
|
|
164
|
+
> - Qdrant (${DEFAULT_QDRANT_URL}): ${qdrantStatus}
|
|
140
165
|
>
|
|
141
166
|
> **DO NOT** ignore this. Read the \`jeeves-watcher\` skill's **Bootstrap** section immediately and walk the user through the setup process. Use the diagnostic info above to determine if Qdrant needs installing/starting, or if only the watcher service needs attention.`;
|
|
142
167
|
}
|
|
@@ -153,10 +178,10 @@ async function generateWatcherMenu(apiUrl) {
|
|
|
153
178
|
'**Scan-first rule:** When a task involves structural queries (file enumeration, staleness checks, domain listing, counts), use `watcher_scan` instead of `watcher_search`. Scan does NOT use embeddings and does NOT accept a query string.',
|
|
154
179
|
'**Search-first rule:** When a task involves finding, reading, or modifying files in indexed paths, run `watcher_search` FIRST — even if you already know the file path. Search surfaces related files you may not have considered and catches stale artifacts. Direct filesystem access is for acting on search results, not bypassing them.',
|
|
155
180
|
'',
|
|
156
|
-
'### Score Interpretation:',
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
181
|
+
'### Score Interpretation (see skill for detail):',
|
|
182
|
+
`* **Strong:** >= ${String(scoreThresholds.strong)} — High confidence. Use directly.`,
|
|
183
|
+
`* **Relevant:** >= ${String(scoreThresholds.relevant)} — Likely useful. Verify context before relying on it.`,
|
|
184
|
+
`* **Noise:** < ${String(scoreThresholds.noise)} — Discard. If all results are noise, broaden your query or try different terms.`,
|
|
160
185
|
'',
|
|
161
186
|
"### What's on the menu:",
|
|
162
187
|
];
|
|
@@ -187,104 +212,79 @@ async function generateWatcherMenu(apiUrl) {
|
|
|
187
212
|
}
|
|
188
213
|
|
|
189
214
|
/**
|
|
190
|
-
* @module plugin/
|
|
191
|
-
*
|
|
192
|
-
* Replaces the agent:bootstrap hook approach which was unreliable due to
|
|
193
|
-
* OpenClaw's clearInternalHooks() wiping plugin-registered hooks on startup.
|
|
194
|
-
*/
|
|
195
|
-
const REFRESH_INTERVAL_MS = 60_000;
|
|
196
|
-
let intervalHandle = null;
|
|
197
|
-
let lastWrittenMenu = '';
|
|
198
|
-
/**
|
|
199
|
-
* Resolve the workspace TOOLS.md path.
|
|
200
|
-
* Uses api.resolvePath if available, otherwise falls back to CWD.
|
|
201
|
-
*/
|
|
202
|
-
function resolveToolsPath(api) {
|
|
203
|
-
const resolvePath = api
|
|
204
|
-
.resolvePath;
|
|
205
|
-
if (typeof resolvePath === 'function') {
|
|
206
|
-
return resolvePath('TOOLS.md');
|
|
207
|
-
}
|
|
208
|
-
return resolve(process.cwd(), 'TOOLS.md');
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Upsert the watcher section in TOOLS.md content.
|
|
215
|
+
* @module plugin/watcherComponent
|
|
216
|
+
* Jeeves component integration for the watcher OpenClaw plugin.
|
|
212
217
|
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
218
|
+
* @remarks
|
|
219
|
+
* Uses `createAsyncContentCache()` from core to bridge the sync/async gap:
|
|
220
|
+
* `generateToolsContent()` must be synchronous, but the watcher menu requires
|
|
221
|
+
* async HTTP calls. The cache triggers a background refresh on each call and
|
|
222
|
+
* returns the most recent successful result.
|
|
217
223
|
*/
|
|
218
|
-
|
|
219
|
-
const section = `## Watcher\n\n${watcherMenu}`;
|
|
220
|
-
// Replace existing watcher section (match from ## Watcher to next ## or # or EOF)
|
|
221
|
-
const re = /^## Watcher\n[\s\S]*?(?=\n## |\n# |$(?![\s\S]))/m;
|
|
222
|
-
if (re.test(existing)) {
|
|
223
|
-
return existing.replace(re, section);
|
|
224
|
-
}
|
|
225
|
-
// No existing section. Prepend under a platform tools H1.
|
|
226
|
-
const platformH1 = '# Jeeves Platform Tools';
|
|
227
|
-
if (existing.includes(platformH1)) {
|
|
228
|
-
// Insert after the H1
|
|
229
|
-
const idx = existing.indexOf(platformH1) + platformH1.length;
|
|
230
|
-
return existing.slice(0, idx) + `\n\n${section}\n` + existing.slice(idx);
|
|
231
|
-
}
|
|
232
|
-
// Prepend platform header + watcher section before existing content
|
|
233
|
-
const trimmed = existing.trim();
|
|
234
|
-
if (trimmed.length === 0) {
|
|
235
|
-
return `${platformH1}\n\n${section}\n`;
|
|
236
|
-
}
|
|
237
|
-
return `${platformH1}\n\n${section}\n\n${trimmed}\n`;
|
|
238
|
-
}
|
|
224
|
+
const execFile = promisify(execFile$1);
|
|
239
225
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
226
|
+
* Probe the watcher HTTP API for service health.
|
|
227
|
+
*
|
|
228
|
+
* @param apiUrl - Base URL of the watcher API.
|
|
229
|
+
* @returns Service status with running flag, optional version and uptime.
|
|
242
230
|
*/
|
|
243
|
-
async function
|
|
244
|
-
const apiUrl = getApiUrl(api);
|
|
245
|
-
const menu = await generateWatcherMenu(apiUrl);
|
|
246
|
-
if (menu === lastWrittenMenu) {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
const toolsPath = resolveToolsPath(api);
|
|
250
|
-
let current = '';
|
|
231
|
+
async function getServiceStatus(apiUrl) {
|
|
251
232
|
try {
|
|
252
|
-
|
|
233
|
+
const res = await fetch(`${apiUrl}/status`, {
|
|
234
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
235
|
+
});
|
|
236
|
+
if (!res.ok)
|
|
237
|
+
return { running: false };
|
|
238
|
+
const json = (await res.json());
|
|
239
|
+
return {
|
|
240
|
+
running: true,
|
|
241
|
+
version: typeof json.version === 'string' ? json.version : undefined,
|
|
242
|
+
uptimeSeconds: typeof json.uptime === 'number' ? json.uptime : undefined,
|
|
243
|
+
};
|
|
253
244
|
}
|
|
254
245
|
catch {
|
|
255
|
-
|
|
246
|
+
return { running: false };
|
|
256
247
|
}
|
|
257
|
-
const updated = upsertWatcherContent(current, menu);
|
|
258
|
-
if (updated !== current) {
|
|
259
|
-
await writeFile(toolsPath, updated, 'utf8');
|
|
260
|
-
lastWrittenMenu = menu;
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
263
|
-
lastWrittenMenu = menu;
|
|
264
|
-
return false;
|
|
265
248
|
}
|
|
266
249
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
250
|
+
* Create the watcher `JeevesComponent` descriptor.
|
|
251
|
+
*
|
|
252
|
+
* @param options - API URL and plugin version.
|
|
253
|
+
* @returns A fully configured component descriptor ready for `createComponentWriter()`.
|
|
269
254
|
*/
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
255
|
+
function createWatcherComponent(options) {
|
|
256
|
+
const { apiUrl, pluginVersion } = options;
|
|
257
|
+
const getContent = createAsyncContentCache({
|
|
258
|
+
fetch: async () => generateWatcherMenu(apiUrl),
|
|
259
|
+
placeholder: '> Initializing watcher menu...',
|
|
274
260
|
});
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
261
|
+
return {
|
|
262
|
+
name: 'watcher',
|
|
263
|
+
version: pluginVersion,
|
|
264
|
+
sectionId: 'Watcher',
|
|
265
|
+
refreshIntervalSeconds: 71,
|
|
266
|
+
generateToolsContent: getContent,
|
|
267
|
+
serviceCommands: {
|
|
268
|
+
async stop() {
|
|
269
|
+
await execFile('jeeves-watcher', ['service', 'stop']);
|
|
270
|
+
},
|
|
271
|
+
async uninstall() {
|
|
272
|
+
await execFile('jeeves-watcher', ['service', 'uninstall']);
|
|
273
|
+
},
|
|
274
|
+
async status() {
|
|
275
|
+
return getServiceStatus(apiUrl);
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
pluginCommands: {
|
|
279
|
+
async uninstall() {
|
|
280
|
+
await execFile('npx', [
|
|
281
|
+
'-y',
|
|
282
|
+
`@karmaniverous/${PLUGIN_ID}`,
|
|
283
|
+
'uninstall',
|
|
284
|
+
]);
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
/**
|
|
@@ -379,23 +379,23 @@ function registerWatcherTools(api, baseUrl) {
|
|
|
379
379
|
],
|
|
380
380
|
},
|
|
381
381
|
{
|
|
382
|
-
name: '
|
|
383
|
-
description: 'Query the
|
|
382
|
+
name: 'watcher_config',
|
|
383
|
+
description: 'Query the effective runtime config via JSONPath. Returns the full resolved merged document when no path is provided.',
|
|
384
384
|
parameters: {
|
|
385
385
|
type: 'object',
|
|
386
|
-
required: ['path'],
|
|
387
386
|
properties: {
|
|
388
|
-
path: {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
items: { type: 'string', enum: ['files', 'globals'] },
|
|
392
|
-
description: 'Resolution scopes to include (e.g., ["files"], ["globals"], or both).',
|
|
387
|
+
path: {
|
|
388
|
+
type: 'string',
|
|
389
|
+
description: 'JSONPath expression (optional).',
|
|
393
390
|
},
|
|
394
391
|
},
|
|
395
392
|
},
|
|
396
393
|
buildRequest: (params) => {
|
|
397
|
-
const
|
|
398
|
-
|
|
394
|
+
const path = params.path;
|
|
395
|
+
if (path) {
|
|
396
|
+
return [`/config?path=${encodeURIComponent(path)}`];
|
|
397
|
+
}
|
|
398
|
+
return ['/config'];
|
|
399
399
|
},
|
|
400
400
|
},
|
|
401
401
|
{
|
|
@@ -443,14 +443,29 @@ function registerWatcherTools(api, baseUrl) {
|
|
|
443
443
|
properties: {
|
|
444
444
|
scope: {
|
|
445
445
|
type: 'string',
|
|
446
|
-
enum: ['rules', 'full'],
|
|
447
|
-
description: 'Reindex scope: "rules" (default) re-applies inference rules; "full" re-embeds everything.',
|
|
446
|
+
enum: ['rules', 'full', 'issues', 'path', 'prune'],
|
|
447
|
+
description: 'Reindex scope: "rules" (default) re-applies inference rules; "full" re-embeds everything; "issues" re-processes files with errors; "path" reindexes a specific file or directory (requires path parameter); "prune" deletes points for files no longer in watch scope.',
|
|
448
|
+
},
|
|
449
|
+
path: {
|
|
450
|
+
oneOf: [
|
|
451
|
+
{ type: 'string' },
|
|
452
|
+
{ type: 'array', items: { type: 'string' } },
|
|
453
|
+
],
|
|
454
|
+
description: 'Target file or directory path (required when scope is "path"). Accepts a single path or array of paths.',
|
|
455
|
+
},
|
|
456
|
+
dryRun: {
|
|
457
|
+
type: 'boolean',
|
|
458
|
+
description: 'When true, compute and return the blast area plan without executing. Returns counts by root showing impact.',
|
|
448
459
|
},
|
|
449
460
|
},
|
|
450
461
|
},
|
|
451
462
|
buildRequest: (params) => [
|
|
452
|
-
'/
|
|
453
|
-
{
|
|
463
|
+
'/reindex',
|
|
464
|
+
{
|
|
465
|
+
scope: params.scope ?? 'rules',
|
|
466
|
+
...(params.path ? { path: params.path } : {}),
|
|
467
|
+
...(params.dryRun ? { dryRun: true } : {}),
|
|
468
|
+
},
|
|
454
469
|
],
|
|
455
470
|
},
|
|
456
471
|
{
|
|
@@ -500,6 +515,22 @@ function registerWatcherTools(api, baseUrl) {
|
|
|
500
515
|
parameters: { type: 'object', properties: {} },
|
|
501
516
|
buildRequest: () => ['/issues'],
|
|
502
517
|
},
|
|
518
|
+
{
|
|
519
|
+
name: 'watcher_walk',
|
|
520
|
+
description: 'Walk watched filesystem paths with glob intersection. Returns matching file paths from all configured watch roots, applying watch.ignored and gitignore filtering.',
|
|
521
|
+
parameters: {
|
|
522
|
+
type: 'object',
|
|
523
|
+
required: ['globs'],
|
|
524
|
+
properties: {
|
|
525
|
+
globs: {
|
|
526
|
+
type: 'array',
|
|
527
|
+
items: { type: 'string' },
|
|
528
|
+
description: 'Glob patterns to intersect with watch paths (e.g., ["**/.meta/meta.json"]).',
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
buildRequest: (params) => ['/walk', { globs: params.globs }],
|
|
533
|
+
},
|
|
503
534
|
];
|
|
504
535
|
for (const tool of tools) {
|
|
505
536
|
registerApiTool(api, baseUrl, tool);
|
|
@@ -508,17 +539,52 @@ function registerWatcherTools(api, baseUrl) {
|
|
|
508
539
|
|
|
509
540
|
/**
|
|
510
541
|
* @module plugin
|
|
511
|
-
* OpenClaw plugin entry point. Registers all jeeves-watcher tools
|
|
542
|
+
* OpenClaw plugin entry point. Registers all jeeves-watcher tools and starts
|
|
543
|
+
* the managed content writer via `@karmaniverous/jeeves` core.
|
|
512
544
|
*/
|
|
545
|
+
/**
|
|
546
|
+
* Read the plugin version from the nearest package.json.
|
|
547
|
+
*
|
|
548
|
+
* @remarks
|
|
549
|
+
* Uses `createRequire(import.meta.url)` — the same pattern as
|
|
550
|
+
* `@karmaniverous/jeeves` core. Works whether executed from
|
|
551
|
+
* `src/index.ts` (dev/test) or `dist/index.js` (built), since
|
|
552
|
+
* both are exactly one directory level below `package.json`.
|
|
553
|
+
*/
|
|
554
|
+
const require$1 = createRequire(import.meta.url);
|
|
555
|
+
const pkg = require$1('../package.json');
|
|
556
|
+
const PLUGIN_VERSION = pkg.version;
|
|
557
|
+
/** Resolve the workspace root from the OpenClaw plugin API. */
|
|
558
|
+
function resolveWorkspacePath(api) {
|
|
559
|
+
if (typeof api.resolvePath === 'function') {
|
|
560
|
+
return api.resolvePath('.');
|
|
561
|
+
}
|
|
562
|
+
return process.cwd();
|
|
563
|
+
}
|
|
564
|
+
/** Detect test environments to avoid timers and filesystem writes. */
|
|
565
|
+
function isTestEnv() {
|
|
566
|
+
return process.env.NODE_ENV === 'test' || process.env.VITEST !== undefined;
|
|
567
|
+
}
|
|
513
568
|
/** Register all jeeves-watcher tools with the OpenClaw plugin API. */
|
|
514
569
|
function register(api) {
|
|
515
|
-
const
|
|
516
|
-
registerWatcherTools(api,
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
//
|
|
521
|
-
|
|
570
|
+
const apiUrl = getApiUrl(api);
|
|
571
|
+
registerWatcherTools(api, apiUrl);
|
|
572
|
+
// Avoid timers + filesystem writes in unit tests.
|
|
573
|
+
if (isTestEnv())
|
|
574
|
+
return;
|
|
575
|
+
// Initialize jeeves-core for managed content writing.
|
|
576
|
+
init({
|
|
577
|
+
workspacePath: resolveWorkspacePath(api),
|
|
578
|
+
configRoot: getConfigRoot(api),
|
|
579
|
+
});
|
|
580
|
+
const component = createWatcherComponent({
|
|
581
|
+
apiUrl,
|
|
582
|
+
pluginVersion: PLUGIN_VERSION,
|
|
583
|
+
});
|
|
584
|
+
const writer = createComponentWriter(component, {
|
|
585
|
+
probeTimeoutMs: PROBE_TIMEOUT_MS,
|
|
586
|
+
});
|
|
587
|
+
writer.start();
|
|
522
588
|
}
|
|
523
589
|
|
|
524
590
|
export { register as default };
|
|
@@ -30,9 +30,7 @@ curl -X POST http://127.0.0.1:<PORT>/search \
|
|
|
30
30
|
-d '{"query": "search text", "limit": 5}'
|
|
31
31
|
|
|
32
32
|
# Query config
|
|
33
|
-
curl
|
|
34
|
-
-H "Content-Type: application/json" \
|
|
35
|
-
-d '{"path": "$.inferenceRules[*].name"}'
|
|
33
|
+
curl http://127.0.0.1:<PORT>/config
|
|
36
34
|
```
|
|
37
35
|
|
|
38
36
|
**Key endpoints:**
|
|
@@ -40,14 +38,15 @@ curl -X POST http://127.0.0.1:<PORT>/config/query \
|
|
|
40
38
|
|----------|--------|---------|
|
|
41
39
|
| `/status` | GET | Health check, uptime, collection stats |
|
|
42
40
|
| `/search` | POST | Semantic search (main query interface) |
|
|
43
|
-
| `/config
|
|
41
|
+
| `/config` | GET | Full resolved config; optional `?path=<jsonpath>` filter |
|
|
44
42
|
| `/config/validate` | POST | Validate candidate config |
|
|
45
43
|
| `/config/apply` | POST | Apply config changes |
|
|
46
|
-
| `/
|
|
44
|
+
| `/reindex` | POST | Trigger reindex |
|
|
47
45
|
| `/metadata` | POST | Enrich document metadata |
|
|
48
46
|
| `/scan` | POST | Filter-only point query (no embeddings) |
|
|
47
|
+
| `/walk` | POST | Filesystem walk with glob intersection |
|
|
49
48
|
| `/issues` | GET | Runtime embedding failures |
|
|
50
|
-
| `/rules/register` | POST | Register virtual inference rules |
|
|
49
|
+
| `/rules/register` | POST | Register virtual inference rules (auto-triggers rules reindex) |
|
|
51
50
|
| `/rules/unregister` | DELETE | Remove virtual rules by source |
|
|
52
51
|
| `/points/delete` | POST | Delete points matching a Qdrant filter |
|
|
53
52
|
|
|
@@ -73,6 +72,21 @@ You have access to a **semantic archive** of your human's working world. Documen
|
|
|
73
72
|
|
|
74
73
|
**The principle:** Memory-core is your curated highlights. The watcher archive is your perfect recall. Use memory first for speed and signal, but never let its narrow scope be the ceiling of what you can remember.
|
|
75
74
|
|
|
75
|
+
## ⚠️ Embedding Cost — Hard Behavioral Gate
|
|
76
|
+
|
|
77
|
+
**The watcher embeds every file it sees.** Embedding is the most expensive operation in the pipeline — each file triggers a Gemini API call. The index may contain hundreds of thousands of points. Any action that causes the watcher to process a large number of files has real, potentially significant cost.
|
|
78
|
+
|
|
79
|
+
**STOP and ask for a human decision before:**
|
|
80
|
+
|
|
81
|
+
- **Renaming, moving, or reorganizing watched directories.** A directory rename at the filesystem level is cheap — chokidar sees unlink + add events, but the content hasn't changed. However, if a rename is blocked (e.g., by a file lock from the watcher or another service), **do NOT work around it by creating new directories and copying/moving files**. That creates duplicate embeddings at the new paths while the old paths still exist. Instead: stop the blocking service, perform the rename, restart. This is a 30-second operation. The workaround can cost thousands of API calls.
|
|
82
|
+
- **Changing `watch.paths` to add large directory trees.** Adding a new watch root triggers initial indexing of every matching file under it.
|
|
83
|
+
- **Running `scope: "full"` reindex.** This re-embeds the entire index. Use `scope: "rules"` for inference rule changes (zero embedding cost — only metadata reapplication).
|
|
84
|
+
- **Any bulk file operation** (mass copy, mass move, template-based file generation) under watched paths.
|
|
85
|
+
|
|
86
|
+
**The right instinct when blocked:** If a filesystem operation fails because a service has a lock, the correct response is to stop the service, do the operation, and restart — NOT to find a creative workaround. Creative workarounds in the presence of a live watcher are how you accidentally trigger re-embedding of tens of thousands of files.
|
|
87
|
+
|
|
88
|
+
**Cost context:** A single file embedding costs a Gemini API call. 1,000 unnecessary embeddings is a noticeable cost. 100,000 unnecessary embeddings (e.g., copying an entire domain directory to a new path) is a billing event worth flagging.
|
|
89
|
+
|
|
76
90
|
## Plugin Installation
|
|
77
91
|
|
|
78
92
|
```
|
|
@@ -350,12 +364,17 @@ Set or update metadata on a document.
|
|
|
350
364
|
- `metadata` (object, required) — key-value metadata to merge
|
|
351
365
|
|
|
352
366
|
### `watcher_status`
|
|
353
|
-
Service health check. Returns uptime, collection stats, reindex status.
|
|
367
|
+
Service health check. Returns uptime, version, collection stats, reindex status, and initial scan progress.
|
|
354
368
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
- `
|
|
358
|
-
|
|
369
|
+
After a service restart, the `initialScan` field shows scan progress:
|
|
370
|
+
- `active: true` — filesystem walk in progress; `filesMatched` and `filesEnqueued` grow until chokidar completes
|
|
371
|
+
- `active: false` with `completedAt`/`durationMs` — scan finished
|
|
372
|
+
|
|
373
|
+
Use this to determine if the service is still initializing after a restart.
|
|
374
|
+
|
|
375
|
+
### `watcher_config`
|
|
376
|
+
Query the effective runtime config via JSONPath. Returns the full resolved merged document when no path is provided.
|
|
377
|
+
- `path` (string, optional) — JSONPath expression
|
|
359
378
|
|
|
360
379
|
### `watcher_validate`
|
|
361
380
|
Validate config and optionally test file paths.
|
|
@@ -371,10 +390,38 @@ Apply config changes atomically.
|
|
|
371
390
|
Validates, writes to disk, and triggers configured reindex behavior. Returns validation errors if invalid.
|
|
372
391
|
|
|
373
392
|
### `watcher_reindex`
|
|
374
|
-
Trigger a reindex.
|
|
375
|
-
|
|
393
|
+
Trigger a reindex operation. All scopes return a `plan` object showing blast area before execution begins.
|
|
394
|
+
|
|
395
|
+
**Parameters:**
|
|
396
|
+
- `scope` (string, optional) — Reindex scope. Default: `"rules"`. One of:
|
|
397
|
+
- `"rules"` — Re-apply inference rules to all watched files. No re-embedding. Lightweight.
|
|
398
|
+
- `"full"` — Re-extract text, re-embed, and re-apply rules for all watched files. Expensive.
|
|
399
|
+
- `"issues"` — Re-process only files that previously failed embedding (from `watcher_issues`).
|
|
400
|
+
- `"path"` — Re-embed a specific file or all files under a directory. Requires `path` parameter.
|
|
401
|
+
- `"prune"` — Delete Qdrant points for files no longer in watch scope (removed paths, gitignored files, stale data). No re-embedding. Pure cleanup.
|
|
402
|
+
- `path` (string, required when scope is `"path"`) — Target file or directory path.
|
|
403
|
+
- `dryRun` (boolean, optional) — When `true`, compute and return the blast area plan without executing. Returns synchronously.
|
|
376
404
|
|
|
377
|
-
|
|
405
|
+
**Response (normal):**
|
|
406
|
+
```json
|
|
407
|
+
{ "status": "started", "scope": "rules", "plan": { "total": 148000, "toProcess": 148000, "toDelete": 0, "byRoot": { "j:/domains": 95000, "j:/config": 3000 } } }
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Response (dryRun):**
|
|
411
|
+
```json
|
|
412
|
+
{ "status": "dry_run", "scope": "prune", "plan": { "total": 562000, "toProcess": 0, "toDelete": 2300, "byRoot": { "j:/jeeves/node_modules": 1800, "j:/jeeves/.bridge": 500 } } }
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Plan fields:**
|
|
416
|
+
- `total` — Total points (prune) or files (other scopes) examined.
|
|
417
|
+
- `toProcess` — Items to embed/re-apply rules (0 for prune).
|
|
418
|
+
- `toDelete` — Points to delete (prune only, 0 for others).
|
|
419
|
+
- `byRoot` — Counts grouped by watch root prefix. Shows where the impact concentrates.
|
|
420
|
+
|
|
421
|
+
**Guidance:**
|
|
422
|
+
- Use `dryRun: true` before any large-blast operation to preview impact.
|
|
423
|
+
- `prune` is safe — it only deletes orphaned points, never re-embeds. Use after changing watch paths, fixing gitignore, or cleaning up stale data.
|
|
424
|
+
- `prune` is NOT triggered by config-watch auto-reindex (too dangerous for auto-trigger).
|
|
378
425
|
|
|
379
426
|
|
|
380
427
|
### `watcher_scan`
|
|
@@ -417,6 +464,30 @@ do {
|
|
|
417
464
|
### `watcher_issues`
|
|
418
465
|
Get runtime embedding failures. Returns `{ filePath: IssueRecord }` showing files that failed and why.
|
|
419
466
|
|
|
467
|
+
### `watcher_walk`
|
|
468
|
+
Walk watched filesystem paths with glob intersection. Returns matching file paths from all configured watch roots.
|
|
469
|
+
- `globs` (string[], required) — glob patterns to intersect with watch paths
|
|
470
|
+
|
|
471
|
+
**Response:**
|
|
472
|
+
```json
|
|
473
|
+
{
|
|
474
|
+
"paths": ["j:/domains/foo/.meta/meta.json", "j:/domains/bar/.meta/meta.json"],
|
|
475
|
+
"matchedCount": 2,
|
|
476
|
+
"scannedRoots": ["j:/domains", "j:/config"]
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**Use cases:**
|
|
481
|
+
- Discover files matching a pattern across all watched directories (e.g., `["**/.meta/meta.json"]`)
|
|
482
|
+
- Enumerate files before rule registration to understand scope
|
|
483
|
+
- Find files that aren't yet indexed (no Qdrant dependency — works even before first embedding)
|
|
484
|
+
|
|
485
|
+
**Key differences from `watcher_scan`:**
|
|
486
|
+
- Walks the actual filesystem, not the Qdrant index
|
|
487
|
+
- No embedding or indexing required — works immediately after service start
|
|
488
|
+
- Returns file paths only (no metadata, no vectors)
|
|
489
|
+
- Applies `watch.ignored` and gitignore filtering automatically
|
|
490
|
+
|
|
420
491
|
## Query Planning: Scan vs Search
|
|
421
492
|
|
|
422
493
|
**Decision rule:** If the query has no semantic/natural-language dimension, use `watcher_scan`. If you need meaning-based similarity, use `watcher_search`.
|
|
@@ -523,7 +594,7 @@ Each result from `watcher_search` contains:
|
|
|
523
594
|
| `payload.content_hash` | string | Hash of the full document content |
|
|
524
595
|
| `payload.matched_rules` | string[] | Names of inference rules that matched |
|
|
525
596
|
|
|
526
|
-
Additional metadata fields depend on the deployment's inference rules (e.g., `domain`, `status`, `author`). Use `
|
|
597
|
+
Additional metadata fields depend on the deployment's inference rules (e.g., `domain`, `status`, `author`). Use `watcher_config` to discover available fields.
|
|
527
598
|
|
|
528
599
|
## Query Planning (Per Search Task)
|
|
529
600
|
|
|
@@ -531,7 +602,7 @@ Identify relevant rule(s) from the orientation model, then retrieve their schema
|
|
|
531
602
|
|
|
532
603
|
**Retrieve complete schema for a rule:**
|
|
533
604
|
```
|
|
534
|
-
|
|
605
|
+
watcher_config: path="$.inferenceRules[?(@.name=='jira-issue')].schema"
|
|
535
606
|
resolve=["files","globals"]
|
|
536
607
|
```
|
|
537
608
|
|
|
@@ -539,7 +610,7 @@ Returns the fully merged schema with properties, types, `set` provenance, `uiHin
|
|
|
539
610
|
|
|
540
611
|
**For select/multiselect fields without `enum` in schema:**
|
|
541
612
|
```
|
|
542
|
-
|
|
613
|
+
watcher_config: path="$.inferenceRules[?(@.name=='jira-issue')].values.status"
|
|
543
614
|
```
|
|
544
615
|
|
|
545
616
|
Retrieves valid filter values from the runtime values index (distinct values accumulated during embedding).
|
|
@@ -613,9 +684,9 @@ A consuming UI will necessarily compose simple single-field filters. The assista
|
|
|
613
684
|
|
|
614
685
|
### Score Interpretation
|
|
615
686
|
Use `scoreThresholds` from config (queried during orientation). Values are deployment-specific, constrained to [-1, 1]:
|
|
616
|
-
- `strong` — minimum score for a strong match
|
|
617
|
-
- `relevant` — minimum score for relevance
|
|
618
|
-
- `noise` — maximum score below which results are noise
|
|
687
|
+
- `strong` — minimum score for a strong match. **Action:** High confidence. Use these results directly.
|
|
688
|
+
- `relevant` — minimum score for relevance. **Action:** Likely useful but verify context before relying on them.
|
|
689
|
+
- `noise` — maximum score below which results are noise. **Action:** Discard. If all results fall below this threshold, broaden your query or try different terms.
|
|
619
690
|
|
|
620
691
|
### Chunk Grouping
|
|
621
692
|
Multiple results with the same `file_path` are chunks of one document. Read the full file for complete context.
|
|
@@ -623,7 +694,7 @@ Multiple results with the same `file_path` are chunks of one document. Read the
|
|
|
623
694
|
### Schema Lookup
|
|
624
695
|
Use `matched_rules` on results to look up applicable schemas for metadata interpretation:
|
|
625
696
|
```
|
|
626
|
-
|
|
697
|
+
watcher_config: path="$.inferenceRules[?(@.name=='jira-issue')].schema"
|
|
627
698
|
resolve=["files","globals"]
|
|
628
699
|
```
|
|
629
700
|
|
|
@@ -636,7 +707,7 @@ Search gives you chunks; use `read` with `file_path` for the complete document.
|
|
|
636
707
|
|
|
637
708
|
When uncertain whether a file is indexed, use the path test endpoint:
|
|
638
709
|
```
|
|
639
|
-
|
|
710
|
+
watcher_config: path="$.inferenceRules[?(@.name=='<rule>')].match"
|
|
640
711
|
```
|
|
641
712
|
|
|
642
713
|
Or check if a specific path would match:
|
|
@@ -672,15 +743,18 @@ Progress is reported via `watcher_status` (`reindex.filesProcessed` / `reindex.t
|
|
|
672
743
|
### When to Reindex
|
|
673
744
|
- **Rules scope** (`"rules"`): Changed rule matching patterns, set expressions, schema mappings. No re-embedding needed.
|
|
674
745
|
- **Full scope** (`"full"`): Changed embedding config, added watch paths, broad schema restructuring. Re-embeds everything.
|
|
746
|
+
- **Issues scope** (`"issues"`): After fixing the root cause of embedding failures (permissions, encoding, file format). Re-processes only failed files.
|
|
747
|
+
- **Path scope** (`"path"`): Edited files in a specific directory and want to force re-embedding without a full reindex. Or a single file's embedding looks wrong.
|
|
748
|
+
- **Prune scope** (`"prune"`): After changing `watch.paths`, adding gitignore rules, or discovering stale/orphaned points (e.g., indexed `node_modules`). Deletes points for out-of-scope files. Always `dryRun: true` first to preview.
|
|
675
749
|
|
|
676
750
|
---
|
|
677
751
|
|
|
678
752
|
## Diagnostics
|
|
679
753
|
|
|
680
754
|
### Escalation Path
|
|
681
|
-
1. `watcher_status` — is the service healthy? Is a reindex running?
|
|
755
|
+
1. `watcher_status` — is the service healthy? Is a reindex running? Is the initial scan still active?
|
|
682
756
|
2. `watcher_issues` — what files are failing and why?
|
|
683
|
-
3. `
|
|
757
|
+
3. `watcher_config` with `$.issues` — same data via JSONPath
|
|
684
758
|
4. Check logs at the configured log path
|
|
685
759
|
|
|
686
760
|
### Error Categories
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugin/constants
|
|
3
|
+
* Shared constants for the OpenClaw plugin package.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Imported by both the plugin bundle (`index.ts`) and the CLI bundle
|
|
7
|
+
* (`cli.ts`). Rollup inlines these into each output independently.
|
|
8
|
+
*/
|
|
9
|
+
/** Plugin identifier used in OpenClaw config and extensions directory. */
|
|
10
|
+
export declare const PLUGIN_ID = "jeeves-watcher-openclaw";
|
|
11
|
+
/** Default watcher API base URL. */
|
|
12
|
+
export declare const DEFAULT_API_URL = "http://127.0.0.1:1936";
|
|
13
|
+
/** Default platform config root path. */
|
|
14
|
+
export declare const DEFAULT_CONFIG_ROOT = "j:/config";
|
|
15
|
+
/** Default Qdrant health check URL (used in diagnostic output). */
|
|
16
|
+
export declare const DEFAULT_QDRANT_URL = "http://127.0.0.1:6333";
|
|
17
|
+
/** Timeout in milliseconds for service health probes. */
|
|
18
|
+
export declare const PROBE_TIMEOUT_MS = 1500;
|
package/dist/src/helpers.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ export interface PluginApi {
|
|
|
11
11
|
}>;
|
|
12
12
|
};
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a path relative to the OpenClaw workspace.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* Present on newer OpenClaw builds; optional for backwards compatibility.
|
|
19
|
+
*/
|
|
20
|
+
resolvePath?: (input: string) => string;
|
|
14
21
|
registerTool(tool: {
|
|
15
22
|
name: string;
|
|
16
23
|
description: string;
|
|
@@ -19,15 +26,6 @@ export interface PluginApi {
|
|
|
19
26
|
}, options?: {
|
|
20
27
|
optional?: boolean;
|
|
21
28
|
}): void;
|
|
22
|
-
/**
|
|
23
|
-
* Optional internal hook registration (available on newer OpenClaw builds).
|
|
24
|
-
* We keep this optional to preserve compatibility.
|
|
25
|
-
*/
|
|
26
|
-
registerHook?: (event: string | string[], handler: (event: unknown) => Promise<void> | void, opts?: {
|
|
27
|
-
name?: string;
|
|
28
|
-
description?: string;
|
|
29
|
-
register?: boolean;
|
|
30
|
-
}) => void;
|
|
31
29
|
}
|
|
32
30
|
/** Result shape returned by each tool execution. */
|
|
33
31
|
export interface ToolResult {
|
|
@@ -39,12 +37,10 @@ export interface ToolResult {
|
|
|
39
37
|
}
|
|
40
38
|
/** Resolve the watcher API base URL from plugin config. */
|
|
41
39
|
export declare function getApiUrl(api: PluginApi): string;
|
|
42
|
-
/** Resolve the
|
|
43
|
-
export declare function
|
|
40
|
+
/** Resolve the platform config root path from plugin config. */
|
|
41
|
+
export declare function getConfigRoot(api: PluginApi): string;
|
|
44
42
|
/** Format a successful tool result. */
|
|
45
43
|
export declare function ok(data: unknown): ToolResult;
|
|
46
|
-
/** Format an error tool result. */
|
|
47
|
-
export declare function fail(error: unknown): ToolResult;
|
|
48
44
|
/** Format a connection error with actionable guidance. */
|
|
49
45
|
export declare function connectionFail(error: unknown, baseUrl: string): ToolResult;
|
|
50
46
|
/** Fetch JSON from a URL, throwing on non-OK responses. */
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module plugin
|
|
3
|
-
* OpenClaw plugin entry point. Registers all jeeves-watcher tools
|
|
3
|
+
* OpenClaw plugin entry point. Registers all jeeves-watcher tools and starts
|
|
4
|
+
* the managed content writer via `@karmaniverous/jeeves` core.
|
|
4
5
|
*/
|
|
5
6
|
import type { PluginApi } from './helpers.js';
|
|
6
7
|
/** Register all jeeves-watcher tools with the OpenClaw plugin API. */
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { type PluginApi } from './helpers.js';
|
|
2
1
|
/**
|
|
3
2
|
* Fetches data from the watcher API and generates a Markdown menu string.
|
|
4
3
|
* The string is platform-agnostic and safe to inject into TOOLS.md.
|
|
5
4
|
*/
|
|
6
5
|
export declare function generateWatcherMenu(apiUrl: string): Promise<string>;
|
|
7
|
-
/**
|
|
8
|
-
* Hook handler for agent:bootstrap.
|
|
9
|
-
* Injects/updates the Watcher Menu into the TOOLS.md payload.
|
|
10
|
-
*/
|
|
11
|
-
export declare function handleAgentBootstrap(event: unknown, api: PluginApi): Promise<void>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugin/watcherComponent
|
|
3
|
+
* Jeeves component integration for the watcher OpenClaw plugin.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Uses `createAsyncContentCache()` from core to bridge the sync/async gap:
|
|
7
|
+
* `generateToolsContent()` must be synchronous, but the watcher menu requires
|
|
8
|
+
* async HTTP calls. The cache triggers a background refresh on each call and
|
|
9
|
+
* returns the most recent successful result.
|
|
10
|
+
*/
|
|
11
|
+
import { type JeevesComponent } from '@karmaniverous/jeeves';
|
|
12
|
+
/** Options for creating the watcher component descriptor. */
|
|
13
|
+
interface CreateWatcherComponentOptions {
|
|
14
|
+
/** Base URL of the jeeves-watcher HTTP API. */
|
|
15
|
+
apiUrl: string;
|
|
16
|
+
/** Plugin package version. */
|
|
17
|
+
pluginVersion: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create the watcher `JeevesComponent` descriptor.
|
|
21
|
+
*
|
|
22
|
+
* @param options - API URL and plugin version.
|
|
23
|
+
* @returns A fully configured component descriptor ready for `createComponentWriter()`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createWatcherComponent(options: CreateWatcherComponentOptions): JeevesComponent;
|
|
26
|
+
export {};
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "jeeves-watcher-openclaw",
|
|
3
3
|
"name": "Jeeves Watcher",
|
|
4
4
|
"description": "Semantic search, metadata enrichment, and instance administration for a jeeves-watcher deployment.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.8.0",
|
|
6
6
|
"skills": [
|
|
7
7
|
"dist/skills/jeeves-watcher"
|
|
8
8
|
],
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
"type": "string",
|
|
15
15
|
"description": "jeeves-watcher API base URL",
|
|
16
16
|
"default": "http://127.0.0.1:1936"
|
|
17
|
+
},
|
|
18
|
+
"configRoot": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Platform config root path (e.g., j:/config)",
|
|
21
|
+
"default": "j:/config"
|
|
17
22
|
}
|
|
18
23
|
}
|
|
19
24
|
},
|
|
@@ -21,6 +26,10 @@
|
|
|
21
26
|
"apiUrl": {
|
|
22
27
|
"label": "Watcher API URL",
|
|
23
28
|
"placeholder": "http://127.0.0.1:1936"
|
|
29
|
+
},
|
|
30
|
+
"configRoot": {
|
|
31
|
+
"label": "Config root",
|
|
32
|
+
"placeholder": "j:/config"
|
|
24
33
|
}
|
|
25
34
|
}
|
|
26
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karmaniverous/jeeves-watcher-openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"author": "Jason Williscroft",
|
|
5
5
|
"description": "OpenClaw plugin for jeeves-watcher — semantic search and metadata enrichment tools",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -50,20 +50,24 @@
|
|
|
50
50
|
},
|
|
51
51
|
"auto-changelog": {
|
|
52
52
|
"output": "CHANGELOG.md",
|
|
53
|
+
"tagPrefix": "openclaw/",
|
|
53
54
|
"unreleased": true,
|
|
54
55
|
"commitLimit": false,
|
|
55
56
|
"hideCredit": true
|
|
56
57
|
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@karmaniverous/jeeves": "^0.1.1"
|
|
60
|
+
},
|
|
57
61
|
"devDependencies": {
|
|
58
|
-
"@dotenvx/dotenvx": "^1.
|
|
62
|
+
"@dotenvx/dotenvx": "^1.55.1",
|
|
59
63
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
60
64
|
"auto-changelog": "^2.5.0",
|
|
61
65
|
"cross-env": "^10.1.0",
|
|
62
|
-
"knip": "^5.
|
|
66
|
+
"knip": "^5.87.0",
|
|
63
67
|
"release-it": "^19.2.4",
|
|
64
68
|
"rollup": "^4.59.0",
|
|
65
69
|
"tslib": "^2.8.1",
|
|
66
|
-
"vitest": "^4.0
|
|
70
|
+
"vitest": "^4.1.0"
|
|
67
71
|
},
|
|
68
72
|
"scripts": {
|
|
69
73
|
"build:plugin": "rimraf dist && cross-env NO_COLOR=1 rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module plugin/toolsWriter
|
|
3
|
-
* Writes the Watcher menu section directly to TOOLS.md on disk.
|
|
4
|
-
* Replaces the agent:bootstrap hook approach which was unreliable due to
|
|
5
|
-
* OpenClaw's clearInternalHooks() wiping plugin-registered hooks on startup.
|
|
6
|
-
*/
|
|
7
|
-
import { type PluginApi } from './helpers.js';
|
|
8
|
-
/**
|
|
9
|
-
* Start the periodic TOOLS.md writer.
|
|
10
|
-
* Writes immediately on startup, then refreshes every REFRESH_INTERVAL_MS.
|
|
11
|
-
*/
|
|
12
|
-
export declare function startToolsWriter(api: PluginApi): void;
|
|
13
|
-
/**
|
|
14
|
-
* Force an immediate refresh (e.g., after watcher_config_apply).
|
|
15
|
-
*/
|
|
16
|
-
export declare function forceRefreshToolsMd(api: PluginApi): Promise<void>;
|
|
17
|
-
/**
|
|
18
|
-
* Stop the periodic writer (for cleanup).
|
|
19
|
-
*/
|
|
20
|
-
export declare function stopToolsWriter(): void;
|