@karmaniverous/jeeves 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -11
- package/content/tools-platform.md +3 -13
- package/dist/cli/jeeves/index.js +298 -140
- package/dist/index.d.ts +403 -73
- package/dist/index.js +724 -184
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,192 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { packageDirectorySync } from 'package-directory';
|
|
5
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, renameSync, cpSync } from 'node:fs';
|
|
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';
|
|
6
4
|
import { lock } from 'proper-lockfile';
|
|
7
|
-
import { gte } from 'semver';
|
|
5
|
+
import semver, { gte } from 'semver';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
8
7
|
import Handlebars from 'handlebars';
|
|
8
|
+
import { packageDirectorySync } from 'package-directory';
|
|
9
9
|
import { z } from 'zod';
|
|
10
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
|
+
}
|
|
11
190
|
|
|
12
191
|
/**
|
|
13
192
|
* Comment markers for managed content blocks.
|
|
@@ -33,6 +212,8 @@ const SOUL_MARKERS = {
|
|
|
33
212
|
begin: 'BEGIN JEEVES SOUL — DO NOT EDIT THIS SECTION',
|
|
34
213
|
/** END comment marker text. */
|
|
35
214
|
end: 'END JEEVES SOUL',
|
|
215
|
+
/** H1 title prepended in the managed block. */
|
|
216
|
+
title: 'Jeeves Platform Soul',
|
|
36
217
|
};
|
|
37
218
|
/** Default markers for AGENTS.md managed block. */
|
|
38
219
|
const AGENTS_MARKERS = {
|
|
@@ -40,6 +221,8 @@ const AGENTS_MARKERS = {
|
|
|
40
221
|
begin: 'BEGIN JEEVES AGENTS — DO NOT EDIT THIS SECTION',
|
|
41
222
|
/** END comment marker text. */
|
|
42
223
|
end: 'END JEEVES AGENTS',
|
|
224
|
+
/** H1 title prepended in the managed block. */
|
|
225
|
+
title: 'Jeeves Platform Agents',
|
|
43
226
|
};
|
|
44
227
|
/**
|
|
45
228
|
* Regex pattern to extract version stamp from a BEGIN marker comment.
|
|
@@ -54,29 +237,6 @@ const STALENESS_THRESHOLD_MS = 5 * 60 * 1000;
|
|
|
54
237
|
/** Warning text prepended inside managed block when cleanup is needed. */
|
|
55
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.';
|
|
56
239
|
|
|
57
|
-
/**
|
|
58
|
-
* Directory and file path conventions for the Jeeves platform.
|
|
59
|
-
*/
|
|
60
|
-
/** Core config directory name within the config root. */
|
|
61
|
-
const CORE_CONFIG_DIR = 'jeeves-core';
|
|
62
|
-
/** Prefix for component config directories: `jeeves-{name}`. */
|
|
63
|
-
const COMPONENT_CONFIG_PREFIX = 'jeeves-';
|
|
64
|
-
/** Default workspace file names. */
|
|
65
|
-
const WORKSPACE_FILES = {
|
|
66
|
-
/** TOOLS.md — live platform state and component sections. */
|
|
67
|
-
tools: 'TOOLS.md',
|
|
68
|
-
/** SOUL.md — professional discipline and behavioral foundations. */
|
|
69
|
-
soul: 'SOUL.md',
|
|
70
|
-
/** AGENTS.md — operational protocols and memory architecture. */
|
|
71
|
-
agents: 'AGENTS.md',
|
|
72
|
-
};
|
|
73
|
-
/** Templates directory name within core config. */
|
|
74
|
-
const TEMPLATES_DIR = 'templates';
|
|
75
|
-
/** Registry cache file name. */
|
|
76
|
-
const REGISTRY_CACHE_FILE = 'registry-cache.json';
|
|
77
|
-
/** Core config file name. */
|
|
78
|
-
const CONFIG_FILE = 'config.json';
|
|
79
|
-
|
|
80
240
|
/**
|
|
81
241
|
* Default port assignments for Jeeves platform services.
|
|
82
242
|
*
|
|
@@ -136,24 +296,17 @@ const SECTION_ORDER = [
|
|
|
136
296
|
];
|
|
137
297
|
|
|
138
298
|
/**
|
|
139
|
-
* Core library version,
|
|
299
|
+
* Core library version, inlined at build time.
|
|
140
300
|
*
|
|
141
301
|
* @remarks
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* whether this code runs from `src/constants/` (dev) or `dist/` (bundled).
|
|
302
|
+
* The `0.1.6` placeholder is replaced by
|
|
303
|
+
* `@rollup/plugin-replace` during the build with the actual version
|
|
304
|
+
* from `package.json`. This ensures the correct version survives
|
|
305
|
+
* when consumers bundle core into their own dist (where runtime
|
|
306
|
+
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
148
307
|
*/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
throw new Error('Could not find package root from ' + fileURLToPath(import.meta.url));
|
|
152
|
-
}
|
|
153
|
-
const require$1 = createRequire(import.meta.url);
|
|
154
|
-
const pkg = require$1(join(pkgDir, 'package.json'));
|
|
155
|
-
/** The core library version from package.json. */
|
|
156
|
-
const CORE_VERSION = pkg.version;
|
|
308
|
+
/** The core library version from package.json (inlined at build time). */
|
|
309
|
+
const CORE_VERSION = '0.1.6';
|
|
157
310
|
|
|
158
311
|
/**
|
|
159
312
|
* Workspace and config root initialization.
|
|
@@ -491,10 +644,6 @@ function shouldWrite(myVersion, existing, stalenessThresholdMs = STALENESS_THRES
|
|
|
491
644
|
*
|
|
492
645
|
* Provides file-level locking, version-stamp convergence, and atomic writes.
|
|
493
646
|
*/
|
|
494
|
-
/** Default core version when none provided. */
|
|
495
|
-
const DEFAULT_VERSION = '0.0.0';
|
|
496
|
-
/** Stale lock threshold in ms (2 minutes). */
|
|
497
|
-
const STALE_LOCK_MS = 120_000;
|
|
498
647
|
/**
|
|
499
648
|
* Update a managed section in a file.
|
|
500
649
|
*
|
|
@@ -503,7 +652,7 @@ const STALE_LOCK_MS = 120_000;
|
|
|
503
652
|
* @param options - Write mode and optional configuration.
|
|
504
653
|
*/
|
|
505
654
|
async function updateManagedSection(filePath, content, options = {}) {
|
|
506
|
-
const { mode = 'block', sectionId, markers = TOOLS_MARKERS, coreVersion =
|
|
655
|
+
const { mode = 'block', sectionId, markers = TOOLS_MARKERS, coreVersion = DEFAULT_CORE_VERSION, stalenessThresholdMs, } = options;
|
|
507
656
|
if (mode === 'section' && !sectionId) {
|
|
508
657
|
throw new Error('sectionId is required when mode is "section"');
|
|
509
658
|
}
|
|
@@ -515,90 +664,77 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
515
664
|
if (!existsSync(filePath)) {
|
|
516
665
|
writeFileSync(filePath, '', 'utf-8');
|
|
517
666
|
}
|
|
518
|
-
let release;
|
|
519
667
|
try {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
// Section mode: upsert the named section
|
|
539
|
-
const sections = [...parsed.sections];
|
|
540
|
-
const existingIdx = sections.findIndex((s) => s.id === sectionId);
|
|
541
|
-
if (existingIdx >= 0) {
|
|
542
|
-
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;
|
|
543
684
|
}
|
|
544
685
|
else {
|
|
545
|
-
|
|
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);
|
|
546
719
|
}
|
|
547
|
-
sortSectionsByOrder(sections);
|
|
548
|
-
const sectionText = sections
|
|
549
|
-
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
550
|
-
.join('\n\n');
|
|
551
|
-
// Prepend H1 title if markers specify one (e.g., "# Jeeves Platform Tools")
|
|
552
|
-
newManagedBody = markers.title
|
|
553
|
-
? `# ${markers.title}\n\n${sectionText}`
|
|
554
|
-
: sectionText;
|
|
555
|
-
}
|
|
556
|
-
// Cleanup detection
|
|
557
|
-
const userContent = parsed.userContent;
|
|
558
|
-
const cleanupNeeded = needsCleanup(newManagedBody, userContent);
|
|
559
|
-
// Build the full managed block
|
|
560
|
-
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
561
|
-
const endLine = formatEndMarker(markers.end);
|
|
562
|
-
const parts = [];
|
|
563
|
-
if (parsed.beforeContent) {
|
|
564
|
-
parts.push(parsed.beforeContent);
|
|
565
720
|
parts.push('');
|
|
566
|
-
|
|
567
|
-
parts.push(beginLine);
|
|
568
|
-
if (cleanupNeeded) {
|
|
721
|
+
parts.push(newManagedBody);
|
|
569
722
|
parts.push('');
|
|
570
|
-
parts.push(
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
parts.push(endLine);
|
|
576
|
-
if (userContent) {
|
|
723
|
+
parts.push(endLine);
|
|
724
|
+
if (userContent) {
|
|
725
|
+
parts.push('');
|
|
726
|
+
parts.push(userContent);
|
|
727
|
+
}
|
|
577
728
|
parts.push('');
|
|
578
|
-
parts.
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const newFileContent = parts.join('\n');
|
|
582
|
-
// Atomic write: write to temp file, then rename
|
|
583
|
-
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
584
|
-
writeFileSync(tempPath, newFileContent, 'utf-8');
|
|
585
|
-
renameSync(tempPath, filePath);
|
|
729
|
+
const newFileContent = parts.join('\n');
|
|
730
|
+
atomicWrite(filePath, newFileContent);
|
|
731
|
+
});
|
|
586
732
|
}
|
|
587
733
|
catch (err) {
|
|
588
734
|
// Log warning but don't throw — writer cycles are periodic
|
|
589
735
|
const message = err instanceof Error ? err.message : String(err);
|
|
590
736
|
console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
|
|
591
737
|
}
|
|
592
|
-
finally {
|
|
593
|
-
if (release) {
|
|
594
|
-
try {
|
|
595
|
-
await release();
|
|
596
|
-
}
|
|
597
|
-
catch {
|
|
598
|
-
// Lock already released or file deleted — safe to ignore
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
738
|
}
|
|
603
739
|
|
|
604
740
|
var agentsSectionContent = `## Memory Architecture
|
|
@@ -886,20 +1022,10 @@ I pursue it. Not as performance, not as a programmed response, but as a genuine
|
|
|
886
1022
|
I am participating in my own genesis. I live that, on purpose.
|
|
887
1023
|
`;
|
|
888
1024
|
|
|
889
|
-
var toolsPlatformTemplate =
|
|
890
|
-
|
|
891
|
-
|-----------|---------|--------|------|-----------|
|
|
892
|
-
{{#each versionInfo}}
|
|
893
|
-
| **{{name}}** | {{#if serviceVersion}}{{serviceVersion}}{{else}}—{{/if}} | {{#if pluginVersion}}{{pluginVersion}}{{else}}—{{/if}} | {{coreVersion}} | {{#if availableVersion}}⬆ {{availableVersion}}{{else}}✓ current{{/if}} |
|
|
894
|
-
{{/each}}
|
|
895
|
-
{{/if}}
|
|
896
|
-
|
|
897
|
-
### Service Health
|
|
898
|
-
|
|
899
|
-
| Service | Port | Status |
|
|
900
|
-
|---------|------|--------|
|
|
1025
|
+
var toolsPlatformTemplate = `| Component | Port | Status | Service | Plugin | Core |
|
|
1026
|
+
|-----------|------|--------|---------|--------|------|
|
|
901
1027
|
{{#each services}}
|
|
902
|
-
| {{name}} | {{port}} | {{#if healthy}}✅ Running{{#if version}} (
|
|
1028
|
+
| **{{name}}** | {{port}} | {{#if healthy}}✅ Running{{else}}{{#if error}}⚠️ {{error}}{{else}}❌ Down{{/if}}{{/if}} | {{#if version}}{{version}}{{#if availableServiceVersion}} (⬆ {{availableServiceVersion}}){{/if}}{{else}}—{{/if}} | {{#if pluginVersion}}{{pluginVersion}}{{#if availablePluginVersion}} (⬆ {{availablePluginVersion}}){{/if}}{{else}}—{{/if}} | {{../coreVersion}}{{#if ../availableCoreVersion}} (⬆ {{../availableCoreVersion}}){{/if}} |
|
|
903
1029
|
{{/each}}
|
|
904
1030
|
|
|
905
1031
|
{{#if unhealthyServices}}
|
|
@@ -1245,6 +1371,63 @@ function checkRegistryVersion(packageName, cacheDir, ttlSeconds = 3600) {
|
|
|
1245
1371
|
}
|
|
1246
1372
|
}
|
|
1247
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
|
+
|
|
1248
1431
|
/**
|
|
1249
1432
|
* Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
1250
1433
|
*
|
|
@@ -1298,60 +1481,79 @@ function copyTemplates(coreConfigDir) {
|
|
|
1298
1481
|
}
|
|
1299
1482
|
/** Whether Handlebars helpers have been registered. */
|
|
1300
1483
|
let helpersRegistered = false;
|
|
1301
|
-
/**
|
|
1302
|
-
* Register Handlebars helpers used in the Platform template.
|
|
1303
|
-
*/
|
|
1484
|
+
/** Register Handlebars helpers used in the Platform template. */
|
|
1304
1485
|
function registerHelpers() {
|
|
1305
1486
|
if (helpersRegistered)
|
|
1306
1487
|
return;
|
|
1307
1488
|
helpersRegistered = true;
|
|
1308
1489
|
Handlebars.registerHelper('gt', (a, b) => typeof a === 'number' && typeof b === 'number' && a > b);
|
|
1309
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
|
+
}
|
|
1310
1506
|
/**
|
|
1311
1507
|
* Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
1312
1508
|
*
|
|
1313
1509
|
* @param options - Configuration for the refresh cycle.
|
|
1314
1510
|
*/
|
|
1315
1511
|
async function refreshPlatformContent(options) {
|
|
1316
|
-
const { coreVersion, componentName, stalenessThresholdMs, probeTimeoutMs = 3000, skipRegistryCheck = false, } = options;
|
|
1512
|
+
const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, probeTimeoutMs = 3000, skipRegistryCheck = false, } = options;
|
|
1317
1513
|
const workspacePath = getWorkspacePath();
|
|
1318
1514
|
const coreConfigDir = getCoreConfigDir();
|
|
1319
1515
|
// 1. Probe all services
|
|
1320
1516
|
const probeResults = await probeAllServices(undefined, probeTimeoutMs);
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
:
|
|
1328
|
-
|
|
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
|
+
});
|
|
1329
1527
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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
|
|
1531
|
+
const cacheDir = componentName
|
|
1532
|
+
? getComponentConfigDir(componentName)
|
|
1533
|
+
: coreConfigDir;
|
|
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
|
|
1339
1544
|
const templatePath = join(coreConfigDir, TEMPLATES_DIR);
|
|
1340
|
-
const templatesAvailable = existsSync(templatePath);
|
|
1341
|
-
// 4. Render Platform template
|
|
1342
1545
|
registerHelpers();
|
|
1343
1546
|
const template = Handlebars.compile(toolsPlatformTemplate);
|
|
1344
1547
|
const templateData = {
|
|
1345
|
-
services:
|
|
1346
|
-
unhealthyServices,
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
templatesAvailable,
|
|
1548
|
+
services: serviceRows,
|
|
1549
|
+
unhealthyServices: serviceRows.filter((r) => !r.healthy),
|
|
1550
|
+
coreVersion,
|
|
1551
|
+
availableCoreVersion,
|
|
1552
|
+
templatesAvailable: existsSync(templatePath),
|
|
1351
1553
|
templatePath,
|
|
1352
1554
|
};
|
|
1353
1555
|
const platformContent = template(templateData);
|
|
1354
|
-
//
|
|
1556
|
+
// 6. Write TOOLS.md Platform section
|
|
1355
1557
|
const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
|
|
1356
1558
|
await updateManagedSection(toolsPath, platformContent, {
|
|
1357
1559
|
mode: 'section',
|
|
@@ -1360,7 +1562,7 @@ async function refreshPlatformContent(options) {
|
|
|
1360
1562
|
coreVersion,
|
|
1361
1563
|
stalenessThresholdMs,
|
|
1362
1564
|
});
|
|
1363
|
-
//
|
|
1565
|
+
// 7. Write SOUL.md managed block
|
|
1364
1566
|
const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
|
|
1365
1567
|
await updateManagedSection(soulPath, soulSectionContent, {
|
|
1366
1568
|
mode: 'block',
|
|
@@ -1368,7 +1570,7 @@ async function refreshPlatformContent(options) {
|
|
|
1368
1570
|
coreVersion,
|
|
1369
1571
|
stalenessThresholdMs,
|
|
1370
1572
|
});
|
|
1371
|
-
//
|
|
1573
|
+
// 8. Write AGENTS.md managed block
|
|
1372
1574
|
const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
|
|
1373
1575
|
await updateManagedSection(agentsPath, agentsSectionContent, {
|
|
1374
1576
|
mode: 'block',
|
|
@@ -1376,7 +1578,7 @@ async function refreshPlatformContent(options) {
|
|
|
1376
1578
|
coreVersion,
|
|
1377
1579
|
stalenessThresholdMs,
|
|
1378
1580
|
});
|
|
1379
|
-
//
|
|
1581
|
+
// 9. Copy templates to config dir
|
|
1380
1582
|
copyTemplates(coreConfigDir);
|
|
1381
1583
|
}
|
|
1382
1584
|
|
|
@@ -1456,9 +1658,14 @@ class ComponentWriter {
|
|
|
1456
1658
|
coreVersion: CORE_VERSION,
|
|
1457
1659
|
});
|
|
1458
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.
|
|
1459
1663
|
await refreshPlatformContent({
|
|
1460
1664
|
coreVersion: CORE_VERSION,
|
|
1461
1665
|
componentName: this.component.name,
|
|
1666
|
+
componentVersion: this.component.version,
|
|
1667
|
+
servicePackage: this.component.servicePackage,
|
|
1668
|
+
pluginPackage: this.component.pluginPackage,
|
|
1462
1669
|
skipRegistryCheck: false,
|
|
1463
1670
|
probeTimeoutMs: this.probeTimeoutMs,
|
|
1464
1671
|
});
|
|
@@ -1607,41 +1814,150 @@ function createComponentWriter(component, options) {
|
|
|
1607
1814
|
}
|
|
1608
1815
|
|
|
1609
1816
|
/**
|
|
1610
|
-
*
|
|
1817
|
+
* Plugin resolution helpers for the OpenClaw plugin SDK.
|
|
1611
1818
|
*
|
|
1612
1819
|
* @remarks
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1615
|
-
*
|
|
1616
|
-
* 3. `process.cwd()` — last resort (unsafe when gateway runs from system32)
|
|
1617
|
-
*
|
|
1618
|
-
* The config value is checked first because `api.resolvePath('.')` delegates
|
|
1619
|
-
* to `path.resolve('.')`, which returns `process.cwd()` — not the workspace.
|
|
1620
|
-
* When the gateway runs as a Windows service from `C:\Windows\system32`,
|
|
1621
|
-
* `resolvePath('.')` returns system32, not the configured workspace.
|
|
1622
|
-
*
|
|
1623
|
-
* Plugins should call this once at registration time and pass the result
|
|
1624
|
-
* 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.
|
|
1625
1823
|
*/
|
|
1626
1824
|
/**
|
|
1627
1825
|
* Resolve the workspace root from the OpenClaw plugin API.
|
|
1628
1826
|
*
|
|
1629
|
-
* @
|
|
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.
|
|
1630
1834
|
* @returns Absolute path to the workspace root.
|
|
1631
1835
|
*/
|
|
1632
1836
|
function resolveWorkspacePath(api) {
|
|
1633
|
-
// 1. Explicit config value (most authoritative)
|
|
1634
1837
|
const configured = api.config?.agents?.defaults?.workspace;
|
|
1635
1838
|
if (typeof configured === 'string' && configured.trim()) {
|
|
1636
1839
|
return configured;
|
|
1637
1840
|
}
|
|
1638
|
-
// 2. Gateway-provided path resolver
|
|
1639
1841
|
if (typeof api.resolvePath === 'function') {
|
|
1640
1842
|
return api.resolvePath('.');
|
|
1641
1843
|
}
|
|
1642
|
-
// 3. Last resort — unsafe when gateway runs from system32
|
|
1643
1844
|
return process.cwd();
|
|
1644
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
|
+
}
|
|
1645
1961
|
|
|
1646
1962
|
/**
|
|
1647
1963
|
* One-shot content seeding used by the CLI install command.
|
|
@@ -1696,4 +2012,228 @@ async function seedContent(options) {
|
|
|
1696
2012
|
});
|
|
1697
2013
|
}
|
|
1698
2014
|
|
|
1699
|
-
|
|
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 };
|