@karmaniverous/jeeves-watcher-openclaw 0.5.7 → 0.6.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 CHANGED
@@ -57,6 +57,7 @@ On startup, the plugin writes a `## Watcher` section to `TOOLS.md` in the agent'
57
57
  | `watcher_validate` | Validate a watcher configuration |
58
58
  | `watcher_config_apply` | Apply a new configuration |
59
59
  | `watcher_reindex` | Trigger a full reindex |
60
+ | `watcher_scan` | Filter-only point query with cursor pagination |
60
61
  | `watcher_issues` | List indexing issues and errors |
61
62
 
62
63
  ## Documentation
package/dist/index.js CHANGED
@@ -54,7 +54,7 @@ function connectionFail(error, baseUrl) {
54
54
  return fail(error);
55
55
  }
56
56
  /** Fetch JSON from a URL, throwing on non-OK responses. */
57
- async function fetchJson$1(url, init) {
57
+ async function fetchJson(url, init) {
58
58
  const res = await fetch(url, init);
59
59
  if (!res.ok) {
60
60
  throw new Error('HTTP ' + String(res.status) + ': ' + (await res.text()));
@@ -63,20 +63,13 @@ async function fetchJson$1(url, init) {
63
63
  }
64
64
  /** POST JSON to a URL and return parsed response. */
65
65
  async function postJson(url, body) {
66
- return fetchJson$1(url, {
66
+ return fetchJson(url, {
67
67
  method: 'POST',
68
68
  headers: { 'Content-Type': 'application/json' },
69
69
  body: JSON.stringify(body),
70
70
  });
71
71
  }
72
72
 
73
- async function fetchJson(url, init) {
74
- const res = await fetch(url, init);
75
- if (!res.ok) {
76
- throw new Error(`HTTP ${String(res.status)}: ${await res.text()}`);
77
- }
78
- return res.json();
79
- }
80
73
  /**
81
74
  * Fetches data from the watcher API and generates a Markdown menu string.
82
75
  * The string is platform-agnostic and safe to inject into TOOLS.md.
@@ -87,7 +80,7 @@ async function generateWatcherMenu(apiUrl) {
87
80
  const watchPaths = [];
88
81
  const ignoredPaths = [];
89
82
  try {
90
- const [statusRes, rulesRes, pathsRes, ignoredRes] = await Promise.all([
83
+ const [statusRes, rulesRes, pathsRes, ignoredRes] = (await Promise.all([
91
84
  fetchJson(`${apiUrl}/status`),
92
85
  fetchJson(`${apiUrl}/config/query`, {
93
86
  method: 'POST',
@@ -104,7 +97,7 @@ async function generateWatcherMenu(apiUrl) {
104
97
  headers: { 'Content-Type': 'application/json' },
105
98
  body: JSON.stringify({ path: '$.watch.ignored[*]' }),
106
99
  }),
107
- ]);
100
+ ]));
108
101
  pointCount = statusRes.collection?.pointCount ?? 0;
109
102
  if (Array.isArray(rulesRes.result)) {
110
103
  for (const rule of rulesRes.result) {
@@ -128,11 +121,36 @@ async function generateWatcherMenu(apiUrl) {
128
121
  }
129
122
  }
130
123
  catch {
131
- return '*Watcher service is currently unreachable.*';
124
+ let qdrantStatus = '*Unknown*';
125
+ try {
126
+ // Assuming Qdrant runs locally on the default port
127
+ const res = await fetch('http://127.0.0.1:6333/healthz', {
128
+ signal: AbortSignal.timeout(1000),
129
+ });
130
+ qdrantStatus = res.ok ? 'Running' : 'Error';
131
+ }
132
+ catch {
133
+ qdrantStatus = 'Down / Unreachable';
134
+ }
135
+ return `> **ACTION REQUIRED: Watcher service is unreachable.**
136
+ > The jeeves-watcher service API at ${apiUrl} is down or not installed.
137
+ > **Diagnostic Info:**
138
+ > - Watcher API (${apiUrl}): Unreachable
139
+ > - Qdrant (http://127.0.0.1:6333): ${qdrantStatus}
140
+ >
141
+ > **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
+ }
143
+ if (pointCount === 0) {
144
+ return `> **ACTION REQUIRED: Watcher service is empty.**
145
+ > The jeeves-watcher service is running at ${apiUrl} but has no indexed data.
146
+ > The service has either just been installed (no configuration/data), or indexing hasn't run.
147
+ >
148
+ > **DO NOT** ignore this. Read the \`jeeves-watcher\` skill's **Bootstrap** section immediately and walk the user through the setup process to configure and reindex the workspace.`;
132
149
  }
133
150
  const lines = [
134
151
  `This environment includes a semantic search index (\`watcher_search\`) covering ${pointCount.toLocaleString()} document chunks.`,
135
152
  '**Escalation Rule:** Use `memory_search` for personal operational notes, decisions, and rules. Escalate to `watcher_search` when memory is thin, or when searching the broader archive (tickets, docs, code). ALWAYS use `watcher_search` BEFORE filesystem commands (exec, grep) when looking for information that matches the indexed categories below.',
153
+ '**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.',
136
154
  '**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.',
137
155
  '',
138
156
  '### Score Interpretation:',
@@ -285,7 +303,7 @@ function registerApiTool(api, baseUrl, config) {
285
303
  const url = `${baseUrl}${endpoint}`;
286
304
  const data = body !== undefined
287
305
  ? await postJson(url, body)
288
- : await fetchJson$1(url);
306
+ : await fetchJson(url);
289
307
  return ok(data);
290
308
  }
291
309
  catch (error) {
@@ -303,7 +321,7 @@ function pickDefined(params, keys) {
303
321
  }
304
322
  return body;
305
323
  }
306
- /** Register all 8 watcher_* tools with the OpenClaw plugin API. */
324
+ /** Register all 9 watcher_* tools with the OpenClaw plugin API. */
307
325
  function registerWatcherTools(api, baseUrl) {
308
326
  const tools = [
309
327
  {
@@ -435,6 +453,47 @@ function registerWatcherTools(api, baseUrl) {
435
453
  { scope: params.scope ?? 'rules' },
436
454
  ],
437
455
  },
456
+ {
457
+ name: 'watcher_scan',
458
+ description: 'Filter-only point query without vector search. Returns metadata for points matching a Qdrant filter. Use for structural queries: file enumeration, staleness checks, delta computation. Use watcher_search for semantic/similarity queries.',
459
+ parameters: {
460
+ type: 'object',
461
+ required: ['filter'],
462
+ properties: {
463
+ filter: {
464
+ type: 'object',
465
+ description: 'Qdrant filter object (required).',
466
+ },
467
+ limit: {
468
+ type: 'number',
469
+ description: 'Page size (default 100, max 1000).',
470
+ },
471
+ cursor: {
472
+ type: 'string',
473
+ description: 'Opaque cursor from previous response for pagination.',
474
+ },
475
+ fields: {
476
+ type: 'array',
477
+ items: { type: 'string' },
478
+ description: 'Payload fields to return (projection).',
479
+ },
480
+ countOnly: {
481
+ type: 'boolean',
482
+ description: 'If true, return { count } instead of points.',
483
+ },
484
+ },
485
+ },
486
+ buildRequest: (params) => {
487
+ const body = pickDefined(params, [
488
+ 'filter',
489
+ 'limit',
490
+ 'cursor',
491
+ 'fields',
492
+ 'countOnly',
493
+ ]);
494
+ return ['/scan', body];
495
+ },
496
+ },
438
497
  {
439
498
  name: 'watcher_issues',
440
499
  description: 'Get runtime embedding failures. Shows files that failed processing and why.',
@@ -45,6 +45,7 @@ curl -X POST http://127.0.0.1:<PORT>/config/query \
45
45
  | `/config/apply` | POST | Apply config changes |
46
46
  | `/config-reindex` | POST | Trigger reindex |
47
47
  | `/metadata` | POST | Enrich document metadata |
48
+ | `/scan` | POST | Filter-only point query (no embeddings) |
48
49
  | `/issues` | GET | Runtime embedding failures |
49
50
  | `/rules/register` | POST | Register virtual inference rules |
50
51
  | `/rules/unregister` | DELETE | Remove virtual rules by source |
@@ -78,7 +79,11 @@ You have access to a **semantic archive** of your human's working world. Documen
78
79
  npx @karmaniverous/jeeves-watcher-openclaw install
79
80
  ```
80
81
 
81
- This copies the plugin to OpenClaw's extensions directory and patches `openclaw.json` to register it. Restart the gateway to load the plugin.
82
+ This copies the plugin to OpenClaw's extensions directory and patches `openclaw.json` to register it.
83
+
84
+ **Important:** Add `"jeeves-watcher-openclaw"` to the `tools.allow` array in `openclaw.json` so the agent can use the plugin's tools.
85
+
86
+ Restart the gateway to load the plugin.
82
87
 
83
88
  To remove:
84
89
  ```
@@ -371,9 +376,83 @@ Trigger a reindex.
371
376
 
372
377
  Rules scope re-applies inference rules without re-embedding (lightweight). Full scope re-processes all files.
373
378
 
379
+
380
+ ### `watcher_scan`
381
+ Filter-only point query without vector search. Use for structural queries where the question has no semantic dimension.
382
+ - `filter` (object, required) — Qdrant filter object. Required to prevent accidental full-collection scans.
383
+ - `limit` (number, optional) — page size, default 100, max 1000
384
+ - `cursor` (string, optional) — opaque cursor from previous response for pagination
385
+ - `fields` (string[], optional) — payload fields to return (projection)
386
+ - `countOnly` (boolean, optional) — if true, return `{ count }` instead of points
387
+
388
+ **Response (normal):**
389
+ ```json
390
+ {
391
+ "points": [{ "id": "uuid", "payload": { ... } }],
392
+ "cursor": "opaque-string-or-null"
393
+ }
394
+ ```
395
+
396
+ **Response (countOnly):**
397
+ ```json
398
+ { "count": 1234 }
399
+ ```
400
+
401
+ **Key differences from `watcher_search`:**
402
+ - No `query` parameter — does NOT use embeddings
403
+ - No `score` field — results are unranked filter matches
404
+ - Cursor-based pagination (not offset-based)
405
+ - Zero cost per call beyond Qdrant's filtered scroll
406
+
407
+ **Pagination pattern:**
408
+ ```
409
+ let cursor = undefined;
410
+ do {
411
+ const result = await watcher_scan({ filter, limit: 100, cursor });
412
+ // process result.points
413
+ cursor = result.cursor;
414
+ } while (cursor);
415
+ ```
416
+
374
417
  ### `watcher_issues`
375
418
  Get runtime embedding failures. Returns `{ filePath: IssueRecord }` showing files that failed and why.
376
419
 
420
+ ## Query Planning: Scan vs Search
421
+
422
+ **Decision rule:** If the query has no semantic/natural-language dimension, use `watcher_scan`. If you need meaning-based similarity, use `watcher_search`.
423
+
424
+ | Use `watcher_scan` | Use `watcher_search` |
425
+ |---------------------|----------------------|
426
+ | "List all files in domain X" | "Find documents about authentication" |
427
+ | "Files modified after timestamp T" | "What discusses rate limiting?" |
428
+ | "Enumerate paths under prefix P" | "Prior conversations about deployment" |
429
+ | "Count files matching a condition" | "Related tickets to this issue" |
430
+ | "Staleness detection / delta computation" | "What happened in last week's meetings?" |
431
+
432
+ **Scan-specific filter examples:**
433
+
434
+ **Domain enumeration:**
435
+ ```json
436
+ { "must": [{ "key": "domains", "match": { "value": "email" } }] }
437
+ ```
438
+
439
+ **Modified after timestamp:**
440
+ ```json
441
+ { "must": [{ "key": "modified_at", "range": { "gte": 1772800000 } }] }
442
+ ```
443
+
444
+ **Path prefix matching:**
445
+ ```json
446
+ { "must": [{ "key": "file_path", "match": { "text": "j:/domains/jira" } }] }
447
+ ```
448
+
449
+ **Count files in a domain (no point data transferred):**
450
+ ```
451
+ watcher_scan: filter={"must":[{"key":"domains","match":{"value":"github"}}]}, countOnly=true
452
+ ```
453
+
454
+ ---
455
+
377
456
  ## Qdrant Filter Syntax
378
457
 
379
458
  Filters use Qdrant's native JSON filter format, passed as the `filter` parameter to `watcher_search`.
@@ -3,5 +3,5 @@
3
3
  * Watcher tool registrations (watcher_* tools) for the OpenClaw plugin.
4
4
  */
5
5
  import { type PluginApi } from './helpers.js';
6
- /** Register all 8 watcher_* tools with the OpenClaw plugin API. */
6
+ /** Register all 9 watcher_* tools with the OpenClaw plugin API. */
7
7
  export declare function registerWatcherTools(api: PluginApi, baseUrl: string): void;
@@ -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.7",
5
+ "version": "0.6.0",
6
6
  "skills": [
7
7
  "dist/skills/jeeves-watcher"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/jeeves-watcher-openclaw",
3
- "version": "0.5.7",
3
+ "version": "0.6.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",
@@ -55,7 +55,7 @@
55
55
  "hideCredit": true
56
56
  },
57
57
  "devDependencies": {
58
- "@dotenvx/dotenvx": "^1.52.0",
58
+ "@dotenvx/dotenvx": "^1.54.1",
59
59
  "@rollup/plugin-typescript": "^12.3.0",
60
60
  "auto-changelog": "^2.5.0",
61
61
  "cross-env": "^10.1.0",