@karmaniverous/jeeves-meta-openclaw 0.1.1 → 0.1.3

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,4 @@
1
- import { HttpWatcherClient, paginatedScan, isLocked, normalizePath, globMetas, buildOwnershipTree, findNode, ensureMetaJson, actualStaleness, computeEffectiveStaleness, selectCandidate, filterInScope, computeStructureHash, readLatestArchive, hasSteerChanged, isArchitectTriggered, loadSynthConfig } from '@karmaniverous/jeeves-meta';
1
+ import { HttpWatcherClient, listMetas, normalizePath, findNode, computeEffectiveStaleness, selectCandidate, paginatedScan, filterInScope, computeStructureHash, readLatestArchive, hasSteerChanged, isArchitectTriggered, actualStaleness, loadSynthConfig } from '@karmaniverous/jeeves-meta';
2
2
  import { readFile, writeFile } from 'node:fs/promises';
3
3
  import { resolve } from 'node:path';
4
4
 
@@ -55,168 +55,175 @@ function fail(error) {
55
55
  * @module rules
56
56
  */
57
57
  const SOURCE = 'jeeves-meta';
58
- /** Virtual rule definitions per spec Section 15. */
59
- const SYNTH_RULES = [
60
- {
61
- name: 'synth-meta-live',
62
- description: 'Live jeeves-meta .meta/meta.json files',
63
- match: {
64
- properties: {
65
- file: {
66
- properties: {
67
- path: { type: 'string', glob: '**/.meta/meta.json' },
68
- },
69
- },
70
- },
71
- },
72
- schema: [
73
- 'base',
74
- {
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: {
75
70
  properties: {
76
- domains: { set: ['synth-meta'] },
77
- synth_id: { type: 'string', set: '{{json._id}}' },
78
- synth_steer: { type: 'string', set: '{{json._steer}}' },
79
- synth_depth: { type: 'number', set: '{{json._depth}}' },
80
- synth_emphasis: { type: 'number', set: '{{json._emphasis}}' },
81
- synth_synthesis_count: {
82
- type: 'integer',
83
- set: '{{json._synthesisCount}}',
84
- },
85
- synth_structure_hash: {
86
- type: 'string',
87
- set: '{{json._structureHash}}',
88
- },
89
- synth_architect_tokens: {
90
- type: 'integer',
91
- set: '{{json._architectTokens}}',
92
- },
93
- synth_builder_tokens: {
94
- type: 'integer',
95
- set: '{{json._builderTokens}}',
96
- },
97
- synth_critic_tokens: {
98
- type: 'integer',
99
- set: '{{json._criticTokens}}',
100
- },
101
- synth_error_step: {
102
- type: 'string',
103
- set: '{{json._error.step}}',
104
- },
105
- generated_at_unix: {
106
- type: 'integer',
107
- set: '{{toUnix json._generatedAt}}',
108
- description: 'Synthesis timestamp as Unix seconds for range queries',
109
- },
110
- has_error: {
111
- type: 'boolean',
112
- set: '{{#if json._error}}true{{else}}false{{/if}}',
71
+ file: {
72
+ properties: {
73
+ path: { type: 'string', glob: '**/.meta/meta.json' },
74
+ },
113
75
  },
114
76
  },
115
77
  },
116
- ],
117
- render: {
118
- frontmatter: [
119
- 'synth_id',
120
- 'synth_steer',
121
- 'generated_at_unix',
122
- 'synth_depth',
123
- 'synth_emphasis',
124
- 'synth_architect_tokens',
125
- 'synth_builder_tokens',
126
- 'synth_critic_tokens',
127
- ],
128
- body: [
78
+ schema: [
79
+ 'base',
129
80
  {
130
- path: 'json._content',
131
- heading: 1,
132
- label: 'Synthesis',
133
- },
134
- ],
135
- },
136
- renderAs: 'md',
137
- },
138
- {
139
- name: 'synth-meta-archive',
140
- description: 'Archived jeeves-meta .meta/archive snapshots',
141
- match: {
142
- properties: {
143
- file: {
144
81
  properties: {
145
- path: { type: 'string', glob: '**/.meta/archive/*.json' },
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
+ },
146
120
  },
147
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
+ ],
148
141
  },
142
+ renderAs: 'md',
149
143
  },
150
- schema: [
151
- 'base',
152
- {
144
+ {
145
+ name: 'synth-meta-archive',
146
+ description: 'Archived jeeves-meta .meta/archive snapshots',
147
+ match: {
153
148
  properties: {
154
- domains: { set: ['synth-archive'] },
155
- synth_id: { type: 'string', set: '{{json._id}}' },
156
- archived: { type: 'boolean', set: 'true' },
157
- archived_at: { type: 'string', set: '{{json._archivedAt}}' },
149
+ file: {
150
+ properties: {
151
+ path: { type: 'string', glob: '**/.meta/archive/*.json' },
152
+ },
153
+ },
158
154
  },
159
155
  },
160
- ],
161
- render: {
162
- frontmatter: ['synth_id', 'archived', 'archived_at'],
163
- body: [
156
+ schema: [
157
+ 'base',
164
158
  {
165
- path: 'json._content',
166
- heading: 1,
167
- label: 'Synthesis (archived)',
168
- },
169
- ],
170
- },
171
- renderAs: 'md',
172
- },
173
- {
174
- name: 'synth-config',
175
- description: 'jeeves-meta configuration file',
176
- match: {
177
- properties: {
178
- file: {
179
159
  properties: {
180
- path: { type: 'string', glob: '**/jeeves-meta.config.json' },
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}}' },
181
164
  },
182
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
+ ],
183
176
  },
177
+ renderAs: 'md',
184
178
  },
185
- schema: [
186
- 'base',
187
- {
179
+ {
180
+ name: 'synth-config',
181
+ description: 'jeeves-meta configuration file',
182
+ match: {
188
183
  properties: {
189
- domains: { set: ['synth-config'] },
184
+ file: {
185
+ properties: {
186
+ path: { type: 'string', glob: '**/jeeves-meta.config.json' },
187
+ },
188
+ },
190
189
  },
191
190
  },
192
- ],
193
- render: {
194
- frontmatter: [
195
- 'watchPaths',
196
- 'watcherUrl',
197
- 'gatewayUrl',
198
- 'architectEvery',
199
- 'depthWeight',
200
- 'maxArchive',
201
- 'maxLines',
202
- 'batchSize',
203
- ],
204
- body: [
191
+ schema: [
192
+ 'base',
205
193
  {
206
- path: 'json.defaultArchitect',
207
- heading: 2,
208
- label: 'Default Architect Prompt',
209
- },
210
- {
211
- path: 'json.defaultCritic',
212
- heading: 2,
213
- label: 'Default Critic Prompt',
194
+ properties: {
195
+ domains: { set: ['synth-config'] },
196
+ },
214
197
  },
215
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',
216
224
  },
217
- renderAs: 'md',
218
- },
219
- ];
225
+ ];
226
+ }
220
227
  /**
221
228
  * Register jeeves-meta virtual rules with the watcher.
222
229
  *
@@ -225,9 +232,9 @@ const SYNTH_RULES = [
225
232
  *
226
233
  * @param watcherUrl - Base URL for the watcher service.
227
234
  */
228
- async function registerSynthRules(watcherUrl) {
235
+ async function registerSynthRules(watcherUrl, config) {
229
236
  const client = new HttpWatcherClient({ baseUrl: watcherUrl });
230
- await client.registerRules(SOURCE, SYNTH_RULES);
237
+ await client.registerRules(SOURCE, buildSynthRules(config));
231
238
  }
232
239
 
233
240
  /**
@@ -248,8 +255,8 @@ function registerSynthTools(api) {
248
255
  };
249
256
  /** Derive watcherUrl from loaded config. */
250
257
  const getWatcherUrl = () => getConfig().watcherUrl;
251
- /** Derive watchPaths from loaded config. */
252
- const getWatchPaths = () => getConfig().watchPaths;
258
+ /** Create a watcher client. */
259
+ const getWatcher = () => new HttpWatcherClient({ baseUrl: getWatcherUrl() });
253
260
  // ─── synth_list ──────────────────────────────────────────────
254
261
  api.registerTool({
255
262
  name: 'synth_list',
@@ -281,26 +288,31 @@ function registerSynthTools(api) {
281
288
  execute: async (_id, params) => {
282
289
  try {
283
290
  const pathPrefix = params.pathPrefix;
284
- const watcher = new HttpWatcherClient({ baseUrl: getWatcherUrl() });
285
- // Query watcher for synth-meta domain points
286
- const scanFiles = await paginatedScan(watcher, {
287
- ...(pathPrefix ? { pathPrefix } : {}),
288
- filter: {
289
- must: [{ key: 'domains', match: { value: 'synth-meta' } }],
290
- },
291
- fields: [
292
- 'file_path',
293
- 'synth_depth',
294
- 'synth_emphasis',
295
- 'synth_architect_tokens',
296
- 'synth_builder_tokens',
297
- 'synth_critic_tokens',
298
- 'has_error',
299
- 'generated_at_unix',
300
- 'synth_error_step',
301
- ],
302
- });
303
- const entities = [];
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
+ 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
304
316
  let staleCount = 0;
305
317
  let errorCount = 0;
306
318
  let lockedCount = 0;
@@ -312,113 +324,69 @@ function registerSynthTools(api) {
312
324
  let lastSynthAt = null;
313
325
  let stalestPath = null;
314
326
  let stalestEffective = -1;
315
- const config = getConfig();
316
- for (const sf of scanFiles) {
317
- const filePath = sf.file_path;
318
- const depth = typeof sf['synth_depth'] === 'number' ? sf['synth_depth'] : 0;
319
- const emphasis = typeof sf['synth_emphasis'] === 'number' ? sf['synth_emphasis'] : 1;
320
- const hasError = sf['has_error'] === true || sf['has_error'] === 'true';
321
- const archTokens = typeof sf['synth_architect_tokens'] === 'number'
322
- ? sf['synth_architect_tokens']
323
- : 0;
324
- const buildTokens = typeof sf['synth_builder_tokens'] === 'number'
325
- ? sf['synth_builder_tokens']
326
- : 0;
327
- const critTokens = typeof sf['synth_critic_tokens'] === 'number'
328
- ? sf['synth_critic_tokens']
329
- : 0;
330
- const genAtUnix = typeof sf['generated_at_unix'] === 'number'
331
- ? sf['generated_at_unix']
332
- : 0;
333
- const locked = isLocked(normalizePath(filePath));
334
- const neverSynth = genAtUnix === 0;
335
- // Compute staleness from generated_at_unix
336
- let stalenessSeconds;
337
- if (neverSynth) {
338
- stalenessSeconds = Infinity;
339
- }
340
- else {
341
- stalenessSeconds = Math.floor(Date.now() / 1000) - genAtUnix;
342
- if (stalenessSeconds < 0)
343
- stalenessSeconds = 0;
344
- }
345
- // Apply structured filter
346
- const filter = params.filter;
347
- if (filter) {
348
- if (filter.hasError !== undefined && hasError !== filter.hasError)
349
- continue;
350
- if (filter.neverSynthesized !== undefined &&
351
- neverSynth !== filter.neverSynthesized)
352
- continue;
353
- if (filter.locked !== undefined && locked !== filter.locked)
354
- continue;
355
- if (typeof filter.staleHours === 'number' &&
356
- stalenessSeconds < filter.staleHours * 3600)
357
- continue;
358
- }
359
- if (stalenessSeconds > 0)
327
+ for (const e of entries) {
328
+ if (e.stalenessSeconds > 0)
360
329
  staleCount++;
361
- if (hasError)
330
+ if (e.hasError)
362
331
  errorCount++;
363
- if (locked)
332
+ if (e.locked)
364
333
  lockedCount++;
365
- if (neverSynth)
334
+ if (e.stalenessSeconds === Infinity)
366
335
  neverSynthesizedCount++;
367
- if (archTokens > 0)
368
- totalArchTokens += archTokens;
369
- if (buildTokens > 0)
370
- totalBuilderTokens += buildTokens;
371
- if (critTokens > 0)
372
- totalCriticTokens += critTokens;
373
- const genAtIso = genAtUnix > 0 ? new Date(genAtUnix * 1000).toISOString() : null;
374
- if (genAtIso && (!lastSynthAt || genAtIso > lastSynthAt)) {
375
- lastSynthAt = genAtIso;
376
- lastSynthPath = filePath;
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;
377
346
  }
378
- // Effective staleness for stalest computation
379
- const depthFactor = Math.pow(1 + config.depthWeight, depth);
380
- const effectiveStaleness = (stalenessSeconds === Infinity
347
+ const depthFactor = Math.pow(1 + config.depthWeight, e.depth);
348
+ const effectiveStaleness = (e.stalenessSeconds === Infinity
381
349
  ? Number.MAX_SAFE_INTEGER
382
- : stalenessSeconds) *
350
+ : e.stalenessSeconds) *
383
351
  depthFactor *
384
- emphasis;
352
+ e.emphasis;
385
353
  if (effectiveStaleness > stalestEffective) {
386
354
  stalestEffective = effectiveStaleness;
387
- stalestPath = filePath;
355
+ stalestPath = e.path;
388
356
  }
389
- // Derive meta path from file_path (strip /meta.json)
390
- const metaPath = filePath.replace(/\/meta\.json$/, '');
391
- const fields = params.fields;
392
- const raw = {
393
- path: metaPath,
394
- depth,
395
- emphasis,
396
- stalenessSeconds: stalenessSeconds === Infinity
397
- ? 'never-synthesized'
398
- : Math.round(stalenessSeconds),
399
- lastSynthesized: genAtIso,
400
- hasError,
401
- locked,
402
- architectTokens: archTokens > 0 ? archTokens : null,
403
- builderTokens: buildTokens > 0 ? buildTokens : null,
404
- criticTokens: critTokens > 0 ? critTokens : null,
405
- children: 0,
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,
406
376
  };
407
377
  if (fields) {
408
378
  const projected = {};
409
379
  for (const f of fields) {
410
- if (f in raw)
411
- projected[f] = raw[f];
380
+ if (f in display)
381
+ projected[f] = display[f];
412
382
  }
413
- entities.push(projected);
383
+ return projected;
414
384
  }
415
- else {
416
- entities.push(raw);
417
- }
418
- }
385
+ return display;
386
+ });
419
387
  return ok({
420
388
  summary: {
421
- total: entities.length,
389
+ total: entries.length,
422
390
  stale: staleCount,
423
391
  errors: errorCount,
424
392
  locked: lockedCount,
@@ -432,7 +400,11 @@ function registerSynthTools(api) {
432
400
  lastSynthesizedPath: lastSynthPath,
433
401
  lastSynthesizedAt: lastSynthAt,
434
402
  },
435
- items: entities.sort((a, b) => String(a.path ?? '').localeCompare(String(b.path ?? ''))),
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
+ }),
436
408
  });
437
409
  }
438
410
  catch (error) {
@@ -475,13 +447,14 @@ function registerSynthTools(api) {
475
447
  '_feedback',
476
448
  ]);
477
449
  const fields = params.fields;
478
- const metaPaths = globMetas(getWatchPaths());
479
- const tree = buildOwnershipTree(metaPaths);
480
- const targetNode = findNode(tree, targetPath);
450
+ const result = await listMetas(getConfig(), getWatcher());
451
+ const targetNode = findNode(result.tree, targetPath);
481
452
  if (!targetNode) {
482
453
  return fail('Meta path not found: ' + targetPath);
483
454
  }
484
- const meta = ensureMetaJson(targetNode.metaPath);
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'));
485
458
  // Apply field projection
486
459
  const projectMeta = (m) => {
487
460
  if (fields) {
@@ -490,7 +463,6 @@ function registerSynthTools(api) {
490
463
  result[f] = m[f];
491
464
  return result;
492
465
  }
493
- // Default: exclude big text blobs
494
466
  const result = {};
495
467
  for (const [k, v] of Object.entries(m)) {
496
468
  if (!defaultExclude.has(k))
@@ -510,7 +482,6 @@ function registerSynthTools(api) {
510
482
  const limit = typeof includeArchive === 'number'
511
483
  ? includeArchive
512
484
  : archiveFiles.length;
513
- // Most recent first (files are sorted by timestamp)
514
485
  const selected = archiveFiles.slice(-limit).reverse();
515
486
  const archives = selected.map((af) => {
516
487
  const raw = readFileSync(join(targetNode.metaPath, 'archive', af), 'utf8');
@@ -542,27 +513,27 @@ function registerSynthTools(api) {
542
513
  execute: async (_id, params) => {
543
514
  try {
544
515
  const targetPath = params.path;
545
- const metaPaths = globMetas(getWatchPaths());
546
- const tree = buildOwnershipTree(metaPaths);
516
+ const config = getConfig();
517
+ const watcher = getWatcher();
518
+ const result = await listMetas(config, watcher);
547
519
  let targetNode;
548
520
  if (targetPath) {
549
521
  const normalized = normalizePath(targetPath);
550
- targetNode = findNode(tree, normalized);
522
+ targetNode = findNode(result.tree, normalized);
551
523
  if (!targetNode) {
552
524
  return fail('Meta path not found: ' + targetPath);
553
525
  }
554
526
  }
555
527
  else {
556
- // Select stalest
557
- const candidates = [];
558
- for (const node of tree.nodes.values()) {
559
- const meta = ensureMetaJson(node.metaPath);
560
- const staleness = actualStaleness(meta);
561
- if (staleness > 0) {
562
- candidates.push({ node, meta, actualStaleness: staleness });
563
- }
564
- }
565
- const weighted = computeEffectiveStaleness(candidates, getConfig().depthWeight);
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);
566
537
  const winner = selectCandidate(weighted);
567
538
  if (!winner) {
568
539
  return ok({
@@ -571,23 +542,20 @@ function registerSynthTools(api) {
571
542
  }
572
543
  targetNode = winner.node;
573
544
  }
574
- const meta = ensureMetaJson(targetNode.metaPath);
575
- const watcher = new HttpWatcherClient({ baseUrl: getWatcherUrl() });
576
- // Scope files (paginated for completeness)
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
577
549
  const allScanFiles = await paginatedScan(watcher, {
578
550
  pathPrefix: targetNode.ownerPath,
579
551
  });
580
552
  const allFiles = allScanFiles.map((f) => f.file_path);
581
553
  const scopeFiles = filterInScope(targetNode, allFiles);
582
- // Structure hash on scope-filtered files (matches orchestrator)
583
554
  const structureHash = computeStructureHash(scopeFiles);
584
555
  const structureChanged = structureHash !== meta._structureHash;
585
- // Steer change
586
556
  const latestArchive = readLatestArchive(targetNode.metaPath);
587
557
  const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
588
- // Architect trigger check
589
- const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, getConfig().architectEvery);
590
- // Delta files
558
+ const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
591
559
  let deltaFiles = [];
592
560
  if (meta._generatedAt) {
593
561
  const modifiedAfter = Math.floor(new Date(meta._generatedAt).getTime() / 1000);
@@ -622,7 +590,7 @@ function registerSynthTools(api) {
622
590
  ...(!meta._builder ? ['no cached builder (first run)'] : []),
623
591
  ...(structureChanged ? ['structure changed'] : []),
624
592
  ...(steerChanged ? ['steer changed'] : []),
625
- ...((meta._synthesisCount ?? 0) >= getConfig().architectEvery
593
+ ...((meta._synthesisCount ?? 0) >= config.architectEvery
626
594
  ? ['periodic refresh (architectEvery)']
627
595
  : []),
628
596
  ],
@@ -654,22 +622,14 @@ function registerSynthTools(api) {
654
622
  try {
655
623
  const { orchestrate } = await import('@karmaniverous/jeeves-meta');
656
624
  const { GatewayExecutor } = await import('@karmaniverous/jeeves-meta');
657
- // Load config from canonical config file
658
625
  const config = getConfig();
659
626
  const executor = new GatewayExecutor({
660
627
  gatewayUrl: config.gatewayUrl,
661
628
  apiKey: config.gatewayApiKey,
662
629
  });
663
- const watcher = new HttpWatcherClient({ baseUrl: getWatcherUrl() });
664
- // If path specified, temporarily override watchPaths to target it
630
+ const watcher = getWatcher();
665
631
  const targetPath = params.path;
666
- const effectiveConfig = targetPath
667
- ? {
668
- ...config,
669
- watchPaths: [targetPath.replace(/[/\\]\.meta[/\\]?$/, '')],
670
- }
671
- : config;
672
- const results = await orchestrate(effectiveConfig, executor, watcher);
632
+ const results = await orchestrate(config, executor, watcher, targetPath);
673
633
  const synthesized = results.filter((r) => r.synthesized);
674
634
  if (synthesized.length === 0) {
675
635
  return ok({
@@ -712,46 +672,28 @@ function registerSynthTools(api) {
712
672
  * 2. No entities found - ACTION REQUIRED with setup guidance
713
673
  * 3. Healthy - entity stats + tool listing + skill reference
714
674
  *
715
- * @param watcherUrl - Watcher API base URL.
675
+ * @param config - Full synth config (for listMetas and watcherUrl).
716
676
  * @returns Markdown string for the Meta section.
717
677
  */
718
- async function generateMetaMenu(watcherUrl) {
719
- let entities = [];
678
+ async function generateMetaMenu(config) {
679
+ let result;
720
680
  try {
721
- const watcher = new HttpWatcherClient({ baseUrl: watcherUrl });
722
- entities = await paginatedScan(watcher, {
723
- filter: {
724
- must: [
725
- {
726
- key: 'domains',
727
- match: { value: 'synth-meta' },
728
- },
729
- ],
730
- },
731
- fields: [
732
- 'synth_depth',
733
- 'synth_emphasis',
734
- 'synth_synthesis_count',
735
- 'synth_architect_tokens',
736
- 'synth_builder_tokens',
737
- 'synth_critic_tokens',
738
- 'synth_error_step',
739
- 'generated_at_unix',
740
- 'has_error',
741
- ],
742
- });
681
+ const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
682
+ result = await listMetas(config, watcher);
743
683
  }
744
684
  catch {
745
685
  return [
746
686
  '> **ACTION REQUIRED: jeeves-watcher is unreachable.**',
747
687
  '> The jeeves-meta synthesis engine requires a running jeeves-watcher service.',
748
- `> The watcher API at ${watcherUrl} is down or not configured.`,
688
+ '> The watcher API at ' +
689
+ config.watcherUrl +
690
+ ' is down or not configured.',
749
691
  '>',
750
692
  "> **Read the `jeeves-meta` skill's Bootstrap section immediately**",
751
693
  '> for setup instructions. Do not attempt synthesis until watcher is available.',
752
694
  ].join('\n');
753
695
  }
754
- if (entities.length === 0) {
696
+ if (result.entries.length === 0) {
755
697
  return [
756
698
  '> **ACTION REQUIRED: No synthesis entities found.**',
757
699
  '> The watcher is running but no `.meta/` directories were discovered',
@@ -761,55 +703,7 @@ async function generateMetaMenu(watcherUrl) {
761
703
  '> on creating `.meta/` directories and configuring watch paths.',
762
704
  ].join('\n');
763
705
  }
764
- // Compute stats
765
- const now = Math.floor(Date.now() / 1000);
766
- let staleCount = 0;
767
- let errorCount = 0;
768
- let neverSynthesized = 0;
769
- let totalArchTokens = 0;
770
- let totalBuilderTokens = 0;
771
- let totalCriticTokens = 0;
772
- let stalestPath = '';
773
- let stalestAge = 0;
774
- let lastSynthPath = '';
775
- let lastSynthUnix = 0;
776
- for (const e of entities) {
777
- const generatedAt = e['generated_at_unix'];
778
- const hasError = e['has_error'];
779
- const archTokens = e['synth_architect_tokens'];
780
- const builderTokens = e['synth_builder_tokens'];
781
- const criticTokens = e['synth_critic_tokens'];
782
- if (!generatedAt) {
783
- neverSynthesized++;
784
- staleCount++;
785
- if (!isFinite(stalestAge)) ;
786
- else {
787
- stalestAge = Infinity;
788
- stalestPath = e.file_path;
789
- }
790
- }
791
- else {
792
- const age = now - generatedAt;
793
- if (age > 0)
794
- staleCount++;
795
- if (age > stalestAge && isFinite(age)) {
796
- stalestAge = age;
797
- stalestPath = e.file_path;
798
- }
799
- if (generatedAt > lastSynthUnix) {
800
- lastSynthUnix = generatedAt;
801
- lastSynthPath = e.file_path;
802
- }
803
- }
804
- if (hasError)
805
- errorCount++;
806
- if (archTokens)
807
- totalArchTokens += archTokens;
808
- if (builderTokens)
809
- totalBuilderTokens += builderTokens;
810
- if (criticTokens)
811
- totalCriticTokens += criticTokens;
812
- }
706
+ const { summary, entries } = result;
813
707
  const formatAge = (seconds) => {
814
708
  if (!isFinite(seconds))
815
709
  return 'never synthesized';
@@ -819,25 +713,42 @@ async function generateMetaMenu(watcherUrl) {
819
713
  return Math.round(seconds / 3600).toString() + 'h';
820
714
  return Math.round(seconds / 86400).toString() + 'd';
821
715
  };
716
+ // Find stalest age for display
717
+ let stalestAge = 0;
718
+ for (const e of entries) {
719
+ if (e.stalenessSeconds > stalestAge)
720
+ stalestAge = e.stalenessSeconds;
721
+ }
722
+ const stalestDisplay = summary.stalestPath
723
+ ? summary.stalestPath + ' (' + formatAge(stalestAge) + ')'
724
+ : 'n/a';
725
+ const lastSynthDisplay = summary.lastSynthesizedAt
726
+ ? (summary.lastSynthesizedPath ?? '') +
727
+ ' (' +
728
+ summary.lastSynthesizedAt +
729
+ ')'
730
+ : 'n/a';
822
731
  const lines = [
823
- `The jeeves-meta synthesis engine manages ${entities.length.toString()} meta entities.`,
732
+ 'The jeeves-meta synthesis engine manages ' +
733
+ entries.length.toString() +
734
+ ' meta entities.',
824
735
  '',
825
736
  '### Entity Summary',
826
737
  '| Metric | Value |',
827
738
  '|--------|-------|',
828
- `| Total | ${entities.length.toString()} |`,
829
- `| Stale | ${staleCount.toString()} |`,
830
- `| Errors | ${errorCount.toString()} |`,
831
- `| Never synthesized | ${neverSynthesized.toString()} |`,
832
- `| Stalest | ${stalestPath ? stalestPath + ' (' + formatAge(stalestAge) + ')' : 'n/a'} |`,
833
- `| Last synthesized | ${lastSynthPath ? lastSynthPath + ' (' + new Date(lastSynthUnix * 1000).toISOString() + ')' : 'n/a'} |`,
739
+ '| Total | ' + summary.total.toString() + ' |',
740
+ '| Stale | ' + summary.stale.toString() + ' |',
741
+ '| Errors | ' + summary.errors.toString() + ' |',
742
+ '| Never synthesized | ' + summary.neverSynthesized.toString() + ' |',
743
+ '| Stalest | ' + stalestDisplay + ' |',
744
+ '| Last synthesized | ' + lastSynthDisplay + ' |',
834
745
  '',
835
746
  '### Token Usage (cumulative)',
836
747
  '| Step | Tokens |',
837
748
  '|------|--------|',
838
- `| Architect | ${totalArchTokens.toLocaleString()} |`,
839
- `| Builder | ${totalBuilderTokens.toLocaleString()} |`,
840
- `| Critic | ${totalCriticTokens.toLocaleString()} |`,
749
+ '| Architect | ' + summary.tokens.architect.toLocaleString() + ' |',
750
+ '| Builder | ' + summary.tokens.builder.toLocaleString() + ' |',
751
+ '| Critic | ' + summary.tokens.critic.toLocaleString() + ' |',
841
752
  '',
842
753
  '### Tools',
843
754
  '| Tool | Description |',
@@ -926,8 +837,8 @@ function upsertMetaSection(existing, metaMenu) {
926
837
  * @param watcherUrl - Watcher API base URL.
927
838
  * @returns True if the file was updated.
928
839
  */
929
- async function refreshToolsMd(api, watcherUrl) {
930
- const menu = await generateMetaMenu(watcherUrl);
840
+ async function refreshToolsMd(api, config) {
841
+ const menu = await generateMetaMenu(config);
931
842
  if (menu === lastWrittenMenu) {
932
843
  return false;
933
844
  }
@@ -955,10 +866,10 @@ async function refreshToolsMd(api, watcherUrl) {
955
866
  * @param api - Plugin API.
956
867
  * @param watcherUrl - Watcher API base URL.
957
868
  */
958
- function startToolsWriter(api, watcherUrl) {
869
+ function startToolsWriter(api, config) {
959
870
  // Deferred initial write
960
871
  setTimeout(() => {
961
- refreshToolsMd(api, watcherUrl).catch((err) => {
872
+ refreshToolsMd(api, config).catch((err) => {
962
873
  console.error('[jeeves-meta] Failed to write TOOLS.md:', err);
963
874
  });
964
875
  }, INITIAL_DELAY_MS);
@@ -967,7 +878,7 @@ function startToolsWriter(api, watcherUrl) {
967
878
  clearInterval(intervalHandle);
968
879
  }
969
880
  intervalHandle = setInterval(() => {
970
- refreshToolsMd(api, watcherUrl).catch((err) => {
881
+ refreshToolsMd(api, config).catch((err) => {
971
882
  console.error('[jeeves-meta] Failed to refresh TOOLS.md:', err);
972
883
  });
973
884
  }, REFRESH_INTERVAL_MS);
@@ -990,12 +901,12 @@ function register(api) {
990
901
  // Load config for rule registration and tools writer
991
902
  const config = loadSynthConfig(getConfigPath(api));
992
903
  // Register virtual rules with watcher (fire-and-forget at startup)
993
- registerSynthRules(config.watcherUrl).catch((err) => {
904
+ registerSynthRules(config.watcherUrl, config).catch((err) => {
994
905
  const message = err instanceof Error ? err.message : String(err);
995
906
  console.error('[jeeves-meta] Failed to register virtual rules:', message);
996
907
  });
997
908
  // Start periodic TOOLS.md writer
998
- startToolsWriter(api, config.watcherUrl);
909
+ startToolsWriter(api, config);
999
910
  }
1000
911
 
1001
912
  export { register as default };
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @module promptInjection
8
8
  */
9
+ import { type SynthConfig } from '@karmaniverous/jeeves-meta';
9
10
  /**
10
11
  * Generate the Meta menu Markdown for TOOLS.md.
11
12
  *
@@ -14,7 +15,7 @@
14
15
  * 2. No entities found - ACTION REQUIRED with setup guidance
15
16
  * 3. Healthy - entity stats + tool listing + skill reference
16
17
  *
17
- * @param watcherUrl - Watcher API base URL.
18
+ * @param config - Full synth config (for listMetas and watcherUrl).
18
19
  * @returns Markdown string for the Meta section.
19
20
  */
20
- export declare function generateMetaMenu(watcherUrl: string): Promise<string>;
21
+ export declare function generateMetaMenu(config: SynthConfig): Promise<string>;
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * @module rules
10
10
  */
11
+ import type { SynthConfig } from '@karmaniverous/jeeves-meta';
11
12
  /**
12
13
  * Register jeeves-meta virtual rules with the watcher.
13
14
  *
@@ -16,4 +17,4 @@
16
17
  *
17
18
  * @param watcherUrl - Base URL for the watcher service.
18
19
  */
19
- export declare function registerSynthRules(watcherUrl: string): Promise<void>;
20
+ export declare function registerSynthRules(watcherUrl: string, config: SynthConfig): Promise<void>;
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @module toolsWriter
8
8
  */
9
+ import { type SynthConfig } from '@karmaniverous/jeeves-meta';
9
10
  import type { PluginApi } from './helpers.js';
10
11
  /**
11
12
  * Upsert the Meta section in TOOLS.md content.
@@ -23,4 +24,4 @@ export declare function upsertMetaSection(existing: string, metaMenu: string): s
23
24
  * @param api - Plugin API.
24
25
  * @param watcherUrl - Watcher API base URL.
25
26
  */
26
- export declare function startToolsWriter(api: PluginApi, watcherUrl: string): void;
27
+ export declare function startToolsWriter(api: PluginApi, config: SynthConfig): void;
@@ -2,7 +2,7 @@
2
2
  "id": "jeeves-meta-openclaw",
3
3
  "name": "Jeeves Meta",
4
4
  "description": "Knowledge synthesis tools — trigger synthesis, view status, manage entities.",
5
- "version": "0.1.1",
5
+ "version": "0.1.3",
6
6
  "skills": [
7
7
  "dist/skills/jeeves-meta"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/jeeves-meta-openclaw",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "author": "Jason Williscroft",
5
5
  "description": "OpenClaw plugin for jeeves-meta — synthesis tools and virtual rule registration",
6
6
  "license": "BSD-3-Clause",