@karmaniverous/jeeves-meta 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +387 -335
- package/dist/index.d.ts +232 -262
- package/dist/index.js +331 -343
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, readdirSync, unlinkSync, mkdirSync, writeFileSync, existsSync
|
|
2
|
+
import { readFileSync, readdirSync, unlinkSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, relative, sep, resolve } from 'node:path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import {
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Zod schema for jeeves-meta configuration.
|
|
@@ -15,7 +15,6 @@ import { randomUUID, createHash } from 'node:crypto';
|
|
|
15
15
|
/** Zod schema for jeeves-meta configuration. */
|
|
16
16
|
const synthConfigSchema = z.object({
|
|
17
17
|
/** Filesystem paths to watch for .meta/ directories. */
|
|
18
|
-
watchPaths: z.array(z.string()).min(1),
|
|
19
18
|
/** Watcher service base URL. */
|
|
20
19
|
watcherUrl: z.url(),
|
|
21
20
|
/** OpenClaw gateway base URL for subprocess spawning. */
|
|
@@ -48,6 +47,21 @@ const synthConfigSchema = z.object({
|
|
|
48
47
|
skipUnchanged: z.boolean().default(true),
|
|
49
48
|
/** Number of metas to synthesize per invocation. */
|
|
50
49
|
batchSize: z.number().int().min(1).default(1),
|
|
50
|
+
/**
|
|
51
|
+
* Watcher metadata properties for live .meta/meta.json files.
|
|
52
|
+
* Virtual rules use these to tag live metas; scan queries derive
|
|
53
|
+
* their filter from the first domain value.
|
|
54
|
+
*/
|
|
55
|
+
metaProperty: z
|
|
56
|
+
.object({ domains: z.array(z.string()).min(1) })
|
|
57
|
+
.default({ domains: ['meta'] }),
|
|
58
|
+
/**
|
|
59
|
+
* Watcher metadata properties for .meta/archive/** snapshots.
|
|
60
|
+
* Virtual rules use these to tag archive files.
|
|
61
|
+
*/
|
|
62
|
+
metaArchiveProperty: z
|
|
63
|
+
.object({ domains: z.array(z.string()).min(1) })
|
|
64
|
+
.default({ domains: ['meta-archive'] }),
|
|
51
65
|
});
|
|
52
66
|
|
|
53
67
|
/**
|
|
@@ -310,81 +324,100 @@ var index$1 = /*#__PURE__*/Object.freeze({
|
|
|
310
324
|
});
|
|
311
325
|
|
|
312
326
|
/**
|
|
313
|
-
*
|
|
327
|
+
* Normalize file paths to forward slashes for consistency with watcher-indexed paths.
|
|
314
328
|
*
|
|
315
|
-
*
|
|
329
|
+
* Watcher indexes paths with forward slashes (`j:/domains/...`). This utility
|
|
330
|
+
* ensures all paths in the library use the same convention, regardless of
|
|
331
|
+
* the platform's native separator.
|
|
316
332
|
*
|
|
317
|
-
* @module
|
|
333
|
+
* @module normalizePath
|
|
318
334
|
*/
|
|
319
335
|
/**
|
|
320
|
-
*
|
|
336
|
+
* Normalize a file path to forward slashes.
|
|
321
337
|
*
|
|
322
|
-
* @param
|
|
323
|
-
* @returns
|
|
338
|
+
* @param p - File path (may contain backslashes).
|
|
339
|
+
* @returns Path with all backslashes replaced by forward slashes.
|
|
324
340
|
*/
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
if (existsSync(filePath)) {
|
|
328
|
-
const raw = readFileSync(filePath, 'utf8');
|
|
329
|
-
return JSON.parse(raw);
|
|
330
|
-
}
|
|
331
|
-
// Create the archive subdirectory while we're at it
|
|
332
|
-
const archivePath = join(metaPath, 'archive');
|
|
333
|
-
if (!existsSync(archivePath)) {
|
|
334
|
-
mkdirSync(archivePath, { recursive: true });
|
|
335
|
-
}
|
|
336
|
-
const meta = { _id: randomUUID() };
|
|
337
|
-
writeFileSync(filePath, JSON.stringify(meta, null, 2) + '\n');
|
|
338
|
-
return meta;
|
|
341
|
+
function normalizePath$1(p) {
|
|
342
|
+
return p.replaceAll('\\', '/');
|
|
339
343
|
}
|
|
340
344
|
|
|
341
345
|
/**
|
|
342
|
-
*
|
|
346
|
+
* Paginated scan helper for exhaustive scope enumeration.
|
|
343
347
|
*
|
|
344
|
-
*
|
|
345
|
-
|
|
348
|
+
* @module paginatedScan
|
|
349
|
+
*/
|
|
350
|
+
/**
|
|
351
|
+
* Perform a paginated scan that follows cursor tokens until exhausted.
|
|
346
352
|
*
|
|
347
|
-
* @
|
|
353
|
+
* @param watcher - WatcherClient instance.
|
|
354
|
+
* @param params - Base scan parameters (cursor is managed internally).
|
|
355
|
+
* @returns All matching files across all pages.
|
|
348
356
|
*/
|
|
357
|
+
async function paginatedScan(watcher, params) {
|
|
358
|
+
const allFiles = [];
|
|
359
|
+
let cursor;
|
|
360
|
+
do {
|
|
361
|
+
const result = await watcher.scan({ ...params, cursor });
|
|
362
|
+
allFiles.push(...result.files);
|
|
363
|
+
cursor = result.next;
|
|
364
|
+
} while (cursor);
|
|
365
|
+
return allFiles;
|
|
366
|
+
}
|
|
367
|
+
|
|
349
368
|
/**
|
|
350
|
-
*
|
|
369
|
+
* Discover .meta/ directories via watcher scan.
|
|
370
|
+
*
|
|
371
|
+
* Replaces filesystem-based globMetas() with a watcher query
|
|
372
|
+
* that returns indexed .meta/meta.json points, filtered by domain.
|
|
351
373
|
*
|
|
352
|
-
* @
|
|
353
|
-
* @returns Array of absolute paths to .meta/ directories.
|
|
374
|
+
* @module discovery/discoverMetas
|
|
354
375
|
*/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
376
|
+
/**
|
|
377
|
+
* Build a Qdrant filter from config metaProperty.
|
|
378
|
+
*
|
|
379
|
+
* @param config - Synth config with metaProperty.
|
|
380
|
+
* @returns Qdrant filter object for scanning live metas.
|
|
381
|
+
*/
|
|
382
|
+
function buildMetaFilter(config) {
|
|
383
|
+
return {
|
|
384
|
+
must: [
|
|
385
|
+
{
|
|
386
|
+
key: 'domains',
|
|
387
|
+
match: { value: config.metaProperty.domains[0] },
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Discover all .meta/ directories via watcher scan.
|
|
394
|
+
*
|
|
395
|
+
* Queries the watcher for indexed .meta/meta.json points using the
|
|
396
|
+
* configured domain filter. Returns deduplicated meta directory paths.
|
|
397
|
+
*
|
|
398
|
+
* @param config - Synth config (for domain filter).
|
|
399
|
+
* @param watcher - WatcherClient for scan queries.
|
|
400
|
+
* @returns Array of normalized .meta/ directory paths.
|
|
401
|
+
*/
|
|
402
|
+
async function discoverMetas(config, watcher) {
|
|
403
|
+
const filter = buildMetaFilter(config);
|
|
404
|
+
const scanFiles = await paginatedScan(watcher, {
|
|
405
|
+
filter,
|
|
406
|
+
fields: ['file_path'],
|
|
407
|
+
});
|
|
408
|
+
// Deduplicate by file_path (multi-chunk files)
|
|
409
|
+
const seen = new Set();
|
|
410
|
+
const metaPaths = [];
|
|
411
|
+
for (const sf of scanFiles) {
|
|
412
|
+
const fp = normalizePath$1(sf.file_path);
|
|
413
|
+
if (seen.has(fp))
|
|
414
|
+
continue;
|
|
415
|
+
seen.add(fp);
|
|
416
|
+
// Derive .meta/ directory from file_path (strip /meta.json)
|
|
417
|
+
const metaPath = fp.replace(/\/meta\.json$/, '');
|
|
418
|
+
metaPaths.push(metaPath);
|
|
386
419
|
}
|
|
387
|
-
return
|
|
420
|
+
return metaPaths;
|
|
388
421
|
}
|
|
389
422
|
|
|
390
423
|
/**
|
|
@@ -397,7 +430,7 @@ function globMetas(watchPaths) {
|
|
|
397
430
|
* @module discovery/ownershipTree
|
|
398
431
|
*/
|
|
399
432
|
/** Normalize path separators to forward slashes for consistent comparison. */
|
|
400
|
-
function normalizePath
|
|
433
|
+
function normalizePath(p) {
|
|
401
434
|
return p.split(sep).join('/');
|
|
402
435
|
}
|
|
403
436
|
/**
|
|
@@ -411,8 +444,8 @@ function buildOwnershipTree(metaPaths) {
|
|
|
411
444
|
// Create nodes, sorted by ownerPath length (shortest first = shallowest)
|
|
412
445
|
const sorted = [...metaPaths]
|
|
413
446
|
.map((mp) => ({
|
|
414
|
-
metaPath: normalizePath
|
|
415
|
-
ownerPath: normalizePath
|
|
447
|
+
metaPath: normalizePath(mp),
|
|
448
|
+
ownerPath: normalizePath(dirname(mp)),
|
|
416
449
|
}))
|
|
417
450
|
.sort((a, b) => a.ownerPath.length - b.ownerPath.length);
|
|
418
451
|
for (const { metaPath, ownerPath } of sorted) {
|
|
@@ -535,26 +568,200 @@ function computeEma(current, previous, decay = DEFAULT_DECAY) {
|
|
|
535
568
|
}
|
|
536
569
|
|
|
537
570
|
/**
|
|
538
|
-
*
|
|
571
|
+
* Shared error utilities.
|
|
539
572
|
*
|
|
540
|
-
* @module
|
|
573
|
+
* @module errors
|
|
541
574
|
*/
|
|
542
575
|
/**
|
|
543
|
-
*
|
|
576
|
+
* Wrap an unknown caught value into a SynthError.
|
|
544
577
|
*
|
|
545
|
-
* @param
|
|
546
|
-
* @param
|
|
547
|
-
* @
|
|
578
|
+
* @param step - Which synthesis step failed.
|
|
579
|
+
* @param err - The caught error value.
|
|
580
|
+
* @param code - Error classification code.
|
|
581
|
+
* @returns A structured SynthError.
|
|
548
582
|
*/
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
583
|
+
function toSynthError(step, err, code = 'FAILED') {
|
|
584
|
+
return {
|
|
585
|
+
step,
|
|
586
|
+
code,
|
|
587
|
+
message: err instanceof Error ? err.message : String(err),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* SynthExecutor implementation using the OpenClaw gateway HTTP API.
|
|
593
|
+
*
|
|
594
|
+
* Lives in the library package so both plugin and runner can import it.
|
|
595
|
+
* Spawns sub-agent sessions via the gateway, polls for completion,
|
|
596
|
+
* and extracts output text.
|
|
597
|
+
*
|
|
598
|
+
* @module executor/GatewayExecutor
|
|
599
|
+
*/
|
|
600
|
+
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
601
|
+
const DEFAULT_TIMEOUT_MS = 600_000; // 10 minutes
|
|
602
|
+
/** Sleep helper. */
|
|
603
|
+
function sleep$1(ms) {
|
|
604
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* SynthExecutor that spawns OpenClaw sessions via the gateway HTTP API.
|
|
608
|
+
*
|
|
609
|
+
* Used by both the OpenClaw plugin (in-process tool calls) and the
|
|
610
|
+
* runner/CLI (external invocation). Constructs from `gatewayUrl` and
|
|
611
|
+
* optional `apiKey` — typically sourced from `SynthConfig`.
|
|
612
|
+
*/
|
|
613
|
+
class GatewayExecutor {
|
|
614
|
+
gatewayUrl;
|
|
615
|
+
apiKey;
|
|
616
|
+
pollIntervalMs;
|
|
617
|
+
constructor(options = {}) {
|
|
618
|
+
this.gatewayUrl = (options.gatewayUrl ?? 'http://127.0.0.1:3000').replace(/\/+$/, '');
|
|
619
|
+
this.apiKey = options.apiKey;
|
|
620
|
+
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
621
|
+
}
|
|
622
|
+
async spawn(task, options) {
|
|
623
|
+
const timeoutMs = (options?.timeout ?? DEFAULT_TIMEOUT_MS / 1000) * 1000;
|
|
624
|
+
const deadline = Date.now() + timeoutMs;
|
|
625
|
+
const headers = {
|
|
626
|
+
'Content-Type': 'application/json',
|
|
627
|
+
};
|
|
628
|
+
if (this.apiKey) {
|
|
629
|
+
headers['Authorization'] = 'Bearer ' + this.apiKey;
|
|
630
|
+
}
|
|
631
|
+
const spawnRes = await fetch(this.gatewayUrl + '/api/sessions/spawn', {
|
|
632
|
+
method: 'POST',
|
|
633
|
+
headers,
|
|
634
|
+
body: JSON.stringify({
|
|
635
|
+
task,
|
|
636
|
+
mode: 'run',
|
|
637
|
+
model: options?.model,
|
|
638
|
+
runTimeoutSeconds: options?.timeout,
|
|
639
|
+
}),
|
|
640
|
+
});
|
|
641
|
+
if (!spawnRes.ok) {
|
|
642
|
+
const text = await spawnRes.text();
|
|
643
|
+
throw new Error('Gateway spawn failed: HTTP ' +
|
|
644
|
+
spawnRes.status.toString() +
|
|
645
|
+
' - ' +
|
|
646
|
+
text);
|
|
647
|
+
}
|
|
648
|
+
const spawnData = (await spawnRes.json());
|
|
649
|
+
if (!spawnData.sessionKey) {
|
|
650
|
+
throw new Error('Gateway spawn returned no sessionKey: ' + JSON.stringify(spawnData));
|
|
651
|
+
}
|
|
652
|
+
const { sessionKey } = spawnData;
|
|
653
|
+
// Poll for completion
|
|
654
|
+
while (Date.now() < deadline) {
|
|
655
|
+
await sleep$1(this.pollIntervalMs);
|
|
656
|
+
const historyRes = await fetch(this.gatewayUrl +
|
|
657
|
+
'/api/sessions/' +
|
|
658
|
+
encodeURIComponent(sessionKey) +
|
|
659
|
+
'/history?limit=50', { headers });
|
|
660
|
+
if (!historyRes.ok)
|
|
661
|
+
continue;
|
|
662
|
+
const history = (await historyRes.json());
|
|
663
|
+
if (history.status === 'completed' || history.status === 'done') {
|
|
664
|
+
// Extract token usage from session-level or message-level usage
|
|
665
|
+
let tokens;
|
|
666
|
+
if (history.usage?.totalTokens) {
|
|
667
|
+
tokens = history.usage.totalTokens;
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
// Sum message-level usage as fallback
|
|
671
|
+
let sum = 0;
|
|
672
|
+
for (const msg of history.messages ?? []) {
|
|
673
|
+
if (msg.usage?.totalTokens)
|
|
674
|
+
sum += msg.usage.totalTokens;
|
|
675
|
+
}
|
|
676
|
+
if (sum > 0)
|
|
677
|
+
tokens = sum;
|
|
678
|
+
}
|
|
679
|
+
// Extract the last assistant message as output
|
|
680
|
+
const messages = history.messages ?? [];
|
|
681
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
682
|
+
if (messages[i].role === 'assistant' && messages[i].content) {
|
|
683
|
+
return { output: messages[i].content, tokens };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return { output: '', tokens };
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
throw new Error('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms');
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* File-system lock for preventing concurrent synthesis on the same meta.
|
|
695
|
+
*
|
|
696
|
+
* Lock file: .meta/.lock containing PID + timestamp.
|
|
697
|
+
* Stale timeout: 30 minutes.
|
|
698
|
+
*
|
|
699
|
+
* @module lock
|
|
700
|
+
*/
|
|
701
|
+
const LOCK_FILE = '.lock';
|
|
702
|
+
const STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
703
|
+
/**
|
|
704
|
+
* Attempt to acquire a lock on a .meta directory.
|
|
705
|
+
*
|
|
706
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
707
|
+
* @returns True if lock was acquired, false if already locked (non-stale).
|
|
708
|
+
*/
|
|
709
|
+
function acquireLock(metaPath) {
|
|
710
|
+
const lockPath = join(metaPath, LOCK_FILE);
|
|
711
|
+
if (existsSync(lockPath)) {
|
|
712
|
+
try {
|
|
713
|
+
const raw = readFileSync(lockPath, 'utf8');
|
|
714
|
+
const data = JSON.parse(raw);
|
|
715
|
+
const lockAge = Date.now() - new Date(data.startedAt).getTime();
|
|
716
|
+
if (lockAge < STALE_TIMEOUT_MS) {
|
|
717
|
+
return false; // Lock is active
|
|
718
|
+
}
|
|
719
|
+
// Stale lock — fall through to overwrite
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
// Corrupt lock file — overwrite
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const lock = {
|
|
726
|
+
pid: process.pid,
|
|
727
|
+
startedAt: new Date().toISOString(),
|
|
728
|
+
};
|
|
729
|
+
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Release a lock on a .meta directory.
|
|
734
|
+
*
|
|
735
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
736
|
+
*/
|
|
737
|
+
function releaseLock(metaPath) {
|
|
738
|
+
const lockPath = join(metaPath, LOCK_FILE);
|
|
739
|
+
try {
|
|
740
|
+
unlinkSync(lockPath);
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
// Already removed or never existed
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Check if a .meta directory is currently locked (non-stale).
|
|
748
|
+
*
|
|
749
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
750
|
+
* @returns True if locked and not stale.
|
|
751
|
+
*/
|
|
752
|
+
function isLocked(metaPath) {
|
|
753
|
+
const lockPath = join(metaPath, LOCK_FILE);
|
|
754
|
+
if (!existsSync(lockPath))
|
|
755
|
+
return false;
|
|
756
|
+
try {
|
|
757
|
+
const raw = readFileSync(lockPath, 'utf8');
|
|
758
|
+
const data = JSON.parse(raw);
|
|
759
|
+
const lockAge = Date.now() - new Date(data.startedAt).getTime();
|
|
760
|
+
return lockAge < STALE_TIMEOUT_MS;
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
return false; // Corrupt lock = not locked
|
|
764
|
+
}
|
|
558
765
|
}
|
|
559
766
|
|
|
560
767
|
/**
|
|
@@ -846,120 +1053,6 @@ function mergeAndWrite(options) {
|
|
|
846
1053
|
return result.data;
|
|
847
1054
|
}
|
|
848
1055
|
|
|
849
|
-
/**
|
|
850
|
-
* Shared error utilities.
|
|
851
|
-
*
|
|
852
|
-
* @module errors
|
|
853
|
-
*/
|
|
854
|
-
/**
|
|
855
|
-
* Wrap an unknown caught value into a SynthError.
|
|
856
|
-
*
|
|
857
|
-
* @param step - Which synthesis step failed.
|
|
858
|
-
* @param err - The caught error value.
|
|
859
|
-
* @param code - Error classification code.
|
|
860
|
-
* @returns A structured SynthError.
|
|
861
|
-
*/
|
|
862
|
-
function toSynthError(step, err, code = 'FAILED') {
|
|
863
|
-
return {
|
|
864
|
-
step,
|
|
865
|
-
code,
|
|
866
|
-
message: err instanceof Error ? err.message : String(err),
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
/**
|
|
871
|
-
* File-system lock for preventing concurrent synthesis on the same meta.
|
|
872
|
-
*
|
|
873
|
-
* Lock file: .meta/.lock containing PID + timestamp.
|
|
874
|
-
* Stale timeout: 30 minutes.
|
|
875
|
-
*
|
|
876
|
-
* @module lock
|
|
877
|
-
*/
|
|
878
|
-
const LOCK_FILE = '.lock';
|
|
879
|
-
const STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
880
|
-
/**
|
|
881
|
-
* Attempt to acquire a lock on a .meta directory.
|
|
882
|
-
*
|
|
883
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
884
|
-
* @returns True if lock was acquired, false if already locked (non-stale).
|
|
885
|
-
*/
|
|
886
|
-
function acquireLock(metaPath) {
|
|
887
|
-
const lockPath = join(metaPath, LOCK_FILE);
|
|
888
|
-
if (existsSync(lockPath)) {
|
|
889
|
-
try {
|
|
890
|
-
const raw = readFileSync(lockPath, 'utf8');
|
|
891
|
-
const data = JSON.parse(raw);
|
|
892
|
-
const lockAge = Date.now() - new Date(data.startedAt).getTime();
|
|
893
|
-
if (lockAge < STALE_TIMEOUT_MS) {
|
|
894
|
-
return false; // Lock is active
|
|
895
|
-
}
|
|
896
|
-
// Stale lock — fall through to overwrite
|
|
897
|
-
}
|
|
898
|
-
catch {
|
|
899
|
-
// Corrupt lock file — overwrite
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
const lock = {
|
|
903
|
-
pid: process.pid,
|
|
904
|
-
startedAt: new Date().toISOString(),
|
|
905
|
-
};
|
|
906
|
-
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
907
|
-
return true;
|
|
908
|
-
}
|
|
909
|
-
/**
|
|
910
|
-
* Release a lock on a .meta directory.
|
|
911
|
-
*
|
|
912
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
913
|
-
*/
|
|
914
|
-
function releaseLock(metaPath) {
|
|
915
|
-
const lockPath = join(metaPath, LOCK_FILE);
|
|
916
|
-
try {
|
|
917
|
-
unlinkSync(lockPath);
|
|
918
|
-
}
|
|
919
|
-
catch {
|
|
920
|
-
// Already removed or never existed
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
/**
|
|
924
|
-
* Check if a .meta directory is currently locked (non-stale).
|
|
925
|
-
*
|
|
926
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
927
|
-
* @returns True if locked and not stale.
|
|
928
|
-
*/
|
|
929
|
-
function isLocked(metaPath) {
|
|
930
|
-
const lockPath = join(metaPath, LOCK_FILE);
|
|
931
|
-
if (!existsSync(lockPath))
|
|
932
|
-
return false;
|
|
933
|
-
try {
|
|
934
|
-
const raw = readFileSync(lockPath, 'utf8');
|
|
935
|
-
const data = JSON.parse(raw);
|
|
936
|
-
const lockAge = Date.now() - new Date(data.startedAt).getTime();
|
|
937
|
-
return lockAge < STALE_TIMEOUT_MS;
|
|
938
|
-
}
|
|
939
|
-
catch {
|
|
940
|
-
return false; // Corrupt lock = not locked
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
/**
|
|
945
|
-
* Normalize file paths to forward slashes for consistency with watcher-indexed paths.
|
|
946
|
-
*
|
|
947
|
-
* Watcher indexes paths with forward slashes (`j:/domains/...`). This utility
|
|
948
|
-
* ensures all paths in the library use the same convention, regardless of
|
|
949
|
-
* the platform's native separator.
|
|
950
|
-
*
|
|
951
|
-
* @module normalizePath
|
|
952
|
-
*/
|
|
953
|
-
/**
|
|
954
|
-
* Normalize a file path to forward slashes.
|
|
955
|
-
*
|
|
956
|
-
* @param p - File path (may contain backslashes).
|
|
957
|
-
* @returns Path with all backslashes replaced by forward slashes.
|
|
958
|
-
*/
|
|
959
|
-
function normalizePath(p) {
|
|
960
|
-
return p.replaceAll('\\', '/');
|
|
961
|
-
}
|
|
962
|
-
|
|
963
1056
|
/**
|
|
964
1057
|
* Select the best synthesis candidate from stale metas.
|
|
965
1058
|
*
|
|
@@ -1216,17 +1309,32 @@ function finalizeCycle(metaPath, current, config, architect, builder, critic, bu
|
|
|
1216
1309
|
* @param watcher - Watcher HTTP client.
|
|
1217
1310
|
* @returns Result indicating whether synthesis occurred.
|
|
1218
1311
|
*/
|
|
1219
|
-
async function orchestrateOnce(config, executor, watcher) {
|
|
1220
|
-
// Step 1: Discover
|
|
1221
|
-
const metaPaths =
|
|
1312
|
+
async function orchestrateOnce(config, executor, watcher, targetPath) {
|
|
1313
|
+
// Step 1: Discover via watcher scan
|
|
1314
|
+
const metaPaths = await discoverMetas(config, watcher);
|
|
1222
1315
|
if (metaPaths.length === 0)
|
|
1223
1316
|
return { synthesized: false };
|
|
1224
|
-
//
|
|
1317
|
+
// Read meta.json for each discovered meta
|
|
1225
1318
|
const metas = new Map();
|
|
1226
1319
|
for (const mp of metaPaths) {
|
|
1227
|
-
|
|
1320
|
+
const metaFilePath = join(mp, 'meta.json');
|
|
1321
|
+
try {
|
|
1322
|
+
metas.set(normalizePath$1(mp), JSON.parse(readFileSync(metaFilePath, 'utf8')));
|
|
1323
|
+
}
|
|
1324
|
+
catch {
|
|
1325
|
+
// Skip metas with unreadable meta.json
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1228
1328
|
}
|
|
1229
1329
|
const tree = buildOwnershipTree(metaPaths);
|
|
1330
|
+
// If targetPath specified, skip candidate selection — go directly to that meta
|
|
1331
|
+
let targetNode;
|
|
1332
|
+
if (targetPath) {
|
|
1333
|
+
const normalized = normalizePath$1(targetPath);
|
|
1334
|
+
targetNode = findNode(tree, normalized) ?? undefined;
|
|
1335
|
+
if (!targetNode)
|
|
1336
|
+
return { synthesized: false };
|
|
1337
|
+
}
|
|
1230
1338
|
// Steps 3-4: Staleness check + candidate selection
|
|
1231
1339
|
const candidates = [];
|
|
1232
1340
|
for (const node of tree.nodes.values()) {
|
|
@@ -1261,9 +1369,13 @@ async function orchestrateOnce(config, executor, watcher) {
|
|
|
1261
1369
|
winner = candidate;
|
|
1262
1370
|
break;
|
|
1263
1371
|
}
|
|
1264
|
-
if (!winner)
|
|
1372
|
+
if (!winner && !targetNode)
|
|
1265
1373
|
return { synthesized: false };
|
|
1266
|
-
const
|
|
1374
|
+
const node = targetNode ?? winner.node;
|
|
1375
|
+
// For targeted path, acquire lock now (candidate selection already locked for stalest)
|
|
1376
|
+
if (targetNode && !acquireLock(node.metaPath)) {
|
|
1377
|
+
return { synthesized: false };
|
|
1378
|
+
}
|
|
1267
1379
|
try {
|
|
1268
1380
|
// Re-read meta after lock (may have changed)
|
|
1269
1381
|
const currentMeta = JSON.parse(readFileSync(join(node.metaPath, 'meta.json'), 'utf8'));
|
|
@@ -1373,12 +1485,13 @@ async function orchestrateOnce(config, executor, watcher) {
|
|
|
1373
1485
|
* @param config - Validated synthesis config.
|
|
1374
1486
|
* @param executor - Pluggable LLM executor.
|
|
1375
1487
|
* @param watcher - Watcher HTTP client.
|
|
1488
|
+
* @param targetPath - Optional: specific meta/owner path to synthesize instead of stalest candidate.
|
|
1376
1489
|
* @returns Array of results, one per cycle attempted.
|
|
1377
1490
|
*/
|
|
1378
|
-
async function orchestrate(config, executor, watcher) {
|
|
1491
|
+
async function orchestrate(config, executor, watcher, targetPath) {
|
|
1379
1492
|
const results = [];
|
|
1380
1493
|
for (let i = 0; i < config.batchSize; i++) {
|
|
1381
|
-
const result = await orchestrateOnce(config, executor, watcher);
|
|
1494
|
+
const result = await orchestrateOnce(config, executor, watcher, targetPath);
|
|
1382
1495
|
results.push(result);
|
|
1383
1496
|
if (!result.synthesized)
|
|
1384
1497
|
break; // No more candidates
|
|
@@ -1386,108 +1499,6 @@ async function orchestrate(config, executor, watcher) {
|
|
|
1386
1499
|
return results;
|
|
1387
1500
|
}
|
|
1388
1501
|
|
|
1389
|
-
/**
|
|
1390
|
-
* SynthExecutor implementation using the OpenClaw gateway HTTP API.
|
|
1391
|
-
*
|
|
1392
|
-
* Lives in the library package so both plugin and runner can import it.
|
|
1393
|
-
* Spawns sub-agent sessions via the gateway, polls for completion,
|
|
1394
|
-
* and extracts output text.
|
|
1395
|
-
*
|
|
1396
|
-
* @module executor/GatewayExecutor
|
|
1397
|
-
*/
|
|
1398
|
-
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
1399
|
-
const DEFAULT_TIMEOUT_MS = 600_000; // 10 minutes
|
|
1400
|
-
/** Sleep helper. */
|
|
1401
|
-
function sleep$1(ms) {
|
|
1402
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1403
|
-
}
|
|
1404
|
-
/**
|
|
1405
|
-
* SynthExecutor that spawns OpenClaw sessions via the gateway HTTP API.
|
|
1406
|
-
*
|
|
1407
|
-
* Used by both the OpenClaw plugin (in-process tool calls) and the
|
|
1408
|
-
* runner/CLI (external invocation). Constructs from `gatewayUrl` and
|
|
1409
|
-
* optional `apiKey` — typically sourced from `SynthConfig`.
|
|
1410
|
-
*/
|
|
1411
|
-
class GatewayExecutor {
|
|
1412
|
-
gatewayUrl;
|
|
1413
|
-
apiKey;
|
|
1414
|
-
pollIntervalMs;
|
|
1415
|
-
constructor(options = {}) {
|
|
1416
|
-
this.gatewayUrl = (options.gatewayUrl ?? 'http://127.0.0.1:3000').replace(/\/+$/, '');
|
|
1417
|
-
this.apiKey = options.apiKey;
|
|
1418
|
-
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1419
|
-
}
|
|
1420
|
-
async spawn(task, options) {
|
|
1421
|
-
const timeoutMs = (options?.timeout ?? DEFAULT_TIMEOUT_MS / 1000) * 1000;
|
|
1422
|
-
const deadline = Date.now() + timeoutMs;
|
|
1423
|
-
const headers = {
|
|
1424
|
-
'Content-Type': 'application/json',
|
|
1425
|
-
};
|
|
1426
|
-
if (this.apiKey) {
|
|
1427
|
-
headers['Authorization'] = 'Bearer ' + this.apiKey;
|
|
1428
|
-
}
|
|
1429
|
-
const spawnRes = await fetch(this.gatewayUrl + '/api/sessions/spawn', {
|
|
1430
|
-
method: 'POST',
|
|
1431
|
-
headers,
|
|
1432
|
-
body: JSON.stringify({
|
|
1433
|
-
task,
|
|
1434
|
-
mode: 'run',
|
|
1435
|
-
model: options?.model,
|
|
1436
|
-
runTimeoutSeconds: options?.timeout,
|
|
1437
|
-
}),
|
|
1438
|
-
});
|
|
1439
|
-
if (!spawnRes.ok) {
|
|
1440
|
-
const text = await spawnRes.text();
|
|
1441
|
-
throw new Error('Gateway spawn failed: HTTP ' +
|
|
1442
|
-
spawnRes.status.toString() +
|
|
1443
|
-
' - ' +
|
|
1444
|
-
text);
|
|
1445
|
-
}
|
|
1446
|
-
const spawnData = (await spawnRes.json());
|
|
1447
|
-
if (!spawnData.sessionKey) {
|
|
1448
|
-
throw new Error('Gateway spawn returned no sessionKey: ' + JSON.stringify(spawnData));
|
|
1449
|
-
}
|
|
1450
|
-
const { sessionKey } = spawnData;
|
|
1451
|
-
// Poll for completion
|
|
1452
|
-
while (Date.now() < deadline) {
|
|
1453
|
-
await sleep$1(this.pollIntervalMs);
|
|
1454
|
-
const historyRes = await fetch(this.gatewayUrl +
|
|
1455
|
-
'/api/sessions/' +
|
|
1456
|
-
encodeURIComponent(sessionKey) +
|
|
1457
|
-
'/history?limit=50', { headers });
|
|
1458
|
-
if (!historyRes.ok)
|
|
1459
|
-
continue;
|
|
1460
|
-
const history = (await historyRes.json());
|
|
1461
|
-
if (history.status === 'completed' || history.status === 'done') {
|
|
1462
|
-
// Extract token usage from session-level or message-level usage
|
|
1463
|
-
let tokens;
|
|
1464
|
-
if (history.usage?.totalTokens) {
|
|
1465
|
-
tokens = history.usage.totalTokens;
|
|
1466
|
-
}
|
|
1467
|
-
else {
|
|
1468
|
-
// Sum message-level usage as fallback
|
|
1469
|
-
let sum = 0;
|
|
1470
|
-
for (const msg of history.messages ?? []) {
|
|
1471
|
-
if (msg.usage?.totalTokens)
|
|
1472
|
-
sum += msg.usage.totalTokens;
|
|
1473
|
-
}
|
|
1474
|
-
if (sum > 0)
|
|
1475
|
-
tokens = sum;
|
|
1476
|
-
}
|
|
1477
|
-
// Extract the last assistant message as output
|
|
1478
|
-
const messages = history.messages ?? [];
|
|
1479
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1480
|
-
if (messages[i].role === 'assistant' && messages[i].content) {
|
|
1481
|
-
return { output: messages[i].content, tokens };
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
return { output: '', tokens };
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
throw new Error('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms');
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
1502
|
/**
|
|
1492
1503
|
* HTTP implementation of the WatcherClient interface.
|
|
1493
1504
|
*
|
|
@@ -1565,8 +1576,20 @@ class HttpWatcherClient {
|
|
|
1565
1576
|
if (params.cursor !== undefined) {
|
|
1566
1577
|
body.cursor = params.cursor;
|
|
1567
1578
|
}
|
|
1568
|
-
const
|
|
1569
|
-
|
|
1579
|
+
const raw = (await this.post('/scan', body));
|
|
1580
|
+
// jeeves-watcher returns { points, cursor }; map to ScanResponse.
|
|
1581
|
+
const points = (raw.points ?? raw.files ?? []);
|
|
1582
|
+
const next = (raw.cursor ?? raw.next);
|
|
1583
|
+
const files = points.map((p) => {
|
|
1584
|
+
const payload = (p.payload ?? p);
|
|
1585
|
+
return {
|
|
1586
|
+
file_path: (payload.file_path ?? payload.path ?? ''),
|
|
1587
|
+
modified_at: (payload.modified_at ?? payload.mtime ?? 0),
|
|
1588
|
+
content_hash: (payload.content_hash ?? ''),
|
|
1589
|
+
...payload,
|
|
1590
|
+
};
|
|
1591
|
+
});
|
|
1592
|
+
return { files, next: next ?? undefined };
|
|
1570
1593
|
}
|
|
1571
1594
|
async registerRules(source, rules) {
|
|
1572
1595
|
await this.post('/rules/register', { source, rules });
|
|
@@ -1592,16 +1615,16 @@ var index = /*#__PURE__*/Object.freeze({
|
|
|
1592
1615
|
buildBuilderTask: buildBuilderTask,
|
|
1593
1616
|
buildContextPackage: buildContextPackage,
|
|
1594
1617
|
buildCriticTask: buildCriticTask,
|
|
1618
|
+
buildMetaFilter: buildMetaFilter,
|
|
1595
1619
|
buildOwnershipTree: buildOwnershipTree,
|
|
1596
1620
|
computeEffectiveStaleness: computeEffectiveStaleness,
|
|
1597
1621
|
computeEma: computeEma,
|
|
1598
1622
|
computeStructureHash: computeStructureHash,
|
|
1599
1623
|
createSnapshot: createSnapshot,
|
|
1600
|
-
|
|
1624
|
+
discoverMetas: discoverMetas,
|
|
1601
1625
|
filterInScope: filterInScope,
|
|
1602
1626
|
findNode: findNode,
|
|
1603
1627
|
getScopePrefix: getScopePrefix,
|
|
1604
|
-
globMetas: globMetas,
|
|
1605
1628
|
hasSteerChanged: hasSteerChanged,
|
|
1606
1629
|
isArchitectTriggered: isArchitectTriggered,
|
|
1607
1630
|
isLocked: isLocked,
|
|
@@ -1610,7 +1633,7 @@ var index = /*#__PURE__*/Object.freeze({
|
|
|
1610
1633
|
loadSynthConfig: loadSynthConfig,
|
|
1611
1634
|
mergeAndWrite: mergeAndWrite,
|
|
1612
1635
|
metaJsonSchema: metaJsonSchema,
|
|
1613
|
-
normalizePath: normalizePath,
|
|
1636
|
+
normalizePath: normalizePath$1,
|
|
1614
1637
|
orchestrate: orchestrate,
|
|
1615
1638
|
paginatedScan: paginatedScan,
|
|
1616
1639
|
parseArchitectOutput: parseArchitectOutput,
|
|
@@ -1636,6 +1659,10 @@ var index = /*#__PURE__*/Object.freeze({
|
|
|
1636
1659
|
*
|
|
1637
1660
|
* @module cli
|
|
1638
1661
|
*/
|
|
1662
|
+
/** Read and parse a meta.json file with proper typing. */
|
|
1663
|
+
function readMeta(metaPath) {
|
|
1664
|
+
return JSON.parse(readFileSync(join(metaPath, 'meta.json'), 'utf8'));
|
|
1665
|
+
}
|
|
1639
1666
|
const args = process.argv.slice(2);
|
|
1640
1667
|
const command = args.find((a) => !a.startsWith('-'));
|
|
1641
1668
|
const jsonOutput = args.includes('--json');
|
|
@@ -1671,8 +1698,9 @@ function output(data) {
|
|
|
1671
1698
|
console.log(JSON.stringify(data, null, 2));
|
|
1672
1699
|
}
|
|
1673
1700
|
}
|
|
1674
|
-
function runStatus(config) {
|
|
1675
|
-
const
|
|
1701
|
+
async function runStatus(config) {
|
|
1702
|
+
const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
|
|
1703
|
+
const metaPaths = await discoverMetas(config, watcher);
|
|
1676
1704
|
const tree = buildOwnershipTree(metaPaths);
|
|
1677
1705
|
let stale = 0;
|
|
1678
1706
|
let errors = 0;
|
|
@@ -1682,13 +1710,19 @@ function runStatus(config) {
|
|
|
1682
1710
|
let buildTokens = 0;
|
|
1683
1711
|
let critTokens = 0;
|
|
1684
1712
|
for (const node of tree.nodes.values()) {
|
|
1685
|
-
|
|
1713
|
+
let meta;
|
|
1714
|
+
try {
|
|
1715
|
+
meta = readMeta(node.metaPath);
|
|
1716
|
+
}
|
|
1717
|
+
catch {
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1686
1720
|
const s = actualStaleness(meta);
|
|
1687
1721
|
if (s > 0)
|
|
1688
1722
|
stale++;
|
|
1689
1723
|
if (meta._error)
|
|
1690
1724
|
errors++;
|
|
1691
|
-
if (isLocked(normalizePath(node.metaPath)))
|
|
1725
|
+
if (isLocked(normalizePath$1(node.metaPath)))
|
|
1692
1726
|
locked++;
|
|
1693
1727
|
if (!meta._generatedAt)
|
|
1694
1728
|
neverSynth++;
|
|
@@ -1708,19 +1742,26 @@ function runStatus(config) {
|
|
|
1708
1742
|
tokens: { architect: archTokens, builder: buildTokens, critic: critTokens },
|
|
1709
1743
|
});
|
|
1710
1744
|
}
|
|
1711
|
-
function runList(config) {
|
|
1745
|
+
async function runList(config) {
|
|
1712
1746
|
const prefix = getArg('--prefix');
|
|
1713
1747
|
const filter = getArg('--filter');
|
|
1714
|
-
const
|
|
1748
|
+
const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
|
|
1749
|
+
const metaPaths = await discoverMetas(config, watcher);
|
|
1715
1750
|
const tree = buildOwnershipTree(metaPaths);
|
|
1716
1751
|
const rows = [];
|
|
1717
1752
|
for (const node of tree.nodes.values()) {
|
|
1718
1753
|
if (prefix && !node.metaPath.includes(prefix))
|
|
1719
1754
|
continue;
|
|
1720
|
-
|
|
1755
|
+
let meta;
|
|
1756
|
+
try {
|
|
1757
|
+
meta = readMeta(node.metaPath);
|
|
1758
|
+
}
|
|
1759
|
+
catch {
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1721
1762
|
const s = actualStaleness(meta);
|
|
1722
1763
|
const hasError = Boolean(meta._error);
|
|
1723
|
-
const isLockedNow = isLocked(normalizePath(node.metaPath));
|
|
1764
|
+
const isLockedNow = isLocked(normalizePath$1(node.metaPath));
|
|
1724
1765
|
if (filter === 'hasError' && !hasError)
|
|
1725
1766
|
continue;
|
|
1726
1767
|
if (filter === 'stale' && s <= 0)
|
|
@@ -1747,15 +1788,16 @@ async function runDetail(config) {
|
|
|
1747
1788
|
process.exit(1);
|
|
1748
1789
|
}
|
|
1749
1790
|
const archiveArg = getArg('--archive');
|
|
1750
|
-
const
|
|
1791
|
+
const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
|
|
1792
|
+
const metaPaths = await discoverMetas(config, watcher);
|
|
1751
1793
|
const tree = buildOwnershipTree(metaPaths);
|
|
1752
|
-
const normalized = normalizePath(targetPath);
|
|
1794
|
+
const normalized = normalizePath$1(targetPath);
|
|
1753
1795
|
const node = findNode(tree, normalized);
|
|
1754
1796
|
if (!node) {
|
|
1755
1797
|
console.error('Meta not found: ' + targetPath);
|
|
1756
1798
|
process.exit(1);
|
|
1757
1799
|
}
|
|
1758
|
-
const meta =
|
|
1800
|
+
const meta = readMeta(node.metaPath);
|
|
1759
1801
|
const result = { meta };
|
|
1760
1802
|
if (archiveArg) {
|
|
1761
1803
|
const { listArchiveFiles } = await Promise.resolve().then(function () { return index$1; });
|
|
@@ -1772,12 +1814,12 @@ async function runDetail(config) {
|
|
|
1772
1814
|
async function runPreview(config) {
|
|
1773
1815
|
const targetPath = getArg('--path');
|
|
1774
1816
|
const { filterInScope, paginatedScan, readLatestArchive, computeStructureHash, selectCandidate, } = await Promise.resolve().then(function () { return index; });
|
|
1775
|
-
const metaPaths = globMetas(config.watchPaths);
|
|
1776
|
-
const tree = buildOwnershipTree(metaPaths);
|
|
1777
1817
|
const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
|
|
1818
|
+
const metaPaths = await discoverMetas(config, watcher);
|
|
1819
|
+
const tree = buildOwnershipTree(metaPaths);
|
|
1778
1820
|
let targetNode;
|
|
1779
1821
|
if (targetPath) {
|
|
1780
|
-
const normalized = normalizePath(targetPath);
|
|
1822
|
+
const normalized = normalizePath$1(targetPath);
|
|
1781
1823
|
targetNode = findNode(tree, normalized);
|
|
1782
1824
|
if (!targetNode) {
|
|
1783
1825
|
console.error('Meta not found: ' + targetPath);
|
|
@@ -1787,7 +1829,13 @@ async function runPreview(config) {
|
|
|
1787
1829
|
else {
|
|
1788
1830
|
const candidates = [];
|
|
1789
1831
|
for (const node of tree.nodes.values()) {
|
|
1790
|
-
|
|
1832
|
+
let meta;
|
|
1833
|
+
try {
|
|
1834
|
+
meta = readMeta(node.metaPath);
|
|
1835
|
+
}
|
|
1836
|
+
catch {
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1791
1839
|
const s = actualStaleness(meta);
|
|
1792
1840
|
if (s > 0)
|
|
1793
1841
|
candidates.push({ node, meta, actualStaleness: s });
|
|
@@ -1800,7 +1848,7 @@ async function runPreview(config) {
|
|
|
1800
1848
|
}
|
|
1801
1849
|
targetNode = winner.node;
|
|
1802
1850
|
}
|
|
1803
|
-
const meta =
|
|
1851
|
+
const meta = readMeta(targetNode.metaPath);
|
|
1804
1852
|
const allFiles = await paginatedScan(watcher, {
|
|
1805
1853
|
pathPrefix: targetNode.ownerPath,
|
|
1806
1854
|
});
|
|
@@ -1828,9 +1876,6 @@ async function runSynthesize(config) {
|
|
|
1828
1876
|
const batchArg = getArg('--batch');
|
|
1829
1877
|
const effectiveConfig = {
|
|
1830
1878
|
...config,
|
|
1831
|
-
...(targetPath
|
|
1832
|
-
? { watchPaths: [targetPath.replace(/[/\\]\.meta[/\\]?$/, '')] }
|
|
1833
|
-
: {}),
|
|
1834
1879
|
...(batchArg ? { batchSize: parseInt(batchArg, 10) } : {}),
|
|
1835
1880
|
};
|
|
1836
1881
|
const executor = new GatewayExecutor({
|
|
@@ -1838,7 +1883,7 @@ async function runSynthesize(config) {
|
|
|
1838
1883
|
apiKey: config.gatewayApiKey,
|
|
1839
1884
|
});
|
|
1840
1885
|
const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
|
|
1841
|
-
const results = await orchestrate(effectiveConfig, executor, watcher);
|
|
1886
|
+
const results = await orchestrate(effectiveConfig, executor, watcher, targetPath ?? undefined);
|
|
1842
1887
|
const synthesized = results.filter((r) => r.synthesized);
|
|
1843
1888
|
output({
|
|
1844
1889
|
synthesizedCount: synthesized.length,
|
|
@@ -1909,9 +1954,16 @@ async function runValidate(config) {
|
|
|
1909
1954
|
catch {
|
|
1910
1955
|
checks.gateway = 'UNREACHABLE (' + config.gatewayUrl + ')';
|
|
1911
1956
|
}
|
|
1912
|
-
// Check
|
|
1913
|
-
|
|
1914
|
-
|
|
1957
|
+
// Check meta discovery via watcher
|
|
1958
|
+
try {
|
|
1959
|
+
const watcherClient = new HttpWatcherClient({ baseUrl: config.watcherUrl });
|
|
1960
|
+
const metaPaths = await discoverMetas(config, watcherClient);
|
|
1961
|
+
checks.metas =
|
|
1962
|
+
String(metaPaths.length) + ' .meta/ entities discovered via watcher';
|
|
1963
|
+
}
|
|
1964
|
+
catch {
|
|
1965
|
+
checks.metas = 'FAILED — could not discover metas (watcher may be down)';
|
|
1966
|
+
}
|
|
1915
1967
|
output({ config: 'valid', checks });
|
|
1916
1968
|
}
|
|
1917
1969
|
function runConfigShow(config) {
|
|
@@ -1971,10 +2023,10 @@ async function main() {
|
|
|
1971
2023
|
}
|
|
1972
2024
|
switch (command) {
|
|
1973
2025
|
case 'status':
|
|
1974
|
-
runStatus(config);
|
|
2026
|
+
await runStatus(config);
|
|
1975
2027
|
break;
|
|
1976
2028
|
case 'list':
|
|
1977
|
-
runList(config);
|
|
2029
|
+
await runList(config);
|
|
1978
2030
|
break;
|
|
1979
2031
|
case 'detail':
|
|
1980
2032
|
await runDetail(config);
|