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