@karmaniverous/jeeves-watcher-openclaw 0.5.4 → 0.5.6

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/cli.js CHANGED
@@ -199,10 +199,62 @@ function uninstall() {
199
199
  writeJson(configPath, config);
200
200
  }
201
201
  }
202
+ // Clean up TOOLS.md watcher section
203
+ cleanupToolsMd(home, configPath);
202
204
  console.log();
203
205
  console.log('✅ Plugin uninstalled successfully.');
204
206
  console.log(' Restart the OpenClaw gateway to complete removal.');
205
207
  }
208
+ /** Resolve the workspace directory from OpenClaw config. */
209
+ function resolveWorkspaceDir(home, configPath) {
210
+ const config = readJson(configPath);
211
+ if (!config)
212
+ return null;
213
+ // Check agents.defaults.workspace
214
+ const agents = config.agents;
215
+ const defaults = agents?.defaults;
216
+ const workspace = defaults?.workspace;
217
+ if (workspace) {
218
+ return resolve(workspace.replace(/^~/, homedir()));
219
+ }
220
+ // Default workspace location
221
+ return join(home, 'workspace');
222
+ }
223
+ /** Remove the ## Watcher section from TOOLS.md on uninstall. */
224
+ function cleanupToolsMd(home, configPath) {
225
+ const workspaceDir = resolveWorkspaceDir(home, configPath);
226
+ if (!workspaceDir)
227
+ return;
228
+ const toolsPath = join(workspaceDir, 'TOOLS.md');
229
+ if (!existsSync(toolsPath))
230
+ return;
231
+ let content = readFileSync(toolsPath, 'utf8');
232
+ // Remove ## Watcher section (from ## Watcher to next ## or # or EOF)
233
+ const watcherRe = /^## Watcher\n[\s\S]*?(?=\n## |\n# |$(?![\s\S]))/m;
234
+ if (!watcherRe.test(content))
235
+ return;
236
+ content = content.replace(watcherRe, '').replace(/\n{3,}/g, '\n\n');
237
+ // If # Jeeves Platform Tools has no remaining ## sections, remove it too
238
+ const platformH1 = '# Jeeves Platform Tools';
239
+ if (content.includes(platformH1)) {
240
+ const h1Idx = content.indexOf(platformH1);
241
+ const afterH1 = content.slice(h1Idx + platformH1.length);
242
+ // Check if there's a ## before the next # or EOF
243
+ const nextH2Match = afterH1.match(/^## /m);
244
+ const nextH1Match = afterH1.match(/^# /m);
245
+ const h2Pos = nextH2Match ? afterH1.indexOf(nextH2Match[0]) : Infinity;
246
+ const h1Pos = nextH1Match ? afterH1.indexOf(nextH1Match[0]) : Infinity;
247
+ if (h2Pos >= h1Pos) {
248
+ // No child ## sections remain — remove the empty H1
249
+ content =
250
+ content.slice(0, h1Idx) + content.slice(h1Idx + platformH1.length);
251
+ content = content.replace(/^\n{2,}/, '').replace(/\n{3,}/g, '\n\n');
252
+ }
253
+ }
254
+ content = content.trim() + '\n';
255
+ writeFileSync(toolsPath, content);
256
+ console.log('\u2713 Cleaned up TOOLS.md (removed Watcher section)');
257
+ }
206
258
  // Main
207
259
  const command = process.argv[2];
208
260
  switch (command) {
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,68 +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;
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;
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');
192
188
  }
193
- return `# Jeeves Platform Tools\n\n${toolsMd}`;
189
+ return resolve(process.cwd(), 'TOOLS.md');
194
190
  }
195
- function upsertWatcherSection(toolsMd, watcherMenu) {
196
- const section = `## Watcher\n\n${watcherMenu}\n`;
197
- // If we already injected a Watcher section, replace it.
198
- // The `m` flag is needed so `^` matches line starts, but it also makes
199
- // `$` match end-of-line instead of end-of-string. Use a negative
200
- // lookahead `$(?![\s\S])` to anchor at true end-of-string.
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)
201
202
  const re = /^## Watcher\n[\s\S]*?(?=\n## |\n# |$(?![\s\S]))/m;
202
- if (re.test(toolsMd)) {
203
- return toolsMd.replace(re, section);
203
+ if (re.test(existing)) {
204
+ return existing.replace(re, section);
204
205
  }
205
- // Otherwise insert immediately after the H1 if present, else append.
206
- const h1 = '# Jeeves Platform Tools';
207
- const idx = toolsMd.indexOf(h1);
208
- if (idx !== -1) {
209
- const afterH1 = idx + h1.length;
210
- return (toolsMd.slice(0, afterH1) + `\n\n${section}` + toolsMd.slice(afterH1));
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);
211
212
  }
212
- return `${toolsMd}\n\n${section}`;
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`;
213
219
  }
214
220
  /**
215
- * Hook handler for agent:bootstrap.
216
- * Injects/updates the Watcher Menu into the TOOLS.md payload.
221
+ * Fetch the current watcher menu and write it to TOOLS.md if changed.
222
+ * Returns true if the file was updated.
217
223
  */
218
- async function handleAgentBootstrap(event, api) {
219
- const context = event?.context;
220
- if (!isAgentBootstrapEventContext(context)) {
221
- return;
222
- }
224
+ async function refreshToolsMd(api) {
223
225
  const apiUrl = getApiUrl(api);
224
- const cacheTtlMs = getCacheTtlMs(api);
225
- const watcherMenu = await getCachedWatcherMenu(apiUrl, cacheTtlMs);
226
- let toolsFile = context.bootstrapFiles.find((f) => f.name === 'TOOLS.md');
227
- if (!toolsFile) {
228
- toolsFile = { name: 'TOOLS.md', content: '', missing: false };
229
- context.bootstrapFiles.push(toolsFile);
226
+ const menu = await generateWatcherMenu(apiUrl);
227
+ if (menu === lastWrittenMenu) {
228
+ return false;
229
+ }
230
+ const toolsPath = resolveToolsPath(api);
231
+ let current = '';
232
+ try {
233
+ current = await readFile(toolsPath, 'utf8');
234
+ }
235
+ catch {
236
+ // File doesn't exist yet — we'll create it
230
237
  }
231
- const current = toolsFile.content ?? '';
232
- // Guard: the bootstrap hook fires on every message turn because OpenClaw
233
- // caches bootstrapFiles per session and returns the same mutable objects.
234
- // If we already injected the watcher menu into this content, skip to
235
- // avoid accumulating duplicate sections.
236
- if (current.includes('## Watcher') && current.includes(watcherMenu)) {
237
- return;
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;
246
+ }
247
+ /**
248
+ * Start the periodic TOOLS.md writer.
249
+ * Writes immediately on startup, then refreshes every REFRESH_INTERVAL_MS.
250
+ */
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);
259
+ }
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();
238
268
  }
239
- const withH1 = ensurePlatformToolsSection(current);
240
- const updated = upsertWatcherSection(withH1, watcherMenu);
241
- toolsFile.content = updated;
242
269
  }
243
270
 
244
271
  /**
@@ -427,13 +454,11 @@ function registerWatcherTools(api, baseUrl) {
427
454
  function register(api) {
428
455
  const baseUrl = getApiUrl(api);
429
456
  registerWatcherTools(api, baseUrl);
430
- // Register the agent:bootstrap hook if the host OpenClaw version supports it
431
- const registerHook = api.registerHook;
432
- if (typeof registerHook === 'function') {
433
- registerHook('agent:bootstrap', async (event) => {
434
- await handleAgentBootstrap(event, api);
435
- }, { name: 'jeeves-watcher-openclaw' });
436
- }
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);
437
462
  }
438
463
 
439
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.4",
5
+ "version": "0.5.6",
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.4",
3
+ "version": "0.5.6",
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",