@karmaniverous/jeeves-meta-openclaw 0.1.4 → 0.2.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/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import { HttpWatcherClient, listMetas, normalizePath, findNode, computeEffectiveStaleness, selectCandidate, paginatedScan, filterInScope, computeStructureHash, readLatestArchive, hasSteerChanged, isArchitectTriggered, actualStaleness, loadSynthConfig } from '@karmaniverous/jeeves-meta';
2
1
  import { readFile, writeFile } from 'node:fs/promises';
3
2
  import { resolve } from 'node:path';
4
3
 
@@ -8,26 +7,27 @@ import { resolve } from 'node:path';
8
7
  * @module helpers
9
8
  */
10
9
  const PLUGIN_NAME = 'jeeves-meta-openclaw';
10
+ const DEFAULT_SERVICE_URL = 'http://127.0.0.1:1938';
11
11
  /** Get plugin config. */
12
12
  function getPluginConfig(api) {
13
13
  return api.config?.plugins?.entries?.[PLUGIN_NAME]?.config;
14
14
  }
15
15
  /**
16
- * Resolve the config file path.
16
+ * Resolve the service URL.
17
17
  *
18
18
  * Resolution order:
19
- * 1. Plugin config `configPath` setting
20
- * 2. `JEEVES_META_CONFIG` environment variable
21
- * 3. Error — no default path
19
+ * 1. Plugin config `serviceUrl` setting
20
+ * 2. `JEEVES_META_URL` environment variable
21
+ * 3. Default: http://127.0.0.1:1938
22
22
  */
23
- function getConfigPath(api) {
24
- const fromPlugin = getPluginConfig(api)?.configPath;
23
+ function getServiceUrl(api) {
24
+ const fromPlugin = getPluginConfig(api)?.serviceUrl;
25
25
  if (typeof fromPlugin === 'string')
26
26
  return fromPlugin;
27
- const fromEnv = process.env['JEEVES_META_CONFIG'];
27
+ const fromEnv = process.env['JEEVES_META_URL'];
28
28
  if (fromEnv)
29
29
  return fromEnv;
30
- throw new Error('jeeves-meta config path not found. Set configPath in plugin config or JEEVES_META_CONFIG env var.');
30
+ return DEFAULT_SERVICE_URL;
31
31
  }
32
32
  /** Format a successful tool result. */
33
33
  function ok(data) {
@@ -45,222 +45,108 @@ function fail(error) {
45
45
  }
46
46
 
47
47
  /**
48
- * Virtual rule definitions and registration for jeeves-meta.
48
+ * Thin HTTP client for the jeeves-meta service.
49
49
  *
50
- * Registers three inference rules with the watcher at plugin startup:
51
- * 1. synth-meta-live — indexes live .meta/meta.json files
52
- * 2. synth-meta-archive — indexes archived snapshots
53
- * 3. synth-config — indexes the synth config file
50
+ * Plugin delegates all operations to the running service via HTTP.
54
51
  *
55
- * @module rules
52
+ * @module serviceClient
56
53
  */
57
- const SOURCE = 'jeeves-meta';
58
- /**
59
- * Build virtual rule definitions using configured domain tags.
60
- *
61
- * @param config - Synth config with metaProperty/metaArchiveProperty.
62
- * @returns Array of inference rule specs.
63
- */
64
- function buildSynthRules(config) {
65
- return [
66
- {
67
- name: 'synth-meta-live',
68
- description: 'Live jeeves-meta .meta/meta.json files',
69
- match: {
70
- properties: {
71
- file: {
72
- properties: {
73
- path: { type: 'string', glob: '**/.meta/meta.json' },
74
- },
75
- },
76
- },
77
- },
78
- schema: [
79
- 'base',
80
- {
81
- properties: {
82
- domains: { set: config.metaProperty.domains },
83
- synth_id: { type: 'string', set: '{{json._id}}' },
84
- synth_steer: { type: 'string', set: '{{json._steer}}' },
85
- synth_depth: { type: 'number', set: '{{json._depth}}' },
86
- synth_emphasis: { type: 'number', set: '{{json._emphasis}}' },
87
- synth_synthesis_count: {
88
- type: 'integer',
89
- set: '{{json._synthesisCount}}',
90
- },
91
- synth_structure_hash: {
92
- type: 'string',
93
- set: '{{json._structureHash}}',
94
- },
95
- synth_architect_tokens: {
96
- type: 'integer',
97
- set: '{{json._architectTokens}}',
98
- },
99
- synth_builder_tokens: {
100
- type: 'integer',
101
- set: '{{json._builderTokens}}',
102
- },
103
- synth_critic_tokens: {
104
- type: 'integer',
105
- set: '{{json._criticTokens}}',
106
- },
107
- synth_error_step: {
108
- type: 'string',
109
- set: '{{json._error.step}}',
110
- },
111
- generated_at_unix: {
112
- type: 'integer',
113
- set: '{{toUnix json._generatedAt}}',
114
- description: 'Synthesis timestamp as Unix seconds for range queries',
115
- },
116
- has_error: {
117
- type: 'boolean',
118
- set: '{{#if json._error}}true{{else}}false{{/if}}',
119
- },
120
- },
121
- },
122
- ],
123
- render: {
124
- frontmatter: [
125
- 'synth_id',
126
- 'synth_steer',
127
- 'generated_at_unix',
128
- 'synth_depth',
129
- 'synth_emphasis',
130
- 'synth_architect_tokens',
131
- 'synth_builder_tokens',
132
- 'synth_critic_tokens',
133
- ],
134
- body: [
135
- {
136
- path: 'json._content',
137
- heading: 1,
138
- label: 'Synthesis',
139
- },
140
- ],
141
- },
142
- renderAs: 'md',
143
- },
144
- {
145
- name: 'synth-meta-archive',
146
- description: 'Archived jeeves-meta .meta/archive snapshots',
147
- match: {
148
- properties: {
149
- file: {
150
- properties: {
151
- path: { type: 'string', glob: '**/.meta/archive/*.json' },
152
- },
153
- },
154
- },
155
- },
156
- schema: [
157
- 'base',
158
- {
159
- properties: {
160
- domains: { set: config.metaArchiveProperty.domains },
161
- synth_id: { type: 'string', set: '{{json._id}}' },
162
- archived: { type: 'boolean', set: 'true' },
163
- archived_at: { type: 'string', set: '{{json._archivedAt}}' },
164
- },
165
- },
166
- ],
167
- render: {
168
- frontmatter: ['synth_id', 'archived', 'archived_at'],
169
- body: [
170
- {
171
- path: 'json._content',
172
- heading: 1,
173
- label: 'Synthesis (archived)',
174
- },
175
- ],
176
- },
177
- renderAs: 'md',
178
- },
179
- {
180
- name: 'synth-config',
181
- description: 'jeeves-meta configuration file',
182
- match: {
183
- properties: {
184
- file: {
185
- properties: {
186
- path: { type: 'string', glob: '**/jeeves-meta.config.json' },
187
- },
188
- },
189
- },
190
- },
191
- schema: [
192
- 'base',
193
- {
194
- properties: {
195
- domains: { set: ['synth-config'] },
196
- },
197
- },
198
- ],
199
- render: {
200
- frontmatter: [
201
- 'watchPaths',
202
- 'watcherUrl',
203
- 'gatewayUrl',
204
- 'architectEvery',
205
- 'depthWeight',
206
- 'maxArchive',
207
- 'maxLines',
208
- 'batchSize',
209
- ],
210
- body: [
211
- {
212
- path: 'json.defaultArchitect',
213
- heading: 2,
214
- label: 'Default Architect Prompt',
215
- },
216
- {
217
- path: 'json.defaultCritic',
218
- heading: 2,
219
- label: 'Default Critic Prompt',
220
- },
221
- ],
222
- },
223
- renderAs: 'md',
224
- },
225
- ];
226
- }
227
- /**
228
- * Register jeeves-meta virtual rules with the watcher.
229
- *
230
- * Called at plugin startup. Rules are additive — the watcher appends
231
- * them after config-file rules (last-match-wins).
232
- *
233
- * @param watcherUrl - Base URL for the watcher service.
234
- */
235
- async function registerSynthRules(watcherUrl, config) {
236
- const client = new HttpWatcherClient({ baseUrl: watcherUrl });
237
- await client.registerRules(SOURCE, buildSynthRules(config));
54
+ class MetaServiceClient {
55
+ baseUrl;
56
+ constructor(config) {
57
+ this.baseUrl = config.serviceUrl.replace(/\/$/, '');
58
+ }
59
+ /** GET helper returns parsed JSON. */
60
+ async get(path) {
61
+ const res = await fetch(this.baseUrl + path);
62
+ if (!res.ok) {
63
+ const text = await res.text();
64
+ throw new Error(`META ${path} ${String(res.status)} ${res.statusText}: ${text}`);
65
+ }
66
+ return res.json();
67
+ }
68
+ /** POST helper — returns parsed JSON. */
69
+ async post(path, body) {
70
+ const res = await fetch(this.baseUrl + path, {
71
+ method: 'POST',
72
+ headers: { 'content-type': 'application/json' },
73
+ body: body !== undefined ? JSON.stringify(body) : undefined,
74
+ });
75
+ if (!res.ok) {
76
+ const text = await res.text();
77
+ throw new Error(`META ${path} ${String(res.status)} ${res.statusText}: ${text}`);
78
+ }
79
+ return res.json();
80
+ }
81
+ /** GET /status service health + queue state. */
82
+ async status() {
83
+ return this.get('/status');
84
+ }
85
+ /** GET /metas — list all meta entities with summary. */
86
+ async listMetas(params) {
87
+ const qs = new URLSearchParams();
88
+ if (params?.pathPrefix)
89
+ qs.set('pathPrefix', params.pathPrefix);
90
+ if (params?.hasError !== undefined)
91
+ qs.set('hasError', String(params.hasError));
92
+ if (params?.staleHours !== undefined)
93
+ qs.set('staleHours', String(params.staleHours));
94
+ if (params?.neverSynthesized !== undefined)
95
+ qs.set('neverSynthesized', String(params.neverSynthesized));
96
+ if (params?.locked !== undefined)
97
+ qs.set('locked', String(params.locked));
98
+ if (params?.fields?.length)
99
+ qs.set('fields', params.fields.join(','));
100
+ const query = qs.toString();
101
+ return this.get('/metas' + (query ? '?' + query : ''));
102
+ }
103
+ /** GET /metas/:path — detail for a single meta. */
104
+ async detail(metaPath, options) {
105
+ const encoded = encodeURIComponent(metaPath);
106
+ const qs = new URLSearchParams();
107
+ if (options?.includeArchive !== undefined)
108
+ qs.set('includeArchive', String(options.includeArchive));
109
+ if (options?.fields?.length)
110
+ qs.set('fields', options.fields.join(','));
111
+ const query = qs.toString();
112
+ return this.get(`/metas/${encoded}` + (query ? '?' + query : ''));
113
+ }
114
+ /** GET /preview — dry-run next synthesis candidate. */
115
+ async preview(path) {
116
+ const qs = path ? '?path=' + encodeURIComponent(path) : '';
117
+ return this.get('/preview' + qs);
118
+ }
119
+ /** POST /synthesize — enqueue synthesis. */
120
+ async synthesize(path) {
121
+ return this.post('/synthesize', path ? { path } : {});
122
+ }
123
+ /** POST /seed — create .meta/ for a path. */
124
+ async seed(path) {
125
+ return this.post('/seed', { path });
126
+ }
127
+ /** POST /unlock — remove .lock from a meta entity. */
128
+ async unlock(path) {
129
+ return this.post('/unlock', { path });
130
+ }
131
+ /** GET /config/validate — validate current config. */
132
+ async validate() {
133
+ return this.get('/config/validate');
134
+ }
238
135
  }
239
136
 
240
137
  /**
241
- * Synth tool registrations for OpenClaw.
138
+ * Meta tool registrations for OpenClaw.
139
+ *
140
+ * All tools delegate to the jeeves-meta HTTP service.
242
141
  *
243
142
  * @module tools
244
143
  */
245
- /** Register all synth_* tools. */
246
- function registerSynthTools(api) {
247
- const configPath = getConfigPath(api);
248
- // Lazy-load config (resolved once on first use)
249
- let _config = null;
250
- const getConfig = () => {
251
- if (!_config) {
252
- _config = loadSynthConfig(configPath);
253
- }
254
- return _config;
255
- };
256
- /** Derive watcherUrl from loaded config. */
257
- const getWatcherUrl = () => getConfig().watcherUrl;
258
- /** Create a watcher client. */
259
- const getWatcher = () => new HttpWatcherClient({ baseUrl: getWatcherUrl() });
260
- // ─── synth_list ──────────────────────────────────────────────
144
+ /** Register all meta_* tools. */
145
+ function registerMetaTools(api, client) {
146
+ // ─── meta_list ──────────────────────────────────────────────
261
147
  api.registerTool({
262
- name: 'synth_list',
263
- description: 'List metas with summary stats and per-meta projection. Replaces synth_status + synth_entities.',
148
+ name: 'meta_list',
149
+ description: 'List metas with summary stats and per-meta projection. Replaces meta_status + meta_entities.',
264
150
  parameters: {
265
151
  type: 'object',
266
152
  properties: {
@@ -287,134 +173,25 @@ function registerSynthTools(api) {
287
173
  },
288
174
  execute: async (_id, params) => {
289
175
  try {
290
- const pathPrefix = params.pathPrefix;
291
- const config = getConfig();
292
- const result = await listMetas(config, getWatcher());
293
- // Apply path prefix filter
294
- let entries = result.entries;
295
- if (pathPrefix) {
296
- entries = entries.filter((e) => e.path.includes(pathPrefix));
297
- }
298
- // Apply structured filter
299
176
  const filter = params.filter;
300
- if (filter) {
301
- entries = entries.filter((e) => {
302
- if (filter.hasError !== undefined && e.hasError !== filter.hasError)
303
- return false;
304
- if (filter.neverSynthesized !== undefined &&
305
- (e.stalenessSeconds === Infinity) !== filter.neverSynthesized)
306
- return false;
307
- if (filter.locked !== undefined && e.locked !== filter.locked)
308
- return false;
309
- if (typeof filter.staleHours === 'number' &&
310
- e.stalenessSeconds < filter.staleHours * 3600)
311
- return false;
312
- return true;
313
- });
314
- }
315
- // Recompute summary for filtered entries
316
- let staleCount = 0;
317
- let errorCount = 0;
318
- let lockedCount = 0;
319
- let neverSynthesizedCount = 0;
320
- let totalArchTokens = 0;
321
- let totalBuilderTokens = 0;
322
- let totalCriticTokens = 0;
323
- let lastSynthPath = null;
324
- let lastSynthAt = null;
325
- let stalestPath = null;
326
- let stalestEffective = -1;
327
- for (const e of entries) {
328
- if (e.stalenessSeconds > 0)
329
- staleCount++;
330
- if (e.hasError)
331
- errorCount++;
332
- if (e.locked)
333
- lockedCount++;
334
- if (e.stalenessSeconds === Infinity)
335
- neverSynthesizedCount++;
336
- if (e.architectTokens)
337
- totalArchTokens += e.architectTokens;
338
- if (e.builderTokens)
339
- totalBuilderTokens += e.builderTokens;
340
- if (e.criticTokens)
341
- totalCriticTokens += e.criticTokens;
342
- if (e.lastSynthesized &&
343
- (!lastSynthAt || e.lastSynthesized > lastSynthAt)) {
344
- lastSynthAt = e.lastSynthesized;
345
- lastSynthPath = e.path;
346
- }
347
- const depthFactor = Math.pow(1 + config.depthWeight, e.depth);
348
- const effectiveStaleness = (e.stalenessSeconds === Infinity
349
- ? Number.MAX_SAFE_INTEGER
350
- : e.stalenessSeconds) *
351
- depthFactor *
352
- e.emphasis;
353
- if (effectiveStaleness > stalestEffective) {
354
- stalestEffective = effectiveStaleness;
355
- stalestPath = e.path;
356
- }
357
- }
358
- // Project fields
359
- const fields = params.fields;
360
- const items = entries.map((e) => {
361
- const stalenessDisplay = e.stalenessSeconds === Infinity
362
- ? 'never-synthesized'
363
- : Math.round(e.stalenessSeconds);
364
- const display = {
365
- path: e.path,
366
- depth: e.depth,
367
- emphasis: e.emphasis,
368
- stalenessSeconds: stalenessDisplay,
369
- lastSynthesized: e.lastSynthesized,
370
- hasError: e.hasError,
371
- locked: e.locked,
372
- architectTokens: e.architectTokens,
373
- builderTokens: e.builderTokens,
374
- criticTokens: e.criticTokens,
375
- children: e.children,
376
- };
377
- if (fields) {
378
- const projected = {};
379
- for (const f of fields) {
380
- if (f in display)
381
- projected[f] = display[f];
382
- }
383
- return projected;
384
- }
385
- return display;
386
- });
387
- return ok({
388
- summary: {
389
- total: entries.length,
390
- stale: staleCount,
391
- errors: errorCount,
392
- locked: lockedCount,
393
- neverSynthesized: neverSynthesizedCount,
394
- tokens: {
395
- architect: totalArchTokens,
396
- builder: totalBuilderTokens,
397
- critic: totalCriticTokens,
398
- },
399
- stalestPath,
400
- lastSynthesizedPath: lastSynthPath,
401
- lastSynthesizedAt: lastSynthAt,
402
- },
403
- items: items.sort((a, b) => {
404
- const ap = typeof a.path === 'string' ? a.path : '';
405
- const bp = typeof b.path === 'string' ? b.path : '';
406
- return ap.localeCompare(bp);
407
- }),
177
+ const data = await client.listMetas({
178
+ pathPrefix: params.pathPrefix,
179
+ hasError: filter?.hasError,
180
+ staleHours: filter?.staleHours,
181
+ neverSynthesized: filter?.neverSynthesized,
182
+ locked: filter?.locked,
183
+ fields: params.fields,
408
184
  });
185
+ return ok(data);
409
186
  }
410
187
  catch (error) {
411
188
  return fail(error);
412
189
  }
413
190
  },
414
191
  });
415
- // ─── synth_detail ────────────────────────────────────────────
192
+ // ─── meta_detail ────────────────────────────────────────────
416
193
  api.registerTool({
417
- name: 'synth_detail',
194
+ name: 'meta_detail',
418
195
  description: 'Full detail for a single meta, with optional archive history.',
419
196
  parameters: {
420
197
  type: 'object',
@@ -437,69 +214,20 @@ function registerSynthTools(api) {
437
214
  },
438
215
  execute: async (_id, params) => {
439
216
  try {
440
- const targetPath = normalizePath(params.path);
441
- const includeArchive = params.includeArchive;
442
- const defaultExclude = new Set([
443
- '_architect',
444
- '_builder',
445
- '_critic',
446
- '_content',
447
- '_feedback',
448
- ]);
449
- const fields = params.fields;
450
- const result = await listMetas(getConfig(), getWatcher());
451
- const targetNode = findNode(result.tree, targetPath);
452
- if (!targetNode) {
453
- return fail('Meta path not found: ' + targetPath);
454
- }
455
- const { readFileSync } = await import('node:fs');
456
- const { join } = await import('node:path');
457
- const meta = JSON.parse(readFileSync(join(targetNode.metaPath, 'meta.json'), 'utf8'));
458
- // Apply field projection
459
- const projectMeta = (m) => {
460
- if (fields) {
461
- const result = {};
462
- for (const f of fields)
463
- result[f] = m[f];
464
- return result;
465
- }
466
- const result = {};
467
- for (const [k, v] of Object.entries(m)) {
468
- if (!defaultExclude.has(k))
469
- result[k] = v;
470
- }
471
- return result;
472
- };
473
- const response = {
474
- meta: projectMeta(meta),
475
- };
476
- // Archive history
477
- if (includeArchive) {
478
- const { readFileSync } = await import('node:fs');
479
- const { join } = await import('node:path');
480
- const { listArchiveFiles } = await import('@karmaniverous/jeeves-meta');
481
- const archiveFiles = listArchiveFiles(targetNode.metaPath);
482
- const limit = typeof includeArchive === 'number'
483
- ? includeArchive
484
- : archiveFiles.length;
485
- const selected = archiveFiles.slice(-limit).reverse();
486
- const archives = selected.map((af) => {
487
- const raw = readFileSync(join(targetNode.metaPath, 'archive', af), 'utf8');
488
- const parsed = JSON.parse(raw);
489
- return projectMeta(parsed);
490
- });
491
- response.archive = archives;
492
- }
493
- return ok(response);
217
+ const data = await client.detail(params.path, {
218
+ includeArchive: params.includeArchive,
219
+ fields: params.fields,
220
+ });
221
+ return ok(data);
494
222
  }
495
223
  catch (error) {
496
224
  return fail(error);
497
225
  }
498
226
  },
499
227
  });
500
- // ─── synth_preview ────────────────────────────────────────────
228
+ // ─── meta_preview ────────────────────────────────────────────
501
229
  api.registerTool({
502
- name: 'synth_preview',
230
+ name: 'meta_preview',
503
231
  description: 'Dry-run: show what inputs would be gathered for the next synthesis cycle without running LLM.',
504
232
  parameters: {
505
233
  type: 'object',
@@ -512,102 +240,17 @@ function registerSynthTools(api) {
512
240
  },
513
241
  execute: async (_id, params) => {
514
242
  try {
515
- const targetPath = params.path;
516
- const config = getConfig();
517
- const watcher = getWatcher();
518
- const result = await listMetas(config, watcher);
519
- let targetNode;
520
- if (targetPath) {
521
- const normalized = normalizePath(targetPath);
522
- targetNode = findNode(result.tree, normalized);
523
- if (!targetNode) {
524
- return fail('Meta path not found: ' + targetPath);
525
- }
526
- }
527
- else {
528
- // Select stalest candidate
529
- const candidates = result.entries
530
- .filter((e) => e.stalenessSeconds > 0)
531
- .map((e) => ({
532
- node: e.node,
533
- meta: e.meta,
534
- actualStaleness: e.stalenessSeconds,
535
- }));
536
- const weighted = computeEffectiveStaleness(candidates, config.depthWeight);
537
- const winner = selectCandidate(weighted);
538
- if (!winner) {
539
- return ok({
540
- message: 'No stale metas found. Nothing to synthesize.',
541
- });
542
- }
543
- targetNode = winner.node;
544
- }
545
- const { readFileSync: readMeta } = await import('node:fs');
546
- const { join: joinMeta } = await import('node:path');
547
- const meta = JSON.parse(readMeta(joinMeta(targetNode.metaPath, 'meta.json'), 'utf8'));
548
- // Scope files
549
- const allScanFiles = await paginatedScan(watcher, {
550
- pathPrefix: targetNode.ownerPath,
551
- });
552
- const allFiles = allScanFiles.map((f) => f.file_path);
553
- const scopeFiles = filterInScope(targetNode, allFiles);
554
- const structureHash = computeStructureHash(scopeFiles);
555
- const structureChanged = structureHash !== meta._structureHash;
556
- const latestArchive = readLatestArchive(targetNode.metaPath);
557
- const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
558
- const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
559
- let deltaFiles = [];
560
- if (meta._generatedAt) {
561
- const modifiedAfter = Math.floor(new Date(meta._generatedAt).getTime() / 1000);
562
- const deltaScanFiles = await paginatedScan(watcher, {
563
- pathPrefix: targetNode.ownerPath,
564
- modifiedAfter,
565
- });
566
- deltaFiles = filterInScope(targetNode, deltaScanFiles.map((f) => f.file_path));
567
- }
568
- else {
569
- deltaFiles = scopeFiles;
570
- }
571
- return ok({
572
- target: targetNode.metaPath,
573
- ownerPath: targetNode.ownerPath,
574
- depth: meta._depth ?? targetNode.treeDepth,
575
- staleness: actualStaleness(meta) === Infinity
576
- ? 'never-synthesized'
577
- : Math.round(actualStaleness(meta)).toString() + 's',
578
- scopeFiles: {
579
- count: scopeFiles.length,
580
- sample: scopeFiles.slice(0, 20),
581
- },
582
- deltaFiles: {
583
- count: deltaFiles.length,
584
- sample: deltaFiles.slice(0, 20),
585
- },
586
- structureChanged,
587
- steerChanged,
588
- architectTriggered,
589
- architectTriggerReasons: [
590
- ...(!meta._builder ? ['no cached builder (first run)'] : []),
591
- ...(structureChanged ? ['structure changed'] : []),
592
- ...(steerChanged ? ['steer changed'] : []),
593
- ...((meta._synthesisCount ?? 0) >= config.architectEvery
594
- ? ['periodic refresh (architectEvery)']
595
- : []),
596
- ],
597
- currentSteer: meta._steer ?? null,
598
- hasExistingContent: Boolean(meta._content),
599
- hasExistingFeedback: Boolean(meta._feedback),
600
- children: targetNode.children.map((c) => c.metaPath),
601
- });
243
+ const data = await client.preview(params.path);
244
+ return ok(data);
602
245
  }
603
246
  catch (error) {
604
247
  return fail(error);
605
248
  }
606
249
  },
607
250
  });
608
- // ─── synth_trigger ────────────────────────────────────────────
251
+ // ─── meta_trigger ────────────────────────────────────────────
609
252
  api.registerTool({
610
- name: 'synth_trigger',
253
+ name: 'meta_trigger',
611
254
  description: 'Manually trigger synthesis for a specific meta or the next-stalest candidate. Runs the full 3-step cycle (architect, builder, critic).',
612
255
  parameters: {
613
256
  type: 'object',
@@ -620,34 +263,8 @@ function registerSynthTools(api) {
620
263
  },
621
264
  execute: async (_id, params) => {
622
265
  try {
623
- const { orchestrate } = await import('@karmaniverous/jeeves-meta');
624
- const { GatewayExecutor } = await import('@karmaniverous/jeeves-meta');
625
- const config = getConfig();
626
- const executor = new GatewayExecutor({
627
- gatewayUrl: config.gatewayUrl,
628
- apiKey: config.gatewayApiKey,
629
- });
630
- const watcher = getWatcher();
631
- const targetPath = params.path;
632
- const results = await orchestrate(config, executor, watcher, targetPath);
633
- const synthesized = results.filter((r) => r.synthesized);
634
- if (synthesized.length === 0) {
635
- return ok({
636
- message: 'No synthesis performed — no stale metas found or all locked.',
637
- });
638
- }
639
- return ok({
640
- synthesizedCount: synthesized.length,
641
- results: synthesized.map((r) => ({
642
- metaPath: r.metaPath,
643
- error: r.error ?? null,
644
- })),
645
- message: synthesized.length.toString() +
646
- ' meta(s) synthesized.' +
647
- (synthesized.some((r) => r.error)
648
- ? ' Some completed with errors.'
649
- : ''),
650
- });
266
+ const data = await client.synthesize(params.path);
267
+ return ok(data);
651
268
  }
652
269
  catch (error) {
653
270
  return fail(error);
@@ -659,7 +276,7 @@ function registerSynthTools(api) {
659
276
  /**
660
277
  * Generate the Meta menu content for TOOLS.md injection.
661
278
  *
662
- * Queries the watcher API for synthesis entity stats and produces
279
+ * Queries the jeeves-meta service for entity stats and produces
663
280
  * a Markdown section suitable for agent system prompt injection.
664
281
  *
665
282
  * @module promptInjection
@@ -667,43 +284,39 @@ function registerSynthTools(api) {
667
284
  /**
668
285
  * Generate the Meta menu Markdown for TOOLS.md.
669
286
  *
670
- * Three output modes:
671
- * 1. Watcher unreachable - ACTION REQUIRED with diagnostic
672
- * 2. No entities found - ACTION REQUIRED with setup guidance
673
- * 3. Healthy - entity stats + tool listing + skill reference
674
- *
675
- * @param config - Full synth config (for listMetas and watcherUrl).
287
+ * @param client - MetaServiceClient instance.
676
288
  * @returns Markdown string for the Meta section.
677
289
  */
678
- async function generateMetaMenu(config) {
679
- let result;
290
+ async function generateMetaMenu(client) {
291
+ let status;
292
+ let metas;
680
293
  try {
681
- const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
682
- result = await listMetas(config, watcher);
294
+ status = (await client.status());
295
+ metas = (await client.listMetas());
683
296
  }
684
297
  catch {
685
298
  return [
686
- '> **ACTION REQUIRED: jeeves-watcher is unreachable.**',
687
- '> The jeeves-meta synthesis engine requires a running jeeves-watcher service.',
688
- '> The watcher API at ' +
689
- config.watcherUrl +
690
- ' is down or not configured.',
299
+ '> **ACTION REQUIRED: jeeves-meta service is unreachable.**',
300
+ '> The service API is down or not configured.',
301
+ '>',
302
+ '> **Troubleshooting:**',
303
+ '> - Verify the service is installed: `npm list -g @karmaniverous/jeeves-meta`',
304
+ '> - Check if running: `curl http://localhost:1938/status`',
305
+ '> - Verify `serviceUrl` in plugin config if using a non-default port',
691
306
  '>',
692
- "> **Read the `jeeves-meta` skill's Bootstrap section immediately**",
693
- '> for setup instructions. Do not attempt synthesis until watcher is available.',
307
+ "> **Read the `jeeves-meta` skill's Bootstrapping section** for full setup guidance.",
694
308
  ].join('\n');
695
309
  }
696
- if (result.entries.length === 0) {
310
+ if (metas.summary.total === 0) {
697
311
  return [
698
312
  '> **ACTION REQUIRED: No synthesis entities found.**',
699
- '> The watcher is running but no `.meta/` directories were discovered',
700
- '> in the configured watch paths.',
313
+ '> The service is running but no `.meta/` directories were discovered.',
701
314
  '>',
702
- "> **Read the `jeeves-meta` skill's Bootstrap section** for guidance",
703
- '> on creating `.meta/` directories and configuring watch paths.',
315
+ "> **Read the `jeeves-meta` skill's Bootstrapping section** for guidance",
316
+ '> on creating `.meta/` directories.',
704
317
  ].join('\n');
705
318
  }
706
- const { summary, entries } = result;
319
+ const { summary } = metas;
707
320
  const formatAge = (seconds) => {
708
321
  if (!isFinite(seconds))
709
322
  return 'never synthesized';
@@ -713,11 +326,12 @@ async function generateMetaMenu(config) {
713
326
  return Math.round(seconds / 3600).toString() + 'h';
714
327
  return Math.round(seconds / 86400).toString() + 'd';
715
328
  };
716
- // Find stalest age for display
329
+ // Find stalest age
717
330
  let stalestAge = 0;
718
- for (const e of entries) {
719
- if (e.stalenessSeconds > stalestAge)
720
- stalestAge = e.stalenessSeconds;
331
+ for (const item of metas.metas) {
332
+ const s = item.stalenessSeconds !== null ? item.stalenessSeconds : Infinity;
333
+ if (s > stalestAge)
334
+ stalestAge = s;
721
335
  }
722
336
  const stalestDisplay = summary.stalestPath
723
337
  ? summary.stalestPath + ' (' + formatAge(stalestAge) + ')'
@@ -728,9 +342,17 @@ async function generateMetaMenu(config) {
728
342
  summary.lastSynthesizedAt +
729
343
  ')'
730
344
  : 'n/a';
731
- const lines = [
345
+ // Service status + dependency health
346
+ const depLines = [];
347
+ if (status.dependencies.watcher.status !== 'ok') {
348
+ depLines.push('> ⚠️ **Watcher**: ' + status.dependencies.watcher.status);
349
+ }
350
+ if (status.dependencies.gateway.status !== 'ok') {
351
+ depLines.push('> ⚠️ **Gateway**: ' + status.dependencies.gateway.status);
352
+ }
353
+ return [
732
354
  'The jeeves-meta synthesis engine manages ' +
733
- entries.length.toString() +
355
+ summary.total.toString() +
734
356
  ' meta entities.',
735
357
  '',
736
358
  '### Entity Summary',
@@ -742,6 +364,7 @@ async function generateMetaMenu(config) {
742
364
  '| Never synthesized | ' + summary.neverSynthesized.toString() + ' |',
743
365
  '| Stalest | ' + stalestDisplay + ' |',
744
366
  '| Last synthesized | ' + lastSynthDisplay + ' |',
367
+ ...(depLines.length > 0 ? ['', '### Dependencies', ...depLines] : []),
745
368
  '',
746
369
  '### Token Usage (cumulative)',
747
370
  '| Step | Tokens |',
@@ -753,14 +376,13 @@ async function generateMetaMenu(config) {
753
376
  '### Tools',
754
377
  '| Tool | Description |',
755
378
  '|------|-------------|',
756
- '| `synth_list` | List metas with summary stats and per-meta projection |',
757
- '| `synth_detail` | Full detail for a single meta with optional archive history |',
758
- '| `synth_trigger` | Manually trigger synthesis for a specific meta or next-stalest |',
759
- '| `synth_preview` | Dry-run: show what inputs would be gathered without running LLM |',
379
+ '| `meta_list` | List metas with summary stats and per-meta projection |',
380
+ '| `meta_detail` | Full detail for a single meta with optional archive history |',
381
+ '| `meta_trigger` | Manually trigger synthesis for a specific meta or next-stalest |',
382
+ '| `meta_preview` | Dry-run: show what inputs would be gathered without running LLM |',
760
383
  '',
761
384
  'Read the `jeeves-meta` skill for usage guidance, configuration, and troubleshooting.',
762
- ];
763
- return lines.join('\n');
385
+ ].join('\n');
764
386
  }
765
387
 
766
388
  /**
@@ -798,9 +420,12 @@ function resolveToolsPath(api) {
798
420
  function upsertMetaSection(existing, metaMenu) {
799
421
  const section = '## Meta\n\n' + metaMenu;
800
422
  // Replace existing Meta section (match from ## Meta to next ## or # or EOF)
801
- const re = /^## Meta\n[\s\S]*?(?=\n## |\n# |$(?![\s\S]))/m;
802
- if (re.test(existing)) {
803
- return existing.replace(re, section);
423
+ // Remove ALL existing ## Meta sections (handles duplicates from prior bugs)
424
+ const re = /^## Meta\n[\s\S]*?(?=\n## |\n# |$(?![\s\S]))/gm;
425
+ const cleaned = existing.replace(re, '').replace(/\n{3,}/g, '\n\n');
426
+ if (cleaned !== existing) {
427
+ // Had at least one section — re-insert at correct position
428
+ return upsertMetaSection(cleaned.trim() + '\n', metaMenu);
804
429
  }
805
430
  // No existing section. Insert in correct order.
806
431
  const platformH1 = '# Jeeves Platform Tools';
@@ -834,11 +459,11 @@ function upsertMetaSection(existing, metaMenu) {
834
459
  * Fetch the current meta menu and write it to TOOLS.md if changed.
835
460
  *
836
461
  * @param api - Plugin API.
837
- * @param watcherUrl - Watcher API base URL.
462
+ * @param client - MetaServiceClient instance.
838
463
  * @returns True if the file was updated.
839
464
  */
840
- async function refreshToolsMd(api, config) {
841
- const menu = await generateMetaMenu(config);
465
+ async function refreshToolsMd(api, client) {
466
+ const menu = await generateMetaMenu(client);
842
467
  if (menu === lastWrittenMenu) {
843
468
  return false;
844
469
  }
@@ -864,12 +489,12 @@ async function refreshToolsMd(api, config) {
864
489
  * Defers first write by 5s, then refreshes every 60s.
865
490
  *
866
491
  * @param api - Plugin API.
867
- * @param watcherUrl - Watcher API base URL.
492
+ * @param client - MetaServiceClient instance.
868
493
  */
869
- function startToolsWriter(api, config) {
494
+ function startToolsWriter(api, client) {
870
495
  // Deferred initial write
871
496
  setTimeout(() => {
872
- refreshToolsMd(api, config).catch((err) => {
497
+ refreshToolsMd(api, client).catch((err) => {
873
498
  console.error('[jeeves-meta] Failed to write TOOLS.md:', err);
874
499
  });
875
500
  }, INITIAL_DELAY_MS);
@@ -878,7 +503,7 @@ function startToolsWriter(api, config) {
878
503
  clearInterval(intervalHandle);
879
504
  }
880
505
  intervalHandle = setInterval(() => {
881
- refreshToolsMd(api, config).catch((err) => {
506
+ refreshToolsMd(api, client).catch((err) => {
882
507
  console.error('[jeeves-meta] Failed to refresh TOOLS.md:', err);
883
508
  });
884
509
  }, REFRESH_INTERVAL_MS);
@@ -890,23 +515,18 @@ function startToolsWriter(api, config) {
890
515
  /**
891
516
  * OpenClaw plugin for jeeves-meta.
892
517
  *
893
- * Registers synthesis tools, virtual inference rules, and starts
894
- * the periodic TOOLS.md writer at gateway startup.
518
+ * Thin HTTP client all operations delegate to the jeeves-meta service.
519
+ * The plugin registers tools and starts the periodic TOOLS.md writer.
895
520
  *
896
521
  * @packageDocumentation
897
522
  */
898
- /** Register all jeeves-meta tools and rules with the OpenClaw plugin API. */
523
+ /** Register all jeeves-meta tools with the OpenClaw plugin API. */
899
524
  function register(api) {
900
- registerSynthTools(api);
901
- // Load config for rule registration and tools writer
902
- const config = loadSynthConfig(getConfigPath(api));
903
- // Register virtual rules with watcher (fire-and-forget at startup)
904
- registerSynthRules(config.watcherUrl, config).catch((err) => {
905
- const message = err instanceof Error ? err.message : String(err);
906
- console.error('[jeeves-meta] Failed to register virtual rules:', message);
907
- });
908
- // Start periodic TOOLS.md writer
909
- startToolsWriter(api, config);
525
+ const serviceUrl = getServiceUrl(api);
526
+ const client = new MetaServiceClient({ serviceUrl });
527
+ registerMetaTools(api, client);
528
+ // Start periodic TOOLS.md writer (fire-and-forget)
529
+ startToolsWriter(api, client);
910
530
  }
911
531
 
912
532
  export { register as default };