@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/content/agents-section.md +11 -1
- package/content/soul-section.md +9 -0
- package/content/templates/spec.md +6 -0
- package/content/tools-platform.md +5 -15
- package/dist/cli/jeeves/index.js +235 -356
- package/dist/index.d.ts +144 -184
- package/dist/index.js +475 -560
- package/package.json +1 -2
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
|
|
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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1082
|
-
Reference templates are available at \`
|
|
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
|
-
|
|
1145
|
+
<!-- ELSE_TEMPLATES -->
|
|
1091
1146
|
> Reference templates not yet installed. Run \`npx @karmaniverous/jeeves install\` to seed templates.
|
|
1092
|
-
|
|
1147
|
+
<!-- ENDIF_TEMPLATES -->
|
|
1093
1148
|
`;
|
|
1094
1149
|
|
|
1095
1150
|
/**
|
|
1096
|
-
*
|
|
1151
|
+
* Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
1097
1152
|
*
|
|
1098
1153
|
* @remarks
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
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
|
-
*
|
|
1160
|
+
* Resolve the package's content directory for template file copying.
|
|
1137
1161
|
*
|
|
1138
|
-
* @
|
|
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
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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
|
-
*
|
|
1184
|
+
* Copy templates from content/templates/ to the core config directory.
|
|
1179
1185
|
*
|
|
1180
|
-
* @param
|
|
1181
|
-
* @returns Parsed config or undefined.
|
|
1186
|
+
* @param coreConfigDir - Core config directory path.
|
|
1182
1187
|
*/
|
|
1183
|
-
function
|
|
1184
|
-
const
|
|
1185
|
-
if (!
|
|
1186
|
-
return
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
-
*
|
|
1202
|
+
* Render the Platform template using simple string replacement.
|
|
1199
1203
|
*
|
|
1200
|
-
* @
|
|
1201
|
-
*
|
|
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
|
-
*
|
|
1221
|
+
* Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
1209
1222
|
*
|
|
1210
|
-
* @param
|
|
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
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1271
|
+
* Timer-based orchestrator for managed content writing.
|
|
1240
1272
|
*
|
|
1241
1273
|
* @remarks
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
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
|
-
*
|
|
1279
|
+
* Orchestrates managed content writing for a single Jeeves component.
|
|
1247
1280
|
*
|
|
1248
|
-
* @
|
|
1249
|
-
*
|
|
1250
|
-
|
|
1251
|
-
|
|
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
|
|
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
|
|
1489
|
+
function createComponentWriter(component) {
|
|
1812
1490
|
validateDescriptor(component);
|
|
1813
|
-
return new ComponentWriter(component
|
|
1491
|
+
return new ComponentWriter(component);
|
|
1814
1492
|
}
|
|
1815
1493
|
|
|
1816
1494
|
/**
|
|
1817
|
-
*
|
|
1495
|
+
* Core configuration schema and resolution.
|
|
1818
1496
|
*
|
|
1819
1497
|
* @remarks
|
|
1820
|
-
*
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1829
|
-
*
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
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
|
|
1834
|
-
* @
|
|
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
|
|
1837
|
-
|
|
1838
|
-
if (
|
|
1839
|
-
|
|
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
|
-
|
|
1842
|
-
|
|
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
|
-
|
|
1634
|
+
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
1845
1635
|
}
|
|
1636
|
+
|
|
1846
1637
|
/**
|
|
1847
|
-
*
|
|
1848
|
-
* plugin config → environment variable → fallback value.
|
|
1638
|
+
* Registry version cache for npm package update awareness.
|
|
1849
1639
|
*
|
|
1850
|
-
* @
|
|
1851
|
-
*
|
|
1852
|
-
*
|
|
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
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
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,
|
|
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 };
|