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