@karmaniverous/jeeves-watcher-openclaw 0.7.0 → 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 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 `apiUrl` in the plugin configuration to point at your jeeves-watcher service:
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
- ## Dynamic TOOLS.md Injection
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
- On startup, the plugin writes a `## Watcher` section to `TOOLS.md` in the agent's workspace, providing a live menu of indexed content, score thresholds, and escalation rules. This refreshes every 60 seconds. On uninstall, the CLI removes the section.
49
+ ## Architecture
50
+
51
+ ![Plugin Architecture](assets/plugin-architecture.png)
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
 
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
- /** Remove the ## Watcher section from TOOLS.md on uninstall. */
224
- function cleanupToolsMd(home, configPath) {
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
- 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))
239
+ const content = readFileSync(toolsPath, 'utf8');
240
+ const parsed = parseManaged(content, TOOLS_MARKERS);
241
+ if (!parsed.found)
235
242
  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');
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 { readFile, writeFile } from 'node:fs/promises';
2
- import { resolve } from 'node:path';
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
- const DEFAULT_API_URL = 'http://127.0.0.1:1936';
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?.['jeeves-watcher-openclaw']?.config;
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 {
@@ -124,7 +149,7 @@ async function generateWatcherMenu(apiUrl) {
124
149
  let qdrantStatus = '*Unknown*';
125
150
  try {
126
151
  // Assuming Qdrant runs locally on the default port
127
- const res = await fetch('http://127.0.0.1:6333/healthz', {
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 (http://127.0.0.1:6333): ${qdrantStatus}
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
  }
@@ -187,104 +212,79 @@ async function generateWatcherMenu(apiUrl) {
187
212
  }
188
213
 
189
214
  /**
190
- * @module plugin/toolsWriter
191
- * Writes the Watcher menu section directly to TOOLS.md on disk.
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
- * Strategy:
214
- * - If a `## Watcher` section already exists, replace it in place.
215
- * - Otherwise, prepend `# Jeeves Platform Tools\n\n## Watcher\n\n...`
216
- * before any existing content.
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
- function upsertWatcherContent(existing, watcherMenu) {
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
- * Fetch the current watcher menu and write it to TOOLS.md if changed.
241
- * Returns true if the file was updated.
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 refreshToolsMd(api) {
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
- current = await readFile(toolsPath, 'utf8');
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
- // File doesn't exist yet — we'll create it
256
- }
257
- const updated = upsertWatcherContent(current, menu);
258
- if (updated !== current) {
259
- await writeFile(toolsPath, updated, 'utf8');
260
- lastWrittenMenu = menu;
261
- return true;
246
+ return { running: false };
262
247
  }
263
- lastWrittenMenu = menu;
264
- return false;
265
248
  }
266
249
  /**
267
- * Start the periodic TOOLS.md writer.
268
- * Writes immediately on startup, then refreshes every REFRESH_INTERVAL_MS.
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 startToolsWriter(api) {
271
- // Initial write (fire and forget — errors logged but not thrown)
272
- refreshToolsMd(api).catch((err) => {
273
- console.error('[jeeves-watcher] Failed to write TOOLS.md:', err);
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
- // Periodic refresh
276
- if (intervalHandle) {
277
- clearInterval(intervalHandle);
278
- }
279
- intervalHandle = setInterval(() => {
280
- refreshToolsMd(api).catch((err) => {
281
- console.error('[jeeves-watcher] Failed to refresh TOOLS.md:', err);
282
- });
283
- }, REFRESH_INTERVAL_MS);
284
- // Don't keep the process alive just for this interval
285
- if (typeof intervalHandle === 'object' && 'unref' in intervalHandle) {
286
- intervalHandle.unref();
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
  /**
@@ -539,17 +539,52 @@ function registerWatcherTools(api, baseUrl) {
539
539
 
540
540
  /**
541
541
  * @module plugin
542
- * 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.
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`.
543
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
+ }
544
568
  /** Register all jeeves-watcher tools with the OpenClaw plugin API. */
545
569
  function register(api) {
546
- const baseUrl = getApiUrl(api);
547
- registerWatcherTools(api, baseUrl);
548
- // Write the watcher menu to TOOLS.md on disk, refreshing periodically.
549
- // This replaces the agent:bootstrap hook approach which was unreliable
550
- // because OpenClaw's clearInternalHooks() wipes plugin-registered hooks
551
- // during the async startup sequence.
552
- startToolsWriter(api);
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();
553
588
  }
554
589
 
555
590
  export { register as default };
@@ -72,6 +72,21 @@ You have access to a **semantic archive** of your human's working world. Documen
72
72
 
73
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.
74
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
+
75
90
  ## Plugin Installation
76
91
 
77
92
  ```
@@ -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;
@@ -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,6 +37,8 @@ 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;
40
+ /** Resolve the platform config root path from plugin config. */
41
+ export declare function getConfigRoot(api: PluginApi): string;
42
42
  /** Format a successful tool result. */
43
43
  export declare function ok(data: unknown): ToolResult;
44
44
  /** Format a connection error with actionable guidance. */
@@ -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. */
@@ -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 {};
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @module plugin/watcherComponent.test
3
+ * Unit tests for the watcher JeevesComponent implementation.
4
+ */
5
+ export {};
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @module plugin/writerIntegration.test
3
+ * Integration tests for the ComponentWriter lifecycle via jeeves-core.
4
+ */
5
+ export {};
@@ -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.7.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.7.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.54.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.85.0",
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.18"
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,12 +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;