@karmaniverous/jeeves-watcher-openclaw 0.5.3 → 0.5.5

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
@@ -1,9 +1,11 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+
1
4
  /**
2
5
  * @module plugin/helpers
3
6
  * Shared types and utility functions for the OpenClaw plugin tool registrations.
4
7
  */
5
8
  const DEFAULT_API_URL = 'http://127.0.0.1:1936';
6
- const DEFAULT_CACHE_TTL_MS = 30000;
7
9
  /** Extract plugin config from the API object */
8
10
  function getPluginConfig(api) {
9
11
  return api.config?.plugins?.entries?.['jeeves-watcher-openclaw']?.config;
@@ -13,11 +15,6 @@ function getApiUrl(api) {
13
15
  const url = getPluginConfig(api)?.apiUrl;
14
16
  return typeof url === 'string' ? url : DEFAULT_API_URL;
15
17
  }
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
- }
21
18
  /** Format a successful tool result. */
22
19
  function ok(data) {
23
20
  return {
@@ -73,14 +70,6 @@ async function postJson(url, body) {
73
70
  });
74
71
  }
75
72
 
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
73
  async function fetchJson(url, init) {
85
74
  const res = await fetch(url, init);
86
75
  if (!res.ok) {
@@ -177,58 +166,106 @@ async function generateWatcherMenu(apiUrl) {
177
166
  }
178
167
  return lines.join('\n');
179
168
  }
180
- async function getCachedWatcherMenu(apiUrl, ttlMs) {
181
- const now = Date.now();
182
- if (menuCache && menuCache.expiresAt > now) {
183
- return menuCache.value;
169
+
170
+ /**
171
+ * @module plugin/toolsWriter
172
+ * Writes the Watcher menu section directly to TOOLS.md on disk.
173
+ * Replaces the agent:bootstrap hook approach which was unreliable due to
174
+ * OpenClaw's clearInternalHooks() wiping plugin-registered hooks on startup.
175
+ */
176
+ const REFRESH_INTERVAL_MS = 60_000;
177
+ let intervalHandle = null;
178
+ let lastWrittenMenu = '';
179
+ /**
180
+ * Resolve the workspace TOOLS.md path.
181
+ * Uses api.resolvePath if available, otherwise falls back to CWD.
182
+ */
183
+ function resolveToolsPath(api) {
184
+ const resolvePath = api
185
+ .resolvePath;
186
+ if (typeof resolvePath === 'function') {
187
+ return resolvePath('TOOLS.md');
184
188
  }
185
- const menu = await generateWatcherMenu(apiUrl);
186
- menuCache = { value: menu, expiresAt: now + ttlMs };
187
- return menu;
189
+ return resolve(process.cwd(), 'TOOLS.md');
188
190
  }
189
- function ensurePlatformToolsSection(toolsMd) {
190
- if (toolsMd.includes('# Jeeves Platform Tools')) {
191
- return toolsMd;
191
+ /**
192
+ * Upsert the watcher section in TOOLS.md content.
193
+ *
194
+ * Strategy:
195
+ * - If a `## Watcher` section already exists, replace it in place.
196
+ * - Otherwise, prepend `# Jeeves Platform Tools\n\n## Watcher\n\n...`
197
+ * before any existing content.
198
+ */
199
+ function upsertWatcherContent(existing, watcherMenu) {
200
+ const section = `## Watcher\n\n${watcherMenu}`;
201
+ // Replace existing watcher section (match from ## Watcher to next ## or # or EOF)
202
+ const re = /^## Watcher\n[\s\S]*?(?=\n## |\n# |$(?![\s\S]))/m;
203
+ if (re.test(existing)) {
204
+ return existing.replace(re, section);
205
+ }
206
+ // No existing section. Prepend under a platform tools H1.
207
+ const platformH1 = '# Jeeves Platform Tools';
208
+ if (existing.includes(platformH1)) {
209
+ // Insert after the H1
210
+ const idx = existing.indexOf(platformH1) + platformH1.length;
211
+ return existing.slice(0, idx) + `\n\n${section}\n` + existing.slice(idx);
192
212
  }
193
- return `# Jeeves Platform Tools\n\n${toolsMd}`;
213
+ // Prepend platform header + watcher section before existing content
214
+ const trimmed = existing.trim();
215
+ if (trimmed.length === 0) {
216
+ return `${platformH1}\n\n${section}\n`;
217
+ }
218
+ return `${platformH1}\n\n${section}\n\n${trimmed}\n`;
194
219
  }
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);
220
+ /**
221
+ * Fetch the current watcher menu and write it to TOOLS.md if changed.
222
+ * Returns true if the file was updated.
223
+ */
224
+ async function refreshToolsMd(api) {
225
+ const apiUrl = getApiUrl(api);
226
+ const menu = await generateWatcherMenu(apiUrl);
227
+ if (menu === lastWrittenMenu) {
228
+ return false;
201
229
  }
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));
230
+ const toolsPath = resolveToolsPath(api);
231
+ let current = '';
232
+ try {
233
+ current = await readFile(toolsPath, 'utf8');
208
234
  }
209
- return `${toolsMd}\n\n${section}`;
235
+ catch {
236
+ // File doesn't exist yet — we'll create it
237
+ }
238
+ const updated = upsertWatcherContent(current, menu);
239
+ if (updated !== current) {
240
+ await writeFile(toolsPath, updated, 'utf8');
241
+ lastWrittenMenu = menu;
242
+ return true;
243
+ }
244
+ lastWrittenMenu = menu;
245
+ return false;
210
246
  }
211
247
  /**
212
- * Hook handler for agent:bootstrap.
213
- * Injects/updates the Watcher Menu into the TOOLS.md payload.
248
+ * Start the periodic TOOLS.md writer.
249
+ * Writes immediately on startup, then refreshes every REFRESH_INTERVAL_MS.
214
250
  */
215
- async function handleAgentBootstrap(event, api) {
216
- const context = event?.context;
217
- if (!isAgentBootstrapEventContext(context)) {
218
- return;
251
+ function startToolsWriter(api) {
252
+ // Initial write (fire and forget — errors logged but not thrown)
253
+ refreshToolsMd(api).catch((err) => {
254
+ console.error('[jeeves-watcher] Failed to write TOOLS.md:', err);
255
+ });
256
+ // Periodic refresh
257
+ if (intervalHandle) {
258
+ clearInterval(intervalHandle);
219
259
  }
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);
260
+ intervalHandle = setInterval(() => {
261
+ refreshToolsMd(api).catch((err) => {
262
+ console.error('[jeeves-watcher] Failed to refresh TOOLS.md:', err);
263
+ });
264
+ }, REFRESH_INTERVAL_MS);
265
+ // Don't keep the process alive just for this interval
266
+ if (typeof intervalHandle === 'object' && 'unref' in intervalHandle) {
267
+ intervalHandle.unref();
227
268
  }
228
- const current = toolsFile.content ?? '';
229
- const withH1 = ensurePlatformToolsSection(current);
230
- const updated = upsertWatcherSection(withH1, watcherMenu);
231
- toolsFile.content = updated;
232
269
  }
233
270
 
234
271
  /**
@@ -417,13 +454,11 @@ function registerWatcherTools(api, baseUrl) {
417
454
  function register(api) {
418
455
  const baseUrl = getApiUrl(api);
419
456
  registerWatcherTools(api, baseUrl);
420
- // Register the agent:bootstrap hook if the host OpenClaw version supports it
421
- const registerHook = api.registerHook;
422
- if (typeof registerHook === 'function') {
423
- registerHook('agent:bootstrap', async (event) => {
424
- await handleAgentBootstrap(event, api);
425
- }, { name: 'jeeves-watcher-openclaw' });
426
- }
457
+ // Write the watcher menu to TOOLS.md on disk, refreshing periodically.
458
+ // This replaces the agent:bootstrap hook approach which was unreliable
459
+ // because OpenClaw's clearInternalHooks() wipes plugin-registered hooks
460
+ // during the async startup sequence.
461
+ startToolsWriter(api);
427
462
  }
428
463
 
429
464
  export { register as default };
@@ -1,4 +1,9 @@
1
1
  import { type PluginApi } from './helpers.js';
2
+ /**
3
+ * Fetches data from the watcher API and generates a Markdown menu string.
4
+ * The string is platform-agnostic and safe to inject into TOOLS.md.
5
+ */
6
+ export declare function generateWatcherMenu(apiUrl: string): Promise<string>;
2
7
  /**
3
8
  * Hook handler for agent:bootstrap.
4
9
  * Injects/updates the Watcher Menu into the TOOLS.md payload.
@@ -0,0 +1,20 @@
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;
@@ -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.3",
5
+ "version": "0.5.5",
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.3",
3
+ "version": "0.5.5",
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",