@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/dist/index.js CHANGED
@@ -1,13 +1,192 @@
1
- import { join, dirname } from 'node:path';
2
- import { createRequire } from 'node:module';
3
- import { fileURLToPath } from 'node:url';
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, read from package.json at runtime.
299
+ * Core library version, inlined at build time.
140
300
  *
141
301
  * @remarks
142
- * Used for version-stamp convergence (Decision 21). The version stamp
143
- * on managed content reflects the actual published library version,
144
- * enabling higher-version writers to take precedence.
145
- *
146
- * Uses `package-directory` to locate the package root regardless of
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
- const pkgDir = packageDirectorySync({ cwd: fileURLToPath(import.meta.url) });
150
- if (!pkgDir) {
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 = DEFAULT_VERSION, stalenessThresholdMs, } = options;
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
- release = await lock(filePath, {
521
- stale: STALE_LOCK_MS,
522
- retries: { retries: 5, minTimeout: 100, maxTimeout: 1000 },
523
- });
524
- const fileContent = readFileSync(filePath, 'utf-8');
525
- const parsed = parseManaged(fileContent, markers);
526
- // Version-stamp convergence check (block mode only).
527
- // In section mode, components always write their own sections — the version
528
- // stamp governs shared content convergence, not component-specific sections.
529
- if (mode === 'block' &&
530
- !shouldWrite(coreVersion, parsed.versionStamp, stalenessThresholdMs)) {
531
- return;
532
- }
533
- let newManagedBody;
534
- if (mode === 'block') {
535
- newManagedBody = content;
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
- sections.push({ id: sectionId, content });
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(CLEANUP_FLAG);
571
- }
572
- parts.push('');
573
- parts.push(newManagedBody);
574
- parts.push('');
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.push(userContent);
579
- }
580
- parts.push('');
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 = `{{#if versionInfo}}
890
- | Component | Service | Plugin | Core | Available |
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}} (v{{version}}){{/if}}{{else}}{{#if error}}⚠️ {{error}}{{else}}❌ Down{{/if}}{{/if}} |
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
- const unhealthyServices = probeResults.filter((r) => !r.healthy);
1322
- // 2. Build version info (registry check for the core package)
1323
- let availableVersion;
1324
- if (!skipRegistryCheck) {
1325
- const cacheDir = componentName
1326
- ? getComponentConfigDir(componentName)
1327
- : coreConfigDir;
1328
- availableVersion = checkRegistryVersion('@karmaniverous/jeeves', cacheDir);
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
- const versionInfo = probeResults.map((r) => ({
1331
- name: r.name,
1332
- serviceVersion: r.version,
1333
- coreVersion,
1334
- availableVersion: availableVersion && availableVersion !== coreVersion
1335
- ? availableVersion
1336
- : undefined,
1337
- }));
1338
- // 3. Check if templates are available
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: probeResults,
1346
- unhealthyServices,
1347
- versionInfo: versionInfo.some((v) => v.serviceVersion)
1348
- ? versionInfo
1349
- : undefined,
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
- // 5. Write TOOLS.md Platform section
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
- // 6. Write SOUL.md managed block
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
- // 7. Write AGENTS.md managed block
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
- // 8. Copy templates to config dir
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
- * Resolve the OpenClaw workspace root from the plugin API.
1817
+ * Plugin resolution helpers for the OpenClaw plugin SDK.
1611
1818
  *
1612
1819
  * @remarks
1613
- * Tries three sources in order:
1614
- * 1. `api.config.agents.defaults.workspace` — explicit config (most authoritative)
1615
- * 2. `api.resolvePath('.')` — gateway-provided path resolver
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
- * @param api - The plugin API object provided by the gateway at registration.
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
- export { AGENTS_MARKERS, CLEANUP_FLAG, COMPONENT_CONFIG_PREFIX, CONFIG_FILE, CORE_CONFIG_DIR, CORE_VERSION, ComponentWriter, DEFAULT_PORTS, META_PORT, REGISTRY_CACHE_FILE, RUNNER_PORT, SECTION_IDS, SECTION_ORDER, SERVER_PORT, SOUL_MARKERS, STALENESS_THRESHOLD_MS, TEMPLATES_DIR, TOOLS_MARKERS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_FILES, checkRegistryVersion, coreConfigSchema, createAsyncContentCache, createComponentWriter, formatBeginMarker, formatEndMarker, generateJsonSchema, getComponentConfigDir, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getServiceUrl, getWorkspacePath, init, jaccard, needsCleanup, parseManaged, probeAllServices, probeService, refreshPlatformContent, resetInit, resolveWorkspacePath, seedContent, shingles, shouldWrite, updateManagedSection };
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 };