@karmaniverous/jeeves-watcher-openclaw 0.4.2 → 0.5.1

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/dist/index.js CHANGED
@@ -3,11 +3,21 @@
3
3
  * Shared types and utility functions for the OpenClaw plugin tool registrations.
4
4
  */
5
5
  const DEFAULT_API_URL = 'http://127.0.0.1:1936';
6
+ const DEFAULT_CACHE_TTL_MS = 30000;
7
+ /** Extract plugin config from the API object */
8
+ function getPluginConfig(api) {
9
+ return api.config?.plugins?.entries?.['jeeves-watcher-openclaw']?.config;
10
+ }
6
11
  /** Resolve the watcher API base URL from plugin config. */
7
12
  function getApiUrl(api) {
8
- const url = api.config?.plugins?.entries?.['jeeves-watcher-openclaw']?.config?.apiUrl;
13
+ const url = getPluginConfig(api)?.apiUrl;
9
14
  return typeof url === 'string' ? url : DEFAULT_API_URL;
10
15
  }
16
+ /** Resolve the cache TTL for plugin hooks from config. */
17
+ function getCacheTtlMs(api) {
18
+ const ttl = getPluginConfig(api)?.cacheTtlMs;
19
+ return typeof ttl === 'number' ? ttl : DEFAULT_CACHE_TTL_MS;
20
+ }
11
21
  /** Format a successful tool result. */
12
22
  function ok(data) {
13
23
  return {
@@ -18,7 +28,7 @@ function ok(data) {
18
28
  function fail(error) {
19
29
  const message = error instanceof Error ? error.message : String(error);
20
30
  return {
21
- content: [{ type: 'text', text: `Error: ${message}` }],
31
+ content: [{ type: 'text', text: 'Error: ' + message }],
22
32
  isError: true,
23
33
  };
24
34
  }
@@ -35,7 +45,7 @@ function connectionFail(error, baseUrl) {
35
45
  {
36
46
  type: 'text',
37
47
  text: [
38
- `Watcher service not reachable at ${baseUrl}.`,
48
+ 'Watcher service not reachable at ' + baseUrl + '.',
39
49
  'Either start the watcher service, or if it runs on a different port,',
40
50
  'set plugins.entries.jeeves-watcher-openclaw.config.apiUrl in openclaw.json.',
41
51
  ].join('\n'),
@@ -47,22 +57,180 @@ function connectionFail(error, baseUrl) {
47
57
  return fail(error);
48
58
  }
49
59
  /** Fetch JSON from a URL, throwing on non-OK responses. */
50
- async function fetchJson(url, init) {
60
+ async function fetchJson$1(url, init) {
51
61
  const res = await fetch(url, init);
52
62
  if (!res.ok) {
53
- throw new Error(`HTTP ${String(res.status)}: ${await res.text()}`);
63
+ throw new Error('HTTP ' + String(res.status) + ': ' + (await res.text()));
54
64
  }
55
65
  return res.json();
56
66
  }
57
67
  /** POST JSON to a URL and return parsed response. */
58
68
  async function postJson(url, body) {
59
- return fetchJson(url, {
69
+ return fetchJson$1(url, {
60
70
  method: 'POST',
61
71
  headers: { 'Content-Type': 'application/json' },
62
72
  body: JSON.stringify(body),
63
73
  });
64
74
  }
65
75
 
76
+ let menuCache = null;
77
+ function isAgentBootstrapEventContext(value) {
78
+ if (!value || typeof value !== 'object') {
79
+ return false;
80
+ }
81
+ const v = value;
82
+ return Array.isArray(v.bootstrapFiles);
83
+ }
84
+ async function fetchJson(url, init) {
85
+ const res = await fetch(url, init);
86
+ if (!res.ok) {
87
+ throw new Error(`HTTP ${String(res.status)}: ${await res.text()}`);
88
+ }
89
+ return res.json();
90
+ }
91
+ /**
92
+ * Fetches data from the watcher API and generates a Markdown menu string.
93
+ * The string is platform-agnostic and safe to inject into TOOLS.md.
94
+ */
95
+ async function generateWatcherMenu(apiUrl) {
96
+ let pointCount = 0;
97
+ const activeRules = [];
98
+ const watchPaths = [];
99
+ const ignoredPaths = [];
100
+ try {
101
+ const [statusRes, rulesRes, pathsRes, ignoredRes] = await Promise.all([
102
+ fetchJson(`${apiUrl}/status`),
103
+ fetchJson(`${apiUrl}/config/query`, {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({ path: '$.inferenceRules[*]' }),
107
+ }),
108
+ fetchJson(`${apiUrl}/config/query`, {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify({ path: '$.watch.paths[*]' }),
112
+ }),
113
+ fetchJson(`${apiUrl}/config/query`, {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json' },
116
+ body: JSON.stringify({ path: '$.watch.ignored[*]' }),
117
+ }),
118
+ ]);
119
+ pointCount = statusRes.collection?.pointCount ?? 0;
120
+ if (Array.isArray(rulesRes.result)) {
121
+ for (const rule of rulesRes.result) {
122
+ if (rule.name && rule.description) {
123
+ activeRules.push({ name: rule.name, description: rule.description });
124
+ }
125
+ }
126
+ }
127
+ if (Array.isArray(pathsRes.result)) {
128
+ for (const p of pathsRes.result) {
129
+ if (typeof p === 'string') {
130
+ watchPaths.push(p);
131
+ }
132
+ }
133
+ }
134
+ if (Array.isArray(ignoredRes.result)) {
135
+ for (const p of ignoredRes.result) {
136
+ if (typeof p === 'string')
137
+ ignoredPaths.push(p);
138
+ }
139
+ }
140
+ }
141
+ catch {
142
+ return '*Watcher service is currently unreachable.*';
143
+ }
144
+ const lines = [
145
+ `This environment includes a semantic search index (\`watcher_search\`) covering ${pointCount.toLocaleString()} document chunks.`,
146
+ '**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.',
147
+ '',
148
+ '### Score Interpretation:',
149
+ '* **Strong:** >= 0.75',
150
+ '* **Relevant:** >= 0.50',
151
+ '* **Noise:** < 0.25',
152
+ '',
153
+ "### What's on the menu:",
154
+ ];
155
+ if (activeRules.length) {
156
+ for (const rule of activeRules) {
157
+ lines.push(`* **${rule.name}**: ${rule.description}`);
158
+ }
159
+ }
160
+ else {
161
+ lines.push('* (No inference rules configured)');
162
+ }
163
+ lines.push('', '### Indexed paths:');
164
+ if (watchPaths.length) {
165
+ for (const p of watchPaths) {
166
+ lines.push(`* \`${p}\``);
167
+ }
168
+ }
169
+ else {
170
+ lines.push('* (No watch paths configured)');
171
+ }
172
+ if (ignoredPaths.length > 0) {
173
+ lines.push('', '### Ignored paths:');
174
+ for (const p of ignoredPaths) {
175
+ lines.push(`* \`${p}\``);
176
+ }
177
+ }
178
+ return lines.join('\n');
179
+ }
180
+ async function getCachedWatcherMenu(apiUrl, ttlMs) {
181
+ const now = Date.now();
182
+ if (menuCache && menuCache.expiresAt > now) {
183
+ return menuCache.value;
184
+ }
185
+ const menu = await generateWatcherMenu(apiUrl);
186
+ menuCache = { value: menu, expiresAt: now + ttlMs };
187
+ return menu;
188
+ }
189
+ function ensurePlatformToolsSection(toolsMd) {
190
+ if (toolsMd.includes('# Jeeves Platform Tools')) {
191
+ return toolsMd;
192
+ }
193
+ return `# Jeeves Platform Tools\n\n${toolsMd}`;
194
+ }
195
+ function upsertWatcherSection(toolsMd, watcherMenu) {
196
+ const section = `## Watcher\n\n${watcherMenu}\n`;
197
+ // If we already injected a Watcher section, replace it.
198
+ const re = /^## Watcher\n[\s\S]*?(?=^##\s|$)/m;
199
+ if (re.test(toolsMd)) {
200
+ return toolsMd.replace(re, section);
201
+ }
202
+ // Otherwise insert immediately after the H1 if present, else append.
203
+ const h1 = '# Jeeves Platform Tools';
204
+ const idx = toolsMd.indexOf(h1);
205
+ if (idx !== -1) {
206
+ const afterH1 = idx + h1.length;
207
+ return (toolsMd.slice(0, afterH1) + `\n\n${section}` + toolsMd.slice(afterH1));
208
+ }
209
+ return `${toolsMd}\n\n${section}`;
210
+ }
211
+ /**
212
+ * Hook handler for agent:bootstrap.
213
+ * Injects/updates the Watcher Menu into the TOOLS.md payload.
214
+ */
215
+ async function handleAgentBootstrap(event, api) {
216
+ const context = event?.context;
217
+ if (!isAgentBootstrapEventContext(context)) {
218
+ return;
219
+ }
220
+ const apiUrl = getApiUrl(api);
221
+ const cacheTtlMs = getCacheTtlMs(api);
222
+ const watcherMenu = await getCachedWatcherMenu(apiUrl, cacheTtlMs);
223
+ let toolsFile = context.bootstrapFiles.find((f) => f.name === 'TOOLS.md');
224
+ if (!toolsFile) {
225
+ toolsFile = { name: 'TOOLS.md', content: '', missing: false };
226
+ context.bootstrapFiles.push(toolsFile);
227
+ }
228
+ const current = toolsFile.content ?? '';
229
+ const withH1 = ensurePlatformToolsSection(current);
230
+ const updated = upsertWatcherSection(withH1, watcherMenu);
231
+ toolsFile.content = updated;
232
+ }
233
+
66
234
  /**
67
235
  * @module plugin/watcherTools
68
236
  * Watcher tool registrations (watcher_* tools) for the OpenClaw plugin.
@@ -79,7 +247,7 @@ function registerApiTool(api, baseUrl, config) {
79
247
  const url = `${baseUrl}${endpoint}`;
80
248
  const data = body !== undefined
81
249
  ? await postJson(url, body)
82
- : await fetchJson(url);
250
+ : await fetchJson$1(url);
83
251
  return ok(data);
84
252
  }
85
253
  catch (error) {
@@ -249,6 +417,13 @@ function registerWatcherTools(api, baseUrl) {
249
417
  function register(api) {
250
418
  const baseUrl = getApiUrl(api);
251
419
  registerWatcherTools(api, baseUrl);
420
+ // Register the agent:bootstrap hook if the host OpenClaw version supports it
421
+ const registerHook = api.registerInternalHook;
422
+ if (typeof registerHook === 'function') {
423
+ registerHook('agent:bootstrap', async (event) => {
424
+ await handleAgentBootstrap(event, api);
425
+ });
426
+ }
252
427
  }
253
428
 
254
429
  export { register as default };
@@ -56,8 +56,6 @@ curl -X POST http://127.0.0.1:<PORT>/config/query \
56
56
 
57
57
  You have access to a **semantic archive** of your human's working world. Documents, messages, tickets, notes, code, and other artifacts are indexed, chunked, embedded, and searchable. This is your long-term recall for anything beyond the current conversation.
58
58
 
59
- **Every deployment is different.** The archive's structure, domains, metadata fields, and record types are all defined by the deployment's configuration. Do not assume any particular domains exist (e.g., "email", "slack", "jira"). Always discover what's available using the Orientation Pattern below. Examples in this skill use common domain names for illustration only.
60
-
61
59
  **When to reach for the watcher:**
62
60
 
63
61
  - **Someone asks about something that happened.** A meeting, a decision, a conversation, a ticket, a message. You weren't there, but the archive was. Search it.
@@ -72,28 +70,6 @@ You have access to a **semantic archive** of your human's working world. Documen
72
70
  - The question is about general knowledge, not the human's specific context
73
71
  - The watcher is unreachable (fall back to filesystem browsing)
74
72
 
75
- ## Memory → Archive Escalation
76
-
77
- You have two complementary tools with different scopes:
78
-
79
- - **memory-core** (`memory_search` / `memory_get`) — OpenClaw's built-in memory provider. Manages curated notes in MEMORY.md and memory/*.md. High signal, small scope. This is your long-term memory: decisions, rules, people, project context. Always check here first.
80
- - **watcher** (`watcher_search`) — the full indexed archive across all configured domains. Broad scope, raw record.
81
-
82
- **The escalation rule:** When `memory_search` returns thin, zero, or low-confidence results for something your human clearly expects you to know about — a person, a project, an event, a thing — don't stop there. Follow up with `watcher_search` across the full index.
83
-
84
- **Triggers for escalation:**
85
- - Memory returns 0 results for a named entity (person, project, tool, pet)
86
- - Memory returns results but they lack the detail the question needs
87
- - The question is about something that *happened* (a conversation, a meeting, a decision) rather than something you *noted*
88
- - The human seems surprised you don't know something
89
-
90
- **Don't escalate when:**
91
- - Memory gave you a clear, sufficient answer
92
- - The question is about your own operational rules or preferences (that's purely memory)
93
- - You've already searched the archive this turn
94
-
95
- **Example:** "Tell me about Project X" → memory says "started in January" → that's thin → escalate to `watcher_search("Project X")` → tickets, messages, and docs reveal the full history. Report the full picture.
96
-
97
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.
98
74
 
99
75
  ## Plugin Installation
@@ -113,9 +89,8 @@ npx @karmaniverous/jeeves-watcher-openclaw uninstall
113
89
 
114
90
  If the watcher service is already running and healthy:
115
91
 
116
- 1. **Orient yourself** (once per session) — use `watcher_query` to learn the deployment's organizational strategy and available record types (see Orientation Pattern below)
117
- 2. **Search** — use `watcher_search` with a natural language query and optional metadata filters
118
- 3. **Read source** — use `read` (standard file read) with `file_path` from search results for full document content
92
+ 1. **Search** — use `watcher_search` with a natural language query and optional metadata filters
93
+ 2. **Read source** — use `read` (standard file read) with `file_path` from search results for full document content
119
94
 
120
95
  ## Bootstrap (First-Time Setup)
121
96
 
@@ -403,8 +378,6 @@ Get runtime embedding failures. Returns `{ filePath: IssueRecord }` showing file
403
378
 
404
379
  Filters use Qdrant's native JSON filter format, passed as the `filter` parameter to `watcher_search`.
405
380
 
406
- > **Note:** The field names and values used in these examples (e.g., `domain`, `status`, `assignee`) are illustrative. Actual fields depend on the deployment's inference rules. Use the Orientation Pattern and Query Planning sections to discover what's available before constructing filters.
407
-
408
381
  ### Basic Patterns
409
382
 
410
383
  **Match exact value:**
@@ -473,94 +446,6 @@ Each result from `watcher_search` contains:
473
446
 
474
447
  Additional metadata fields depend on the deployment's inference rules (e.g., `domain`, `status`, `author`). Use `watcher_query` to discover available fields.
475
448
 
476
- ## Orientation Pattern (Once Per Session)
477
-
478
- Query the deployment's organizational context and available record types. This information is stable within a session; query once and rely on results for the remainder.
479
-
480
- **Efficient pattern (two calls):**
481
-
482
- 1. **Top-level context:**
483
- ```
484
- watcher_query: path="$.['description','search']"
485
- ```
486
- Returns:
487
- - `description` — organizational strategy (e.g., how domains are structured, what partitioning means)
488
- - `search.scoreThresholds` — score interpretation boundaries (strong, relevant, noise)
489
-
490
- 2. **Available record types:**
491
- ```
492
- watcher_query: path="$.inferenceRules[*].['name','description']"
493
- ```
494
- Returns list of inference rules with their names and descriptions.
495
-
496
- **Example result:**
497
- ```json
498
- [
499
- { "name": "email-archive", "description": "Email archive messages" },
500
- { "name": "slack-message", "description": "Slack channel messages with channel and author metadata" },
501
- { "name": "jira-issue", "description": "Jira issue metadata extracted from issue JSON exports" }
502
- ]
503
- ```
504
-
505
- The top-level `description` explains this deployment's organizational strategy. Each rule's `description` explains what that specific record type represents. Both levels are useful: one orients, the other enumerates.
506
-
507
- ---
508
-
509
- ## `resolve` Usage Guidance
510
-
511
- The `resolve` parameter controls which reference layers are expanded in `watcher_query`:
512
-
513
- - **No `resolve` (default):** Raw config structure with references intact (lightweight)
514
- - **`resolve: ["files"]`:** Resolve file path references to their contents (e.g., `"schemas/base.json"` → the JSON Schema object)
515
- - **`resolve: ["globals"]`:** Resolve named schema references (e.g., `"base"` in a rule's schema array → the global schema object)
516
- - **`resolve: ["files","globals"]`:** Fully inlined, everything expanded
517
-
518
- **When to use:**
519
- - **Orientation:** No resolve (just names and descriptions, lightweight)
520
- - **Query planning:** `resolve: ["files","globals"]` (need complete merged schemas for filter construction)
521
- - **Browsing global schemas:** `resolve: ["files"]` (see schema contents but keep named references visible for DRY structure understanding)
522
-
523
- ---
524
-
525
- ## JSONPath Patterns for Schema Discovery
526
-
527
- Use `watcher_query` to explore the merged virtual document. Common patterns:
528
-
529
- ### Orientation
530
- ```
531
- $.inferenceRules[*].['name','description'] — List all rules with descriptions
532
- $.search.scoreThresholds — Score interpretation thresholds
533
- $.slots — Named filter patterns (e.g., memory)
534
- ```
535
-
536
- ### Schema Discovery
537
- ```
538
- $.inferenceRules[?(@.name=='jira-issue')] — Full rule details
539
- $.inferenceRules[?(@.name=='jira-issue')].values — Distinct values for a rule
540
- $.inferenceRules[?(@.name=='jira-issue')].values.status — Values for a specific field
541
- ```
542
-
543
- ### Helper Enumeration
544
- ```
545
- $.mapHelpers — All JsonMap helper namespaces
546
- $.mapHelpers.slack.exports — Exports from the 'slack' helper
547
- $.templateHelpers — All Handlebars helper namespaces
548
- ```
549
-
550
- ### Issues
551
- ```
552
- $.issues — All runtime embedding failures
553
- ```
554
-
555
- ### Full Config Introspection
556
- ```
557
- $.schemas — Global named schemas
558
- $.maps — Named JsonMap transforms
559
- $.templates — Named Handlebars templates
560
- ```
561
-
562
- ---
563
-
564
449
  ## Query Planning (Per Search Task)
565
450
 
566
451
  Identify relevant rule(s) from the orientation model, then retrieve their schemas:
@@ -593,7 +478,7 @@ Use `uiHint` to determine filter construction strategy. **This table is explicit
593
478
  | `text` | `{ "key": "<field>", "match": { "text": "<value>" } }` | Substring/keyword match |
594
479
  | `select` | `{ "key": "<field>", "match": { "value": "<enum_value>" } }` | Exact match; use `enum` values from schema or runtime values index |
595
480
  | `multiselect` | `{ "key": "<field>", "match": { "value": "<enum_value>" } }` | Any-element match on array field; use `enum` or runtime values index |
596
- | `date` | `{ "key": "<field>", "range": { "gte": <unix_ts>, "lt": <unix_ts> } }` | Either bound optional for open-ended ranges (e.g., "after January" → `gte` only) |
481
+ | `date` | `{ "key": "<field>", "range": { "gte": <unix_ts>, "lt": <unix_ts> } }` | Range filter against integer fields holding Unix timestamps (seconds). Source dates should be normalized in config via `{{toUnix ...}}` in `set` expressions. | for open-ended ranges (e.g., "after January" → `gte` only) |
597
482
  | `number` | `{ "key": "<field>", "range": { "gte": <n>, "lte": <n> } }` | Either bound optional for open-ended ranges |
598
483
  | `check` | `{ "key": "<field>", "match": { "value": true } }` | Boolean match |
599
484
  | *(absent)* | Do not use in filters | Internal bookkeeping field, not intended for search |
@@ -780,3 +665,7 @@ If tools are unavailable (plugin not loaded in this session):
780
665
 
781
666
  - [JSONPath Plus documentation](https://www.npmjs.com/package/jsonpath-plus) for JSONPath syntax
782
667
  - [Qdrant filtering documentation](https://qdrant.tech/documentation/concepts/filtering/) for advanced query patterns and search response format
668
+
669
+
670
+
671
+
@@ -19,6 +19,11 @@ export interface PluginApi {
19
19
  }, options?: {
20
20
  optional?: boolean;
21
21
  }): void;
22
+ /**
23
+ * Optional internal hook registration (available on newer OpenClaw builds).
24
+ * We keep this optional to preserve compatibility.
25
+ */
26
+ registerInternalHook?: (event: string, handler: (event: unknown) => Promise<void> | void) => void;
22
27
  }
23
28
  /** Result shape returned by each tool execution. */
24
29
  export interface ToolResult {
@@ -30,6 +35,8 @@ export interface ToolResult {
30
35
  }
31
36
  /** Resolve the watcher API base URL from plugin config. */
32
37
  export declare function getApiUrl(api: PluginApi): string;
38
+ /** Resolve the cache TTL for plugin hooks from config. */
39
+ export declare function getCacheTtlMs(api: PluginApi): number;
33
40
  /** Format a successful tool result. */
34
41
  export declare function ok(data: unknown): ToolResult;
35
42
  /** Format an error tool result. */
@@ -0,0 +1,6 @@
1
+ import { type PluginApi } from './helpers.js';
2
+ /**
3
+ * Hook handler for agent:bootstrap.
4
+ * Injects/updates the Watcher Menu into the TOOLS.md payload.
5
+ */
6
+ export declare function handleAgentBootstrap(event: unknown, api: PluginApi): Promise<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.4.2",
5
+ "version": "0.5.1",
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.4.2",
3
+ "version": "0.5.1",
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",