@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/README.md +22 -15
- package/dist/index.js +186 -573
- package/dist/skills/jeeves-meta/SKILL.md +217 -62
- package/dist/src/helpers.d.ts +5 -5
- package/dist/src/index.d.ts +4 -4
- package/dist/src/promptInjection.d.ts +4 -9
- package/dist/src/serviceClient.d.ts +45 -0
- package/dist/src/tools.d.ts +6 -3
- package/dist/src/toolsWriter.d.ts +3 -3
- package/openclaw.plugin.json +3 -3
- package/package.json +1 -4
- package/dist/src/configLoader.d.ts +0 -6
- package/dist/src/rules.d.ts +0 -20
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
|
|
16
|
+
* Resolve the service URL.
|
|
17
17
|
*
|
|
18
18
|
* Resolution order:
|
|
19
|
-
* 1. Plugin config `
|
|
20
|
-
* 2. `
|
|
21
|
-
* 3.
|
|
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
|
|
24
|
-
const fromPlugin = getPluginConfig(api)?.
|
|
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['
|
|
27
|
+
const fromEnv = process.env['JEEVES_META_URL'];
|
|
28
28
|
if (fromEnv)
|
|
29
29
|
return fromEnv;
|
|
30
|
-
|
|
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
|
-
*
|
|
48
|
+
* Thin HTTP client for the jeeves-meta service.
|
|
49
49
|
*
|
|
50
|
-
*
|
|
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
|
|
52
|
+
* @module serviceClient
|
|
56
53
|
*/
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
*
|
|
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
|
|
253
|
-
function
|
|
254
|
-
|
|
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: '
|
|
270
|
-
description: 'List metas with summary stats and per-meta projection. Replaces
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
// ───
|
|
192
|
+
// ─── meta_detail ────────────────────────────────────────────
|
|
423
193
|
api.registerTool({
|
|
424
|
-
name: '
|
|
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
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
// ───
|
|
228
|
+
// ─── meta_preview ────────────────────────────────────────────
|
|
508
229
|
api.registerTool({
|
|
509
|
-
name: '
|
|
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
|
|
523
|
-
|
|
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
|
-
// ───
|
|
251
|
+
// ─── meta_trigger ────────────────────────────────────────────
|
|
616
252
|
api.registerTool({
|
|
617
|
-
name: '
|
|
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
|
|
631
|
-
|
|
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
|
|
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
|
-
*
|
|
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(
|
|
686
|
-
let
|
|
290
|
+
async function generateMetaMenu(client) {
|
|
291
|
+
let status;
|
|
292
|
+
let metas;
|
|
687
293
|
try {
|
|
688
|
-
|
|
689
|
-
|
|
294
|
+
status = (await client.status());
|
|
295
|
+
metas = (await client.listMetas());
|
|
690
296
|
}
|
|
691
297
|
catch {
|
|
692
298
|
return [
|
|
693
|
-
'> **ACTION REQUIRED: jeeves-
|
|
694
|
-
'> The
|
|
695
|
-
'>
|
|
696
|
-
|
|
697
|
-
|
|
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
|
|
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 (
|
|
310
|
+
if (metas.summary.total === 0) {
|
|
704
311
|
return [
|
|
705
312
|
'> **ACTION REQUIRED: No synthesis entities found.**',
|
|
706
|
-
'> The
|
|
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
|
|
710
|
-
'> on creating `.meta/` directories
|
|
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
|
|
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
|
|
329
|
+
// Find stalest age
|
|
724
330
|
let stalestAge = 0;
|
|
725
|
-
for (const
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
'| `
|
|
764
|
-
'| `
|
|
765
|
-
'| `
|
|
766
|
-
'| `
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
|
462
|
+
* @param client - MetaServiceClient instance.
|
|
845
463
|
* @returns True if the file was updated.
|
|
846
464
|
*/
|
|
847
|
-
async function refreshToolsMd(api,
|
|
848
|
-
const menu = await generateMetaMenu(
|
|
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
|
|
492
|
+
* @param client - MetaServiceClient instance.
|
|
875
493
|
*/
|
|
876
|
-
function startToolsWriter(api,
|
|
494
|
+
function startToolsWriter(api, client) {
|
|
877
495
|
// Deferred initial write
|
|
878
496
|
setTimeout(() => {
|
|
879
|
-
refreshToolsMd(api,
|
|
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,
|
|
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
|
-
*
|
|
901
|
-
* the periodic TOOLS.md writer
|
|
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
|
|
523
|
+
/** Register all jeeves-meta tools with the OpenClaw plugin API. */
|
|
906
524
|
function register(api) {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
//
|
|
911
|
-
|
|
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 };
|