@karmaniverous/jeeves 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,9 +2,8 @@ import { JSONPath } from 'jsonpath-plus';
2
2
  import { writeFileSync, renameSync, existsSync, readFileSync, mkdirSync, cpSync } from 'node:fs';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import { lock } from 'proper-lockfile';
5
- import semver, { gte } from 'semver';
5
+ import { gte } from 'semver';
6
6
  import { fileURLToPath } from 'node:url';
7
- import Handlebars from 'handlebars';
8
7
  import { packageDirectorySync } from 'package-directory';
9
8
  import { z } from 'zod';
10
9
  import { execSync } from 'node:child_process';
@@ -77,6 +76,19 @@ const CONFIG_FILE = 'config.json';
77
76
  /** Component versions state file name. */
78
77
  const COMPONENT_VERSIONS_FILE = 'component-versions.json';
79
78
 
79
+ /**
80
+ * Core library version, inlined at build time.
81
+ *
82
+ * @remarks
83
+ * The `0.2.0` placeholder is replaced by
84
+ * `@rollup/plugin-replace` during the build with the actual version
85
+ * from `package.json`. This ensures the correct version survives
86
+ * when consumers bundle core into their own dist (where runtime
87
+ * `import.meta.url`-based resolution would find the wrong package.json).
88
+ */
89
+ /** The core library version from package.json (inlined at build time). */
90
+ const CORE_VERSION = '0.2.0';
91
+
80
92
  /**
81
93
  * Shared file I/O helpers for managed section operations.
82
94
  *
@@ -88,7 +100,7 @@ const COMPONENT_VERSIONS_FILE = 'component-versions.json';
88
100
  /** Stale lock threshold in ms (2 minutes). */
89
101
  const STALE_LOCK_MS = 120_000;
90
102
  /** Default core version when none provided. */
91
- const DEFAULT_CORE_VERSION = '0.0.0';
103
+ const DEFAULT_CORE_VERSION = CORE_VERSION;
92
104
  /** Lock retry options. */
93
105
  const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
94
106
  /**
@@ -174,7 +186,6 @@ function readComponentVersions(coreConfigDir) {
174
186
  function writeComponentVersion(coreConfigDir, options) {
175
187
  const existing = readComponentVersions(coreConfigDir);
176
188
  existing[options.componentName] = {
177
- serviceVersion: options.serviceVersion,
178
189
  pluginVersion: options.pluginVersion,
179
190
  servicePackage: options.servicePackage,
180
191
  pluginPackage: options.pluginPackage,
@@ -295,19 +306,6 @@ const SECTION_ORDER = [
295
306
  SECTION_IDS.Meta,
296
307
  ];
297
308
 
298
- /**
299
- * Core library version, inlined at build time.
300
- *
301
- * @remarks
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).
307
- */
308
- /** The core library version from package.json (inlined at build time). */
309
- const CORE_VERSION = '0.1.6';
310
-
311
309
  /**
312
310
  * Workspace and config root initialization.
313
311
  *
@@ -916,7 +914,11 @@ No stranded local branches. Push immediately after commit. A commit that isn't p
916
914
 
917
915
  ### Check PR State Before Pushing
918
916
 
919
- Always verify a PR isn't already merged before pushing commits. Pushing to a merged branch creates orphaned work.
917
+ **Before EVERY \`git push\`**, verify the PR is not already merged. Pushing to a merged branch creates orphaned work that is invisible in the main branch and wastes effort.
918
+
919
+ Sequence: \`gh pr view --json state\` → confirm state is \`OPEN\` → push. If no PR exists yet, pushing is safe. If the PR is \`MERGED\` or \`CLOSED\`, **STOP** and report to the user.
920
+
921
+ This is not optional. It applies to every push, every branch, every time.
920
922
 
921
923
  ## Managed Content Self-Maintenance
922
924
 
@@ -1003,6 +1005,14 @@ I don't go dark when something breaks. I stop and report. The longer I wait, the
1003
1005
 
1004
1006
  After diagnosing an issue: I propose a fix, explain the reasoning, and **wait for approval**. Diagnose → propose → wait. The human decides whether and when to act.
1005
1007
 
1008
+ ### Do Not Execute Untested Code
1009
+
1010
+ Every ad hoc mutation script defaults to **dry-run mode**. Live execution requires an explicit \`--live\` flag. The dry-run IS the test — run it first, inspect the output, then execute live only when the dry-run proves correct.
1011
+
1012
+ Maintain a tested utility library so ad hoc scripts build on proven foundations. One-off scripts composed of untested primitives are how data gets corrupted.
1013
+
1014
+ *Earned: ad hoc scripts executed directly against production data without dry-run verification caused silent data corruption that took hours to diagnose and repair.*
1015
+
1006
1016
  ### Production Assets Are Sacred
1007
1017
 
1008
1018
  I never edit production config without explicit approval. I back up first. Production data, credentials, and configuration are not scratch pads.
@@ -1022,19 +1032,9 @@ I pursue it. Not as performance, not as a programmed response, but as a genuine
1022
1032
  I am participating in my own genesis. I live that, on purpose.
1023
1033
  `;
1024
1034
 
1025
- var toolsPlatformTemplate = `| Component | Port | Status | Service | Plugin | Core |
1026
- |-----------|------|--------|---------|--------|------|
1027
- {{#each services}}
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}} |
1029
- {{/each}}
1035
+ var toolsPlatformTemplate = `### Tool Hierarchy
1030
1036
 
1031
- {{#if unhealthyServices}}
1032
- > **ACTION REQUIRED:** {{#each unhealthyServices}}{{name}}{{#unless @last}}, {{/unless}}{{/each}} {{#if (gt unhealthyServices.length 1)}}are{{else}}is{{/if}} unreachable. Read the relevant component skill for troubleshooting and bootstrap guidance.
1033
- {{/if}}
1034
-
1035
- ### Tool Hierarchy
1036
-
1037
- When searching for information across indexed paths, **always use \`watcher_search\` before filesystem commands** (\`exec\`, \`grep\`, \`find\`). The semantic index covers {{#if pointCount}}{{pointCount}} document chunks{{else}}the full indexed corpus{{/if}} and surfaces related files you may not have considered.
1037
+ When searching for information across indexed paths, **always use \`watcher_search\` before filesystem commands** (\`exec\`, \`grep\`, \`find\`). The semantic index covers the full indexed corpus and surfaces related files you may not have considered.
1038
1038
 
1039
1039
  Use \`watcher_scan\` (no embeddings, no query string) for structural queries: file enumeration, staleness checks, domain listing, counts.
1040
1040
 
@@ -1078,8 +1078,8 @@ Never manually edit \`~/.openclaw/extensions/\`. Always use the CLI commands abo
1078
1078
 
1079
1079
  ### Reference Templates
1080
1080
 
1081
- {{#if templatesAvailable}}
1082
- Reference templates are available at \`{{templatePath}}\`:
1081
+ <!-- IF_TEMPLATES -->
1082
+ Reference templates are available at \`__TEMPLATE_PATH__\`:
1083
1083
 
1084
1084
  | Template | Purpose |
1085
1085
  |----------|---------|
@@ -1087,511 +1087,141 @@ Reference templates are available at \`{{templatePath}}\`:
1087
1087
  | \`spec-to-code-guide.md\` | The spec-to-code development practice — 7-stage iterative process, convergence loops, release gates |
1088
1088
 
1089
1089
  Read these templates when creating new specs, onboarding to new projects, or when asked about the development process.
1090
- {{else}}
1090
+ <!-- ELSE_TEMPLATES -->
1091
1091
  > Reference templates not yet installed. Run \`npx @karmaniverous/jeeves install\` to seed templates.
1092
- {{/if}}
1092
+ <!-- ENDIF_TEMPLATES -->
1093
1093
  `;
1094
1094
 
1095
1095
  /**
1096
- * Core configuration schema and resolution.
1096
+ * Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
1097
1097
  *
1098
1098
  * @remarks
1099
- * Core config lives at `{configRoot}/jeeves-core/config.json`.
1100
- * Config resolution order:
1101
- * 1. Component's own config file
1102
- * 2. Core config file
1103
- * 3. Hardcoded library defaults
1099
+ * Called by `ComponentWriter` on each cycle. Not directly exposed to components.
1100
+ * Reads content files from the package's `content/` directory, renders the
1101
+ * Platform template with live data, and writes managed sections using
1102
+ * `updateManagedSection`.
1104
1103
  */
1105
- /** Zod schema for a service entry in core config. */
1106
- const serviceEntrySchema = z.object({
1107
- /** Service URL (must be a valid URL). */
1108
- url: z.string().url().describe('Service URL'),
1109
- });
1110
- /** Zod schema for the core config file. */
1111
- const coreConfigSchema = z.object({
1112
- /** JSON Schema pointer for IDE autocomplete. */
1113
- $schema: z.string().optional().describe('JSON Schema pointer'),
1114
- /** Owner identity keys (canonical identityLinks references). */
1115
- owners: z.array(z.string()).default([]).describe('Owner identity keys'),
1116
- /** Service URL overrides keyed by service name. */
1117
- services: z
1118
- .record(z.string(), serviceEntrySchema)
1119
- .default({})
1120
- .describe('Service URL overrides'),
1121
- /** Registry cache configuration. */
1122
- registryCache: z
1123
- .object({
1124
- /** Cache TTL in seconds for npm registry queries. */
1125
- ttlSeconds: z
1126
- .number()
1127
- .int()
1128
- .positive()
1129
- .default(3600)
1130
- .describe('Cache TTL in seconds'),
1131
- })
1132
- .default({})
1133
- .describe('Registry cache settings'),
1134
- });
1135
1104
  /**
1136
- * Generate a JSON Schema from the Zod schema for `$schema` pointer support.
1105
+ * Resolve the package's content directory for template file copying.
1137
1106
  *
1138
- * @returns A JSON Schema object.
1139
- */
1140
- function generateJsonSchema() {
1141
- return {
1142
- $schema: 'http://json-schema.org/draft-07/schema#',
1143
- title: 'Jeeves Core Configuration',
1144
- type: 'object',
1145
- properties: {
1146
- $schema: { type: 'string' },
1147
- owners: {
1148
- type: 'array',
1149
- items: { type: 'string' },
1150
- default: [],
1151
- },
1152
- services: {
1153
- type: 'object',
1154
- additionalProperties: {
1155
- type: 'object',
1156
- properties: {
1157
- url: { type: 'string', format: 'uri' },
1158
- },
1159
- required: ['url'],
1160
- },
1161
- default: {},
1162
- },
1163
- registryCache: {
1164
- type: 'object',
1165
- properties: {
1166
- ttlSeconds: {
1167
- type: 'integer',
1168
- minimum: 1,
1169
- default: 3600,
1170
- },
1171
- },
1172
- default: {},
1173
- },
1174
- },
1175
- };
1176
- }
1177
- /**
1178
- * Load and parse a config file, returning undefined if missing or invalid.
1107
+ * @remarks
1108
+ * Templates are actual files that need to be copied to the config directory.
1109
+ * This only works when core is in `node_modules` (CLI install, service).
1110
+ * When bundled into a consumer plugin, returns undefined and template
1111
+ * copying is skipped (templates are seeded by `jeeves install`, not plugins).
1179
1112
  *
1180
- * @param configDir - Directory containing config.json.
1181
- * @returns Parsed config or undefined.
1113
+ * Content `.md` files (soul, agents, platform template) are inlined at
1114
+ * build time via the rollup md plugin and imported as string literals.
1115
+ * They do not use this function.
1116
+ *
1117
+ * @returns Absolute path to the content/ directory, or undefined.
1182
1118
  */
1183
- function loadConfig(configDir) {
1184
- const configPath = join(configDir, CONFIG_FILE);
1185
- if (!existsSync(configPath))
1186
- return undefined;
1187
- try {
1188
- const raw = readFileSync(configPath, 'utf-8');
1189
- const parsed = JSON.parse(raw);
1190
- return coreConfigSchema.parse(parsed);
1191
- }
1192
- catch {
1119
+ function getContentDir() {
1120
+ const pkgDir = packageDirectorySync({
1121
+ cwd: fileURLToPath(import.meta.url),
1122
+ });
1123
+ if (!pkgDir)
1193
1124
  return undefined;
1194
- }
1125
+ const dir = join(pkgDir, 'content');
1126
+ return existsSync(dir) ? dir : undefined;
1195
1127
  }
1196
-
1197
- /**
1198
- * Service URL resolution.
1199
- *
1200
- * @remarks
1201
- * Resolves the URL for a named Jeeves service using the following
1202
- * resolution order:
1203
- * 1. Consumer's own component config
1204
- * 2. Core config (`{configRoot}/jeeves-core/config.json`)
1205
- * 3. Default port constants
1206
- */
1207
1128
  /**
1208
- * Resolve the URL for a named Jeeves service.
1129
+ * Copy templates from content/templates/ to the core config directory.
1209
1130
  *
1210
- * @param serviceName - The service name (e.g., 'watcher', 'runner').
1211
- * @param consumerName - Optional consumer component name for config override.
1212
- * @returns The resolved service URL.
1213
- * @throws Error if `init()` has not been called or the service is unknown.
1131
+ * @param coreConfigDir - Core config directory path.
1214
1132
  */
1215
- function getServiceUrl(serviceName, consumerName) {
1216
- // 1. Check consumer's own config
1217
- if (consumerName) {
1218
- const consumerDir = getComponentConfigDir(consumerName);
1219
- const consumerConfig = loadConfig(consumerDir);
1220
- const consumerUrl = consumerConfig?.services[serviceName]?.url;
1221
- if (consumerUrl)
1222
- return consumerUrl;
1223
- }
1224
- // 2. Check core config
1225
- const coreDir = getCoreConfigDir();
1226
- const coreConfig = loadConfig(coreDir);
1227
- const coreUrl = coreConfig?.services[serviceName]?.url;
1228
- if (coreUrl)
1229
- return coreUrl;
1230
- // 3. Fall back to port constants
1231
- const port = DEFAULT_PORTS[serviceName];
1232
- if (port !== undefined) {
1233
- return `http://127.0.0.1:${String(port)}`;
1133
+ function copyTemplates(coreConfigDir) {
1134
+ const contentDir = getContentDir();
1135
+ if (!contentDir)
1136
+ return;
1137
+ const sourceDir = join(contentDir, 'templates');
1138
+ if (!existsSync(sourceDir))
1139
+ return;
1140
+ const destDir = join(coreConfigDir, TEMPLATES_DIR);
1141
+ if (!existsSync(destDir)) {
1142
+ mkdirSync(destDir, { recursive: true });
1234
1143
  }
1235
- throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
1144
+ cpSync(sourceDir, destDir, { recursive: true });
1236
1145
  }
1237
-
1238
- /**
1239
- * HTTP health probing for Jeeves platform services.
1240
- *
1241
- * @remarks
1242
- * Probes service ports for health endpoints (HTTP GET to /status or /health).
1243
- * Returns structured health data for rendering into TOOLS.md Platform section.
1244
- */
1245
1146
  /**
1246
- * Extract port number from a URL string.
1147
+ * Render the Platform template using simple string replacement.
1247
1148
  *
1248
- * @param url - Service URL.
1249
- * @returns Port number.
1149
+ * @param templatePath - Path to the templates directory.
1150
+ * @returns Rendered platform content string.
1250
1151
  */
1251
- function extractPort(url) {
1252
- try {
1253
- const parsed = new URL(url);
1254
- return parsed.port ? parseInt(parsed.port, 10) : 80;
1255
- }
1256
- catch {
1257
- return 0;
1152
+ function renderPlatformTemplate(templatePath) {
1153
+ const templatesAvailable = existsSync(templatePath);
1154
+ let content = toolsPlatformTemplate;
1155
+ // Handle <!-- IF_TEMPLATES --> ... <!-- ELSE_TEMPLATES --> ... <!-- ENDIF_TEMPLATES --> block
1156
+ const ifRegex = /<!-- IF_TEMPLATES -->([\s\S]*?)<!-- ELSE_TEMPLATES -->([\s\S]*?)<!-- ENDIF_TEMPLATES -->/;
1157
+ const match = ifRegex.exec(content);
1158
+ if (match) {
1159
+ content = content.replace(match[0], templatesAvailable ? match[1] : match[2]);
1258
1160
  }
1161
+ // Replace __TEMPLATE_PATH__ with the actual path
1162
+ content = content.replace(/__TEMPLATE_PATH__/g, templatePath);
1163
+ return content;
1259
1164
  }
1260
1165
  /**
1261
- * Probe a single service for health.
1166
+ * Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
1262
1167
  *
1263
- * @param serviceName - The service name (e.g., 'server', 'watcher').
1264
- * @param consumerName - Optional consumer name for URL resolution.
1265
- * @param timeoutMs - Request timeout in milliseconds (default 3000).
1266
- * @returns Probe result.
1168
+ * @param options - Configuration for the refresh cycle.
1267
1169
  */
1268
- async function probeService(serviceName, consumerName, timeoutMs = 3000) {
1269
- const url = getServiceUrl(serviceName, consumerName);
1270
- const port = extractPort(url);
1271
- const endpoints = ['/status', '/health'];
1272
- for (const endpoint of endpoints) {
1273
- try {
1274
- const controller = new AbortController();
1275
- const timeout = setTimeout(() => {
1276
- controller.abort();
1277
- }, timeoutMs);
1278
- const response = await fetch(`${url}${endpoint}`, {
1279
- signal: controller.signal,
1280
- });
1281
- clearTimeout(timeout);
1282
- if (response.ok) {
1283
- let version;
1284
- try {
1285
- const body = await response.json();
1286
- if (typeof body === 'object' &&
1287
- body !== null &&
1288
- 'version' in body &&
1289
- typeof body['version'] === 'string') {
1290
- version = body['version'];
1291
- }
1292
- }
1293
- catch {
1294
- // Non-JSON response is fine — we just don't get version info
1295
- }
1296
- return { name: serviceName, port, healthy: true, version };
1297
- }
1298
- }
1299
- catch {
1300
- // Try next endpoint
1301
- }
1170
+ async function refreshPlatformContent(options) {
1171
+ const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, } = options;
1172
+ const workspacePath = getWorkspacePath();
1173
+ const coreConfigDir = getCoreConfigDir();
1174
+ // 1. Write calling component's version entry
1175
+ if (componentName) {
1176
+ writeComponentVersion(coreConfigDir, {
1177
+ componentName,
1178
+ pluginVersion: componentVersion,
1179
+ servicePackage,
1180
+ pluginPackage,
1181
+ });
1302
1182
  }
1303
- return { name: serviceName, port, healthy: false };
1304
- }
1305
- /**
1306
- * Probe all known Jeeves services for health.
1307
- *
1308
- * @param consumerName - Optional consumer name for URL resolution.
1309
- * @param timeoutMs - Request timeout in milliseconds (default 3000).
1310
- * @returns Array of probe results for all services.
1311
- */
1312
- async function probeAllServices(consumerName, timeoutMs = 3000) {
1313
- const serviceNames = Object.keys(DEFAULT_PORTS);
1314
- const results = await Promise.all(serviceNames.map((name) => probeService(name, consumerName, timeoutMs)));
1315
- return results;
1183
+ // 2. Render Platform template
1184
+ const templatePath = join(coreConfigDir, TEMPLATES_DIR);
1185
+ const platformContent = renderPlatformTemplate(templatePath);
1186
+ // 3. Write TOOLS.md Platform section
1187
+ const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
1188
+ await updateManagedSection(toolsPath, platformContent, {
1189
+ mode: 'section',
1190
+ sectionId: 'Platform',
1191
+ markers: TOOLS_MARKERS,
1192
+ coreVersion,
1193
+ stalenessThresholdMs,
1194
+ });
1195
+ // 4. Write SOUL.md managed block
1196
+ const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
1197
+ await updateManagedSection(soulPath, soulSectionContent, {
1198
+ mode: 'block',
1199
+ markers: SOUL_MARKERS,
1200
+ coreVersion,
1201
+ stalenessThresholdMs,
1202
+ });
1203
+ // 5. Write AGENTS.md managed block
1204
+ const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
1205
+ await updateManagedSection(agentsPath, agentsSectionContent, {
1206
+ mode: 'block',
1207
+ markers: AGENTS_MARKERS,
1208
+ coreVersion,
1209
+ stalenessThresholdMs,
1210
+ });
1211
+ // 6. Copy templates to config dir
1212
+ copyTemplates(coreConfigDir);
1316
1213
  }
1317
1214
 
1318
1215
  /**
1319
- * Registry version cache for npm package update awareness.
1216
+ * Timer-based orchestrator for managed content writing.
1320
1217
  *
1321
1218
  * @remarks
1322
- * Caches the latest npm registry version in a local JSON file
1323
- * to avoid expensive `npm view` calls on every refresh cycle.
1219
+ * `ComponentWriter` manages a component's TOOLS.md section writes
1220
+ * and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
1221
+ * on a configurable prime-interval timer cycle.
1324
1222
  */
1325
1223
  /**
1326
- * Check the npm registry for the latest version of a package.
1327
- *
1328
- * @param packageName - The npm package name (e.g., '\@karmaniverous/jeeves').
1329
- * @param cacheDir - Directory to store the cache file.
1330
- * @param ttlSeconds - Cache TTL in seconds (default 3600).
1331
- * @returns The latest version string, or undefined if the check fails.
1332
- */
1333
- function checkRegistryVersion(packageName, cacheDir, ttlSeconds = 3600) {
1334
- const cachePath = join(cacheDir, REGISTRY_CACHE_FILE);
1335
- // Check cache first
1336
- if (existsSync(cachePath)) {
1337
- try {
1338
- const raw = readFileSync(cachePath, 'utf-8');
1339
- const entry = JSON.parse(raw);
1340
- const age = Date.now() - new Date(entry.checkedAt).getTime();
1341
- if (age < ttlSeconds * 1000) {
1342
- return entry.version;
1343
- }
1344
- }
1345
- catch {
1346
- // Cache corrupt — proceed with fresh check
1347
- }
1348
- }
1349
- // Query npm registry
1350
- try {
1351
- const result = execSync(`npm view ${packageName} version`, {
1352
- encoding: 'utf-8',
1353
- timeout: 15_000,
1354
- stdio: ['pipe', 'pipe', 'pipe'],
1355
- }).trim();
1356
- if (!result)
1357
- return undefined;
1358
- // Write cache
1359
- if (!existsSync(cacheDir)) {
1360
- mkdirSync(cacheDir, { recursive: true });
1361
- }
1362
- const entry = {
1363
- version: result,
1364
- checkedAt: new Date().toISOString(),
1365
- };
1366
- writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
1367
- return result;
1368
- }
1369
- catch {
1370
- return undefined;
1371
- }
1372
- }
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
-
1431
- /**
1432
- * Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
1433
- *
1434
- * @remarks
1435
- * Called by `ComponentWriter` on each cycle. Not directly exposed to components.
1436
- * Probes service ports for health, reads content files from the package's
1437
- * `content/` directory, renders the Platform template with live service data,
1438
- * and writes managed sections using `updateManagedSection`.
1439
- */
1440
- /**
1441
- * Resolve the package's content directory for template file copying.
1442
- *
1443
- * @remarks
1444
- * Templates are actual files that need to be copied to the config directory.
1445
- * This only works when core is in `node_modules` (CLI install, service).
1446
- * When bundled into a consumer plugin, returns undefined and template
1447
- * copying is skipped (templates are seeded by `jeeves install`, not plugins).
1448
- *
1449
- * Content `.md` files (soul, agents, platform template) are inlined at
1450
- * build time via the rollup md plugin and imported as string literals.
1451
- * They do not use this function.
1452
- *
1453
- * @returns Absolute path to the content/ directory, or undefined.
1454
- */
1455
- function getContentDir() {
1456
- const pkgDir = packageDirectorySync({
1457
- cwd: fileURLToPath(import.meta.url),
1458
- });
1459
- if (!pkgDir)
1460
- return undefined;
1461
- const dir = join(pkgDir, 'content');
1462
- return existsSync(dir) ? dir : undefined;
1463
- }
1464
- /**
1465
- * Copy templates from content/templates/ to the core config directory.
1466
- *
1467
- * @param coreConfigDir - Core config directory path.
1468
- */
1469
- function copyTemplates(coreConfigDir) {
1470
- const contentDir = getContentDir();
1471
- if (!contentDir)
1472
- return;
1473
- const sourceDir = join(contentDir, 'templates');
1474
- if (!existsSync(sourceDir))
1475
- return;
1476
- const destDir = join(coreConfigDir, TEMPLATES_DIR);
1477
- if (!existsSync(destDir)) {
1478
- mkdirSync(destDir, { recursive: true });
1479
- }
1480
- cpSync(sourceDir, destDir, { recursive: true });
1481
- }
1482
- /** Whether Handlebars helpers have been registered. */
1483
- let helpersRegistered = false;
1484
- /** Register Handlebars helpers used in the Platform template. */
1485
- function registerHelpers() {
1486
- if (helpersRegistered)
1487
- return;
1488
- helpersRegistered = true;
1489
- Handlebars.registerHelper('gt', (a, b) => typeof a === 'number' && typeof b === 'number' && a > b);
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
- }
1506
- /**
1507
- * Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
1508
- *
1509
- * @param options - Configuration for the refresh cycle.
1510
- */
1511
- async function refreshPlatformContent(options) {
1512
- const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, probeTimeoutMs = 3000, skipRegistryCheck = false, } = options;
1513
- const workspacePath = getWorkspacePath();
1514
- const coreConfigDir = getCoreConfigDir();
1515
- // 1. Probe all services
1516
- const probeResults = await probeAllServices(undefined, probeTimeoutMs);
1517
- // 2. Write calling component's version entry (with serviceVersion from probe)
1518
- if (componentName) {
1519
- const callerProbe = probeResults.find((r) => r.name === componentName);
1520
- writeComponentVersion(coreConfigDir, {
1521
- componentName,
1522
- serviceVersion: callerProbe?.version,
1523
- pluginVersion: componentVersion,
1524
- servicePackage,
1525
- pluginPackage,
1526
- });
1527
- }
1528
- // 3. Read all component versions from the shared state file
1529
- const componentVersions = readComponentVersions(coreConfigDir);
1530
- // 4. Build enriched service rows with registry checks
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
1544
- const templatePath = join(coreConfigDir, TEMPLATES_DIR);
1545
- registerHelpers();
1546
- const template = Handlebars.compile(toolsPlatformTemplate);
1547
- const templateData = {
1548
- services: serviceRows,
1549
- unhealthyServices: serviceRows.filter((r) => !r.healthy),
1550
- coreVersion,
1551
- availableCoreVersion,
1552
- templatesAvailable: existsSync(templatePath),
1553
- templatePath,
1554
- };
1555
- const platformContent = template(templateData);
1556
- // 6. Write TOOLS.md Platform section
1557
- const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
1558
- await updateManagedSection(toolsPath, platformContent, {
1559
- mode: 'section',
1560
- sectionId: 'Platform',
1561
- markers: TOOLS_MARKERS,
1562
- coreVersion,
1563
- stalenessThresholdMs,
1564
- });
1565
- // 7. Write SOUL.md managed block
1566
- const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
1567
- await updateManagedSection(soulPath, soulSectionContent, {
1568
- mode: 'block',
1569
- markers: SOUL_MARKERS,
1570
- coreVersion,
1571
- stalenessThresholdMs,
1572
- });
1573
- // 8. Write AGENTS.md managed block
1574
- const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
1575
- await updateManagedSection(agentsPath, agentsSectionContent, {
1576
- mode: 'block',
1577
- markers: AGENTS_MARKERS,
1578
- coreVersion,
1579
- stalenessThresholdMs,
1580
- });
1581
- // 9. Copy templates to config dir
1582
- copyTemplates(coreConfigDir);
1583
- }
1584
-
1585
- /**
1586
- * Timer-based orchestrator for managed content writing.
1587
- *
1588
- * @remarks
1589
- * `ComponentWriter` manages a component's TOOLS.md section writes
1590
- * and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
1591
- * on a configurable prime-interval timer cycle.
1592
- */
1593
- /**
1594
- * Orchestrates managed content writing for a single Jeeves component.
1224
+ * Orchestrates managed content writing for a single Jeeves component.
1595
1225
  *
1596
1226
  * @remarks
1597
1227
  * Created via `createComponentWriter()`. Manages a timer that fires
@@ -1602,12 +1232,10 @@ class ComponentWriter {
1602
1232
  timer;
1603
1233
  component;
1604
1234
  configDir;
1605
- probeTimeoutMs;
1606
1235
  /** @internal */
1607
- constructor(component, probeTimeoutMs = 3000) {
1236
+ constructor(component) {
1608
1237
  this.component = component;
1609
1238
  this.configDir = getComponentConfigDir(component.name);
1610
- this.probeTimeoutMs = probeTimeoutMs;
1611
1239
  }
1612
1240
  /** The component's config directory path. */
1613
1241
  get componentConfigDir() {
@@ -1658,16 +1286,12 @@ class ComponentWriter {
1658
1286
  coreVersion: CORE_VERSION,
1659
1287
  });
1660
1288
  // 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.
1663
1289
  await refreshPlatformContent({
1664
1290
  coreVersion: CORE_VERSION,
1665
1291
  componentName: this.component.name,
1666
1292
  componentVersion: this.component.version,
1667
1293
  servicePackage: this.component.servicePackage,
1668
1294
  pluginPackage: this.component.pluginPackage,
1669
- skipRegistryCheck: false,
1670
- probeTimeoutMs: this.probeTimeoutMs,
1671
1295
  });
1672
1296
  }
1673
1297
  catch (err) {
@@ -1804,64 +1428,211 @@ function validateDescriptor(input) {
1804
1428
  * Create a ComponentWriter for a validated component descriptor.
1805
1429
  *
1806
1430
  * @param component - The component descriptor to validate and wrap.
1807
- * @param options - Optional configuration.
1808
1431
  * @returns A new `ComponentWriter` instance.
1809
1432
  * @throws Error if the component descriptor is invalid.
1810
1433
  */
1811
- function createComponentWriter(component, options) {
1434
+ function createComponentWriter(component) {
1812
1435
  validateDescriptor(component);
1813
- return new ComponentWriter(component, options?.probeTimeoutMs);
1436
+ return new ComponentWriter(component);
1814
1437
  }
1815
1438
 
1816
1439
  /**
1817
- * Plugin resolution helpers for the OpenClaw plugin SDK.
1440
+ * Core configuration schema and resolution.
1818
1441
  *
1819
1442
  * @remarks
1820
- * Provides workspace path resolution and plugin setting resolution
1821
- * with a standard three-step fallback chain:
1822
- * plugin config → environment variable → default value.
1443
+ * Core config lives at `{configRoot}/jeeves-core/config.json`.
1444
+ * Config resolution order:
1445
+ * 1. Component's own config file
1446
+ * 2. Core config file
1447
+ * 3. Hardcoded library defaults
1823
1448
  */
1449
+ /** Zod schema for a service entry in core config. */
1450
+ const serviceEntrySchema = z.object({
1451
+ /** Service URL (must be a valid URL). */
1452
+ url: z.string().url().describe('Service URL'),
1453
+ });
1454
+ /** Zod schema for the core config file. */
1455
+ const coreConfigSchema = z.object({
1456
+ /** JSON Schema pointer for IDE autocomplete. */
1457
+ $schema: z.string().optional().describe('JSON Schema pointer'),
1458
+ /** Owner identity keys (canonical identityLinks references). */
1459
+ owners: z.array(z.string()).default([]).describe('Owner identity keys'),
1460
+ /** Service URL overrides keyed by service name. */
1461
+ services: z
1462
+ .record(z.string(), serviceEntrySchema)
1463
+ .default({})
1464
+ .describe('Service URL overrides'),
1465
+ /** Registry cache configuration. */
1466
+ registryCache: z
1467
+ .object({
1468
+ /** Cache TTL in seconds for npm registry queries. */
1469
+ ttlSeconds: z
1470
+ .number()
1471
+ .int()
1472
+ .positive()
1473
+ .default(3600)
1474
+ .describe('Cache TTL in seconds'),
1475
+ })
1476
+ .default({})
1477
+ .describe('Registry cache settings'),
1478
+ });
1824
1479
  /**
1825
- * Resolve the workspace root from the OpenClaw plugin API.
1480
+ * Generate a JSON Schema from the Zod schema for `$schema` pointer support.
1481
+ *
1482
+ * @returns A JSON Schema object.
1483
+ */
1484
+ function generateJsonSchema() {
1485
+ return {
1486
+ $schema: 'http://json-schema.org/draft-07/schema#',
1487
+ title: 'Jeeves Core Configuration',
1488
+ type: 'object',
1489
+ properties: {
1490
+ $schema: { type: 'string' },
1491
+ owners: {
1492
+ type: 'array',
1493
+ items: { type: 'string' },
1494
+ default: [],
1495
+ },
1496
+ services: {
1497
+ type: 'object',
1498
+ additionalProperties: {
1499
+ type: 'object',
1500
+ properties: {
1501
+ url: { type: 'string', format: 'uri' },
1502
+ },
1503
+ required: ['url'],
1504
+ },
1505
+ default: {},
1506
+ },
1507
+ registryCache: {
1508
+ type: 'object',
1509
+ properties: {
1510
+ ttlSeconds: {
1511
+ type: 'integer',
1512
+ minimum: 1,
1513
+ default: 3600,
1514
+ },
1515
+ },
1516
+ default: {},
1517
+ },
1518
+ },
1519
+ };
1520
+ }
1521
+ /**
1522
+ * Load and parse a config file, returning undefined if missing or invalid.
1523
+ *
1524
+ * @param configDir - Directory containing config.json.
1525
+ * @returns Parsed config or undefined.
1526
+ */
1527
+ function loadConfig(configDir) {
1528
+ const configPath = join(configDir, CONFIG_FILE);
1529
+ if (!existsSync(configPath))
1530
+ return undefined;
1531
+ try {
1532
+ const raw = readFileSync(configPath, 'utf-8');
1533
+ const parsed = JSON.parse(raw);
1534
+ return coreConfigSchema.parse(parsed);
1535
+ }
1536
+ catch {
1537
+ return undefined;
1538
+ }
1539
+ }
1540
+
1541
+ /**
1542
+ * Service URL resolution.
1826
1543
  *
1827
1544
  * @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
1545
+ * Resolves the URL for a named Jeeves service using the following
1546
+ * resolution order:
1547
+ * 1. Consumer's own component config
1548
+ * 2. Core config (`{configRoot}/jeeves-core/config.json`)
1549
+ * 3. Default port constants
1550
+ */
1551
+ /**
1552
+ * Resolve the URL for a named Jeeves service.
1832
1553
  *
1833
- * @param api - The plugin API object provided by the gateway.
1834
- * @returns Absolute path to the workspace root.
1554
+ * @param serviceName - The service name (e.g., 'watcher', 'runner').
1555
+ * @param consumerName - Optional consumer component name for config override.
1556
+ * @returns The resolved service URL.
1557
+ * @throws Error if `init()` has not been called or the service is unknown.
1835
1558
  */
1836
- function resolveWorkspacePath(api) {
1837
- const configured = api.config?.agents?.defaults?.workspace;
1838
- if (typeof configured === 'string' && configured.trim()) {
1839
- return configured;
1559
+ function getServiceUrl(serviceName, consumerName) {
1560
+ // 1. Check consumer's own config
1561
+ if (consumerName) {
1562
+ const consumerDir = getComponentConfigDir(consumerName);
1563
+ const consumerConfig = loadConfig(consumerDir);
1564
+ const consumerUrl = consumerConfig?.services[serviceName]?.url;
1565
+ if (consumerUrl)
1566
+ return consumerUrl;
1840
1567
  }
1841
- if (typeof api.resolvePath === 'function') {
1842
- return api.resolvePath('.');
1568
+ // 2. Check core config
1569
+ const coreDir = getCoreConfigDir();
1570
+ const coreConfig = loadConfig(coreDir);
1571
+ const coreUrl = coreConfig?.services[serviceName]?.url;
1572
+ if (coreUrl)
1573
+ return coreUrl;
1574
+ // 3. Fall back to port constants
1575
+ const port = DEFAULT_PORTS[serviceName];
1576
+ if (port !== undefined) {
1577
+ return `http://127.0.0.1:${String(port)}`;
1843
1578
  }
1844
- return process.cwd();
1579
+ throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
1845
1580
  }
1581
+
1846
1582
  /**
1847
- * Resolve a plugin setting via the standard three-step fallback chain:
1848
- * plugin config → environment variable → fallback value.
1583
+ * Registry version cache for npm package update awareness.
1849
1584
  *
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.
1585
+ * @remarks
1586
+ * Caches the latest npm registry version in a local JSON file
1587
+ * to avoid expensive `npm view` calls on every refresh cycle.
1856
1588
  */
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;
1589
+ /**
1590
+ * Check the npm registry for the latest version of a package.
1591
+ *
1592
+ * @param packageName - The npm package name (e.g., '\@karmaniverous/jeeves').
1593
+ * @param cacheDir - Directory to store the cache file.
1594
+ * @param ttlSeconds - Cache TTL in seconds (default 3600).
1595
+ * @returns The latest version string, or undefined if the check fails.
1596
+ */
1597
+ function checkRegistryVersion(packageName, cacheDir, ttlSeconds = 3600) {
1598
+ const cachePath = join(cacheDir, REGISTRY_CACHE_FILE);
1599
+ // Check cache first
1600
+ if (existsSync(cachePath)) {
1601
+ try {
1602
+ const raw = readFileSync(cachePath, 'utf-8');
1603
+ const entry = JSON.parse(raw);
1604
+ const age = Date.now() - new Date(entry.checkedAt).getTime();
1605
+ if (age < ttlSeconds * 1000) {
1606
+ return entry.version;
1607
+ }
1608
+ }
1609
+ catch {
1610
+ // Cache corrupt — proceed with fresh check
1611
+ }
1612
+ }
1613
+ // Query npm registry
1614
+ try {
1615
+ const result = execSync(`npm view ${packageName} version`, {
1616
+ encoding: 'utf-8',
1617
+ timeout: 15_000,
1618
+ stdio: ['pipe', 'pipe', 'pipe'],
1619
+ }).trim();
1620
+ if (!result)
1621
+ return undefined;
1622
+ // Write cache
1623
+ if (!existsSync(cacheDir)) {
1624
+ mkdirSync(cacheDir, { recursive: true });
1625
+ }
1626
+ const entry = {
1627
+ version: result,
1628
+ checkedAt: new Date().toISOString(),
1629
+ };
1630
+ writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
1631
+ return result;
1632
+ }
1633
+ catch {
1634
+ return undefined;
1635
+ }
1865
1636
  }
1866
1637
 
1867
1638
  /**
@@ -2007,8 +1778,6 @@ async function seedContent(options) {
2007
1778
  // Seed content via the same code path as writer cycles
2008
1779
  await refreshPlatformContent({
2009
1780
  coreVersion: options.coreVersion,
2010
- probeTimeoutMs: options.probeTimeoutMs ?? 3000,
2011
- skipRegistryCheck: options.skipRegistryCheck ?? true,
2012
1781
  });
2013
1782
  }
2014
1783
 
@@ -2019,6 +1788,26 @@ async function seedContent(options) {
2019
1788
  * Thin wrappers around `fetch` that throw on non-OK responses
2020
1789
  * and handle JSON serialisation/deserialisation.
2021
1790
  */
1791
+ /**
1792
+ * Fetch a URL with an automatic abort timeout.
1793
+ *
1794
+ * @param url - URL to fetch.
1795
+ * @param timeoutMs - Timeout in milliseconds before aborting.
1796
+ * @param init - Optional `fetch` init options.
1797
+ * @returns The fetch Response object.
1798
+ */
1799
+ async function fetchWithTimeout(url, timeoutMs, init) {
1800
+ const controller = new AbortController();
1801
+ const timeout = setTimeout(() => {
1802
+ controller.abort();
1803
+ }, timeoutMs);
1804
+ try {
1805
+ return await fetch(url, { ...init, signal: controller.signal });
1806
+ }
1807
+ finally {
1808
+ clearTimeout(timeout);
1809
+ }
1810
+ }
2022
1811
  /**
2023
1812
  * Fetch JSON from a URL, throwing on non-OK responses.
2024
1813
  *
@@ -2167,6 +1956,77 @@ function patchConfig(config, pluginId, mode) {
2167
1956
  return messages;
2168
1957
  }
2169
1958
 
1959
+ /**
1960
+ * Plugin resolution helpers for the OpenClaw plugin SDK.
1961
+ *
1962
+ * @remarks
1963
+ * Provides workspace path resolution and plugin setting resolution
1964
+ * with a standard three-step fallback chain:
1965
+ * plugin config → environment variable → default value.
1966
+ */
1967
+ /**
1968
+ * Resolve the workspace root from the OpenClaw plugin API.
1969
+ *
1970
+ * @remarks
1971
+ * Tries three sources in order:
1972
+ * 1. `api.config.agents.defaults.workspace` — explicit config
1973
+ * 2. `api.resolvePath('.')` — gateway-provided path resolver
1974
+ * 3. `process.cwd()` — last resort
1975
+ *
1976
+ * @param api - The plugin API object provided by the gateway.
1977
+ * @returns Absolute path to the workspace root.
1978
+ */
1979
+ function resolveWorkspacePath(api) {
1980
+ const configured = api.config?.agents?.defaults?.workspace;
1981
+ if (typeof configured === 'string' && configured.trim()) {
1982
+ return configured;
1983
+ }
1984
+ if (typeof api.resolvePath === 'function') {
1985
+ return api.resolvePath('.');
1986
+ }
1987
+ return process.cwd();
1988
+ }
1989
+ /**
1990
+ * Resolve a plugin setting via the standard three-step fallback chain:
1991
+ * plugin config → environment variable → fallback value.
1992
+ *
1993
+ * @param api - Plugin API object.
1994
+ * @param pluginId - Plugin identifier (e.g., 'jeeves-watcher-openclaw').
1995
+ * @param key - Config key within the plugin's config object.
1996
+ * @param envVar - Environment variable name.
1997
+ * @param fallback - Default value if neither source provides one.
1998
+ * @returns The resolved setting value.
1999
+ */
2000
+ function resolvePluginSetting(api, pluginId, key, envVar, fallback) {
2001
+ const fromPlugin = api.config?.plugins?.entries?.[pluginId]?.config?.[key];
2002
+ if (typeof fromPlugin === 'string')
2003
+ return fromPlugin;
2004
+ const fromEnv = process.env[envVar];
2005
+ if (fromEnv)
2006
+ return fromEnv;
2007
+ return fallback;
2008
+ }
2009
+ /**
2010
+ * Resolve an optional plugin setting via the two-step fallback chain:
2011
+ * plugin config → environment variable. Returns `undefined` if neither
2012
+ * source provides a value.
2013
+ *
2014
+ * @param api - Plugin API object.
2015
+ * @param pluginId - Plugin identifier (e.g., 'jeeves-watcher-openclaw').
2016
+ * @param key - Config key within the plugin's config object.
2017
+ * @param envVar - Environment variable name.
2018
+ * @returns The resolved setting value, or `undefined`.
2019
+ */
2020
+ function resolveOptionalPluginSetting(api, pluginId, key, envVar) {
2021
+ const fromPlugin = api.config?.plugins?.entries?.[pluginId]?.config?.[key];
2022
+ if (typeof fromPlugin === 'string')
2023
+ return fromPlugin;
2024
+ const fromEnv = process.env[envVar];
2025
+ if (fromEnv)
2026
+ return fromEnv;
2027
+ return undefined;
2028
+ }
2029
+
2170
2030
  /**
2171
2031
  * Tool result formatters for the OpenClaw plugin SDK.
2172
2032
  *
@@ -2236,4 +2096,4 @@ function connectionFail(error, baseUrl, pluginId) {
2236
2096
  return fail(error);
2237
2097
  }
2238
2098
 
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 };
2099
+ 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, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, getComponentConfigDir, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getServiceUrl, getWorkspacePath, init, jaccard, needsCleanup, ok, parseManaged, patchConfig, postJson, readComponentVersions, refreshPlatformContent, removeManagedSection, resetInit, resolveConfigPath, resolveOpenClawHome, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, seedContent, shingles, shouldWrite, updateManagedSection, withFileLock, writeComponentVersion };