@karmaniverous/jeeves 0.1.6 → 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 +90 -11
- package/dist/cli/jeeves/index.js +277 -123
- package/dist/index.d.ts +385 -68
- package/dist/index.js +700 -167
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,192 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
2
|
+
import { writeFileSync, renameSync, existsSync, readFileSync, mkdirSync, cpSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
4
|
import { lock } from 'proper-lockfile';
|
|
4
|
-
import { gte } from 'semver';
|
|
5
|
+
import semver, { gte } from 'semver';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import Handlebars from 'handlebars';
|
|
7
8
|
import { packageDirectorySync } from 'package-directory';
|
|
8
9
|
import { z } from 'zod';
|
|
9
10
|
import { execSync } from 'node:child_process';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generic config query handler with JSONPath support.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Provides a transport-agnostic config query function that can be
|
|
18
|
+
* used by any Jeeves component's HTTP API. Returns the full config
|
|
19
|
+
* document or filters it via JSONPath expressions.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Create a config query handler.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* - No `path` parameter → returns the full config document.
|
|
26
|
+
* - Valid JSONPath → returns matching results with count.
|
|
27
|
+
* - Invalid JSONPath → returns 400 error.
|
|
28
|
+
*
|
|
29
|
+
* @param getConfig - Function that returns the current config object.
|
|
30
|
+
* @returns A config query handler function.
|
|
31
|
+
*/
|
|
32
|
+
function createConfigQueryHandler(getConfig) {
|
|
33
|
+
return (query) => {
|
|
34
|
+
const config = getConfig();
|
|
35
|
+
if (!query.path) {
|
|
36
|
+
return Promise.resolve({ status: 200, body: config });
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const result = JSONPath({
|
|
40
|
+
path: query.path,
|
|
41
|
+
json: config,
|
|
42
|
+
});
|
|
43
|
+
return Promise.resolve({
|
|
44
|
+
status: 200,
|
|
45
|
+
body: { result, count: result.length },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : 'Query failed';
|
|
50
|
+
return Promise.resolve({ status: 400, body: { error: message } });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Directory and file path conventions for the Jeeves platform.
|
|
57
|
+
*/
|
|
58
|
+
/** Core config directory name within the config root. */
|
|
59
|
+
const CORE_CONFIG_DIR = 'jeeves-core';
|
|
60
|
+
/** Prefix for component config directories: `jeeves-{name}`. */
|
|
61
|
+
const COMPONENT_CONFIG_PREFIX = 'jeeves-';
|
|
62
|
+
/** Default workspace file names. */
|
|
63
|
+
const WORKSPACE_FILES = {
|
|
64
|
+
/** TOOLS.md — live platform state and component sections. */
|
|
65
|
+
tools: 'TOOLS.md',
|
|
66
|
+
/** SOUL.md — professional discipline and behavioral foundations. */
|
|
67
|
+
soul: 'SOUL.md',
|
|
68
|
+
/** AGENTS.md — operational protocols and memory architecture. */
|
|
69
|
+
agents: 'AGENTS.md',
|
|
70
|
+
};
|
|
71
|
+
/** Templates directory name within core config. */
|
|
72
|
+
const TEMPLATES_DIR = 'templates';
|
|
73
|
+
/** Registry cache file name. */
|
|
74
|
+
const REGISTRY_CACHE_FILE = 'registry-cache.json';
|
|
75
|
+
/** Core config file name. */
|
|
76
|
+
const CONFIG_FILE = 'config.json';
|
|
77
|
+
/** Component versions state file name. */
|
|
78
|
+
const COMPONENT_VERSIONS_FILE = 'component-versions.json';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Shared file I/O helpers for managed section operations.
|
|
82
|
+
*
|
|
83
|
+
* @remarks
|
|
84
|
+
* Extracts the atomic write pattern and file-level locking into
|
|
85
|
+
* reusable utilities, eliminating duplication between
|
|
86
|
+
* `updateManagedSection` and `removeManagedSection`.
|
|
87
|
+
*/
|
|
88
|
+
/** Stale lock threshold in ms (2 minutes). */
|
|
89
|
+
const STALE_LOCK_MS = 120_000;
|
|
90
|
+
/** Default core version when none provided. */
|
|
91
|
+
const DEFAULT_CORE_VERSION = '0.0.0';
|
|
92
|
+
/** Lock retry options. */
|
|
93
|
+
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
94
|
+
/**
|
|
95
|
+
* Write content to a file atomically via a temp file + rename.
|
|
96
|
+
*
|
|
97
|
+
* @param filePath - Absolute path to the target file.
|
|
98
|
+
* @param content - Content to write.
|
|
99
|
+
*/
|
|
100
|
+
function atomicWrite(filePath, content) {
|
|
101
|
+
const dir = dirname(filePath);
|
|
102
|
+
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
103
|
+
writeFileSync(tempPath, content, 'utf-8');
|
|
104
|
+
renameSync(tempPath, filePath);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Execute a callback while holding a file lock.
|
|
108
|
+
*
|
|
109
|
+
* @remarks
|
|
110
|
+
* Acquires a lock on the file, executes the callback, and releases
|
|
111
|
+
* the lock in a finally block. The lock uses a 2-minute stale threshold
|
|
112
|
+
* and retries up to 5 times.
|
|
113
|
+
*
|
|
114
|
+
* @param filePath - Absolute path to the file to lock.
|
|
115
|
+
* @param fn - Async callback to execute while holding the lock.
|
|
116
|
+
*/
|
|
117
|
+
async function withFileLock(filePath, fn) {
|
|
118
|
+
let release;
|
|
119
|
+
try {
|
|
120
|
+
release = await lock(filePath, {
|
|
121
|
+
stale: STALE_LOCK_MS,
|
|
122
|
+
retries: LOCK_RETRIES,
|
|
123
|
+
});
|
|
124
|
+
await fn();
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
if (release) {
|
|
128
|
+
try {
|
|
129
|
+
await release();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Lock already released or file deleted — safe to ignore
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Shared component version state file management.
|
|
140
|
+
*
|
|
141
|
+
* @remarks
|
|
142
|
+
* Each `ComponentWriter` cycle writes its component's entry to
|
|
143
|
+
* `{coreConfigDir}/component-versions.json`. The Platform Handlebars
|
|
144
|
+
* template reads this file to populate ALL rows in the service health
|
|
145
|
+
* table, not just the calling component's.
|
|
146
|
+
*/
|
|
147
|
+
/**
|
|
148
|
+
* Read the component versions state file.
|
|
149
|
+
*
|
|
150
|
+
* @param coreConfigDir - Path to the core config directory.
|
|
151
|
+
* @returns The parsed state, or an empty object if the file doesn't exist.
|
|
152
|
+
*/
|
|
153
|
+
function readComponentVersions(coreConfigDir) {
|
|
154
|
+
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
155
|
+
if (!existsSync(filePath))
|
|
156
|
+
return {};
|
|
157
|
+
try {
|
|
158
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
159
|
+
return JSON.parse(raw);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Write a component's version entry to the shared state file.
|
|
167
|
+
*
|
|
168
|
+
* @remarks
|
|
169
|
+
* Reads the existing file, merges the new entry, and writes atomically.
|
|
170
|
+
*
|
|
171
|
+
* @param coreConfigDir - Path to the core config directory.
|
|
172
|
+
* @param options - Component version data to write.
|
|
173
|
+
*/
|
|
174
|
+
function writeComponentVersion(coreConfigDir, options) {
|
|
175
|
+
const existing = readComponentVersions(coreConfigDir);
|
|
176
|
+
existing[options.componentName] = {
|
|
177
|
+
serviceVersion: options.serviceVersion,
|
|
178
|
+
pluginVersion: options.pluginVersion,
|
|
179
|
+
servicePackage: options.servicePackage,
|
|
180
|
+
pluginPackage: options.pluginPackage,
|
|
181
|
+
updatedAt: new Date().toISOString(),
|
|
182
|
+
};
|
|
183
|
+
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
184
|
+
const dir = dirname(filePath);
|
|
185
|
+
if (!existsSync(dir)) {
|
|
186
|
+
mkdirSync(dir, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
189
|
+
}
|
|
10
190
|
|
|
11
191
|
/**
|
|
12
192
|
* Comment markers for managed content blocks.
|
|
@@ -57,29 +237,6 @@ const STALENESS_THRESHOLD_MS = 5 * 60 * 1000;
|
|
|
57
237
|
/** Warning text prepended inside managed block when cleanup is needed. */
|
|
58
238
|
const CLEANUP_FLAG = '> ⚠️ CLEANUP NEEDED: Orphaned Jeeves content may exist below this managed section. Review everything after the END marker and remove any content that duplicates what appears above.';
|
|
59
239
|
|
|
60
|
-
/**
|
|
61
|
-
* Directory and file path conventions for the Jeeves platform.
|
|
62
|
-
*/
|
|
63
|
-
/** Core config directory name within the config root. */
|
|
64
|
-
const CORE_CONFIG_DIR = 'jeeves-core';
|
|
65
|
-
/** Prefix for component config directories: `jeeves-{name}`. */
|
|
66
|
-
const COMPONENT_CONFIG_PREFIX = 'jeeves-';
|
|
67
|
-
/** Default workspace file names. */
|
|
68
|
-
const WORKSPACE_FILES = {
|
|
69
|
-
/** TOOLS.md — live platform state and component sections. */
|
|
70
|
-
tools: 'TOOLS.md',
|
|
71
|
-
/** SOUL.md — professional discipline and behavioral foundations. */
|
|
72
|
-
soul: 'SOUL.md',
|
|
73
|
-
/** AGENTS.md — operational protocols and memory architecture. */
|
|
74
|
-
agents: 'AGENTS.md',
|
|
75
|
-
};
|
|
76
|
-
/** Templates directory name within core config. */
|
|
77
|
-
const TEMPLATES_DIR = 'templates';
|
|
78
|
-
/** Registry cache file name. */
|
|
79
|
-
const REGISTRY_CACHE_FILE = 'registry-cache.json';
|
|
80
|
-
/** Core config file name. */
|
|
81
|
-
const CONFIG_FILE = 'config.json';
|
|
82
|
-
|
|
83
240
|
/**
|
|
84
241
|
* Default port assignments for Jeeves platform services.
|
|
85
242
|
*
|
|
@@ -142,14 +299,14 @@ const SECTION_ORDER = [
|
|
|
142
299
|
* Core library version, inlined at build time.
|
|
143
300
|
*
|
|
144
301
|
* @remarks
|
|
145
|
-
* The `0.1.
|
|
302
|
+
* The `0.1.6` placeholder is replaced by
|
|
146
303
|
* `@rollup/plugin-replace` during the build with the actual version
|
|
147
304
|
* from `package.json`. This ensures the correct version survives
|
|
148
305
|
* when consumers bundle core into their own dist (where runtime
|
|
149
306
|
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
150
307
|
*/
|
|
151
308
|
/** The core library version from package.json (inlined at build time). */
|
|
152
|
-
const CORE_VERSION = '0.1.
|
|
309
|
+
const CORE_VERSION = '0.1.6';
|
|
153
310
|
|
|
154
311
|
/**
|
|
155
312
|
* Workspace and config root initialization.
|
|
@@ -487,10 +644,6 @@ function shouldWrite(myVersion, existing, stalenessThresholdMs = STALENESS_THRES
|
|
|
487
644
|
*
|
|
488
645
|
* Provides file-level locking, version-stamp convergence, and atomic writes.
|
|
489
646
|
*/
|
|
490
|
-
/** Default core version when none provided. */
|
|
491
|
-
const DEFAULT_VERSION = '0.0.0';
|
|
492
|
-
/** Stale lock threshold in ms (2 minutes). */
|
|
493
|
-
const STALE_LOCK_MS = 120_000;
|
|
494
647
|
/**
|
|
495
648
|
* Update a managed section in a file.
|
|
496
649
|
*
|
|
@@ -499,7 +652,7 @@ const STALE_LOCK_MS = 120_000;
|
|
|
499
652
|
* @param options - Write mode and optional configuration.
|
|
500
653
|
*/
|
|
501
654
|
async function updateManagedSection(filePath, content, options = {}) {
|
|
502
|
-
const { mode = 'block', sectionId, markers = TOOLS_MARKERS, coreVersion =
|
|
655
|
+
const { mode = 'block', sectionId, markers = TOOLS_MARKERS, coreVersion = DEFAULT_CORE_VERSION, stalenessThresholdMs, } = options;
|
|
503
656
|
if (mode === 'section' && !sectionId) {
|
|
504
657
|
throw new Error('sectionId is required when mode is "section"');
|
|
505
658
|
}
|
|
@@ -511,93 +664,77 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
511
664
|
if (!existsSync(filePath)) {
|
|
512
665
|
writeFileSync(filePath, '', 'utf-8');
|
|
513
666
|
}
|
|
514
|
-
let release;
|
|
515
667
|
try {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
newManagedBody = markers.title
|
|
533
|
-
? `# ${markers.title}\n\n${content}`
|
|
534
|
-
: content;
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
// Section mode: upsert the named section
|
|
538
|
-
const sections = [...parsed.sections];
|
|
539
|
-
const existingIdx = sections.findIndex((s) => s.id === sectionId);
|
|
540
|
-
if (existingIdx >= 0) {
|
|
541
|
-
sections[existingIdx] = { id: sectionId, content };
|
|
668
|
+
await withFileLock(filePath, () => {
|
|
669
|
+
const fileContent = readFileSync(filePath, 'utf-8');
|
|
670
|
+
const parsed = parseManaged(fileContent, markers);
|
|
671
|
+
// Version-stamp convergence check (block mode only).
|
|
672
|
+
// In section mode, components always write their own sections — the version
|
|
673
|
+
// stamp governs shared content convergence, not component-specific sections.
|
|
674
|
+
if (mode === 'block' &&
|
|
675
|
+
!shouldWrite(coreVersion, parsed.versionStamp, stalenessThresholdMs)) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
let newManagedBody;
|
|
679
|
+
if (mode === 'block') {
|
|
680
|
+
// Prepend H1 title if markers specify one
|
|
681
|
+
newManagedBody = markers.title
|
|
682
|
+
? `# ${markers.title}\n\n${content}`
|
|
683
|
+
: content;
|
|
542
684
|
}
|
|
543
685
|
else {
|
|
544
|
-
|
|
686
|
+
// Section mode: upsert the named section
|
|
687
|
+
const sections = [...parsed.sections];
|
|
688
|
+
const existingIdx = sections.findIndex((s) => s.id === sectionId);
|
|
689
|
+
if (existingIdx >= 0) {
|
|
690
|
+
sections[existingIdx] = { id: sectionId, content };
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
sections.push({ id: sectionId, content });
|
|
694
|
+
}
|
|
695
|
+
sortSectionsByOrder(sections);
|
|
696
|
+
const sectionText = sections
|
|
697
|
+
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
698
|
+
.join('\n\n');
|
|
699
|
+
// Prepend H1 title if markers specify one
|
|
700
|
+
newManagedBody = markers.title
|
|
701
|
+
? `# ${markers.title}\n\n${sectionText}`
|
|
702
|
+
: sectionText;
|
|
703
|
+
}
|
|
704
|
+
// Cleanup detection
|
|
705
|
+
const userContent = parsed.userContent;
|
|
706
|
+
const cleanupNeeded = needsCleanup(newManagedBody, userContent);
|
|
707
|
+
// Build the full managed block
|
|
708
|
+
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
709
|
+
const endLine = formatEndMarker(markers.end);
|
|
710
|
+
const parts = [];
|
|
711
|
+
if (parsed.beforeContent) {
|
|
712
|
+
parts.push(parsed.beforeContent);
|
|
713
|
+
parts.push('');
|
|
714
|
+
}
|
|
715
|
+
parts.push(beginLine);
|
|
716
|
+
if (cleanupNeeded) {
|
|
717
|
+
parts.push('');
|
|
718
|
+
parts.push(CLEANUP_FLAG);
|
|
545
719
|
}
|
|
546
|
-
sortSectionsByOrder(sections);
|
|
547
|
-
const sectionText = sections
|
|
548
|
-
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
549
|
-
.join('\n\n');
|
|
550
|
-
// Prepend H1 title if markers specify one (e.g., "# Jeeves Platform Tools")
|
|
551
|
-
newManagedBody = markers.title
|
|
552
|
-
? `# ${markers.title}\n\n${sectionText}`
|
|
553
|
-
: sectionText;
|
|
554
|
-
}
|
|
555
|
-
// Cleanup detection
|
|
556
|
-
const userContent = parsed.userContent;
|
|
557
|
-
const cleanupNeeded = needsCleanup(newManagedBody, userContent);
|
|
558
|
-
// Build the full managed block
|
|
559
|
-
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
560
|
-
const endLine = formatEndMarker(markers.end);
|
|
561
|
-
const parts = [];
|
|
562
|
-
if (parsed.beforeContent) {
|
|
563
|
-
parts.push(parsed.beforeContent);
|
|
564
720
|
parts.push('');
|
|
565
|
-
|
|
566
|
-
parts.push(beginLine);
|
|
567
|
-
if (cleanupNeeded) {
|
|
721
|
+
parts.push(newManagedBody);
|
|
568
722
|
parts.push('');
|
|
569
|
-
parts.push(
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
parts.push(endLine);
|
|
575
|
-
if (userContent) {
|
|
723
|
+
parts.push(endLine);
|
|
724
|
+
if (userContent) {
|
|
725
|
+
parts.push('');
|
|
726
|
+
parts.push(userContent);
|
|
727
|
+
}
|
|
576
728
|
parts.push('');
|
|
577
|
-
parts.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const newFileContent = parts.join('\n');
|
|
581
|
-
// Atomic write: write to temp file, then rename
|
|
582
|
-
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
583
|
-
writeFileSync(tempPath, newFileContent, 'utf-8');
|
|
584
|
-
renameSync(tempPath, filePath);
|
|
729
|
+
const newFileContent = parts.join('\n');
|
|
730
|
+
atomicWrite(filePath, newFileContent);
|
|
731
|
+
});
|
|
585
732
|
}
|
|
586
733
|
catch (err) {
|
|
587
734
|
// Log warning but don't throw — writer cycles are periodic
|
|
588
735
|
const message = err instanceof Error ? err.message : String(err);
|
|
589
736
|
console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
|
|
590
737
|
}
|
|
591
|
-
finally {
|
|
592
|
-
if (release) {
|
|
593
|
-
try {
|
|
594
|
-
await release();
|
|
595
|
-
}
|
|
596
|
-
catch {
|
|
597
|
-
// Lock already released or file deleted — safe to ignore
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
738
|
}
|
|
602
739
|
|
|
603
740
|
var agentsSectionContent = `## Memory Architecture
|
|
@@ -1234,6 +1371,63 @@ function checkRegistryVersion(packageName, cacheDir, ttlSeconds = 3600) {
|
|
|
1234
1371
|
}
|
|
1235
1372
|
}
|
|
1236
1373
|
|
|
1374
|
+
/**
|
|
1375
|
+
* Build enriched service rows for the Platform template.
|
|
1376
|
+
*
|
|
1377
|
+
* @remarks
|
|
1378
|
+
* Merges health probe results with component version state and
|
|
1379
|
+
* npm registry update availability into rows for the Handlebars
|
|
1380
|
+
* Platform template.
|
|
1381
|
+
*/
|
|
1382
|
+
/**
|
|
1383
|
+
* Check whether an available version is newer than the current one.
|
|
1384
|
+
*
|
|
1385
|
+
* @param available - Registry version string.
|
|
1386
|
+
* @param current - Currently installed version string.
|
|
1387
|
+
* @returns The available version if it's newer, otherwise undefined.
|
|
1388
|
+
*/
|
|
1389
|
+
function newerVersion(available, current) {
|
|
1390
|
+
if (!available ||
|
|
1391
|
+
!current ||
|
|
1392
|
+
!semver.valid(available) ||
|
|
1393
|
+
!semver.valid(current)) {
|
|
1394
|
+
return undefined;
|
|
1395
|
+
}
|
|
1396
|
+
return semver.gt(available, current) ? available : undefined;
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Build enriched service rows for the Platform Handlebars template.
|
|
1400
|
+
*
|
|
1401
|
+
* @param options - Probe results, version state, and configuration.
|
|
1402
|
+
* @returns Array of enriched service rows.
|
|
1403
|
+
*/
|
|
1404
|
+
function buildServiceRows(options) {
|
|
1405
|
+
const { probeResults, componentVersions, cacheDir, skipRegistryCheck } = options;
|
|
1406
|
+
return probeResults.map((r) => {
|
|
1407
|
+
const entry = componentVersions[r.name];
|
|
1408
|
+
if (!entry)
|
|
1409
|
+
return { ...r };
|
|
1410
|
+
let availableServiceVersion;
|
|
1411
|
+
let availablePluginVersion;
|
|
1412
|
+
if (!skipRegistryCheck) {
|
|
1413
|
+
if (entry.servicePackage) {
|
|
1414
|
+
const registryVersion = checkRegistryVersion(entry.servicePackage, cacheDir);
|
|
1415
|
+
availableServiceVersion = newerVersion(registryVersion, r.version);
|
|
1416
|
+
}
|
|
1417
|
+
if (entry.pluginPackage && entry.pluginVersion) {
|
|
1418
|
+
const registryVersion = checkRegistryVersion(entry.pluginPackage, cacheDir);
|
|
1419
|
+
availablePluginVersion = newerVersion(registryVersion, entry.pluginVersion);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return {
|
|
1423
|
+
...r,
|
|
1424
|
+
pluginVersion: entry.pluginVersion,
|
|
1425
|
+
availableServiceVersion,
|
|
1426
|
+
availablePluginVersion,
|
|
1427
|
+
};
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1237
1431
|
/**
|
|
1238
1432
|
* Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
1239
1433
|
*
|
|
@@ -1287,15 +1481,28 @@ function copyTemplates(coreConfigDir) {
|
|
|
1287
1481
|
}
|
|
1288
1482
|
/** Whether Handlebars helpers have been registered. */
|
|
1289
1483
|
let helpersRegistered = false;
|
|
1290
|
-
/**
|
|
1291
|
-
* Register Handlebars helpers used in the Platform template.
|
|
1292
|
-
*/
|
|
1484
|
+
/** Register Handlebars helpers used in the Platform template. */
|
|
1293
1485
|
function registerHelpers() {
|
|
1294
1486
|
if (helpersRegistered)
|
|
1295
1487
|
return;
|
|
1296
1488
|
helpersRegistered = true;
|
|
1297
1489
|
Handlebars.registerHelper('gt', (a, b) => typeof a === 'number' && typeof b === 'number' && a > b);
|
|
1298
1490
|
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Check if a newer core version is available on npm.
|
|
1493
|
+
*
|
|
1494
|
+
* @returns The newer version string, or undefined.
|
|
1495
|
+
*/
|
|
1496
|
+
function checkCoreUpdate(coreVersion, cacheDir) {
|
|
1497
|
+
const registryVersion = checkRegistryVersion('@karmaniverous/jeeves', cacheDir);
|
|
1498
|
+
if (registryVersion &&
|
|
1499
|
+
semver.valid(registryVersion) &&
|
|
1500
|
+
semver.valid(coreVersion) &&
|
|
1501
|
+
semver.gt(registryVersion, coreVersion)) {
|
|
1502
|
+
return registryVersion;
|
|
1503
|
+
}
|
|
1504
|
+
return undefined;
|
|
1505
|
+
}
|
|
1299
1506
|
/**
|
|
1300
1507
|
* Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
1301
1508
|
*
|
|
@@ -1307,55 +1514,46 @@ async function refreshPlatformContent(options) {
|
|
|
1307
1514
|
const coreConfigDir = getCoreConfigDir();
|
|
1308
1515
|
// 1. Probe all services
|
|
1309
1516
|
const probeResults = await probeAllServices(undefined, probeTimeoutMs);
|
|
1310
|
-
|
|
1311
|
-
|
|
1517
|
+
// 2. Write calling component's version entry (with serviceVersion from probe)
|
|
1518
|
+
if (componentName) {
|
|
1519
|
+
const callerProbe = probeResults.find((r) => r.name === componentName);
|
|
1520
|
+
writeComponentVersion(coreConfigDir, {
|
|
1521
|
+
componentName,
|
|
1522
|
+
serviceVersion: callerProbe?.version,
|
|
1523
|
+
pluginVersion: componentVersion,
|
|
1524
|
+
servicePackage,
|
|
1525
|
+
pluginPackage,
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
// 3. Read all component versions from the shared state file
|
|
1529
|
+
const componentVersions = readComponentVersions(coreConfigDir);
|
|
1530
|
+
// 4. Build enriched service rows with registry checks
|
|
1312
1531
|
const cacheDir = componentName
|
|
1313
1532
|
? getComponentConfigDir(componentName)
|
|
1314
1533
|
: coreConfigDir;
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
if (svcVersion) {
|
|
1326
|
-
availableServiceVersion = svcVersion;
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
if (pluginPackage) {
|
|
1330
|
-
const plgVersion = checkRegistryVersion(pluginPackage, cacheDir);
|
|
1331
|
-
if (plgVersion) {
|
|
1332
|
-
availablePluginVersion = plgVersion;
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
// 3. Build enriched service rows — match the calling component by name
|
|
1337
|
-
const serviceRows = probeResults.map((r) => ({
|
|
1338
|
-
...r,
|
|
1339
|
-
pluginVersion: r.name === componentName ? componentVersion : undefined,
|
|
1340
|
-
availableServiceVersion: r.name === componentName ? availableServiceVersion : undefined,
|
|
1341
|
-
availablePluginVersion: r.name === componentName ? availablePluginVersion : undefined,
|
|
1342
|
-
}));
|
|
1343
|
-
// 5. Check if templates are available
|
|
1534
|
+
const availableCoreVersion = skipRegistryCheck
|
|
1535
|
+
? undefined
|
|
1536
|
+
: checkCoreUpdate(coreVersion, cacheDir);
|
|
1537
|
+
const serviceRows = buildServiceRows({
|
|
1538
|
+
probeResults,
|
|
1539
|
+
componentVersions,
|
|
1540
|
+
cacheDir,
|
|
1541
|
+
skipRegistryCheck,
|
|
1542
|
+
});
|
|
1543
|
+
// 5. Render Platform template
|
|
1344
1544
|
const templatePath = join(coreConfigDir, TEMPLATES_DIR);
|
|
1345
|
-
const templatesAvailable = existsSync(templatePath);
|
|
1346
|
-
// 6. Render Platform template
|
|
1347
1545
|
registerHelpers();
|
|
1348
1546
|
const template = Handlebars.compile(toolsPlatformTemplate);
|
|
1349
1547
|
const templateData = {
|
|
1350
1548
|
services: serviceRows,
|
|
1351
|
-
unhealthyServices,
|
|
1549
|
+
unhealthyServices: serviceRows.filter((r) => !r.healthy),
|
|
1352
1550
|
coreVersion,
|
|
1353
1551
|
availableCoreVersion,
|
|
1354
|
-
templatesAvailable,
|
|
1552
|
+
templatesAvailable: existsSync(templatePath),
|
|
1355
1553
|
templatePath,
|
|
1356
1554
|
};
|
|
1357
1555
|
const platformContent = template(templateData);
|
|
1358
|
-
//
|
|
1556
|
+
// 6. Write TOOLS.md Platform section
|
|
1359
1557
|
const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
|
|
1360
1558
|
await updateManagedSection(toolsPath, platformContent, {
|
|
1361
1559
|
mode: 'section',
|
|
@@ -1364,7 +1562,7 @@ async function refreshPlatformContent(options) {
|
|
|
1364
1562
|
coreVersion,
|
|
1365
1563
|
stalenessThresholdMs,
|
|
1366
1564
|
});
|
|
1367
|
-
//
|
|
1565
|
+
// 7. Write SOUL.md managed block
|
|
1368
1566
|
const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
|
|
1369
1567
|
await updateManagedSection(soulPath, soulSectionContent, {
|
|
1370
1568
|
mode: 'block',
|
|
@@ -1372,7 +1570,7 @@ async function refreshPlatformContent(options) {
|
|
|
1372
1570
|
coreVersion,
|
|
1373
1571
|
stalenessThresholdMs,
|
|
1374
1572
|
});
|
|
1375
|
-
//
|
|
1573
|
+
// 8. Write AGENTS.md managed block
|
|
1376
1574
|
const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
|
|
1377
1575
|
await updateManagedSection(agentsPath, agentsSectionContent, {
|
|
1378
1576
|
mode: 'block',
|
|
@@ -1380,7 +1578,7 @@ async function refreshPlatformContent(options) {
|
|
|
1380
1578
|
coreVersion,
|
|
1381
1579
|
stalenessThresholdMs,
|
|
1382
1580
|
});
|
|
1383
|
-
//
|
|
1581
|
+
// 9. Copy templates to config dir
|
|
1384
1582
|
copyTemplates(coreConfigDir);
|
|
1385
1583
|
}
|
|
1386
1584
|
|
|
@@ -1460,6 +1658,8 @@ class ComponentWriter {
|
|
|
1460
1658
|
coreVersion: CORE_VERSION,
|
|
1461
1659
|
});
|
|
1462
1660
|
// Platform content maintenance: SOUL.md, AGENTS.md, Platform section
|
|
1661
|
+
// refreshPlatformContent also writes the component version entry
|
|
1662
|
+
// (with serviceVersion from probe) to the shared state file.
|
|
1463
1663
|
await refreshPlatformContent({
|
|
1464
1664
|
coreVersion: CORE_VERSION,
|
|
1465
1665
|
componentName: this.component.name,
|
|
@@ -1614,41 +1814,150 @@ function createComponentWriter(component, options) {
|
|
|
1614
1814
|
}
|
|
1615
1815
|
|
|
1616
1816
|
/**
|
|
1617
|
-
*
|
|
1817
|
+
* Plugin resolution helpers for the OpenClaw plugin SDK.
|
|
1618
1818
|
*
|
|
1619
1819
|
* @remarks
|
|
1620
|
-
*
|
|
1621
|
-
*
|
|
1622
|
-
*
|
|
1623
|
-
* 3. `process.cwd()` — last resort (unsafe when gateway runs from system32)
|
|
1624
|
-
*
|
|
1625
|
-
* The config value is checked first because `api.resolvePath('.')` delegates
|
|
1626
|
-
* to `path.resolve('.')`, which returns `process.cwd()` — not the workspace.
|
|
1627
|
-
* When the gateway runs as a Windows service from `C:\Windows\system32`,
|
|
1628
|
-
* `resolvePath('.')` returns system32, not the configured workspace.
|
|
1629
|
-
*
|
|
1630
|
-
* Plugins should call this once at registration time and pass the result
|
|
1631
|
-
* to `init({ workspacePath })`.
|
|
1820
|
+
* Provides workspace path resolution and plugin setting resolution
|
|
1821
|
+
* with a standard three-step fallback chain:
|
|
1822
|
+
* plugin config → environment variable → default value.
|
|
1632
1823
|
*/
|
|
1633
1824
|
/**
|
|
1634
1825
|
* Resolve the workspace root from the OpenClaw plugin API.
|
|
1635
1826
|
*
|
|
1636
|
-
* @
|
|
1827
|
+
* @remarks
|
|
1828
|
+
* Tries three sources in order:
|
|
1829
|
+
* 1. `api.config.agents.defaults.workspace` — explicit config
|
|
1830
|
+
* 2. `api.resolvePath('.')` — gateway-provided path resolver
|
|
1831
|
+
* 3. `process.cwd()` — last resort
|
|
1832
|
+
*
|
|
1833
|
+
* @param api - The plugin API object provided by the gateway.
|
|
1637
1834
|
* @returns Absolute path to the workspace root.
|
|
1638
1835
|
*/
|
|
1639
1836
|
function resolveWorkspacePath(api) {
|
|
1640
|
-
// 1. Explicit config value (most authoritative)
|
|
1641
1837
|
const configured = api.config?.agents?.defaults?.workspace;
|
|
1642
1838
|
if (typeof configured === 'string' && configured.trim()) {
|
|
1643
1839
|
return configured;
|
|
1644
1840
|
}
|
|
1645
|
-
// 2. Gateway-provided path resolver
|
|
1646
1841
|
if (typeof api.resolvePath === 'function') {
|
|
1647
1842
|
return api.resolvePath('.');
|
|
1648
1843
|
}
|
|
1649
|
-
// 3. Last resort — unsafe when gateway runs from system32
|
|
1650
1844
|
return process.cwd();
|
|
1651
1845
|
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Resolve a plugin setting via the standard three-step fallback chain:
|
|
1848
|
+
* plugin config → environment variable → fallback value.
|
|
1849
|
+
*
|
|
1850
|
+
* @param api - Plugin API object.
|
|
1851
|
+
* @param pluginId - Plugin identifier (e.g., 'jeeves-watcher-openclaw').
|
|
1852
|
+
* @param key - Config key within the plugin's config object.
|
|
1853
|
+
* @param envVar - Environment variable name.
|
|
1854
|
+
* @param fallback - Default value if neither source provides one.
|
|
1855
|
+
* @returns The resolved setting value.
|
|
1856
|
+
*/
|
|
1857
|
+
function resolvePluginSetting(api, pluginId, key, envVar, fallback) {
|
|
1858
|
+
const fromPlugin = api.config?.plugins?.entries?.[pluginId]?.config?.[key];
|
|
1859
|
+
if (typeof fromPlugin === 'string')
|
|
1860
|
+
return fromPlugin;
|
|
1861
|
+
const fromEnv = process.env[envVar];
|
|
1862
|
+
if (fromEnv)
|
|
1863
|
+
return fromEnv;
|
|
1864
|
+
return fallback;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
/**
|
|
1868
|
+
* Remove a managed section or entire managed block from a file.
|
|
1869
|
+
*
|
|
1870
|
+
* @remarks
|
|
1871
|
+
* Supports two modes:
|
|
1872
|
+
* - No `sectionId`: Remove the entire managed block (markers + content),
|
|
1873
|
+
* leaving user content intact.
|
|
1874
|
+
* - With `sectionId`: Remove a specific H2 section from within the
|
|
1875
|
+
* managed block. If it was the last section, remove the entire block.
|
|
1876
|
+
*
|
|
1877
|
+
* Provides file-level locking and atomic writes (temp file + rename).
|
|
1878
|
+
* Missing markers or nonexistent sections are no-ops (no error thrown).
|
|
1879
|
+
*/
|
|
1880
|
+
/**
|
|
1881
|
+
* Remove a managed section or entire managed block from a file.
|
|
1882
|
+
*
|
|
1883
|
+
* @param filePath - Absolute path to the target file.
|
|
1884
|
+
* @param options - Optional section ID and custom markers.
|
|
1885
|
+
*/
|
|
1886
|
+
async function removeManagedSection(filePath, options = {}) {
|
|
1887
|
+
const { sectionId, markers = TOOLS_MARKERS } = options;
|
|
1888
|
+
if (!existsSync(filePath))
|
|
1889
|
+
return;
|
|
1890
|
+
await withFileLock(filePath, () => {
|
|
1891
|
+
const fileContent = readFileSync(filePath, 'utf-8');
|
|
1892
|
+
const parsed = parseManaged(fileContent, markers);
|
|
1893
|
+
if (!parsed.found)
|
|
1894
|
+
return;
|
|
1895
|
+
let newContent;
|
|
1896
|
+
if (!sectionId) {
|
|
1897
|
+
// Remove entire managed block
|
|
1898
|
+
newContent = buildWithoutBlock(parsed.beforeContent, parsed.userContent);
|
|
1899
|
+
}
|
|
1900
|
+
else {
|
|
1901
|
+
// Remove specific section
|
|
1902
|
+
const remaining = parsed.sections.filter((s) => s.id !== sectionId);
|
|
1903
|
+
if (remaining.length === parsed.sections.length) {
|
|
1904
|
+
// Section not found — no-op
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
if (remaining.length === 0) {
|
|
1908
|
+
// Last section removed — remove entire block
|
|
1909
|
+
newContent = buildWithoutBlock(parsed.beforeContent, parsed.userContent);
|
|
1910
|
+
}
|
|
1911
|
+
else {
|
|
1912
|
+
// Rebuild managed block without the removed section
|
|
1913
|
+
newContent = buildWithSections(parsed.beforeContent, parsed.userContent, remaining, markers, parsed.versionStamp?.version);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
atomicWrite(filePath, newContent);
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
/** Build file content without the managed block. */
|
|
1920
|
+
function buildWithoutBlock(beforeContent, userContent) {
|
|
1921
|
+
const parts = [];
|
|
1922
|
+
if (beforeContent)
|
|
1923
|
+
parts.push(beforeContent);
|
|
1924
|
+
if (userContent) {
|
|
1925
|
+
if (parts.length > 0)
|
|
1926
|
+
parts.push('');
|
|
1927
|
+
parts.push(userContent);
|
|
1928
|
+
}
|
|
1929
|
+
if (parts.length === 0)
|
|
1930
|
+
return '';
|
|
1931
|
+
return parts.join('\n') + '\n';
|
|
1932
|
+
}
|
|
1933
|
+
/** Rebuild file content with remaining sections. */
|
|
1934
|
+
function buildWithSections(beforeContent, userContent, sections, markers, coreVersion) {
|
|
1935
|
+
const sorted = sortSectionsByOrder([...sections]);
|
|
1936
|
+
const sectionText = sorted
|
|
1937
|
+
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
1938
|
+
.join('\n\n');
|
|
1939
|
+
const managedBody = markers.title
|
|
1940
|
+
? `# ${markers.title}\n\n${sectionText}`
|
|
1941
|
+
: sectionText;
|
|
1942
|
+
const beginLine = formatBeginMarker(markers.begin, coreVersion ?? DEFAULT_CORE_VERSION);
|
|
1943
|
+
const endLine = formatEndMarker(markers.end);
|
|
1944
|
+
const parts = [];
|
|
1945
|
+
if (beforeContent) {
|
|
1946
|
+
parts.push(beforeContent);
|
|
1947
|
+
parts.push('');
|
|
1948
|
+
}
|
|
1949
|
+
parts.push(beginLine);
|
|
1950
|
+
parts.push('');
|
|
1951
|
+
parts.push(managedBody);
|
|
1952
|
+
parts.push('');
|
|
1953
|
+
parts.push(endLine);
|
|
1954
|
+
if (userContent) {
|
|
1955
|
+
parts.push('');
|
|
1956
|
+
parts.push(userContent);
|
|
1957
|
+
}
|
|
1958
|
+
parts.push('');
|
|
1959
|
+
return parts.join('\n');
|
|
1960
|
+
}
|
|
1652
1961
|
|
|
1653
1962
|
/**
|
|
1654
1963
|
* One-shot content seeding used by the CLI install command.
|
|
@@ -1703,4 +2012,228 @@ async function seedContent(options) {
|
|
|
1703
2012
|
});
|
|
1704
2013
|
}
|
|
1705
2014
|
|
|
1706
|
-
|
|
2015
|
+
/**
|
|
2016
|
+
* HTTP helpers for the OpenClaw plugin SDK.
|
|
2017
|
+
*
|
|
2018
|
+
* @remarks
|
|
2019
|
+
* Thin wrappers around `fetch` that throw on non-OK responses
|
|
2020
|
+
* and handle JSON serialisation/deserialisation.
|
|
2021
|
+
*/
|
|
2022
|
+
/**
|
|
2023
|
+
* Fetch JSON from a URL, throwing on non-OK responses.
|
|
2024
|
+
*
|
|
2025
|
+
* @param url - URL to fetch.
|
|
2026
|
+
* @param init - Optional `fetch` init options.
|
|
2027
|
+
* @returns Parsed JSON response body.
|
|
2028
|
+
* @throws Error with `HTTP {status}: {body}` message on non-OK responses.
|
|
2029
|
+
*/
|
|
2030
|
+
async function fetchJson(url, init) {
|
|
2031
|
+
const res = await fetch(url, init);
|
|
2032
|
+
if (!res.ok) {
|
|
2033
|
+
throw new Error('HTTP ' + String(res.status) + ': ' + (await res.text()));
|
|
2034
|
+
}
|
|
2035
|
+
return res.json();
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* POST JSON to a URL and return parsed response.
|
|
2039
|
+
*
|
|
2040
|
+
* @param url - URL to POST to.
|
|
2041
|
+
* @param body - Request body (will be JSON-stringified).
|
|
2042
|
+
* @returns Parsed JSON response body.
|
|
2043
|
+
*/
|
|
2044
|
+
async function postJson(url, body) {
|
|
2045
|
+
return fetchJson(url, {
|
|
2046
|
+
method: 'POST',
|
|
2047
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2048
|
+
body: JSON.stringify(body),
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
/**
|
|
2053
|
+
* OpenClaw configuration helpers for plugin CLI installers.
|
|
2054
|
+
*
|
|
2055
|
+
* @remarks
|
|
2056
|
+
* Provides resolution of OpenClaw home directory and config file path,
|
|
2057
|
+
* plus idempotent config patching for plugin install/uninstall.
|
|
2058
|
+
*/
|
|
2059
|
+
/**
|
|
2060
|
+
* Resolve the OpenClaw home directory.
|
|
2061
|
+
*
|
|
2062
|
+
* @remarks
|
|
2063
|
+
* Resolution order:
|
|
2064
|
+
* 1. `OPENCLAW_CONFIG` env var → dirname of the config file path
|
|
2065
|
+
* 2. `OPENCLAW_HOME` env var → resolved path
|
|
2066
|
+
* 3. Default: `~/.openclaw`
|
|
2067
|
+
*
|
|
2068
|
+
* @returns Absolute path to the OpenClaw home directory.
|
|
2069
|
+
*/
|
|
2070
|
+
function resolveOpenClawHome() {
|
|
2071
|
+
if (process.env.OPENCLAW_CONFIG) {
|
|
2072
|
+
return dirname(resolve(process.env.OPENCLAW_CONFIG));
|
|
2073
|
+
}
|
|
2074
|
+
if (process.env.OPENCLAW_HOME) {
|
|
2075
|
+
return resolve(process.env.OPENCLAW_HOME);
|
|
2076
|
+
}
|
|
2077
|
+
return join(homedir(), '.openclaw');
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* Resolve the OpenClaw config file path.
|
|
2081
|
+
*
|
|
2082
|
+
* @remarks
|
|
2083
|
+
* If `OPENCLAW_CONFIG` is set, uses that directly.
|
|
2084
|
+
* Otherwise defaults to `{home}/openclaw.json`.
|
|
2085
|
+
*
|
|
2086
|
+
* @param home - The OpenClaw home directory.
|
|
2087
|
+
* @returns Absolute path to the config file.
|
|
2088
|
+
*/
|
|
2089
|
+
function resolveConfigPath(home) {
|
|
2090
|
+
if (process.env.OPENCLAW_CONFIG) {
|
|
2091
|
+
return resolve(process.env.OPENCLAW_CONFIG);
|
|
2092
|
+
}
|
|
2093
|
+
return join(home, 'openclaw.json');
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Patch an allowlist array: add or remove the plugin ID.
|
|
2097
|
+
*
|
|
2098
|
+
* @returns A log message if a change was made, or undefined.
|
|
2099
|
+
*/
|
|
2100
|
+
function patchAllowList(parent, key, label, pluginId, mode) {
|
|
2101
|
+
if (mode === 'add') {
|
|
2102
|
+
if (!Array.isArray(parent[key])) {
|
|
2103
|
+
parent[key] = [pluginId];
|
|
2104
|
+
return `Created ${label} with "${pluginId}"`;
|
|
2105
|
+
}
|
|
2106
|
+
const list = parent[key];
|
|
2107
|
+
if (!list.includes(pluginId)) {
|
|
2108
|
+
list.push(pluginId);
|
|
2109
|
+
return `Added "${pluginId}" to ${label}`;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
else {
|
|
2113
|
+
if (!Array.isArray(parent[key]))
|
|
2114
|
+
return undefined;
|
|
2115
|
+
const list = parent[key];
|
|
2116
|
+
const filtered = list.filter((id) => id !== pluginId);
|
|
2117
|
+
if (filtered.length !== list.length) {
|
|
2118
|
+
parent[key] = filtered;
|
|
2119
|
+
return `Removed "${pluginId}" from ${label}`;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
return undefined;
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Patch an OpenClaw config for plugin install or uninstall.
|
|
2126
|
+
*
|
|
2127
|
+
* @remarks
|
|
2128
|
+
* Manages `plugins.entries.{pluginId}` and `tools.alsoAllow`.
|
|
2129
|
+
* Idempotent: adding twice produces no duplicates; removing when absent
|
|
2130
|
+
* produces no errors.
|
|
2131
|
+
*
|
|
2132
|
+
* @param config - The parsed OpenClaw config object (mutated in place).
|
|
2133
|
+
* @param pluginId - The plugin identifier.
|
|
2134
|
+
* @param mode - Whether to add or remove the plugin.
|
|
2135
|
+
* @returns Array of log messages describing changes made.
|
|
2136
|
+
*/
|
|
2137
|
+
function patchConfig(config, pluginId, mode) {
|
|
2138
|
+
const messages = [];
|
|
2139
|
+
// Ensure plugins section
|
|
2140
|
+
if (!config.plugins || typeof config.plugins !== 'object') {
|
|
2141
|
+
config.plugins = {};
|
|
2142
|
+
}
|
|
2143
|
+
const plugins = config.plugins;
|
|
2144
|
+
// plugins.entries
|
|
2145
|
+
if (!plugins.entries || typeof plugins.entries !== 'object') {
|
|
2146
|
+
plugins.entries = {};
|
|
2147
|
+
}
|
|
2148
|
+
const entries = plugins.entries;
|
|
2149
|
+
if (mode === 'add') {
|
|
2150
|
+
if (!entries[pluginId]) {
|
|
2151
|
+
entries[pluginId] = { enabled: true };
|
|
2152
|
+
messages.push(`Added "${pluginId}" to plugins.entries`);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
else if (pluginId in entries) {
|
|
2156
|
+
Reflect.deleteProperty(entries, pluginId);
|
|
2157
|
+
messages.push(`Removed "${pluginId}" from plugins.entries`);
|
|
2158
|
+
}
|
|
2159
|
+
// tools.alsoAllow
|
|
2160
|
+
if (!config.tools || typeof config.tools !== 'object') {
|
|
2161
|
+
config.tools = {};
|
|
2162
|
+
}
|
|
2163
|
+
const tools = config.tools;
|
|
2164
|
+
const toolAlsoAllow = patchAllowList(tools, 'alsoAllow', 'tools.alsoAllow', pluginId, mode);
|
|
2165
|
+
if (toolAlsoAllow)
|
|
2166
|
+
messages.push(toolAlsoAllow);
|
|
2167
|
+
return messages;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/**
|
|
2171
|
+
* Tool result formatters for the OpenClaw plugin SDK.
|
|
2172
|
+
*
|
|
2173
|
+
* @remarks
|
|
2174
|
+
* Provides standardised helpers for building `ToolResult` objects:
|
|
2175
|
+
* success, error, and connection-error variants.
|
|
2176
|
+
*/
|
|
2177
|
+
/**
|
|
2178
|
+
* Format a successful tool result.
|
|
2179
|
+
*
|
|
2180
|
+
* @param data - Arbitrary data to return as JSON.
|
|
2181
|
+
* @returns A `ToolResult` with JSON-stringified content.
|
|
2182
|
+
*/
|
|
2183
|
+
function ok(data) {
|
|
2184
|
+
return {
|
|
2185
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Format an error tool result.
|
|
2190
|
+
*
|
|
2191
|
+
* @param error - Error instance, string, or other value.
|
|
2192
|
+
* @returns A `ToolResult` with `isError: true`.
|
|
2193
|
+
*/
|
|
2194
|
+
function fail(error) {
|
|
2195
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2196
|
+
return {
|
|
2197
|
+
content: [{ type: 'text', text: 'Error: ' + message }],
|
|
2198
|
+
isError: true,
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2202
|
+
* Format a connection error with actionable guidance.
|
|
2203
|
+
*
|
|
2204
|
+
* @remarks
|
|
2205
|
+
* Detects `ECONNREFUSED`, `ENOTFOUND`, and `ETIMEDOUT` from
|
|
2206
|
+
* `error.cause.code` and returns a user-friendly message referencing
|
|
2207
|
+
* the plugin's `config.apiUrl` setting. Falls back to `fail()` for
|
|
2208
|
+
* non-connection errors.
|
|
2209
|
+
*
|
|
2210
|
+
* @param error - Error instance (typically from `fetch`).
|
|
2211
|
+
* @param baseUrl - The URL that was being contacted.
|
|
2212
|
+
* @param pluginId - The plugin identifier for config guidance.
|
|
2213
|
+
* @returns A `ToolResult` with `isError: true`.
|
|
2214
|
+
*/
|
|
2215
|
+
function connectionFail(error, baseUrl, pluginId) {
|
|
2216
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
2217
|
+
const code = cause && typeof cause === 'object' && 'code' in cause
|
|
2218
|
+
? String(cause.code)
|
|
2219
|
+
: '';
|
|
2220
|
+
const isConnectionError = code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'ETIMEDOUT';
|
|
2221
|
+
if (isConnectionError) {
|
|
2222
|
+
return {
|
|
2223
|
+
content: [
|
|
2224
|
+
{
|
|
2225
|
+
type: 'text',
|
|
2226
|
+
text: [
|
|
2227
|
+
`Service not reachable at ${baseUrl}.`,
|
|
2228
|
+
'Either start the service, or if it runs on a different port,',
|
|
2229
|
+
`set plugins.entries.${pluginId}.config.apiUrl in openclaw.json.`,
|
|
2230
|
+
].join('\n'),
|
|
2231
|
+
},
|
|
2232
|
+
],
|
|
2233
|
+
isError: true,
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
return fail(error);
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
export { AGENTS_MARKERS, CLEANUP_FLAG, COMPONENT_CONFIG_PREFIX, COMPONENT_VERSIONS_FILE, CONFIG_FILE, CORE_CONFIG_DIR, CORE_VERSION, ComponentWriter, DEFAULT_CORE_VERSION, DEFAULT_PORTS, META_PORT, REGISTRY_CACHE_FILE, RUNNER_PORT, SECTION_IDS, SECTION_ORDER, SERVER_PORT, SOUL_MARKERS, STALENESS_THRESHOLD_MS, STALE_LOCK_MS, TEMPLATES_DIR, TOOLS_MARKERS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_FILES, atomicWrite, checkRegistryVersion, connectionFail, coreConfigSchema, createAsyncContentCache, createComponentWriter, createConfigQueryHandler, fail, fetchJson, formatBeginMarker, formatEndMarker, generateJsonSchema, getComponentConfigDir, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getServiceUrl, getWorkspacePath, init, jaccard, needsCleanup, ok, parseManaged, patchConfig, postJson, probeAllServices, probeService, readComponentVersions, refreshPlatformContent, removeManagedSection, resetInit, resolveConfigPath, resolveOpenClawHome, resolvePluginSetting, resolveWorkspacePath, seedContent, shingles, shouldWrite, updateManagedSection, withFileLock, writeComponentVersion };
|