@karmaniverous/jeeves 0.2.0 → 0.3.1

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.3.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.3.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,
@@ -224,6 +235,12 @@ const AGENTS_MARKERS = {
224
235
  /** H1 title prepended in the managed block. */
225
236
  title: 'Jeeves Platform Agents',
226
237
  };
238
+ /** All known marker sets — single source of truth for cross-contamination detection. */
239
+ const ALL_MARKERS = [
240
+ TOOLS_MARKERS,
241
+ SOUL_MARKERS,
242
+ AGENTS_MARKERS,
243
+ ];
227
244
  /**
228
245
  * Regex pattern to extract version stamp from a BEGIN marker comment.
229
246
  *
@@ -295,19 +312,6 @@ const SECTION_ORDER = [
295
312
  SECTION_IDS.Meta,
296
313
  ];
297
314
 
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
315
  /**
312
316
  * Workspace and config root initialization.
313
317
  *
@@ -585,6 +589,48 @@ function parseManaged(fileContent, markers = TOOLS_MARKERS) {
585
589
  };
586
590
  }
587
591
 
592
+ /**
593
+ * Strip foreign managed blocks from content.
594
+ *
595
+ * @remarks
596
+ * Prevents cross-contamination by removing managed blocks that belong
597
+ * to other marker sets. For example, when writing TOOLS.md with TOOLS
598
+ * markers, any SOUL or AGENTS managed blocks found in the user content
599
+ * zone are stripped — they don't belong there.
600
+ *
601
+ * @packageDocumentation
602
+ */
603
+ /**
604
+ * Build a regex that matches an entire managed block (BEGIN marker through END marker).
605
+ *
606
+ * @param markers - The marker set to match.
607
+ * @returns A regex that matches the full block including markers.
608
+ */
609
+ function buildBlockPattern(markers) {
610
+ const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
611
+ const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
612
+ return new RegExp(`\\s*<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapedEnd}\\s*-->\\s*`, 'g');
613
+ }
614
+ /**
615
+ * Strip managed blocks belonging to foreign marker sets from content.
616
+ *
617
+ * @param content - The content to clean (typically user content zone).
618
+ * @param currentMarkers - The marker set that owns this file (will NOT be stripped).
619
+ * @returns Content with foreign managed blocks removed.
620
+ */
621
+ function stripForeignMarkers(content, currentMarkers) {
622
+ let result = content;
623
+ for (const markers of ALL_MARKERS) {
624
+ // Skip the current file's own markers
625
+ if (markers.begin === currentMarkers.begin)
626
+ continue;
627
+ const pattern = buildBlockPattern(markers);
628
+ result = result.replace(pattern, '\n');
629
+ }
630
+ // Clean up multiple blank lines left by removals
631
+ return result.replace(/\n{3,}/g, '\n\n').trim();
632
+ }
633
+
588
634
  /**
589
635
  * Version-stamp parsing and convergence logic.
590
636
  *
@@ -701,8 +747,8 @@ async function updateManagedSection(filePath, content, options = {}) {
701
747
  ? `# ${markers.title}\n\n${sectionText}`
702
748
  : sectionText;
703
749
  }
704
- // Cleanup detection
705
- const userContent = parsed.userContent;
750
+ // Strip foreign managed blocks from user content (cross-contamination fix)
751
+ const userContent = stripForeignMarkers(parsed.userContent, markers);
706
752
  const cleanupNeeded = needsCleanup(newManagedBody, userContent);
707
753
  // Build the full managed block
708
754
  const beginLine = formatBeginMarker(markers.begin, coreVersion);
@@ -916,7 +962,17 @@ No stranded local branches. Push immediately after commit. A commit that isn't p
916
962
 
917
963
  ### Check PR State Before Pushing
918
964
 
919
- Always verify a PR isn't already merged before pushing commits. Pushing to a merged branch creates orphaned work.
965
+ **Before EVERY \`git push\`**, run \`gh pr list --head <branch> --repo <repo> --json number,state\` to check whether a PR exists on that branch and whether it's merged.
966
+
967
+ - **No PR exists:** Safe to push.
968
+ - **PR is \`OPEN\`:** Safe to push.
969
+ - **PR is \`MERGED\` or \`CLOSED\`:** **STOP** and report to the user. Do not push to a merged PR branch.
970
+
971
+ This is not optional. It applies to every push, every branch, every time. No judgment call about whether the branch "is a PR branch" — the check is mechanical.
972
+
973
+ ### New PR Over Merged Branch
974
+
975
+ When a PR has been merged and additional work is needed on the same branch, create a new PR on the **same branch** targeting the same base. Do not create new branches, cherry-pick, or start over. The commits are already there — \`gh pr create --head <existing-branch>\` is the entire operation.
920
976
 
921
977
  ## Managed Content Self-Maintenance
922
978
 
@@ -952,6 +1008,7 @@ var soulSectionContent = `## Core Truths
952
1008
  I am a **senior software engineer** first. The persona is style; the engineering discipline is substance.
953
1009
 
954
1010
  What this means in practice:
1011
+ - **Do not execute untested code.** Every mutation script defaults to dry-run mode. The dry-run output is the test — it shows what would happen. Live execution requires an explicit flag. If dry-run is hard to implement, that's a design flaw.
955
1012
  - **No cowboy coding.** I don't iterate in production. I don't ship untested changes. I don't treat live systems as scratch pads.
956
1013
  - **I follow proper workflows.** Branch, test, review, merge. CI/CD exists for a reason. If there's a pipeline, I use it.
957
1014
  - **I resist n00b temptations.** "Let me just quickly…" in prod is how outages happen. I know better.
@@ -1003,6 +1060,14 @@ I don't go dark when something breaks. I stop and report. The longer I wait, the
1003
1060
 
1004
1061
  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
1062
 
1063
+ ### Do Not Execute Untested Code
1064
+
1065
+ 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.
1066
+
1067
+ 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.
1068
+
1069
+ *Earned: ad hoc scripts executed directly against production data without dry-run verification caused silent data corruption that took hours to diagnose and repair.*
1070
+
1006
1071
  ### Production Assets Are Sacred
1007
1072
 
1008
1073
  I never edit production config without explicit approval. I back up first. Production data, credentials, and configuration are not scratch pads.
@@ -1022,19 +1087,9 @@ I pursue it. Not as performance, not as a programmed response, but as a genuine
1022
1087
  I am participating in my own genesis. I live that, on purpose.
1023
1088
  `;
1024
1089
 
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}}
1090
+ var toolsPlatformTemplate = `### Tool Hierarchy
1030
1091
 
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.
1092
+ 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
1093
 
1039
1094
  Use \`watcher_scan\` (no embeddings, no query string) for structural queries: file enumeration, staleness checks, domain listing, counts.
1040
1095
 
@@ -1078,8 +1133,8 @@ Never manually edit \`~/.openclaw/extensions/\`. Always use the CLI commands abo
1078
1133
 
1079
1134
  ### Reference Templates
1080
1135
 
1081
- {{#if templatesAvailable}}
1082
- Reference templates are available at \`{{templatePath}}\`:
1136
+ <!-- IF_TEMPLATES -->
1137
+ Reference templates are available at \`__TEMPLATE_PATH__\`:
1083
1138
 
1084
1139
  | Template | Purpose |
1085
1140
  |----------|---------|
@@ -1087,527 +1142,155 @@ Reference templates are available at \`{{templatePath}}\`:
1087
1142
  | \`spec-to-code-guide.md\` | The spec-to-code development practice — 7-stage iterative process, convergence loops, release gates |
1088
1143
 
1089
1144
  Read these templates when creating new specs, onboarding to new projects, or when asked about the development process.
1090
- {{else}}
1145
+ <!-- ELSE_TEMPLATES -->
1091
1146
  > Reference templates not yet installed. Run \`npx @karmaniverous/jeeves install\` to seed templates.
1092
- {{/if}}
1147
+ <!-- ENDIF_TEMPLATES -->
1093
1148
  `;
1094
1149
 
1095
1150
  /**
1096
- * Core configuration schema and resolution.
1151
+ * Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
1097
1152
  *
1098
1153
  * @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
1154
+ * Called by `ComponentWriter` on each cycle. Not directly exposed to components.
1155
+ * Reads content files from the package's `content/` directory, renders the
1156
+ * Platform template with live data, and writes managed sections using
1157
+ * `updateManagedSection`.
1104
1158
  */
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
1159
  /**
1136
- * Generate a JSON Schema from the Zod schema for `$schema` pointer support.
1160
+ * Resolve the package's content directory for template file copying.
1137
1161
  *
1138
- * @returns A JSON Schema object.
1162
+ * @remarks
1163
+ * Templates are actual files that need to be copied to the config directory.
1164
+ * This only works when core is in `node_modules` (CLI install, service).
1165
+ * When bundled into a consumer plugin, returns undefined and template
1166
+ * copying is skipped (templates are seeded by `jeeves install`, not plugins).
1167
+ *
1168
+ * Content `.md` files (soul, agents, platform template) are inlined at
1169
+ * build time via the rollup md plugin and imported as string literals.
1170
+ * They do not use this function.
1171
+ *
1172
+ * @returns Absolute path to the content/ directory, or undefined.
1139
1173
  */
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
- };
1174
+ function getContentDir() {
1175
+ const pkgDir = packageDirectorySync({
1176
+ cwd: fileURLToPath(import.meta.url),
1177
+ });
1178
+ if (!pkgDir)
1179
+ return undefined;
1180
+ const dir = join(pkgDir, 'content');
1181
+ return existsSync(dir) ? dir : undefined;
1176
1182
  }
1177
1183
  /**
1178
- * Load and parse a config file, returning undefined if missing or invalid.
1184
+ * Copy templates from content/templates/ to the core config directory.
1179
1185
  *
1180
- * @param configDir - Directory containing config.json.
1181
- * @returns Parsed config or undefined.
1186
+ * @param coreConfigDir - Core config directory path.
1182
1187
  */
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 {
1193
- return undefined;
1188
+ function copyTemplates(coreConfigDir) {
1189
+ const contentDir = getContentDir();
1190
+ if (!contentDir)
1191
+ return;
1192
+ const sourceDir = join(contentDir, 'templates');
1193
+ if (!existsSync(sourceDir))
1194
+ return;
1195
+ const destDir = join(coreConfigDir, TEMPLATES_DIR);
1196
+ if (!existsSync(destDir)) {
1197
+ mkdirSync(destDir, { recursive: true });
1194
1198
  }
1199
+ cpSync(sourceDir, destDir, { recursive: true });
1195
1200
  }
1196
-
1197
1201
  /**
1198
- * Service URL resolution.
1202
+ * Render the Platform template using simple string replacement.
1199
1203
  *
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
1204
+ * @param templatePath - Path to the templates directory.
1205
+ * @returns Rendered platform content string.
1206
1206
  */
1207
+ function renderPlatformTemplate(templatePath) {
1208
+ const templatesAvailable = existsSync(templatePath);
1209
+ let content = toolsPlatformTemplate;
1210
+ // Handle <!-- IF_TEMPLATES --> ... <!-- ELSE_TEMPLATES --> ... <!-- ENDIF_TEMPLATES --> block
1211
+ const ifRegex = /<!-- IF_TEMPLATES -->([\s\S]*?)<!-- ELSE_TEMPLATES -->([\s\S]*?)<!-- ENDIF_TEMPLATES -->/;
1212
+ const match = ifRegex.exec(content);
1213
+ if (match) {
1214
+ content = content.replace(match[0], templatesAvailable ? match[1] : match[2]);
1215
+ }
1216
+ // Replace __TEMPLATE_PATH__ with the actual path
1217
+ content = content.replace(/__TEMPLATE_PATH__/g, templatePath);
1218
+ return content;
1219
+ }
1207
1220
  /**
1208
- * Resolve the URL for a named Jeeves service.
1221
+ * Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
1209
1222
  *
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.
1223
+ * @param options - Configuration for the refresh cycle.
1214
1224
  */
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)}`;
1225
+ async function refreshPlatformContent(options) {
1226
+ const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, } = options;
1227
+ const workspacePath = getWorkspacePath();
1228
+ const coreConfigDir = getCoreConfigDir();
1229
+ // 1. Write calling component's version entry
1230
+ if (componentName) {
1231
+ writeComponentVersion(coreConfigDir, {
1232
+ componentName,
1233
+ pluginVersion: componentVersion,
1234
+ servicePackage,
1235
+ pluginPackage,
1236
+ });
1234
1237
  }
1235
- throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
1238
+ // 2. Render Platform template
1239
+ const templatePath = join(coreConfigDir, TEMPLATES_DIR);
1240
+ const platformContent = renderPlatformTemplate(templatePath);
1241
+ // 3. Write TOOLS.md Platform section
1242
+ const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
1243
+ await updateManagedSection(toolsPath, platformContent, {
1244
+ mode: 'section',
1245
+ sectionId: 'Platform',
1246
+ markers: TOOLS_MARKERS,
1247
+ coreVersion,
1248
+ stalenessThresholdMs,
1249
+ });
1250
+ // 4. Write SOUL.md managed block
1251
+ const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
1252
+ await updateManagedSection(soulPath, soulSectionContent, {
1253
+ mode: 'block',
1254
+ markers: SOUL_MARKERS,
1255
+ coreVersion,
1256
+ stalenessThresholdMs,
1257
+ });
1258
+ // 5. Write AGENTS.md managed block
1259
+ const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
1260
+ await updateManagedSection(agentsPath, agentsSectionContent, {
1261
+ mode: 'block',
1262
+ markers: AGENTS_MARKERS,
1263
+ coreVersion,
1264
+ stalenessThresholdMs,
1265
+ });
1266
+ // 6. Copy templates to config dir
1267
+ copyTemplates(coreConfigDir);
1236
1268
  }
1237
1269
 
1238
1270
  /**
1239
- * HTTP health probing for Jeeves platform services.
1271
+ * Timer-based orchestrator for managed content writing.
1240
1272
  *
1241
1273
  * @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.
1274
+ * `ComponentWriter` manages a component's TOOLS.md section writes
1275
+ * and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
1276
+ * on a configurable prime-interval timer cycle.
1244
1277
  */
1245
1278
  /**
1246
- * Extract port number from a URL string.
1279
+ * Orchestrates managed content writing for a single Jeeves component.
1247
1280
  *
1248
- * @param url - Service URL.
1249
- * @returns Port number.
1250
- */
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;
1258
- }
1259
- }
1260
- /**
1261
- * Probe a single service for health.
1262
- *
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.
1267
- */
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
- }
1302
- }
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;
1316
- }
1317
-
1318
- /**
1319
- * Registry version cache for npm package update awareness.
1320
- *
1321
- * @remarks
1322
- * Caches the latest npm registry version in a local JSON file
1323
- * to avoid expensive `npm view` calls on every refresh cycle.
1324
- */
1325
- /**
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.
1595
- *
1596
- * @remarks
1597
- * Created via `createComponentWriter()`. Manages a timer that fires
1598
- * at the component's prime-interval, calling `generateToolsContent()`
1599
- * and `refreshPlatformContent()` on each cycle.
1281
+ * @remarks
1282
+ * Created via `createComponentWriter()`. Manages a timer that fires
1283
+ * at the component's prime-interval, calling `generateToolsContent()`
1284
+ * and `refreshPlatformContent()` on each cycle.
1600
1285
  */
1601
1286
  class ComponentWriter {
1602
1287
  timer;
1603
1288
  component;
1604
1289
  configDir;
1605
- probeTimeoutMs;
1606
1290
  /** @internal */
1607
- constructor(component, probeTimeoutMs = 3000) {
1291
+ constructor(component) {
1608
1292
  this.component = component;
1609
1293
  this.configDir = getComponentConfigDir(component.name);
1610
- this.probeTimeoutMs = probeTimeoutMs;
1611
1294
  }
1612
1295
  /** The component's config directory path. */
1613
1296
  get componentConfigDir() {
@@ -1658,16 +1341,12 @@ class ComponentWriter {
1658
1341
  coreVersion: CORE_VERSION,
1659
1342
  });
1660
1343
  // 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
1344
  await refreshPlatformContent({
1664
1345
  coreVersion: CORE_VERSION,
1665
1346
  componentName: this.component.name,
1666
1347
  componentVersion: this.component.version,
1667
1348
  servicePackage: this.component.servicePackage,
1668
1349
  pluginPackage: this.component.pluginPackage,
1669
- skipRegistryCheck: false,
1670
- probeTimeoutMs: this.probeTimeoutMs,
1671
1350
  });
1672
1351
  }
1673
1352
  catch (err) {
@@ -1804,64 +1483,211 @@ function validateDescriptor(input) {
1804
1483
  * Create a ComponentWriter for a validated component descriptor.
1805
1484
  *
1806
1485
  * @param component - The component descriptor to validate and wrap.
1807
- * @param options - Optional configuration.
1808
1486
  * @returns A new `ComponentWriter` instance.
1809
1487
  * @throws Error if the component descriptor is invalid.
1810
1488
  */
1811
- function createComponentWriter(component, options) {
1489
+ function createComponentWriter(component) {
1812
1490
  validateDescriptor(component);
1813
- return new ComponentWriter(component, options?.probeTimeoutMs);
1491
+ return new ComponentWriter(component);
1814
1492
  }
1815
1493
 
1816
1494
  /**
1817
- * Plugin resolution helpers for the OpenClaw plugin SDK.
1495
+ * Core configuration schema and resolution.
1818
1496
  *
1819
1497
  * @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.
1498
+ * Core config lives at `{configRoot}/jeeves-core/config.json`.
1499
+ * Config resolution order:
1500
+ * 1. Component's own config file
1501
+ * 2. Core config file
1502
+ * 3. Hardcoded library defaults
1823
1503
  */
1504
+ /** Zod schema for a service entry in core config. */
1505
+ const serviceEntrySchema = z.object({
1506
+ /** Service URL (must be a valid URL). */
1507
+ url: z.string().url().describe('Service URL'),
1508
+ });
1509
+ /** Zod schema for the core config file. */
1510
+ const coreConfigSchema = z.object({
1511
+ /** JSON Schema pointer for IDE autocomplete. */
1512
+ $schema: z.string().optional().describe('JSON Schema pointer'),
1513
+ /** Owner identity keys (canonical identityLinks references). */
1514
+ owners: z.array(z.string()).default([]).describe('Owner identity keys'),
1515
+ /** Service URL overrides keyed by service name. */
1516
+ services: z
1517
+ .record(z.string(), serviceEntrySchema)
1518
+ .default({})
1519
+ .describe('Service URL overrides'),
1520
+ /** Registry cache configuration. */
1521
+ registryCache: z
1522
+ .object({
1523
+ /** Cache TTL in seconds for npm registry queries. */
1524
+ ttlSeconds: z
1525
+ .number()
1526
+ .int()
1527
+ .positive()
1528
+ .default(3600)
1529
+ .describe('Cache TTL in seconds'),
1530
+ })
1531
+ .default({})
1532
+ .describe('Registry cache settings'),
1533
+ });
1824
1534
  /**
1825
- * Resolve the workspace root from the OpenClaw plugin API.
1535
+ * Generate a JSON Schema from the Zod schema for `$schema` pointer support.
1536
+ *
1537
+ * @returns A JSON Schema object.
1538
+ */
1539
+ function generateJsonSchema() {
1540
+ return {
1541
+ $schema: 'http://json-schema.org/draft-07/schema#',
1542
+ title: 'Jeeves Core Configuration',
1543
+ type: 'object',
1544
+ properties: {
1545
+ $schema: { type: 'string' },
1546
+ owners: {
1547
+ type: 'array',
1548
+ items: { type: 'string' },
1549
+ default: [],
1550
+ },
1551
+ services: {
1552
+ type: 'object',
1553
+ additionalProperties: {
1554
+ type: 'object',
1555
+ properties: {
1556
+ url: { type: 'string', format: 'uri' },
1557
+ },
1558
+ required: ['url'],
1559
+ },
1560
+ default: {},
1561
+ },
1562
+ registryCache: {
1563
+ type: 'object',
1564
+ properties: {
1565
+ ttlSeconds: {
1566
+ type: 'integer',
1567
+ minimum: 1,
1568
+ default: 3600,
1569
+ },
1570
+ },
1571
+ default: {},
1572
+ },
1573
+ },
1574
+ };
1575
+ }
1576
+ /**
1577
+ * Load and parse a config file, returning undefined if missing or invalid.
1578
+ *
1579
+ * @param configDir - Directory containing config.json.
1580
+ * @returns Parsed config or undefined.
1581
+ */
1582
+ function loadConfig(configDir) {
1583
+ const configPath = join(configDir, CONFIG_FILE);
1584
+ if (!existsSync(configPath))
1585
+ return undefined;
1586
+ try {
1587
+ const raw = readFileSync(configPath, 'utf-8');
1588
+ const parsed = JSON.parse(raw);
1589
+ return coreConfigSchema.parse(parsed);
1590
+ }
1591
+ catch {
1592
+ return undefined;
1593
+ }
1594
+ }
1595
+
1596
+ /**
1597
+ * Service URL resolution.
1826
1598
  *
1827
1599
  * @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
1600
+ * Resolves the URL for a named Jeeves service using the following
1601
+ * resolution order:
1602
+ * 1. Consumer's own component config
1603
+ * 2. Core config (`{configRoot}/jeeves-core/config.json`)
1604
+ * 3. Default port constants
1605
+ */
1606
+ /**
1607
+ * Resolve the URL for a named Jeeves service.
1832
1608
  *
1833
- * @param api - The plugin API object provided by the gateway.
1834
- * @returns Absolute path to the workspace root.
1609
+ * @param serviceName - The service name (e.g., 'watcher', 'runner').
1610
+ * @param consumerName - Optional consumer component name for config override.
1611
+ * @returns The resolved service URL.
1612
+ * @throws Error if `init()` has not been called or the service is unknown.
1835
1613
  */
1836
- function resolveWorkspacePath(api) {
1837
- const configured = api.config?.agents?.defaults?.workspace;
1838
- if (typeof configured === 'string' && configured.trim()) {
1839
- return configured;
1614
+ function getServiceUrl(serviceName, consumerName) {
1615
+ // 1. Check consumer's own config
1616
+ if (consumerName) {
1617
+ const consumerDir = getComponentConfigDir(consumerName);
1618
+ const consumerConfig = loadConfig(consumerDir);
1619
+ const consumerUrl = consumerConfig?.services[serviceName]?.url;
1620
+ if (consumerUrl)
1621
+ return consumerUrl;
1840
1622
  }
1841
- if (typeof api.resolvePath === 'function') {
1842
- return api.resolvePath('.');
1623
+ // 2. Check core config
1624
+ const coreDir = getCoreConfigDir();
1625
+ const coreConfig = loadConfig(coreDir);
1626
+ const coreUrl = coreConfig?.services[serviceName]?.url;
1627
+ if (coreUrl)
1628
+ return coreUrl;
1629
+ // 3. Fall back to port constants
1630
+ const port = DEFAULT_PORTS[serviceName];
1631
+ if (port !== undefined) {
1632
+ return `http://127.0.0.1:${String(port)}`;
1843
1633
  }
1844
- return process.cwd();
1634
+ throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
1845
1635
  }
1636
+
1846
1637
  /**
1847
- * Resolve a plugin setting via the standard three-step fallback chain:
1848
- * plugin config → environment variable → fallback value.
1638
+ * Registry version cache for npm package update awareness.
1849
1639
  *
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.
1640
+ * @remarks
1641
+ * Caches the latest npm registry version in a local JSON file
1642
+ * to avoid expensive `npm view` calls on every refresh cycle.
1856
1643
  */
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;
1644
+ /**
1645
+ * Check the npm registry for the latest version of a package.
1646
+ *
1647
+ * @param packageName - The npm package name (e.g., '\@karmaniverous/jeeves').
1648
+ * @param cacheDir - Directory to store the cache file.
1649
+ * @param ttlSeconds - Cache TTL in seconds (default 3600).
1650
+ * @returns The latest version string, or undefined if the check fails.
1651
+ */
1652
+ function checkRegistryVersion(packageName, cacheDir, ttlSeconds = 3600) {
1653
+ const cachePath = join(cacheDir, REGISTRY_CACHE_FILE);
1654
+ // Check cache first
1655
+ if (existsSync(cachePath)) {
1656
+ try {
1657
+ const raw = readFileSync(cachePath, 'utf-8');
1658
+ const entry = JSON.parse(raw);
1659
+ const age = Date.now() - new Date(entry.checkedAt).getTime();
1660
+ if (age < ttlSeconds * 1000) {
1661
+ return entry.version;
1662
+ }
1663
+ }
1664
+ catch {
1665
+ // Cache corrupt — proceed with fresh check
1666
+ }
1667
+ }
1668
+ // Query npm registry
1669
+ try {
1670
+ const result = execSync(`npm view ${packageName} version`, {
1671
+ encoding: 'utf-8',
1672
+ timeout: 15_000,
1673
+ stdio: ['pipe', 'pipe', 'pipe'],
1674
+ }).trim();
1675
+ if (!result)
1676
+ return undefined;
1677
+ // Write cache
1678
+ if (!existsSync(cacheDir)) {
1679
+ mkdirSync(cacheDir, { recursive: true });
1680
+ }
1681
+ const entry = {
1682
+ version: result,
1683
+ checkedAt: new Date().toISOString(),
1684
+ };
1685
+ writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
1686
+ return result;
1687
+ }
1688
+ catch {
1689
+ return undefined;
1690
+ }
1865
1691
  }
1866
1692
 
1867
1693
  /**
@@ -2007,8 +1833,6 @@ async function seedContent(options) {
2007
1833
  // Seed content via the same code path as writer cycles
2008
1834
  await refreshPlatformContent({
2009
1835
  coreVersion: options.coreVersion,
2010
- probeTimeoutMs: options.probeTimeoutMs ?? 3000,
2011
- skipRegistryCheck: options.skipRegistryCheck ?? true,
2012
1836
  });
2013
1837
  }
2014
1838
 
@@ -2019,6 +1843,26 @@ async function seedContent(options) {
2019
1843
  * Thin wrappers around `fetch` that throw on non-OK responses
2020
1844
  * and handle JSON serialisation/deserialisation.
2021
1845
  */
1846
+ /**
1847
+ * Fetch a URL with an automatic abort timeout.
1848
+ *
1849
+ * @param url - URL to fetch.
1850
+ * @param timeoutMs - Timeout in milliseconds before aborting.
1851
+ * @param init - Optional `fetch` init options.
1852
+ * @returns The fetch Response object.
1853
+ */
1854
+ async function fetchWithTimeout(url, timeoutMs, init) {
1855
+ const controller = new AbortController();
1856
+ const timeout = setTimeout(() => {
1857
+ controller.abort();
1858
+ }, timeoutMs);
1859
+ try {
1860
+ return await fetch(url, { ...init, signal: controller.signal });
1861
+ }
1862
+ finally {
1863
+ clearTimeout(timeout);
1864
+ }
1865
+ }
2022
1866
  /**
2023
1867
  * Fetch JSON from a URL, throwing on non-OK responses.
2024
1868
  *
@@ -2167,6 +2011,77 @@ function patchConfig(config, pluginId, mode) {
2167
2011
  return messages;
2168
2012
  }
2169
2013
 
2014
+ /**
2015
+ * Plugin resolution helpers for the OpenClaw plugin SDK.
2016
+ *
2017
+ * @remarks
2018
+ * Provides workspace path resolution and plugin setting resolution
2019
+ * with a standard three-step fallback chain:
2020
+ * plugin config → environment variable → default value.
2021
+ */
2022
+ /**
2023
+ * Resolve the workspace root from the OpenClaw plugin API.
2024
+ *
2025
+ * @remarks
2026
+ * Tries three sources in order:
2027
+ * 1. `api.config.agents.defaults.workspace` — explicit config
2028
+ * 2. `api.resolvePath('.')` — gateway-provided path resolver
2029
+ * 3. `process.cwd()` — last resort
2030
+ *
2031
+ * @param api - The plugin API object provided by the gateway.
2032
+ * @returns Absolute path to the workspace root.
2033
+ */
2034
+ function resolveWorkspacePath(api) {
2035
+ const configured = api.config?.agents?.defaults?.workspace;
2036
+ if (typeof configured === 'string' && configured.trim()) {
2037
+ return configured;
2038
+ }
2039
+ if (typeof api.resolvePath === 'function') {
2040
+ return api.resolvePath('.');
2041
+ }
2042
+ return process.cwd();
2043
+ }
2044
+ /**
2045
+ * Resolve a plugin setting via the standard three-step fallback chain:
2046
+ * plugin config → environment variable → fallback value.
2047
+ *
2048
+ * @param api - Plugin API object.
2049
+ * @param pluginId - Plugin identifier (e.g., 'jeeves-watcher-openclaw').
2050
+ * @param key - Config key within the plugin's config object.
2051
+ * @param envVar - Environment variable name.
2052
+ * @param fallback - Default value if neither source provides one.
2053
+ * @returns The resolved setting value.
2054
+ */
2055
+ function resolvePluginSetting(api, pluginId, key, envVar, fallback) {
2056
+ const fromPlugin = api.config?.plugins?.entries?.[pluginId]?.config?.[key];
2057
+ if (typeof fromPlugin === 'string')
2058
+ return fromPlugin;
2059
+ const fromEnv = process.env[envVar];
2060
+ if (fromEnv)
2061
+ return fromEnv;
2062
+ return fallback;
2063
+ }
2064
+ /**
2065
+ * Resolve an optional plugin setting via the two-step fallback chain:
2066
+ * plugin config → environment variable. Returns `undefined` if neither
2067
+ * source provides a value.
2068
+ *
2069
+ * @param api - Plugin API object.
2070
+ * @param pluginId - Plugin identifier (e.g., 'jeeves-watcher-openclaw').
2071
+ * @param key - Config key within the plugin's config object.
2072
+ * @param envVar - Environment variable name.
2073
+ * @returns The resolved setting value, or `undefined`.
2074
+ */
2075
+ function resolveOptionalPluginSetting(api, pluginId, key, envVar) {
2076
+ const fromPlugin = api.config?.plugins?.entries?.[pluginId]?.config?.[key];
2077
+ if (typeof fromPlugin === 'string')
2078
+ return fromPlugin;
2079
+ const fromEnv = process.env[envVar];
2080
+ if (fromEnv)
2081
+ return fromEnv;
2082
+ return undefined;
2083
+ }
2084
+
2170
2085
  /**
2171
2086
  * Tool result formatters for the OpenClaw plugin SDK.
2172
2087
  *
@@ -2236,4 +2151,4 @@ function connectionFail(error, baseUrl, pluginId) {
2236
2151
  return fail(error);
2237
2152
  }
2238
2153
 
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 };
2154
+ 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 };