@karmaniverous/jeeves 0.4.7 → 0.5.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.
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import * as commander from 'commander';
3
- import { execSync, spawnSync } from 'node:child_process';
3
+ import { major, valid, gte } from 'semver';
4
+ import { JSONPath } from 'jsonpath-plus';
5
+ import { join, resolve, dirname } from 'node:path';
4
6
  import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, cpSync, rmSync } from 'node:fs';
5
- import { join, dirname } from 'node:path';
6
- import { valid, gte } from 'semver';
7
7
  import { z } from 'zod';
8
+ import { execSync, spawnSync } from 'node:child_process';
8
9
  import { lock } from 'proper-lockfile';
9
10
  import { fileURLToPath } from 'node:url';
10
11
  import { packageDirectorySync } from 'package-directory';
@@ -157,8 +158,8 @@ const ALL_MARKERS = [
157
158
  const VERSION_STAMP_PATTERN = /<!--\s*(.+?)\s*\|\s*core:(\S+)\s*\|\s*(\S+)\s*-->/;
158
159
  /** Staleness threshold for version-stamp convergence in milliseconds. */
159
160
  const STALENESS_THRESHOLD_MS = 5 * 60 * 1000;
160
- /** Warning text prepended inside managed block when cleanup is needed. */
161
- 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.';
161
+ /** Warning text injected inside managed block when cleanup is needed. */
162
+ const CLEANUP_FLAG = '> ⚠️ CLEANUP NEEDED: Orphaned Jeeves content detected outside this managed block. Review the file and remove any content outside the BEGIN/END markers that duplicates what appears inside them.';
162
163
 
163
164
  /**
164
165
  * Directory and file path conventions for the Jeeves platform.
@@ -175,7 +176,13 @@ const WORKSPACE_FILES = {
175
176
  agents: 'AGENTS.md',
176
177
  /** HEARTBEAT.md — platform status and health alerts. */
177
178
  heartbeat: 'HEARTBEAT.md',
179
+ /** MEMORY.md — curated long-term memory. */
180
+ memory: 'MEMORY.md',
178
181
  };
182
+ /** Skill directory name within workspace. */
183
+ const SKILLS_DIR = 'skills';
184
+ /** Jeeves skill directory name. */
185
+ const JEEVES_SKILL_DIR = 'jeeves';
179
186
  /** Templates directory name within core config. */
180
187
  const TEMPLATES_DIR = 'templates';
181
188
  /** Core config file name. */
@@ -261,14 +268,318 @@ const PLATFORM_COMPONENTS = [
261
268
  * Core library version, inlined at build time.
262
269
  *
263
270
  * @remarks
264
- * The `0.4.6` placeholder is replaced by
271
+ * The `0.5.0` placeholder is replaced by
265
272
  * `@rollup/plugin-replace` during the build with the actual version
266
273
  * from `package.json`. This ensures the correct version survives
267
274
  * when consumers bundle core into their own dist (where runtime
268
275
  * `import.meta.url`-based resolution would find the wrong package.json).
269
276
  */
270
277
  /** The core library version from package.json (inlined at build time). */
271
- const CORE_VERSION = '0.4.6';
278
+ const CORE_VERSION = '0.5.0';
279
+
280
+ /**
281
+ * Runtime Node.js version floor check.
282
+ *
283
+ * @module
284
+ */
285
+ /** Minimum supported Node.js major version. */
286
+ const MIN_NODE_MAJOR = 22;
287
+ /**
288
+ * Check that the running Node.js version meets the minimum requirement.
289
+ * Prints an error and exits with code 1 if the check fails.
290
+ */
291
+ function checkNodeVersion() {
292
+ const nodeMajor = major(process.versions.node);
293
+ if (nodeMajor < MIN_NODE_MAJOR) {
294
+ console.error(`Error: jeeves requires Node.js >= ${String(MIN_NODE_MAJOR)}. Current: ${process.versions.node}`);
295
+ process.exit(1);
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Workspace-level shared configuration: `jeeves.config.json`.
301
+ *
302
+ * @remarks
303
+ * Lives at the OpenClaw workspace root alongside TOOLS.md and SOUL.md.
304
+ * Provides namespaced shared defaults consumed by the root Jeeves CLI.
305
+ * Resolution precedence: CLI flags → env vars → jeeves.config.json → defaults.
306
+ *
307
+ * This does not replace component-owned config schemas (Decision 41).
308
+ */
309
+ /** Workspace config file name. */
310
+ const WORKSPACE_CONFIG_FILE = 'jeeves.config.json';
311
+ /** Core shared config section. */
312
+ const workspaceCoreConfigSchema = z
313
+ .object({
314
+ /** Workspace root path. */
315
+ workspace: z.string().optional().describe('Workspace root path'),
316
+ /** Platform config root path. */
317
+ configRoot: z.string().optional().describe('Platform config root path'),
318
+ /** OpenClaw gateway URL. */
319
+ gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
320
+ })
321
+ .partial();
322
+ /** Memory shared config section. */
323
+ const workspaceMemoryConfigSchema = z
324
+ .object({
325
+ /** MEMORY.md character budget. */
326
+ budget: z.number().int().positive().optional().describe('Memory budget'),
327
+ /** Warning threshold as a fraction of budget. */
328
+ warningThreshold: z
329
+ .number()
330
+ .min(0)
331
+ .max(1)
332
+ .optional()
333
+ .describe('Memory warning threshold'),
334
+ /** Staleness threshold in days. */
335
+ staleDays: z
336
+ .number()
337
+ .int()
338
+ .positive()
339
+ .optional()
340
+ .describe('Memory staleness threshold in days'),
341
+ })
342
+ .partial();
343
+ /** Workspace config Zod schema. */
344
+ const workspaceConfigSchema = z.object({
345
+ /** JSON Schema pointer for IDE autocomplete. */
346
+ $schema: z.string().optional().describe('JSON Schema pointer'),
347
+ /** Core shared defaults. */
348
+ core: workspaceCoreConfigSchema.optional(),
349
+ /** Memory hygiene shared defaults. */
350
+ memory: workspaceMemoryConfigSchema.optional(),
351
+ });
352
+ /** Built-in workspace config defaults. */
353
+ const WORKSPACE_CONFIG_DEFAULTS = {
354
+ core: {
355
+ workspace: '.',
356
+ configRoot: './config',
357
+ gatewayUrl: 'http://127.0.0.1:3000',
358
+ },
359
+ memory: {
360
+ budget: 20_000,
361
+ warningThreshold: 0.8,
362
+ staleDays: 30,
363
+ },
364
+ };
365
+ /**
366
+ * Load workspace config from `jeeves.config.json` at a given path.
367
+ *
368
+ * @param workspacePath - Workspace root directory.
369
+ * @returns Parsed config or undefined if missing or invalid.
370
+ */
371
+ function loadWorkspaceConfig(workspacePath) {
372
+ const configPath = join(workspacePath, WORKSPACE_CONFIG_FILE);
373
+ if (!existsSync(configPath))
374
+ return undefined;
375
+ try {
376
+ const raw = readFileSync(configPath, 'utf-8');
377
+ const parsed = JSON.parse(raw);
378
+ return workspaceConfigSchema.parse(parsed);
379
+ }
380
+ catch (err) {
381
+ const msg = err instanceof Error ? err.message : String(err);
382
+ console.warn(`jeeves-core: failed to load ${configPath}: ${msg}`);
383
+ return undefined;
384
+ }
385
+ }
386
+ /**
387
+ * Resolve a config value with four-tier precedence.
388
+ *
389
+ * @param flagValue - CLI flag value (highest priority).
390
+ * @param envValue - Environment variable value.
391
+ * @param fileValue - Value from jeeves.config.json.
392
+ * @param defaultValue - Built-in default (lowest priority).
393
+ * @returns The resolved value with provenance annotation.
394
+ */
395
+ function resolveConfigValue(flagValue, envValue, fileValue, defaultValue) {
396
+ if (flagValue !== undefined)
397
+ return { value: flagValue, provenance: 'flag' };
398
+ if (envValue !== undefined)
399
+ return { value: envValue, provenance: 'env' };
400
+ if (fileValue !== undefined)
401
+ return { value: fileValue, provenance: 'file' };
402
+ return { value: defaultValue, provenance: 'default' };
403
+ }
404
+
405
+ /**
406
+ * Workspace and config root initialization.
407
+ *
408
+ * @remarks
409
+ * `init()` must be called once before any other core library functions.
410
+ * It caches `workspacePath` and `configRoot` at module level.
411
+ * Core derives all namespaced paths from these values:
412
+ * - `{configRoot}/jeeves-core/` for core config
413
+ * - `{configRoot}/jeeves-{name}/` for each component
414
+ */
415
+ let state;
416
+ /**
417
+ * Initialize the core library with workspace and config root paths.
418
+ *
419
+ * @param options - Workspace and config root paths.
420
+ */
421
+ function init(options) {
422
+ state = {
423
+ workspacePath: options.workspacePath,
424
+ configRoot: options.configRoot,
425
+ coreConfigDir: join(options.configRoot, CORE_CONFIG_DIR),
426
+ };
427
+ }
428
+ /**
429
+ * Get the cached workspace path.
430
+ *
431
+ * @throws Error if `init()` has not been called.
432
+ */
433
+ function getWorkspacePath() {
434
+ if (!state)
435
+ throw new Error('jeeves-core: init() must be called first');
436
+ return state.workspacePath;
437
+ }
438
+ /**
439
+ * Get the core config directory path.
440
+ *
441
+ * @throws Error if `init()` has not been called.
442
+ */
443
+ function getCoreConfigDir() {
444
+ if (!state)
445
+ throw new Error('jeeves-core: init() must be called first');
446
+ return state.coreConfigDir;
447
+ }
448
+
449
+ var init$1 = /*#__PURE__*/Object.freeze({
450
+ __proto__: null,
451
+ getCoreConfigDir: getCoreConfigDir,
452
+ getWorkspacePath: getWorkspacePath,
453
+ init: init
454
+ });
455
+
456
+ /**
457
+ * Shared CLI defaults and resolution for Jeeves CLI commands.
458
+ *
459
+ * @remarks
460
+ * All root CLI commands share workspace/config-root resolution. Values follow
461
+ * the shared precedence model: flags → env → jeeves.config.json → defaults.
462
+ */
463
+ /** Default workspace path. */
464
+ const DEFAULT_WORKSPACE = WORKSPACE_CONFIG_DEFAULTS.core.workspace;
465
+ /** Default config root path. */
466
+ const DEFAULT_CONFIG_ROOT = WORKSPACE_CONFIG_DEFAULTS.core.configRoot;
467
+ /** Read a numeric env var or return undefined if missing, empty, or invalid. */
468
+ function readNumericEnv(name) {
469
+ const raw = process.env[name];
470
+ if (raw === undefined || raw.trim() === '')
471
+ return undefined;
472
+ const value = Number(raw);
473
+ return Number.isFinite(value) ? value : undefined;
474
+ }
475
+ /**
476
+ * Resolve shared CLI config using flags, env, file, and defaults.
477
+ *
478
+ * @param opts - Parsed CLI workspace/config-root options.
479
+ * @returns Resolved config tree with provenance on every leaf.
480
+ */
481
+ function resolveCliConfig(opts) {
482
+ const workspaceSeed = resolveConfigValue(opts.workspace, process.env['JEEVES_WORKSPACE'], undefined, DEFAULT_WORKSPACE);
483
+ const fileConfig = loadWorkspaceConfig(workspaceSeed.value);
484
+ return {
485
+ core: {
486
+ workspace: resolveConfigValue(opts.workspace, process.env['JEEVES_WORKSPACE'], fileConfig?.core?.workspace, DEFAULT_WORKSPACE),
487
+ configRoot: resolveConfigValue(opts.configRoot, process.env['JEEVES_CONFIG_ROOT'], fileConfig?.core?.configRoot, DEFAULT_CONFIG_ROOT),
488
+ gatewayUrl: resolveConfigValue(undefined, process.env['JEEVES_GATEWAY_URL'], fileConfig?.core?.gatewayUrl, WORKSPACE_CONFIG_DEFAULTS.core.gatewayUrl),
489
+ },
490
+ memory: {
491
+ budget: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_BUDGET'), fileConfig?.memory?.budget, WORKSPACE_CONFIG_DEFAULTS.memory.budget),
492
+ warningThreshold: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_WARNING_THRESHOLD'), fileConfig?.memory?.warningThreshold, WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold),
493
+ staleDays: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_STALE_DAYS'), fileConfig?.memory?.staleDays, WORKSPACE_CONFIG_DEFAULTS.memory.staleDays),
494
+ },
495
+ };
496
+ }
497
+ /**
498
+ * Initialize core from standard CLI options after resolving shared defaults.
499
+ *
500
+ * @param opts - Parsed Commander options with workspace and configRoot.
501
+ * @returns Resolved CLI config.
502
+ */
503
+ function initFromOptions(opts) {
504
+ const resolved = resolveCliConfig(opts);
505
+ init({
506
+ workspacePath: resolve(resolved.core.workspace.value),
507
+ configRoot: resolve(resolved.core.configRoot.value),
508
+ });
509
+ return resolved;
510
+ }
511
+
512
+ /**
513
+ * `jeeves config [jsonpath]` — inspect effective shared CLI configuration.
514
+ *
515
+ * @remarks
516
+ * Shows effective values and provenance using the shared precedence model.
517
+ * Optional JSONPath filters the resolved config tree.
518
+ */
519
+ /** Format a provenance tag. */
520
+ function provenanceTag(provenance) {
521
+ return `[${provenance}]`;
522
+ }
523
+ /** Whether a value is a resolved leaf with provenance. */
524
+ function isResolvedLeaf(value) {
525
+ return (typeof value === 'object' &&
526
+ value !== null &&
527
+ 'value' in value &&
528
+ 'provenance' in value);
529
+ }
530
+ /**
531
+ * Build the effective shared CLI config tree.
532
+ *
533
+ * @param opts - Parsed CLI workspace/config-root options.
534
+ * @returns Effective config tree with provenance on each leaf.
535
+ */
536
+ function buildEffectiveConfig(opts) {
537
+ return initFromOptions(opts);
538
+ }
539
+ /** Print the full effective config tree by walking the resolved structure. */
540
+ function printEffectiveConfig(config) {
541
+ console.log('Effective jeeves.config.json:');
542
+ console.log('');
543
+ for (const [section, entries] of Object.entries(config)) {
544
+ for (const [key, leaf] of Object.entries(entries)) {
545
+ console.log(` ${section}.${key}: ${JSON.stringify(leaf.value)} ${provenanceTag(leaf.provenance)}`);
546
+ }
547
+ }
548
+ }
549
+ /**
550
+ * Register the `jeeves config` command.
551
+ *
552
+ * @param program - Root Commander program.
553
+ */
554
+ function registerConfigCommand(program) {
555
+ program
556
+ .command('config')
557
+ .description('Show effective shared configuration with provenance')
558
+ .argument('[jsonpath]', 'JSONPath filter (e.g. $.core.workspace)')
559
+ .option('-w, --workspace <path>', 'Workspace root path')
560
+ .option('-c, --config-root <path>', 'Platform config root path')
561
+ .action((jsonpath, opts) => {
562
+ const effective = buildEffectiveConfig(opts);
563
+ if (jsonpath) {
564
+ const result = JSONPath({
565
+ path: jsonpath,
566
+ json: effective,
567
+ wrap: false,
568
+ });
569
+ if (result === undefined) {
570
+ console.log(`No config value for: ${jsonpath}`);
571
+ return;
572
+ }
573
+ if (isResolvedLeaf(result)) {
574
+ console.log(`${jsonpath}: ${JSON.stringify(result.value)} ${provenanceTag(result.provenance)}`);
575
+ return;
576
+ }
577
+ console.log(JSON.stringify(result, null, 2));
578
+ return;
579
+ }
580
+ printEffectiveConfig(effective);
581
+ });
582
+ }
272
583
 
273
584
  /**
274
585
  * Dynamic discovery of installed Jeeves component CLIs.
@@ -345,57 +656,6 @@ function registerComponentProxies(program, componentNames) {
345
656
  }
346
657
  }
347
658
 
348
- /**
349
- * Workspace and config root initialization.
350
- *
351
- * @remarks
352
- * `init()` must be called once before any other core library functions.
353
- * It caches `workspacePath` and `configRoot` at module level.
354
- * Core derives all namespaced paths from these values:
355
- * - `{configRoot}/jeeves-core/` for core config
356
- * - `{configRoot}/jeeves-{name}/` for each component
357
- */
358
- let state;
359
- /**
360
- * Initialize the core library with workspace and config root paths.
361
- *
362
- * @param options - Workspace and config root paths.
363
- */
364
- function init(options) {
365
- state = {
366
- workspacePath: options.workspacePath,
367
- configRoot: options.configRoot,
368
- coreConfigDir: join(options.configRoot, CORE_CONFIG_DIR),
369
- };
370
- }
371
- /**
372
- * Get the cached workspace path.
373
- *
374
- * @throws Error if `init()` has not been called.
375
- */
376
- function getWorkspacePath() {
377
- if (!state)
378
- throw new Error('jeeves-core: init() must be called first');
379
- return state.workspacePath;
380
- }
381
- /**
382
- * Get the core config directory path.
383
- *
384
- * @throws Error if `init()` has not been called.
385
- */
386
- function getCoreConfigDir() {
387
- if (!state)
388
- throw new Error('jeeves-core: init() must be called first');
389
- return state.coreConfigDir;
390
- }
391
-
392
- var init$1 = /*#__PURE__*/Object.freeze({
393
- __proto__: null,
394
- getCoreConfigDir: getCoreConfigDir,
395
- getWorkspacePath: getWorkspacePath,
396
- init: init
397
- });
398
-
399
659
  /**
400
660
  * Core configuration schema and resolution.
401
661
  *
@@ -747,7 +1007,7 @@ function parseHeartbeat(fileContent) {
747
1007
  const userContent = fileContent.slice(0, headingIndex).trim();
748
1008
  const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
749
1009
  const entries = [];
750
- const h2Re = /^## (jeeves-\S+?)(?:: declined)?$/gm;
1010
+ const h2Re = /^## (jeeves-\S+?|MEMORY\.md)(?:: declined)?$/gm;
751
1011
  let match;
752
1012
  const h2Positions = [];
753
1013
  while ((match = h2Re.exec(sectionContent)) !== null) {
@@ -1550,10 +1810,10 @@ async function updateManagedSection(filePath, content, options = {}) {
1550
1810
  ? `# ${markers.title}\n\n${sectionText}`
1551
1811
  : sectionText;
1552
1812
  }
1553
- // Combine beforeContent + userContent for the user zone.
1554
- // When migrating from top→bottom, beforeContent is empty and
1555
- // userContent has the real content. When already at bottom,
1556
- // beforeContent has the user content and userContent is empty.
1813
+ // Build the full managed block
1814
+ const beginLine = formatBeginMarker(markers.begin, coreVersion);
1815
+ const endLine = formatEndMarker(markers.end);
1816
+ // Combine all user content for cleanup detection
1557
1817
  const rawUserContent = [parsed.beforeContent, parsed.userContent]
1558
1818
  .filter(Boolean)
1559
1819
  .join('\n\n')
@@ -1561,9 +1821,6 @@ async function updateManagedSection(filePath, content, options = {}) {
1561
1821
  // Strip foreign managed blocks from user content (cross-contamination fix)
1562
1822
  const userContent = stripForeignMarkers(rawUserContent, markers);
1563
1823
  const cleanupNeeded = needsCleanup(newManagedBody, userContent);
1564
- // Build the full managed block
1565
- const beginLine = formatBeginMarker(markers.begin, coreVersion);
1566
- const endLine = formatEndMarker(markers.end);
1567
1824
  const managedParts = [];
1568
1825
  managedParts.push(beginLine);
1569
1826
  if (cleanupNeeded) {
@@ -1575,26 +1832,54 @@ async function updateManagedSection(filePath, content, options = {}) {
1575
1832
  managedParts.push('');
1576
1833
  managedParts.push(endLine);
1577
1834
  const managedBlock = managedParts.join('\n');
1578
- const position = markers.position ?? 'top';
1579
- const fileParts = [];
1580
- if (position === 'bottom') {
1581
- // User content first, managed block at end
1582
- if (userContent) {
1583
- fileParts.push(userContent);
1835
+ let newFileContent;
1836
+ if (parsed.found) {
1837
+ // Existing block: update in place — preserve position, don't move.
1838
+ // Strip foreign managed blocks from both content zones (cross-contamination fix).
1839
+ const cleanBefore = stripForeignMarkers(parsed.beforeContent, markers);
1840
+ const cleanAfter = stripForeignMarkers(parsed.userContent, markers);
1841
+ const fileParts = [];
1842
+ if (cleanBefore) {
1843
+ fileParts.push(cleanBefore);
1584
1844
  fileParts.push('');
1585
1845
  }
1586
1846
  fileParts.push(managedBlock);
1847
+ if (cleanAfter) {
1848
+ fileParts.push('');
1849
+ fileParts.push(cleanAfter);
1850
+ }
1851
+ fileParts.push('');
1852
+ newFileContent = fileParts.join('\n');
1587
1853
  }
1588
1854
  else {
1589
- // Managed block first (legacy default), user content below
1590
- fileParts.push(managedBlock);
1591
- if (userContent) {
1592
- fileParts.push('');
1593
- fileParts.push(userContent);
1855
+ // No existing block: insert new block using the configured position.
1856
+ // Strip orphaned same-type BEGIN markers from user content to prevent
1857
+ // the parser from pairing them with the new END marker on the next cycle.
1858
+ const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1859
+ const orphanedBeginRe = new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$\\n?`, 'gm');
1860
+ const cleanUserContent = userContent
1861
+ .replace(orphanedBeginRe, '')
1862
+ .replace(/\n{3,}/g, '\n\n')
1863
+ .trim();
1864
+ const position = markers.position ?? 'top';
1865
+ const fileParts = [];
1866
+ if (position === 'bottom') {
1867
+ if (cleanUserContent) {
1868
+ fileParts.push(cleanUserContent);
1869
+ fileParts.push('');
1870
+ }
1871
+ fileParts.push(managedBlock);
1872
+ }
1873
+ else {
1874
+ fileParts.push(managedBlock);
1875
+ if (cleanUserContent) {
1876
+ fileParts.push('');
1877
+ fileParts.push(cleanUserContent);
1878
+ }
1594
1879
  }
1880
+ fileParts.push('');
1881
+ newFileContent = fileParts.join('\n');
1595
1882
  }
1596
- fileParts.push('');
1597
- const newFileContent = fileParts.join('\n');
1598
1883
  atomicWrite(filePath, newFileContent);
1599
1884
  });
1600
1885
  }
@@ -1725,6 +2010,135 @@ async function refreshPlatformContent(options) {
1725
2010
  copyTemplates(coreConfigDir);
1726
2011
  }
1727
2012
 
2013
+ var skillContent = `---
2014
+ name: jeeves
2015
+ description: Jeeves platform architecture, data flow, component interaction, scripts repo, and coordination knowledge. Use when making architectural decisions, coordinating across components, checking platform health, managing service lifecycle, or working with the scripts repo.
2016
+ ---
2017
+
2018
+ # Jeeves Platform Skill
2019
+
2020
+ ## Platform Architecture
2021
+
2022
+ Jeeves is a four-component platform coordinated by a shared library (\`@karmaniverous/jeeves\`):
2023
+
2024
+ | Component | Role | Port |
2025
+ |-----------|------|------|
2026
+ | **jeeves-runner** | Execute: scheduled jobs, SQLite state, HTTP API | 1937 |
2027
+ | **jeeves-watcher** | Index: file→Qdrant semantic indexing, inference rules | 1936 |
2028
+ | **jeeves-server** | Present: web UI, file browser, doc render, export | 1934 |
2029
+ | **jeeves-meta** | Distill: LLM synthesis, .meta/ directories, scheduling | 1938 |
2030
+
2031
+ Core (\`@karmaniverous/jeeves\`) is a **library + CLI**, not a service. No port.
2032
+
2033
+ ## Data Flow
2034
+
2035
+ \`\`\`
2036
+ Files → Watcher (index) → Qdrant → Meta (synthesize) → .meta/ → Watcher (re-index)
2037
+ ↓
2038
+ Runner (schedule) → Scripts → Services ← Server (present) ← Browser
2039
+ \`\`\`
2040
+
2041
+ ## Component Interaction
2042
+
2043
+ - **Watcher** indexes files into Qdrant with inference rules and enrichments.
2044
+ - **Meta** reads from Qdrant, synthesizes \`.meta/\` directories, which watcher re-indexes.
2045
+ - **Runner** executes scheduled scripts that may call any service's HTTP API.
2046
+ - **Server** presents files, renders documents, and provides the event gateway.
2047
+ - **Core** provides shared content management (TOOLS.md, SOUL.md, AGENTS.md), service discovery, config resolution, and the component SDK.
2048
+
2049
+ ## Service Discovery
2050
+
2051
+ Services find each other via config resolution:
2052
+ 1. Component's own config file (\`{configRoot}/jeeves-{name}/config.json\`)
2053
+ 2. Core config file (\`{configRoot}/jeeves-core/config.json\`)
2054
+ 3. Default port constants
2055
+
2056
+ ## Scripts Repo
2057
+
2058
+ Location: \`{configRoot}/jeeves-core/scripts/\`
2059
+ Template: \`@karmaniverous/jeeves-scripts-template\`
2060
+
2061
+ Scripts use utilities from \`@karmaniverous/jeeves\` (general) and \`@karmaniverous/jeeves-runner\` (runner-specific). Any script that could be useful outside runner scheduling belongs in core.
2062
+
2063
+ ## Managed Content System
2064
+
2065
+ Core maintains managed sections in workspace files using comment markers:
2066
+ - **TOOLS.md** — Component sections (section mode) + Platform section
2067
+ - **SOUL.md** — Professional discipline and behavioral foundations (block mode)
2068
+ - **AGENTS.md** — Operational protocols and memory architecture (block mode)
2069
+ - **HEARTBEAT.md** — Platform health status (heading-based)
2070
+
2071
+ Managed blocks are stationary after initial insertion. Cleanup detection uses Jaccard similarity on 3-word shingles. Cleanup escalation spawns a gateway session when orphaned content is detected.
2072
+
2073
+ ## Workspace Configuration
2074
+
2075
+ \`jeeves.config.json\` at workspace root provides shared defaults:
2076
+ - Precedence: CLI flags → env vars → file → defaults
2077
+ - Namespaced: \`core.*\` (workspace, configRoot, gatewayUrl) and \`memory.*\` (budget, warningThreshold, staleDays)
2078
+ - Inspect with \`jeeves config [jsonpath]\`
2079
+
2080
+ ## HEARTBEAT Protocol
2081
+
2082
+ The HEARTBEAT system uses a state machine per component:
2083
+ \`not_installed → deps_missing → config_missing → service_not_installed → service_stopped → healthy\`
2084
+
2085
+ Dependency-aware: hard deps block alerts, soft deps add informational notes. Declined components are tracked via heading suffix.
2086
+
2087
+ ## Plugin Lifecycle
2088
+
2089
+ \`\`\`bash
2090
+ # Core install (seed workspace content)
2091
+ npx @karmaniverous/jeeves install
2092
+
2093
+ # Component plugin install
2094
+ npx @karmaniverous/jeeves-{component}-openclaw install
2095
+
2096
+ # Component plugin uninstall
2097
+ npx @karmaniverous/jeeves-{component}-openclaw uninstall
2098
+
2099
+ # Core uninstall (remove managed sections)
2100
+ npx @karmaniverous/jeeves uninstall
2101
+ \`\`\`
2102
+
2103
+ ## Memory Hygiene
2104
+
2105
+ MEMORY.md has a character budget (default 20,000). Core tracks:
2106
+ - Character count and usage percentage
2107
+ - Warning at 80% of budget
2108
+ - Stale section candidates (H2 sections whose most recent ISO date exceeds the staleness threshold)
2109
+ - Evergreen sections (no dates) are never flagged
2110
+
2111
+ Review is human/agent-mediated — core does not auto-delete.
2112
+
2113
+ ### HEARTBEAT Integration
2114
+
2115
+ Memory hygiene is checked on every \`ComponentWriter\` cycle alongside component health. When budget or staleness thresholds are breached, a \`## MEMORY.md\` alert appears in HEARTBEAT.md under \`# Jeeves Platform Status\`. The alert includes character count, budget usage percentage, and any stale section names. When memory is healthy, the heading is absent — no alert content, no LLM cost on heartbeat polls.
2116
+
2117
+ The \`## MEMORY.md\` heading follows the same declined/active lifecycle as component headings (\`## jeeves-{name}\`). Users can decline memory alerts by changing the heading to \`## MEMORY.md: declined\`.
2118
+ `;
2119
+
2120
+ /**
2121
+ * Skill seeding: write the `jeeves` workspace skill unconditionally.
2122
+ *
2123
+ * @remarks
2124
+ * The skill file is entirely generated — no user-authored content (Decision 48).
2125
+ * Every installer (core CLI and component plugins) writes it unconditionally.
2126
+ * Content is inlined at build time via `rollup-plugin-md.ts`.
2127
+ */
2128
+ /**
2129
+ * Seed the jeeves workspace skill file.
2130
+ *
2131
+ * @param workspacePath - Workspace root directory.
2132
+ */
2133
+ function seedSkill(workspacePath) {
2134
+ const skillDir = join(workspacePath, SKILLS_DIR, JEEVES_SKILL_DIR);
2135
+ if (!existsSync(skillDir)) {
2136
+ mkdirSync(skillDir, { recursive: true });
2137
+ }
2138
+ const skillPath = join(skillDir, 'SKILL.md');
2139
+ writeFileSync(skillPath, skillContent, 'utf-8');
2140
+ }
2141
+
1728
2142
  /**
1729
2143
  * One-shot content seeding used by the CLI install command.
1730
2144
  *
@@ -1783,27 +2197,8 @@ async function seedContent(options) {
1783
2197
  content: `- ${NOT_INSTALLED_ALERTS[name]}`,
1784
2198
  }));
1785
2199
  await writeHeartbeatSection(heartbeatPath, entries);
1786
- }
1787
-
1788
- /**
1789
- * Shared CLI defaults and option registration for Jeeves CLI commands.
1790
- *
1791
- * @remarks
1792
- * All three CLI commands (install, uninstall, status) share the same
1793
- * `--workspace` and `--config-root` options with the same defaults.
1794
- * This module centralizes them to eliminate duplication.
1795
- */
1796
- /** Default workspace path (current directory). */
1797
- const DEFAULT_WORKSPACE = '.';
1798
- /** Default config root path. */
1799
- const DEFAULT_CONFIG_ROOT = './config';
1800
- /**
1801
- * Initialize core from standard CLI options.
1802
- *
1803
- * @param opts - Parsed Commander options with workspace and configRoot.
1804
- */
1805
- function initFromOptions(opts) {
1806
- init({ workspacePath: opts.workspace, configRoot: opts.configRoot });
2200
+ // Seed jeeves workspace skill (Decision 48: overwrite-on-install)
2201
+ seedSkill(getWorkspacePath());
1807
2202
  }
1808
2203
 
1809
2204
  /**
@@ -1824,14 +2219,14 @@ function registerInstallCommand(program) {
1824
2219
  program
1825
2220
  .command('install')
1826
2221
  .description('Seed Jeeves platform content into the workspace')
1827
- .option('-w, --workspace <path>', 'Workspace root path', DEFAULT_WORKSPACE)
1828
- .option('-c, --config-root <path>', 'Platform config root path', DEFAULT_CONFIG_ROOT)
2222
+ .option('-w, --workspace <path>', 'Workspace root path')
2223
+ .option('-c, --config-root <path>', 'Platform config root path')
1829
2224
  .action(async (opts) => {
2225
+ const resolved = initFromOptions(opts);
1830
2226
  console.log('Jeeves platform install');
1831
- console.log(` Workspace: ${opts.workspace}`);
1832
- console.log(` Config root: ${opts.configRoot}`);
2227
+ console.log(` Workspace: ${resolved.core.workspace.value}`);
2228
+ console.log(` Config root: ${resolved.core.configRoot.value}`);
1833
2229
  console.log();
1834
- initFromOptions(opts);
1835
2230
  await seedContent({
1836
2231
  coreVersion: CORE_VERSION,
1837
2232
  });
@@ -1845,6 +2240,92 @@ function registerInstallCommand(program) {
1845
2240
  });
1846
2241
  }
1847
2242
 
2243
+ /**
2244
+ * Memory budget accounting and staleness detection for MEMORY.md.
2245
+ *
2246
+ * @remarks
2247
+ * Scans MEMORY.md for ISO date patterns in H2/H3 headings and bullet items.
2248
+ * Reports character count against a configured budget, warning threshold state,
2249
+ * and stale section candidates. Does not auto-delete: review remains
2250
+ * human- or agent-mediated (Decision 42).
2251
+ */
2252
+ /** ISO date pattern: YYYY-MM-DD. */
2253
+ const ISO_DATE_RE = /\b(\d{4}-\d{2}-\d{2})\b/g;
2254
+ /** H2 heading pattern used to split sections. */
2255
+ const H2_RE = /^## /m;
2256
+ /**
2257
+ * Extract the most recent ISO date from a string.
2258
+ *
2259
+ * @param text - Text to scan for dates.
2260
+ * @returns The most recent date found, or undefined.
2261
+ */
2262
+ function extractMostRecentDate(text) {
2263
+ const matches = text.match(ISO_DATE_RE);
2264
+ if (!matches)
2265
+ return undefined;
2266
+ let latest;
2267
+ for (const match of matches) {
2268
+ const d = new Date(match + 'T00:00:00Z');
2269
+ if (!Number.isNaN(d.getTime())) {
2270
+ if (!latest || d > latest)
2271
+ latest = d;
2272
+ }
2273
+ }
2274
+ return latest;
2275
+ }
2276
+ /**
2277
+ * Analyze MEMORY.md for budget and staleness.
2278
+ *
2279
+ * @param options - Analysis configuration.
2280
+ * @returns Memory hygiene result.
2281
+ */
2282
+ function analyzeMemory(options) {
2283
+ const { workspacePath, budget, warningThreshold, staleDays } = options;
2284
+ const memoryPath = join(workspacePath, WORKSPACE_FILES.memory);
2285
+ if (!existsSync(memoryPath)) {
2286
+ return {
2287
+ exists: false,
2288
+ charCount: 0,
2289
+ budget,
2290
+ usage: 0,
2291
+ warning: false,
2292
+ overBudget: false,
2293
+ staleCandidates: 0,
2294
+ staleSectionNames: [],
2295
+ };
2296
+ }
2297
+ const content = readFileSync(memoryPath, 'utf-8');
2298
+ const charCount = content.length;
2299
+ const usage = budget > 0 ? charCount / budget : charCount > 0 ? Infinity : 0;
2300
+ const warning = usage >= warningThreshold;
2301
+ const overBudget = usage > 1;
2302
+ // Split into H2 sections and scan for staleness
2303
+ const sections = content.split(H2_RE).slice(1); // skip content before first H2
2304
+ const now = Date.now();
2305
+ const thresholdMs = staleDays * 24 * 60 * 60 * 1000;
2306
+ const staleSectionNames = [];
2307
+ for (const section of sections) {
2308
+ const sectionName = section.split('\n')[0]?.trim() ?? '';
2309
+ const recentDate = extractMostRecentDate(section);
2310
+ // Sections without dates are evergreen — never flagged (Decision 47)
2311
+ if (!recentDate)
2312
+ continue;
2313
+ if (now - recentDate.getTime() > thresholdMs) {
2314
+ staleSectionNames.push(sectionName);
2315
+ }
2316
+ }
2317
+ return {
2318
+ exists: true,
2319
+ charCount,
2320
+ budget,
2321
+ usage,
2322
+ warning,
2323
+ overBudget,
2324
+ staleCandidates: staleSectionNames.length,
2325
+ staleSectionNames,
2326
+ };
2327
+ }
2328
+
1848
2329
  /**
1849
2330
  * CLI status command: discover components and probe their health.
1850
2331
  *
@@ -1862,12 +2343,12 @@ function registerStatusCommand(program) {
1862
2343
  program
1863
2344
  .command('status')
1864
2345
  .description('Discover Jeeves components and probe their health')
1865
- .option('-w, --workspace <path>', 'Workspace root path', DEFAULT_WORKSPACE)
1866
- .option('-c, --config-root <path>', 'Platform config root path', DEFAULT_CONFIG_ROOT)
2346
+ .option('-w, --workspace <path>', 'Workspace root path')
2347
+ .option('-c, --config-root <path>', 'Platform config root path')
1867
2348
  .option('-t, --timeout <ms>', 'Probe timeout in milliseconds', '3000')
1868
2349
  .action(async (opts) => {
1869
2350
  const timeoutMs = parseInt(opts.timeout, 10);
1870
- initFromOptions(opts);
2351
+ const resolved = initFromOptions(opts);
1871
2352
  console.log('Jeeves Platform Status');
1872
2353
  console.log('='.repeat(60));
1873
2354
  console.log();
@@ -1875,62 +2356,87 @@ function registerStatusCommand(program) {
1875
2356
  const coreConfigDir = getCoreConfigDir();
1876
2357
  const componentVersions = readComponentVersions(coreConfigDir);
1877
2358
  const componentNames = Object.keys(componentVersions);
2359
+ let allHealthy = true;
1878
2360
  if (componentNames.length === 0) {
1879
2361
  console.log('No components registered.');
1880
- return;
2362
+ console.log();
1881
2363
  }
1882
- const nameWidth = 10;
1883
- const statusWidth = 30;
1884
- const versionWidth = 12;
1885
- const header = [
1886
- 'Component'.padEnd(nameWidth),
1887
- 'Status'.padEnd(statusWidth),
1888
- 'Version'.padEnd(versionWidth),
1889
- ].join(' ');
1890
- const separator = [
1891
- '-'.repeat(nameWidth),
1892
- '-'.repeat(statusWidth),
1893
- '-'.repeat(versionWidth),
1894
- ].join(' ');
1895
- console.log(header);
1896
- console.log(separator);
1897
- let allHealthy = true;
1898
- for (const name of componentNames) {
1899
- let status;
1900
- let version = '—';
1901
- try {
1902
- const url = getServiceUrl(name);
1903
- const response = await fetchWithTimeout(`${url}/status`, timeoutMs);
1904
- if (response.ok) {
1905
- status = '✅ Running';
1906
- try {
1907
- const body = await response.json();
1908
- if (typeof body === 'object' &&
1909
- body !== null &&
1910
- 'version' in body &&
1911
- typeof body['version'] === 'string') {
1912
- version = body['version'];
2364
+ else {
2365
+ const nameWidth = 10;
2366
+ const statusWidth = 30;
2367
+ const versionWidth = 12;
2368
+ const header = [
2369
+ 'Component'.padEnd(nameWidth),
2370
+ 'Status'.padEnd(statusWidth),
2371
+ 'Version'.padEnd(versionWidth),
2372
+ ].join(' ');
2373
+ const separator = [
2374
+ '-'.repeat(nameWidth),
2375
+ '-'.repeat(statusWidth),
2376
+ '-'.repeat(versionWidth),
2377
+ ].join(' ');
2378
+ console.log(header);
2379
+ console.log(separator);
2380
+ for (const name of componentNames) {
2381
+ let status;
2382
+ let version = '—';
2383
+ try {
2384
+ const url = getServiceUrl(name);
2385
+ const response = await fetchWithTimeout(`${url}/status`, timeoutMs);
2386
+ if (response.ok) {
2387
+ status = '✅ Running';
2388
+ try {
2389
+ const body = await response.json();
2390
+ if (typeof body === 'object' &&
2391
+ body !== null &&
2392
+ 'version' in body &&
2393
+ typeof body['version'] ===
2394
+ 'string') {
2395
+ version = body['version'];
2396
+ }
2397
+ }
2398
+ catch {
2399
+ // Non-JSON response — version stays unknown
1913
2400
  }
1914
2401
  }
1915
- catch {
1916
- // Non-JSON response — version stays unknown
2402
+ else {
2403
+ status = `❌ HTTP ${String(response.status)}`;
2404
+ allHealthy = false;
1917
2405
  }
1918
2406
  }
1919
- else {
1920
- status = `❌ HTTP ${String(response.status)}`;
2407
+ catch {
2408
+ status = '❌ Down';
1921
2409
  allHealthy = false;
1922
2410
  }
2411
+ const row = [
2412
+ name.padEnd(nameWidth),
2413
+ status.padEnd(statusWidth),
2414
+ version.padEnd(versionWidth),
2415
+ ].join(' ');
2416
+ console.log(row);
1923
2417
  }
1924
- catch {
1925
- status = '❌ Down';
1926
- allHealthy = false;
1927
- }
1928
- const row = [
1929
- name.padEnd(nameWidth),
1930
- status.padEnd(statusWidth),
1931
- version.padEnd(versionWidth),
1932
- ].join(' ');
1933
- console.log(row);
2418
+ console.log();
2419
+ }
2420
+ const memory = analyzeMemory({
2421
+ workspacePath: getWorkspacePath(),
2422
+ budget: resolved.memory.budget.value,
2423
+ warningThreshold: resolved.memory.warningThreshold.value,
2424
+ staleDays: resolved.memory.staleDays.value,
2425
+ });
2426
+ console.log('Memory hygiene');
2427
+ console.log('-'.repeat(60));
2428
+ if (!memory.exists) {
2429
+ console.log('MEMORY.md not found.');
2430
+ }
2431
+ else {
2432
+ const usagePct = Math.round(memory.usage * 100);
2433
+ const status = memory.overBudget
2434
+ ? '❌ Over budget'
2435
+ : memory.warning
2436
+ ? '⚠ Warning'
2437
+ : '✅ OK';
2438
+ console.log(`Chars: ${String(memory.charCount)} / ${String(memory.budget)} (${String(usagePct)}%) — ${status}`);
2439
+ console.log(`Stale candidates: ${String(memory.staleCandidates)}`);
1934
2440
  }
1935
2441
  console.log();
1936
2442
  if (!allHealthy) {
@@ -1989,14 +2495,14 @@ function registerUninstallCommand(program) {
1989
2495
  program
1990
2496
  .command('uninstall')
1991
2497
  .description('Remove Jeeves managed sections and platform artifacts')
1992
- .option('-w, --workspace <path>', 'Workspace root path', DEFAULT_WORKSPACE)
1993
- .option('-c, --config-root <path>', 'Platform config root path', DEFAULT_CONFIG_ROOT)
2498
+ .option('-w, --workspace <path>', 'Workspace root path')
2499
+ .option('-c, --config-root <path>', 'Platform config root path')
1994
2500
  .action(async (opts) => {
2501
+ const resolved = initFromOptions(opts);
1995
2502
  console.log('Jeeves platform uninstall');
1996
- console.log(` Workspace: ${opts.workspace}`);
1997
- console.log(` Config root: ${opts.configRoot}`);
2503
+ console.log(` Workspace: ${resolved.core.workspace.value}`);
2504
+ console.log(` Config root: ${resolved.core.configRoot.value}`);
1998
2505
  console.log();
1999
- initFromOptions(opts);
2000
2506
  const wsPath = getWorkspacePath();
2001
2507
  const coreConfigDir = getCoreConfigDir();
2002
2508
  // Remove managed sections from workspace files
@@ -2064,6 +2570,7 @@ function registerUninstallCommand(program) {
2064
2570
  * and status subcommands, plus dynamic proxy commands for any installed
2065
2571
  * `@karmaniverous/jeeves-*` component packages.
2066
2572
  */
2573
+ checkNodeVersion();
2067
2574
  const cli = new Command()
2068
2575
  .name('jeeves')
2069
2576
  .description('Jeeves AI assistant platform — shared library and CLI')
@@ -2073,6 +2580,7 @@ const cli = new Command()
2073
2580
  registerInstallCommand(cli);
2074
2581
  registerUninstallCommand(cli);
2075
2582
  registerStatusCommand(cli);
2583
+ registerConfigCommand(cli);
2076
2584
  // Dynamic discovery: register proxy subcommands for installed components
2077
2585
  const discoveredComponents = discoverComponentPackages();
2078
2586
  registerComponentProxies(cli, discoveredComponents);