@karmaniverous/jeeves 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1232 @@
1
+ import { join, dirname } from 'node:path';
2
+ import { createRequire } from 'node:module';
3
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, renameSync, cpSync } from 'node:fs';
4
+ import { lock } from 'proper-lockfile';
5
+ import { gte } from 'semver';
6
+ import { fileURLToPath } from 'node:url';
7
+ import Handlebars from 'handlebars';
8
+ import { z } from 'zod';
9
+ import { execSync } from 'node:child_process';
10
+
11
+ /**
12
+ * Comment markers for managed content blocks.
13
+ *
14
+ * @remarks
15
+ * Managed content in TOOLS.md, SOUL.md, and AGENTS.md is enclosed
16
+ * in HTML comment markers. Content between markers is refreshed
17
+ * atomically on each writer cycle. User content outside the markers
18
+ * is never touched.
19
+ */
20
+ /** Default markers for TOOLS.md managed block. */
21
+ const TOOLS_MARKERS = {
22
+ /** BEGIN comment marker text. */
23
+ begin: 'BEGIN JEEVES PLATFORM TOOLS — DO NOT EDIT THIS SECTION',
24
+ /** END comment marker text. */
25
+ end: 'END JEEVES PLATFORM TOOLS',
26
+ /** H1 title prepended in section mode. */
27
+ title: 'Jeeves Platform Tools',
28
+ };
29
+ /** Default markers for SOUL.md managed block. */
30
+ const SOUL_MARKERS = {
31
+ /** BEGIN comment marker text. */
32
+ begin: 'BEGIN JEEVES SOUL — DO NOT EDIT THIS SECTION',
33
+ /** END comment marker text. */
34
+ end: 'END JEEVES SOUL',
35
+ };
36
+ /** Default markers for AGENTS.md managed block. */
37
+ const AGENTS_MARKERS = {
38
+ /** BEGIN comment marker text. */
39
+ begin: 'BEGIN JEEVES AGENTS — DO NOT EDIT THIS SECTION',
40
+ /** END comment marker text. */
41
+ end: 'END JEEVES AGENTS',
42
+ };
43
+ /**
44
+ * Regex pattern to extract version stamp from a BEGIN marker comment.
45
+ *
46
+ * @remarks
47
+ * Format: `\<!-- BEGIN MARKER | core:X.Y.Z | ISO-TIMESTAMP --\>`
48
+ * Captures: [1] marker text, [2] version, [3] timestamp
49
+ */
50
+ const VERSION_STAMP_PATTERN = /<!--\s*(.+?)\s*\|\s*core:(\S+)\s*\|\s*(\S+)\s*-->/;
51
+ /** Staleness threshold for version-stamp convergence in milliseconds. */
52
+ const STALENESS_THRESHOLD_MS = 5 * 60 * 1000;
53
+ /** Warning text prepended inside managed block when cleanup is needed. */
54
+ const CLEANUP_FLAG = '> ⚠️ CLEANUP NEEDED: Orphaned Jeeves content may exist below this managed section. Review everything after the END marker and remove any content that duplicates what appears above.';
55
+
56
+ /**
57
+ * Directory and file path conventions for the Jeeves platform.
58
+ */
59
+ /** Core config directory name within the config root. */
60
+ const CORE_CONFIG_DIR = 'jeeves-core';
61
+ /** Prefix for component config directories: `jeeves-{name}`. */
62
+ const COMPONENT_CONFIG_PREFIX = 'jeeves-';
63
+ /** Default workspace file names. */
64
+ const WORKSPACE_FILES = {
65
+ /** TOOLS.md — live platform state and component sections. */
66
+ tools: 'TOOLS.md',
67
+ /** SOUL.md — professional discipline and behavioral foundations. */
68
+ soul: 'SOUL.md',
69
+ /** AGENTS.md — operational protocols and memory architecture. */
70
+ agents: 'AGENTS.md',
71
+ };
72
+ /** Templates directory name within core config. */
73
+ const TEMPLATES_DIR = 'templates';
74
+ /** Registry cache file name. */
75
+ const REGISTRY_CACHE_FILE = 'registry-cache.json';
76
+ /** Core config file name. */
77
+ const CONFIG_FILE = 'config.json';
78
+
79
+ /**
80
+ * Default port assignments for Jeeves platform services.
81
+ *
82
+ * @remarks
83
+ * Each port number is a historical reference:
84
+ * - 1934: Wodehouse's *Thank You, Jeeves*; Popper's *Logic of Scientific Discovery*
85
+ * - 1936: Turing's "On Computable Numbers"; Church's lambda calculus
86
+ * - 1937: Turing's paper in *Proceedings of the London Mathematical Society*
87
+ * - 1938: Wodehouse's *The Code of the Woosters*; Shannon's relay/switching paper
88
+ */
89
+ /** Default port for jeeves-server. */
90
+ const SERVER_PORT = 1934;
91
+ /** Default port for jeeves-watcher. */
92
+ const WATCHER_PORT = 1936;
93
+ /** Default port for jeeves-runner. */
94
+ const RUNNER_PORT = 1937;
95
+ /** Default port for jeeves-meta. */
96
+ const META_PORT = 1938;
97
+ /** Map of service names to their default ports. */
98
+ const DEFAULT_PORTS = {
99
+ server: SERVER_PORT,
100
+ watcher: WATCHER_PORT,
101
+ runner: RUNNER_PORT,
102
+ meta: META_PORT,
103
+ };
104
+
105
+ /**
106
+ * Managed section IDs and their stable ordering for TOOLS.md.
107
+ *
108
+ * @remarks
109
+ * Section ordering is fixed to prevent diff churn regardless of which
110
+ * component writes last. Sections always appear in this order.
111
+ */
112
+ /** Known section IDs for TOOLS.md managed block. */
113
+ const SECTION_IDS = {
114
+ /** Platform health and guidance section. */
115
+ Platform: 'Platform',
116
+ /** Watcher index stats and search configuration. */
117
+ Watcher: 'Watcher',
118
+ /** Server export capabilities and connected services. */
119
+ Server: 'Server',
120
+ /** Runner job status and active scripts. */
121
+ Runner: 'Runner',
122
+ /** Meta synthesis entity summary and tools. */
123
+ Meta: 'Meta',
124
+ };
125
+ /**
126
+ * Stable ordering of sections within the managed TOOLS.md block.
127
+ * Sections always appear in this order regardless of write order.
128
+ */
129
+ const SECTION_ORDER = [
130
+ SECTION_IDS.Platform,
131
+ SECTION_IDS.Watcher,
132
+ SECTION_IDS.Server,
133
+ SECTION_IDS.Runner,
134
+ SECTION_IDS.Meta,
135
+ ];
136
+
137
+ /**
138
+ * Core library version, read from package.json at runtime.
139
+ *
140
+ * @remarks
141
+ * Used for version-stamp convergence (Decision 21). The version stamp
142
+ * on managed content reflects the actual published library version,
143
+ * enabling higher-version writers to take precedence.
144
+ */
145
+ const require$1 = createRequire(import.meta.url);
146
+ const pkg = require$1('../../package.json');
147
+ /** The core library version from package.json. */
148
+ const CORE_VERSION = pkg.version;
149
+
150
+ /**
151
+ * Workspace and config root initialization.
152
+ *
153
+ * @remarks
154
+ * `init()` must be called once before any other core library functions.
155
+ * It caches `workspacePath` and `configRoot` at module level.
156
+ * Core derives all namespaced paths from these values:
157
+ * - `{configRoot}/jeeves-core/` for core config
158
+ * - `{configRoot}/jeeves-{name}/` for each component
159
+ */
160
+ let state;
161
+ /**
162
+ * Initialize the core library with workspace and config root paths.
163
+ *
164
+ * @param options - Workspace and config root paths.
165
+ */
166
+ function init(options) {
167
+ state = {
168
+ workspacePath: options.workspacePath,
169
+ configRoot: options.configRoot,
170
+ coreConfigDir: join(options.configRoot, CORE_CONFIG_DIR),
171
+ };
172
+ }
173
+ /**
174
+ * Get the cached workspace path.
175
+ *
176
+ * @throws Error if `init()` has not been called.
177
+ */
178
+ function getWorkspacePath() {
179
+ if (!state)
180
+ throw new Error('jeeves-core: init() must be called first');
181
+ return state.workspacePath;
182
+ }
183
+ /**
184
+ * Get the cached config root path.
185
+ *
186
+ * @throws Error if `init()` has not been called.
187
+ */
188
+ function getConfigRoot() {
189
+ if (!state)
190
+ throw new Error('jeeves-core: init() must be called first');
191
+ return state.configRoot;
192
+ }
193
+ /**
194
+ * Get the core config directory path.
195
+ *
196
+ * @throws Error if `init()` has not been called.
197
+ */
198
+ function getCoreConfigDir() {
199
+ if (!state)
200
+ throw new Error('jeeves-core: init() must be called first');
201
+ return state.coreConfigDir;
202
+ }
203
+ /**
204
+ * Get the core config file path.
205
+ *
206
+ * @throws Error if `init()` has not been called.
207
+ */
208
+ function getCoreConfigFile() {
209
+ if (!state)
210
+ throw new Error('jeeves-core: init() must be called first');
211
+ return join(state.coreConfigDir, CONFIG_FILE);
212
+ }
213
+ /**
214
+ * Derive the component config directory from the component name.
215
+ *
216
+ * @param componentName - The component name (e.g., 'watcher', 'runner').
217
+ * @returns Absolute path to the component's config directory.
218
+ * @throws Error if `init()` has not been called.
219
+ */
220
+ function getComponentConfigDir(componentName) {
221
+ if (!state)
222
+ throw new Error('jeeves-core: init() must be called first');
223
+ return join(state.configRoot, `${COMPONENT_CONFIG_PREFIX}${componentName}`);
224
+ }
225
+ /**
226
+ * Reset initialization state. Used for testing only.
227
+ */
228
+ function resetInit() {
229
+ state = undefined;
230
+ }
231
+
232
+ /**
233
+ * Similarity-based cleanup detection for orphaned managed content.
234
+ *
235
+ * @remarks
236
+ * Uses Jaccard similarity on 3-word shingles (Decision 22) to detect
237
+ * when orphaned managed content exists in the user content zone.
238
+ */
239
+ /** Default similarity threshold for cleanup detection. */
240
+ const DEFAULT_THRESHOLD = 0.15;
241
+ /**
242
+ * Generate a set of n-word shingles from text.
243
+ *
244
+ * @param text - Input text.
245
+ * @param n - Shingle size (default 3).
246
+ * @returns Set of n-word shingles.
247
+ */
248
+ function shingles(text, n = 3) {
249
+ const words = text.toLowerCase().split(/\s+/).filter(Boolean);
250
+ const set = new Set();
251
+ for (let i = 0; i <= words.length - n; i++) {
252
+ set.add(words.slice(i, i + n).join(' '));
253
+ }
254
+ return set;
255
+ }
256
+ /**
257
+ * Compute Jaccard similarity between two sets.
258
+ *
259
+ * @param a - First set.
260
+ * @param b - Second set.
261
+ * @returns Jaccard similarity coefficient (0 to 1).
262
+ */
263
+ function jaccard(a, b) {
264
+ if (a.size === 0 && b.size === 0)
265
+ return 0;
266
+ let intersection = 0;
267
+ for (const item of a) {
268
+ if (b.has(item))
269
+ intersection++;
270
+ }
271
+ return intersection / (a.size + b.size - intersection);
272
+ }
273
+ /**
274
+ * Check whether user content contains orphaned managed content.
275
+ *
276
+ * @param managedContent - The current managed block content.
277
+ * @param userContent - Content below the END marker.
278
+ * @param threshold - Jaccard threshold (default 0.15).
279
+ * @returns `true` if cleanup is needed.
280
+ */
281
+ function needsCleanup(managedContent, userContent, threshold = DEFAULT_THRESHOLD) {
282
+ if (!userContent.trim())
283
+ return false;
284
+ return jaccard(shingles(managedContent), shingles(userContent)) > threshold;
285
+ }
286
+
287
+ /**
288
+ * Stable section ordering for managed TOOLS.md blocks.
289
+ *
290
+ * @remarks
291
+ * Sorts sections by the canonical SECTION_ORDER: known sections
292
+ * appear in their defined order, unknown sections are appended after.
293
+ * Used by both parseManaged (for consistent output) and
294
+ * updateManagedSection (for reassembly).
295
+ */
296
+ /**
297
+ * Sort sections in place by stable ordering.
298
+ *
299
+ * @param sections - Array of managed sections to sort.
300
+ * @returns The sorted array (same reference, mutated in place).
301
+ */
302
+ function sortSectionsByOrder(sections) {
303
+ return sections.sort((a, b) => {
304
+ const aIdx = SECTION_ORDER.indexOf(a.id);
305
+ const bIdx = SECTION_ORDER.indexOf(b.id);
306
+ const aOrder = aIdx === -1 ? SECTION_ORDER.length : aIdx;
307
+ const bOrder = bIdx === -1 ? SECTION_ORDER.length : bIdx;
308
+ return aOrder - bOrder;
309
+ });
310
+ }
311
+
312
+ /**
313
+ * Parse managed block from file content.
314
+ *
315
+ * @remarks
316
+ * Extracts managed content delimited by comment markers, parses H2
317
+ * sections within the block, and returns the structured result plus
318
+ * user content outside the markers.
319
+ */
320
+ /**
321
+ * Build regex patterns for the given markers.
322
+ *
323
+ * @param markers - Begin/end marker strings.
324
+ * @returns Object with begin and end regex patterns.
325
+ */
326
+ function buildMarkerPatterns(markers) {
327
+ const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
328
+ const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
329
+ return {
330
+ beginRe: new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
331
+ endRe: new RegExp(`^<!--\\s*${escapedEnd}\\s*-->\\s*$`, 'm'),
332
+ };
333
+ }
334
+ /**
335
+ * Parse H2 sections from managed block content.
336
+ *
337
+ * @param content - Raw managed block content.
338
+ * @returns Array of parsed sections in stable order.
339
+ */
340
+ function parseSections(content) {
341
+ const lines = content.split('\n');
342
+ const sections = [];
343
+ let currentId;
344
+ let currentLines = [];
345
+ for (const line of lines) {
346
+ const h2Match = /^## (.+)$/.exec(line);
347
+ if (h2Match) {
348
+ if (currentId !== undefined) {
349
+ sections.push({
350
+ id: currentId,
351
+ content: currentLines.join('\n').trim(),
352
+ });
353
+ }
354
+ currentId = h2Match[1];
355
+ currentLines = [];
356
+ }
357
+ else if (currentId !== undefined) {
358
+ currentLines.push(line);
359
+ }
360
+ }
361
+ if (currentId !== undefined) {
362
+ sections.push({
363
+ id: currentId,
364
+ content: currentLines.join('\n').trim(),
365
+ });
366
+ }
367
+ return sortSectionsByOrder(sections);
368
+ }
369
+ /**
370
+ * Parse a managed block from file content.
371
+ *
372
+ * @param fileContent - Full file content.
373
+ * @param markers - Optional custom markers (defaults to TOOLS markers).
374
+ * @returns Parsed result with sections, version stamp, and user content.
375
+ */
376
+ function parseManaged(fileContent, markers = TOOLS_MARKERS) {
377
+ const { beginRe, endRe } = buildMarkerPatterns(markers);
378
+ const beginMatch = beginRe.exec(fileContent);
379
+ if (!beginMatch) {
380
+ return {
381
+ found: false,
382
+ versionStamp: undefined,
383
+ managedContent: '',
384
+ sections: [],
385
+ beforeContent: '',
386
+ userContent: fileContent,
387
+ };
388
+ }
389
+ const endMatch = endRe.exec(fileContent.slice(beginMatch.index + beginMatch[0].length));
390
+ if (!endMatch) {
391
+ // Corrupt: BEGIN without END — treat as fresh file
392
+ return {
393
+ found: false,
394
+ versionStamp: undefined,
395
+ managedContent: '',
396
+ sections: [],
397
+ beforeContent: '',
398
+ userContent: fileContent,
399
+ };
400
+ }
401
+ const beforeContent = fileContent.slice(0, beginMatch.index).trim();
402
+ const managedStart = beginMatch.index + beginMatch[0].length;
403
+ const managedEnd = managedStart + endMatch.index;
404
+ const managedContent = fileContent.slice(managedStart, managedEnd).trim();
405
+ const afterEnd = managedStart + endMatch.index + endMatch[0].length;
406
+ const userContent = fileContent.slice(afterEnd).trim();
407
+ // Extract version stamp from BEGIN marker line
408
+ let versionStamp;
409
+ const stampMatch = VERSION_STAMP_PATTERN.exec(beginMatch[0]);
410
+ if (stampMatch?.[2] && stampMatch[3]) {
411
+ versionStamp = {
412
+ version: stampMatch[2],
413
+ timestamp: stampMatch[3],
414
+ };
415
+ }
416
+ const sections = parseSections(managedContent);
417
+ return {
418
+ found: true,
419
+ versionStamp,
420
+ managedContent,
421
+ sections,
422
+ beforeContent,
423
+ userContent,
424
+ };
425
+ }
426
+
427
+ /**
428
+ * Version-stamp parsing and convergence logic.
429
+ *
430
+ * @remarks
431
+ * When multiple component plugins bundle different core library versions,
432
+ * they independently maintain shared managed content. The version-stamp
433
+ * mechanism ensures convergence without coordination state.
434
+ */
435
+ /**
436
+ * Format the BEGIN marker comment with a version stamp.
437
+ *
438
+ * @param markerText - The marker text (e.g., 'BEGIN JEEVES PLATFORM TOOLS').
439
+ * @param version - The core library version.
440
+ * @returns Formatted comment line.
441
+ */
442
+ function formatBeginMarker(markerText, version) {
443
+ const timestamp = new Date().toISOString();
444
+ return `<!-- ${markerText} | core:${version} | ${timestamp} -->`;
445
+ }
446
+ /**
447
+ * Format the END marker comment.
448
+ *
449
+ * @param markerText - The marker text (e.g., 'END JEEVES PLATFORM TOOLS').
450
+ * @returns Formatted comment line.
451
+ */
452
+ function formatEndMarker(markerText) {
453
+ return `<!-- ${markerText} -->`;
454
+ }
455
+ /**
456
+ * Determine whether this writer should proceed based on version-stamp
457
+ * convergence rules.
458
+ *
459
+ * @param myVersion - The current core library version.
460
+ * @param existing - The existing version stamp (if any).
461
+ * @param stalenessThresholdMs - Staleness threshold in ms (default: 5 min).
462
+ * @returns `true` if the writer should proceed with the write.
463
+ */
464
+ function shouldWrite(myVersion, existing, stalenessThresholdMs = STALENESS_THRESHOLD_MS) {
465
+ // No existing stamp — always write
466
+ if (!existing)
467
+ return true;
468
+ // My version >= stamped version — always write (I'm current or newer)
469
+ if (gte(myVersion, existing.version))
470
+ return true;
471
+ // My version < stamped version — check staleness
472
+ const stampAge = Date.now() - new Date(existing.timestamp).getTime();
473
+ return stampAge >= stalenessThresholdMs;
474
+ }
475
+
476
+ /**
477
+ * Generic managed-section writer with block and section modes.
478
+ *
479
+ * @remarks
480
+ * Supports two modes:
481
+ * - `block`: Replaces the entire managed block (SOUL.md, AGENTS.md).
482
+ * - `section`: Upserts a named H2 section within the managed block (TOOLS.md).
483
+ *
484
+ * Provides file-level locking, version-stamp convergence, and atomic writes.
485
+ */
486
+ /** Default core version when none provided. */
487
+ const DEFAULT_VERSION = '0.0.0';
488
+ /** Stale lock threshold in ms (2 minutes). */
489
+ const STALE_LOCK_MS = 120_000;
490
+ /**
491
+ * Update a managed section in a file.
492
+ *
493
+ * @param filePath - Absolute path to the target file.
494
+ * @param content - New content to write.
495
+ * @param options - Write mode and optional configuration.
496
+ */
497
+ async function updateManagedSection(filePath, content, options = {}) {
498
+ const { mode = 'block', sectionId, markers = TOOLS_MARKERS, coreVersion = DEFAULT_VERSION, stalenessThresholdMs, } = options;
499
+ if (mode === 'section' && !sectionId) {
500
+ throw new Error('sectionId is required when mode is "section"');
501
+ }
502
+ const dir = dirname(filePath);
503
+ if (!existsSync(dir)) {
504
+ mkdirSync(dir, { recursive: true });
505
+ }
506
+ // Create file if it doesn't exist
507
+ if (!existsSync(filePath)) {
508
+ writeFileSync(filePath, '', 'utf-8');
509
+ }
510
+ let release;
511
+ try {
512
+ release = await lock(filePath, {
513
+ stale: STALE_LOCK_MS,
514
+ retries: { retries: 5, minTimeout: 100, maxTimeout: 1000 },
515
+ });
516
+ const fileContent = readFileSync(filePath, 'utf-8');
517
+ const parsed = parseManaged(fileContent, markers);
518
+ // Version-stamp convergence check (block mode only).
519
+ // In section mode, components always write their own sections — the version
520
+ // stamp governs shared content convergence, not component-specific sections.
521
+ if (mode === 'block' &&
522
+ !shouldWrite(coreVersion, parsed.versionStamp, stalenessThresholdMs)) {
523
+ return;
524
+ }
525
+ let newManagedBody;
526
+ if (mode === 'block') {
527
+ newManagedBody = content;
528
+ }
529
+ else {
530
+ // Section mode: upsert the named section
531
+ const sections = [...parsed.sections];
532
+ const existingIdx = sections.findIndex((s) => s.id === sectionId);
533
+ if (existingIdx >= 0) {
534
+ sections[existingIdx] = { id: sectionId, content };
535
+ }
536
+ else {
537
+ sections.push({ id: sectionId, content });
538
+ }
539
+ sortSectionsByOrder(sections);
540
+ const sectionText = sections
541
+ .map((s) => `## ${s.id}\n\n${s.content}`)
542
+ .join('\n\n');
543
+ // Prepend H1 title if markers specify one (e.g., "# Jeeves Platform Tools")
544
+ newManagedBody = markers.title
545
+ ? `# ${markers.title}\n\n${sectionText}`
546
+ : sectionText;
547
+ }
548
+ // Cleanup detection
549
+ const userContent = parsed.userContent;
550
+ const cleanupNeeded = needsCleanup(newManagedBody, userContent);
551
+ // Build the full managed block
552
+ const beginLine = formatBeginMarker(markers.begin, coreVersion);
553
+ const endLine = formatEndMarker(markers.end);
554
+ const parts = [];
555
+ if (parsed.beforeContent) {
556
+ parts.push(parsed.beforeContent);
557
+ parts.push('');
558
+ }
559
+ parts.push(beginLine);
560
+ if (cleanupNeeded) {
561
+ parts.push('');
562
+ parts.push(CLEANUP_FLAG);
563
+ }
564
+ parts.push('');
565
+ parts.push(newManagedBody);
566
+ parts.push('');
567
+ parts.push(endLine);
568
+ if (userContent) {
569
+ parts.push('');
570
+ parts.push(userContent);
571
+ }
572
+ parts.push('');
573
+ const newFileContent = parts.join('\n');
574
+ // Atomic write: write to temp file, then rename
575
+ const tempPath = join(dir, `.${String(Date.now())}.tmp`);
576
+ writeFileSync(tempPath, newFileContent, 'utf-8');
577
+ renameSync(tempPath, filePath);
578
+ }
579
+ catch (err) {
580
+ // Log warning but don't throw — writer cycles are periodic
581
+ const message = err instanceof Error ? err.message : String(err);
582
+ console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
583
+ }
584
+ finally {
585
+ if (release) {
586
+ try {
587
+ await release();
588
+ }
589
+ catch {
590
+ // Lock already released or file deleted — safe to ignore
591
+ }
592
+ }
593
+ }
594
+ }
595
+
596
+ /**
597
+ * Core configuration schema and resolution.
598
+ *
599
+ * @remarks
600
+ * Core config lives at `{configRoot}/jeeves-core/config.json`.
601
+ * Config resolution order:
602
+ * 1. Component's own config file
603
+ * 2. Core config file
604
+ * 3. Hardcoded library defaults
605
+ */
606
+ /** Zod schema for a service entry in core config. */
607
+ const serviceEntrySchema = z.object({
608
+ /** Service URL (must be a valid URL). */
609
+ url: z.string().url().describe('Service URL'),
610
+ });
611
+ /** Zod schema for the core config file. */
612
+ const coreConfigSchema = z.object({
613
+ /** JSON Schema pointer for IDE autocomplete. */
614
+ $schema: z.string().optional().describe('JSON Schema pointer'),
615
+ /** Owner identity keys (canonical identityLinks references). */
616
+ owners: z.array(z.string()).default([]).describe('Owner identity keys'),
617
+ /** Service URL overrides keyed by service name. */
618
+ services: z
619
+ .record(z.string(), serviceEntrySchema)
620
+ .default({})
621
+ .describe('Service URL overrides'),
622
+ /** Registry cache configuration. */
623
+ registryCache: z
624
+ .object({
625
+ /** Cache TTL in seconds for npm registry queries. */
626
+ ttlSeconds: z
627
+ .number()
628
+ .int()
629
+ .positive()
630
+ .default(3600)
631
+ .describe('Cache TTL in seconds'),
632
+ })
633
+ .default({})
634
+ .describe('Registry cache settings'),
635
+ });
636
+ /**
637
+ * Generate a JSON Schema from the Zod schema for `$schema` pointer support.
638
+ *
639
+ * @returns A JSON Schema object.
640
+ */
641
+ function generateJsonSchema() {
642
+ return {
643
+ $schema: 'http://json-schema.org/draft-07/schema#',
644
+ title: 'Jeeves Core Configuration',
645
+ type: 'object',
646
+ properties: {
647
+ $schema: { type: 'string' },
648
+ owners: {
649
+ type: 'array',
650
+ items: { type: 'string' },
651
+ default: [],
652
+ },
653
+ services: {
654
+ type: 'object',
655
+ additionalProperties: {
656
+ type: 'object',
657
+ properties: {
658
+ url: { type: 'string', format: 'uri' },
659
+ },
660
+ required: ['url'],
661
+ },
662
+ default: {},
663
+ },
664
+ registryCache: {
665
+ type: 'object',
666
+ properties: {
667
+ ttlSeconds: {
668
+ type: 'integer',
669
+ minimum: 1,
670
+ default: 3600,
671
+ },
672
+ },
673
+ default: {},
674
+ },
675
+ },
676
+ };
677
+ }
678
+ /**
679
+ * Load and parse a config file, returning undefined if missing or invalid.
680
+ *
681
+ * @param configDir - Directory containing config.json.
682
+ * @returns Parsed config or undefined.
683
+ */
684
+ function loadConfig(configDir) {
685
+ const configPath = join(configDir, CONFIG_FILE);
686
+ if (!existsSync(configPath))
687
+ return undefined;
688
+ try {
689
+ const raw = readFileSync(configPath, 'utf-8');
690
+ const parsed = JSON.parse(raw);
691
+ return coreConfigSchema.parse(parsed);
692
+ }
693
+ catch {
694
+ return undefined;
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Service URL resolution.
700
+ *
701
+ * @remarks
702
+ * Resolves the URL for a named Jeeves service using the following
703
+ * resolution order:
704
+ * 1. Consumer's own component config
705
+ * 2. Core config (`{configRoot}/jeeves-core/config.json`)
706
+ * 3. Default port constants
707
+ */
708
+ /**
709
+ * Resolve the URL for a named Jeeves service.
710
+ *
711
+ * @param serviceName - The service name (e.g., 'watcher', 'runner').
712
+ * @param consumerName - Optional consumer component name for config override.
713
+ * @returns The resolved service URL.
714
+ * @throws Error if `init()` has not been called or the service is unknown.
715
+ */
716
+ function getServiceUrl(serviceName, consumerName) {
717
+ // 1. Check consumer's own config
718
+ if (consumerName) {
719
+ const consumerDir = getComponentConfigDir(consumerName);
720
+ const consumerConfig = loadConfig(consumerDir);
721
+ const consumerUrl = consumerConfig?.services[serviceName]?.url;
722
+ if (consumerUrl)
723
+ return consumerUrl;
724
+ }
725
+ // 2. Check core config
726
+ const coreDir = getCoreConfigDir();
727
+ const coreConfig = loadConfig(coreDir);
728
+ const coreUrl = coreConfig?.services[serviceName]?.url;
729
+ if (coreUrl)
730
+ return coreUrl;
731
+ // 3. Fall back to port constants
732
+ const port = DEFAULT_PORTS[serviceName];
733
+ if (port !== undefined) {
734
+ return `http://127.0.0.1:${String(port)}`;
735
+ }
736
+ throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
737
+ }
738
+
739
+ /**
740
+ * HTTP health probing for Jeeves platform services.
741
+ *
742
+ * @remarks
743
+ * Probes service ports for health endpoints (HTTP GET to /status or /health).
744
+ * Returns structured health data for rendering into TOOLS.md Platform section.
745
+ */
746
+ /**
747
+ * Extract port number from a URL string.
748
+ *
749
+ * @param url - Service URL.
750
+ * @returns Port number.
751
+ */
752
+ function extractPort(url) {
753
+ try {
754
+ const parsed = new URL(url);
755
+ return parsed.port ? parseInt(parsed.port, 10) : 80;
756
+ }
757
+ catch {
758
+ return 0;
759
+ }
760
+ }
761
+ /**
762
+ * Probe a single service for health.
763
+ *
764
+ * @param serviceName - The service name (e.g., 'server', 'watcher').
765
+ * @param consumerName - Optional consumer name for URL resolution.
766
+ * @param timeoutMs - Request timeout in milliseconds (default 3000).
767
+ * @returns Probe result.
768
+ */
769
+ async function probeService(serviceName, consumerName, timeoutMs = 3000) {
770
+ const url = getServiceUrl(serviceName, consumerName);
771
+ const port = extractPort(url);
772
+ const endpoints = ['/status', '/health'];
773
+ for (const endpoint of endpoints) {
774
+ try {
775
+ const controller = new AbortController();
776
+ const timeout = setTimeout(() => {
777
+ controller.abort();
778
+ }, timeoutMs);
779
+ const response = await fetch(`${url}${endpoint}`, {
780
+ signal: controller.signal,
781
+ });
782
+ clearTimeout(timeout);
783
+ if (response.ok) {
784
+ let version;
785
+ try {
786
+ const body = await response.json();
787
+ if (typeof body === 'object' &&
788
+ body !== null &&
789
+ 'version' in body &&
790
+ typeof body['version'] === 'string') {
791
+ version = body['version'];
792
+ }
793
+ }
794
+ catch {
795
+ // Non-JSON response is fine — we just don't get version info
796
+ }
797
+ return { name: serviceName, port, healthy: true, version };
798
+ }
799
+ }
800
+ catch {
801
+ // Try next endpoint
802
+ }
803
+ }
804
+ return { name: serviceName, port, healthy: false };
805
+ }
806
+ /**
807
+ * Probe all known Jeeves services for health.
808
+ *
809
+ * @param consumerName - Optional consumer name for URL resolution.
810
+ * @param timeoutMs - Request timeout in milliseconds (default 3000).
811
+ * @returns Array of probe results for all services.
812
+ */
813
+ async function probeAllServices(consumerName, timeoutMs = 3000) {
814
+ const serviceNames = Object.keys(DEFAULT_PORTS);
815
+ const results = await Promise.all(serviceNames.map((name) => probeService(name, consumerName, timeoutMs)));
816
+ return results;
817
+ }
818
+
819
+ /**
820
+ * Registry version cache for npm package update awareness.
821
+ *
822
+ * @remarks
823
+ * Caches the latest npm registry version in a local JSON file
824
+ * to avoid expensive `npm view` calls on every refresh cycle.
825
+ */
826
+ /**
827
+ * Check the npm registry for the latest version of a package.
828
+ *
829
+ * @param packageName - The npm package name (e.g., '\@karmaniverous/jeeves').
830
+ * @param cacheDir - Directory to store the cache file.
831
+ * @param ttlSeconds - Cache TTL in seconds (default 3600).
832
+ * @returns The latest version string, or undefined if the check fails.
833
+ */
834
+ function checkRegistryVersion(packageName, cacheDir, ttlSeconds = 3600) {
835
+ const cachePath = join(cacheDir, REGISTRY_CACHE_FILE);
836
+ // Check cache first
837
+ if (existsSync(cachePath)) {
838
+ try {
839
+ const raw = readFileSync(cachePath, 'utf-8');
840
+ const entry = JSON.parse(raw);
841
+ const age = Date.now() - new Date(entry.checkedAt).getTime();
842
+ if (age < ttlSeconds * 1000) {
843
+ return entry.version;
844
+ }
845
+ }
846
+ catch {
847
+ // Cache corrupt — proceed with fresh check
848
+ }
849
+ }
850
+ // Query npm registry
851
+ try {
852
+ const result = execSync(`npm view ${packageName} version`, {
853
+ encoding: 'utf-8',
854
+ timeout: 15_000,
855
+ stdio: ['pipe', 'pipe', 'pipe'],
856
+ }).trim();
857
+ if (!result)
858
+ return undefined;
859
+ // Write cache
860
+ if (!existsSync(cacheDir)) {
861
+ mkdirSync(cacheDir, { recursive: true });
862
+ }
863
+ const entry = {
864
+ version: result,
865
+ checkedAt: new Date().toISOString(),
866
+ };
867
+ writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
868
+ return result;
869
+ }
870
+ catch {
871
+ return undefined;
872
+ }
873
+ }
874
+
875
+ /**
876
+ * Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
877
+ *
878
+ * @remarks
879
+ * Called by `ComponentWriter` on each cycle. Not directly exposed to components.
880
+ * Probes service ports for health, reads content files from the package's
881
+ * `content/` directory, renders the Platform template with live service data,
882
+ * and writes managed sections using `updateManagedSection`.
883
+ */
884
+ /**
885
+ * Resolve the package's content directory.
886
+ *
887
+ * @returns Absolute path to the content/ directory.
888
+ */
889
+ function getContentDir() {
890
+ const thisFile = fileURLToPath(import.meta.url);
891
+ // From src/platform/refreshPlatformContent.ts → ../../content/
892
+ // From dist/platform/refreshPlatformContent.js → ../../content/
893
+ return join(dirname(thisFile), '..', '..', 'content');
894
+ }
895
+ /**
896
+ * Read a content file from the package's content/ directory.
897
+ *
898
+ * @param fileName - File name within content/.
899
+ * @returns File content as string, or empty string if missing.
900
+ */
901
+ function readContentFile(fileName) {
902
+ const filePath = join(getContentDir(), fileName);
903
+ if (!existsSync(filePath)) {
904
+ console.warn(`jeeves-core: content file missing: ${filePath}`);
905
+ return '';
906
+ }
907
+ return readFileSync(filePath, 'utf-8');
908
+ }
909
+ /**
910
+ * Copy templates from content/templates/ to the core config directory.
911
+ *
912
+ * @param coreConfigDir - Core config directory path.
913
+ */
914
+ function copyTemplates(coreConfigDir) {
915
+ const sourceDir = join(getContentDir(), 'templates');
916
+ if (!existsSync(sourceDir))
917
+ return;
918
+ const destDir = join(coreConfigDir, TEMPLATES_DIR);
919
+ if (!existsSync(destDir)) {
920
+ mkdirSync(destDir, { recursive: true });
921
+ }
922
+ cpSync(sourceDir, destDir, { recursive: true });
923
+ }
924
+ /** Whether Handlebars helpers have been registered. */
925
+ let helpersRegistered = false;
926
+ /**
927
+ * Register Handlebars helpers used in the Platform template.
928
+ */
929
+ function registerHelpers() {
930
+ if (helpersRegistered)
931
+ return;
932
+ helpersRegistered = true;
933
+ Handlebars.registerHelper('gt', (a, b) => typeof a === 'number' && typeof b === 'number' && a > b);
934
+ }
935
+ /**
936
+ * Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
937
+ *
938
+ * @param options - Configuration for the refresh cycle.
939
+ */
940
+ async function refreshPlatformContent(options) {
941
+ const { coreVersion, componentName, stalenessThresholdMs, probeTimeoutMs = 3000, skipRegistryCheck = false, } = options;
942
+ const workspacePath = getWorkspacePath();
943
+ const coreConfigDir = getCoreConfigDir();
944
+ // 1. Probe all services
945
+ const probeResults = await probeAllServices(undefined, probeTimeoutMs);
946
+ const unhealthyServices = probeResults.filter((r) => !r.healthy);
947
+ // 2. Build version info (registry check for the core package)
948
+ let availableVersion;
949
+ if (!skipRegistryCheck) {
950
+ const cacheDir = componentName
951
+ ? getComponentConfigDir(componentName)
952
+ : coreConfigDir;
953
+ availableVersion = checkRegistryVersion('@karmaniverous/jeeves', cacheDir);
954
+ }
955
+ const versionInfo = probeResults.map((r) => ({
956
+ name: r.name,
957
+ serviceVersion: r.version,
958
+ coreVersion,
959
+ availableVersion: availableVersion && availableVersion !== coreVersion
960
+ ? availableVersion
961
+ : undefined,
962
+ }));
963
+ // 3. Check if templates are available
964
+ const templatePath = join(coreConfigDir, TEMPLATES_DIR);
965
+ const templatesAvailable = existsSync(templatePath);
966
+ // 4. Render Platform template
967
+ registerHelpers();
968
+ const templateSrc = readContentFile('tools-platform.md');
969
+ const template = Handlebars.compile(templateSrc);
970
+ const templateData = {
971
+ services: probeResults,
972
+ unhealthyServices,
973
+ versionInfo: versionInfo.some((v) => v.serviceVersion)
974
+ ? versionInfo
975
+ : undefined,
976
+ templatesAvailable,
977
+ templatePath,
978
+ };
979
+ const platformContent = template(templateData);
980
+ // 5. Write TOOLS.md Platform section
981
+ const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
982
+ await updateManagedSection(toolsPath, platformContent, {
983
+ mode: 'section',
984
+ sectionId: 'Platform',
985
+ markers: TOOLS_MARKERS,
986
+ coreVersion,
987
+ stalenessThresholdMs,
988
+ });
989
+ // 6. Write SOUL.md managed block
990
+ const soulContent = readContentFile('soul-section.md');
991
+ const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
992
+ await updateManagedSection(soulPath, soulContent, {
993
+ mode: 'block',
994
+ markers: SOUL_MARKERS,
995
+ coreVersion,
996
+ stalenessThresholdMs,
997
+ });
998
+ // 7. Write AGENTS.md managed block
999
+ const agentsContent = readContentFile('agents-section.md');
1000
+ const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
1001
+ await updateManagedSection(agentsPath, agentsContent, {
1002
+ mode: 'block',
1003
+ markers: AGENTS_MARKERS,
1004
+ coreVersion,
1005
+ stalenessThresholdMs,
1006
+ });
1007
+ // 8. Copy templates to config dir
1008
+ copyTemplates(coreConfigDir);
1009
+ }
1010
+
1011
+ /**
1012
+ * Timer-based orchestrator for managed content writing.
1013
+ *
1014
+ * @remarks
1015
+ * `ComponentWriter` manages a component's TOOLS.md section writes
1016
+ * and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
1017
+ * on a configurable prime-interval timer cycle.
1018
+ */
1019
+ /**
1020
+ * Orchestrates managed content writing for a single Jeeves component.
1021
+ *
1022
+ * @remarks
1023
+ * Created via `createComponentWriter()`. Manages a timer that fires
1024
+ * at the component's prime-interval, calling `generateToolsContent()`
1025
+ * and `refreshPlatformContent()` on each cycle.
1026
+ */
1027
+ class ComponentWriter {
1028
+ timer;
1029
+ component;
1030
+ configDir;
1031
+ probeTimeoutMs;
1032
+ /** @internal */
1033
+ constructor(component, probeTimeoutMs = 3000) {
1034
+ this.component = component;
1035
+ this.configDir = getComponentConfigDir(component.name);
1036
+ this.probeTimeoutMs = probeTimeoutMs;
1037
+ }
1038
+ /** The component's config directory path. */
1039
+ get componentConfigDir() {
1040
+ return this.configDir;
1041
+ }
1042
+ /** Whether the writer timer is currently running. */
1043
+ get isRunning() {
1044
+ return this.timer !== undefined;
1045
+ }
1046
+ /**
1047
+ * Start the writer timer.
1048
+ *
1049
+ * @remarks
1050
+ * Performs an immediate first write, then sets up the interval.
1051
+ */
1052
+ start() {
1053
+ if (this.timer)
1054
+ return;
1055
+ // Fire immediately, then on interval
1056
+ void this.cycle();
1057
+ this.timer = setInterval(() => void this.cycle(), this.component.refreshIntervalSeconds * 1000);
1058
+ }
1059
+ /** Stop the writer timer. */
1060
+ stop() {
1061
+ if (this.timer) {
1062
+ clearInterval(this.timer);
1063
+ this.timer = undefined;
1064
+ }
1065
+ }
1066
+ /**
1067
+ * Execute a single write cycle.
1068
+ *
1069
+ * @remarks
1070
+ * Calls `generateToolsContent()` and writes the component's
1071
+ * TOOLS.md section via `updateManagedSection()`. Also calls
1072
+ * `refreshPlatformContent()` for shared content maintenance.
1073
+ */
1074
+ async cycle() {
1075
+ try {
1076
+ const workspacePath = getWorkspacePath();
1077
+ const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
1078
+ // Write the component's TOOLS.md section
1079
+ const toolsContent = this.component.generateToolsContent();
1080
+ await updateManagedSection(toolsPath, toolsContent, {
1081
+ mode: 'section',
1082
+ sectionId: this.component.sectionId,
1083
+ markers: TOOLS_MARKERS,
1084
+ coreVersion: CORE_VERSION,
1085
+ });
1086
+ // Platform content maintenance: SOUL.md, AGENTS.md, Platform section
1087
+ await refreshPlatformContent({
1088
+ coreVersion: CORE_VERSION,
1089
+ componentName: this.component.name,
1090
+ skipRegistryCheck: false,
1091
+ probeTimeoutMs: this.probeTimeoutMs,
1092
+ });
1093
+ }
1094
+ catch (err) {
1095
+ const message = err instanceof Error ? err.message : String(err);
1096
+ console.warn(`jeeves-core: ComponentWriter cycle failed for ${this.component.name}: ${message}`);
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ /**
1102
+ * Factory function for creating a ComponentWriter.
1103
+ *
1104
+ * @remarks
1105
+ * Validates the component descriptor at runtime:
1106
+ * - `refreshIntervalSeconds` must be a prime number
1107
+ * - `serviceCommands` and `pluginCommands` must be provided
1108
+ * - `name`, `version`, `sectionId` must be non-empty strings
1109
+ * - `generateToolsContent` must be a function
1110
+ */
1111
+ /**
1112
+ * Check whether a number is prime.
1113
+ *
1114
+ * @param n - Number to check.
1115
+ * @returns `true` if n is prime.
1116
+ */
1117
+ function isPrime(n) {
1118
+ if (n < 2)
1119
+ return false;
1120
+ if (n === 2)
1121
+ return true;
1122
+ if (n % 2 === 0)
1123
+ return false;
1124
+ for (let i = 3; i * i <= n; i += 2) {
1125
+ if (n % i === 0)
1126
+ return false;
1127
+ }
1128
+ return true;
1129
+ }
1130
+ /**
1131
+ * Validate a component descriptor at runtime.
1132
+ *
1133
+ * @param input - The descriptor to validate (typed as unknown for runtime safety).
1134
+ * @throws Error if the descriptor is invalid.
1135
+ */
1136
+ function validateDescriptor(input) {
1137
+ const component = input;
1138
+ if (!component['name'] || typeof component['name'] !== 'string') {
1139
+ throw new Error('JeevesComponent.name must be a non-empty string');
1140
+ }
1141
+ if (!component['version'] || typeof component['version'] !== 'string') {
1142
+ throw new Error('JeevesComponent.version must be a non-empty string');
1143
+ }
1144
+ if (!component['sectionId'] || typeof component['sectionId'] !== 'string') {
1145
+ throw new Error('JeevesComponent.sectionId must be a non-empty string');
1146
+ }
1147
+ if (typeof component['refreshIntervalSeconds'] !== 'number' ||
1148
+ !isPrime(component['refreshIntervalSeconds'])) {
1149
+ throw new Error(`JeevesComponent.refreshIntervalSeconds must be a prime number, got ${String(component['refreshIntervalSeconds'])}`);
1150
+ }
1151
+ if (typeof component['generateToolsContent'] !== 'function') {
1152
+ throw new Error('JeevesComponent.generateToolsContent must be a function');
1153
+ }
1154
+ const svc = component['serviceCommands'];
1155
+ if (!svc ||
1156
+ typeof svc['stop'] !== 'function' ||
1157
+ typeof svc['uninstall'] !== 'function' ||
1158
+ typeof svc['status'] !== 'function') {
1159
+ throw new Error('JeevesComponent.serviceCommands must provide stop, uninstall, and status functions');
1160
+ }
1161
+ const plg = component['pluginCommands'];
1162
+ if (!plg || typeof plg['uninstall'] !== 'function') {
1163
+ throw new Error('JeevesComponent.pluginCommands must provide an uninstall function');
1164
+ }
1165
+ }
1166
+ /**
1167
+ * Create a ComponentWriter for a validated component descriptor.
1168
+ *
1169
+ * @param component - The component descriptor to validate and wrap.
1170
+ * @param options - Optional configuration.
1171
+ * @returns A new `ComponentWriter` instance.
1172
+ * @throws Error if the component descriptor is invalid.
1173
+ */
1174
+ function createComponentWriter(component, options) {
1175
+ validateDescriptor(component);
1176
+ return new ComponentWriter(component, options?.probeTimeoutMs);
1177
+ }
1178
+
1179
+ /**
1180
+ * One-shot content seeding used by the CLI install command.
1181
+ *
1182
+ * @remarks
1183
+ * Seeds SOUL.md, AGENTS.md, and TOOLS.md Platform section using the same
1184
+ * `updateManagedSection()` code path as writer cycles. Also copies templates
1185
+ * and creates core config with defaults if missing.
1186
+ */
1187
+ /**
1188
+ * Create the core config file with defaults if it doesn't already exist.
1189
+ *
1190
+ * @param coreConfigDir - Path to the core config directory.
1191
+ */
1192
+ function ensureCoreConfig(coreConfigDir) {
1193
+ if (!existsSync(coreConfigDir)) {
1194
+ mkdirSync(coreConfigDir, { recursive: true });
1195
+ }
1196
+ const configPath = join(coreConfigDir, CONFIG_FILE);
1197
+ if (existsSync(configPath))
1198
+ return;
1199
+ const defaults = coreConfigSchema.parse({});
1200
+ const configWithSchema = {
1201
+ $schema: './config.schema.json',
1202
+ ...defaults,
1203
+ };
1204
+ writeFileSync(configPath, JSON.stringify(configWithSchema, null, 2), 'utf-8');
1205
+ // Write JSON schema file alongside config
1206
+ const schemaPath = join(coreConfigDir, 'config.schema.json');
1207
+ const jsonSchema = generateJsonSchema();
1208
+ writeFileSync(schemaPath, JSON.stringify(jsonSchema, null, 2), 'utf-8');
1209
+ }
1210
+ /**
1211
+ * Seed all platform content into the workspace.
1212
+ *
1213
+ * @remarks
1214
+ * Uses the same `updateManagedSection()` code path as writer cycles.
1215
+ * Creates core config with defaults if missing. Copies templates.
1216
+ * Jaccard cleanup detection runs automatically via `updateManagedSection`.
1217
+ *
1218
+ * @param options - Seeding configuration.
1219
+ */
1220
+ async function seedContent(options) {
1221
+ const coreConfigDir = getCoreConfigDir();
1222
+ // Ensure core config exists
1223
+ ensureCoreConfig(coreConfigDir);
1224
+ // Seed content via the same code path as writer cycles
1225
+ await refreshPlatformContent({
1226
+ coreVersion: options.coreVersion,
1227
+ probeTimeoutMs: options.probeTimeoutMs ?? 3000,
1228
+ skipRegistryCheck: options.skipRegistryCheck ?? true,
1229
+ });
1230
+ }
1231
+
1232
+ export { AGENTS_MARKERS, CLEANUP_FLAG, COMPONENT_CONFIG_PREFIX, CONFIG_FILE, CORE_CONFIG_DIR, CORE_VERSION, ComponentWriter, DEFAULT_PORTS, META_PORT, REGISTRY_CACHE_FILE, RUNNER_PORT, SECTION_IDS, SECTION_ORDER, SERVER_PORT, SOUL_MARKERS, STALENESS_THRESHOLD_MS, TEMPLATES_DIR, TOOLS_MARKERS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_FILES, checkRegistryVersion, coreConfigSchema, createComponentWriter, formatBeginMarker, formatEndMarker, generateJsonSchema, getComponentConfigDir, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getServiceUrl, getWorkspacePath, init, jaccard, needsCleanup, parseManaged, probeAllServices, probeService, refreshPlatformContent, resetInit, seedContent, shingles, shouldWrite, updateManagedSection };