@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.
- package/README.md +56 -3
- package/content/skill.md +105 -0
- package/dist/cli/jeeves/index.js +667 -159
- package/dist/cli/plugin/index.js +147 -5
- package/dist/cli/service/index.js +63 -7
- package/dist/index.d.ts +253 -9
- package/dist/index.js +779 -89
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import fs, { writeFileSync, renameSync, unlinkSync, existsSync, readFileSync, mkdirSync, readdirSync, copyFileSync, rmSync, cpSync } from 'node:fs';
|
|
2
|
-
import path, { join, dirname, resolve } from 'node:path';
|
|
2
|
+
import path, { join, dirname, resolve, basename } from 'node:path';
|
|
3
3
|
import { lock } from 'proper-lockfile';
|
|
4
4
|
import { JSONPath } from 'jsonpath-plus';
|
|
5
|
+
import { major, valid, gte, gt } from 'semver';
|
|
6
|
+
import { z } from 'zod';
|
|
5
7
|
import * as commander from 'commander';
|
|
6
8
|
import { packageDirectorySync } from 'package-directory';
|
|
7
|
-
import { valid, gte, gt } from 'semver';
|
|
8
9
|
import { homedir } from 'node:os';
|
|
9
|
-
import { z } from 'zod';
|
|
10
10
|
import cp, { execSync } from 'node:child_process';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import crypto from 'node:crypto';
|
|
@@ -69,8 +69,8 @@ const ALL_MARKERS = [
|
|
|
69
69
|
const VERSION_STAMP_PATTERN = /<!--\s*(.+?)\s*\|\s*core:(\S+)\s*\|\s*(\S+)\s*-->/;
|
|
70
70
|
/** Staleness threshold for version-stamp convergence in milliseconds. */
|
|
71
71
|
const STALENESS_THRESHOLD_MS = 5 * 60 * 1000;
|
|
72
|
-
/** Warning text
|
|
73
|
-
const CLEANUP_FLAG = '> ⚠️ CLEANUP NEEDED: Orphaned Jeeves content
|
|
72
|
+
/** Warning text injected inside managed block when cleanup is needed. */
|
|
73
|
+
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.';
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Directory and file path conventions for the Jeeves platform.
|
|
@@ -89,7 +89,13 @@ const WORKSPACE_FILES = {
|
|
|
89
89
|
agents: 'AGENTS.md',
|
|
90
90
|
/** HEARTBEAT.md — platform status and health alerts. */
|
|
91
91
|
heartbeat: 'HEARTBEAT.md',
|
|
92
|
+
/** MEMORY.md — curated long-term memory. */
|
|
93
|
+
memory: 'MEMORY.md',
|
|
92
94
|
};
|
|
95
|
+
/** Skill directory name within workspace. */
|
|
96
|
+
const SKILLS_DIR = 'skills';
|
|
97
|
+
/** Jeeves skill directory name. */
|
|
98
|
+
const JEEVES_SKILL_DIR = 'jeeves';
|
|
93
99
|
/** Templates directory name within core config. */
|
|
94
100
|
const TEMPLATES_DIR = 'templates';
|
|
95
101
|
/** Registry cache file name. */
|
|
@@ -177,14 +183,14 @@ const PLATFORM_COMPONENTS = [
|
|
|
177
183
|
* Core library version, inlined at build time.
|
|
178
184
|
*
|
|
179
185
|
* @remarks
|
|
180
|
-
* The `0.
|
|
186
|
+
* The `0.5.0` placeholder is replaced by
|
|
181
187
|
* `@rollup/plugin-replace` during the build with the actual version
|
|
182
188
|
* from `package.json`. This ensures the correct version survives
|
|
183
189
|
* when consumers bundle core into their own dist (where runtime
|
|
184
190
|
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
185
191
|
*/
|
|
186
192
|
/** The core library version from package.json (inlined at build time). */
|
|
187
|
-
const CORE_VERSION = '0.
|
|
193
|
+
const CORE_VERSION = '0.5.0';
|
|
188
194
|
|
|
189
195
|
/**
|
|
190
196
|
* Workspace and config root initialization.
|
|
@@ -557,6 +563,257 @@ function createStatusHandler(options) {
|
|
|
557
563
|
};
|
|
558
564
|
}
|
|
559
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Runtime Node.js version floor check.
|
|
568
|
+
*
|
|
569
|
+
* @module
|
|
570
|
+
*/
|
|
571
|
+
/** Minimum supported Node.js major version. */
|
|
572
|
+
const MIN_NODE_MAJOR = 22;
|
|
573
|
+
/**
|
|
574
|
+
* Check that the running Node.js version meets the minimum requirement.
|
|
575
|
+
* Prints an error and exits with code 1 if the check fails.
|
|
576
|
+
*/
|
|
577
|
+
function checkNodeVersion() {
|
|
578
|
+
const nodeMajor = major(process.versions.node);
|
|
579
|
+
if (nodeMajor < MIN_NODE_MAJOR) {
|
|
580
|
+
console.error(`Error: jeeves requires Node.js >= ${String(MIN_NODE_MAJOR)}. Current: ${process.versions.node}`);
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Workspace-level shared configuration: `jeeves.config.json`.
|
|
587
|
+
*
|
|
588
|
+
* @remarks
|
|
589
|
+
* Lives at the OpenClaw workspace root alongside TOOLS.md and SOUL.md.
|
|
590
|
+
* Provides namespaced shared defaults consumed by the root Jeeves CLI.
|
|
591
|
+
* Resolution precedence: CLI flags → env vars → jeeves.config.json → defaults.
|
|
592
|
+
*
|
|
593
|
+
* This does not replace component-owned config schemas (Decision 41).
|
|
594
|
+
*/
|
|
595
|
+
/** Workspace config file name. */
|
|
596
|
+
const WORKSPACE_CONFIG_FILE = 'jeeves.config.json';
|
|
597
|
+
/** Core shared config section. */
|
|
598
|
+
const workspaceCoreConfigSchema = z
|
|
599
|
+
.object({
|
|
600
|
+
/** Workspace root path. */
|
|
601
|
+
workspace: z.string().optional().describe('Workspace root path'),
|
|
602
|
+
/** Platform config root path. */
|
|
603
|
+
configRoot: z.string().optional().describe('Platform config root path'),
|
|
604
|
+
/** OpenClaw gateway URL. */
|
|
605
|
+
gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
|
|
606
|
+
})
|
|
607
|
+
.partial();
|
|
608
|
+
/** Memory shared config section. */
|
|
609
|
+
const workspaceMemoryConfigSchema = z
|
|
610
|
+
.object({
|
|
611
|
+
/** MEMORY.md character budget. */
|
|
612
|
+
budget: z.number().int().positive().optional().describe('Memory budget'),
|
|
613
|
+
/** Warning threshold as a fraction of budget. */
|
|
614
|
+
warningThreshold: z
|
|
615
|
+
.number()
|
|
616
|
+
.min(0)
|
|
617
|
+
.max(1)
|
|
618
|
+
.optional()
|
|
619
|
+
.describe('Memory warning threshold'),
|
|
620
|
+
/** Staleness threshold in days. */
|
|
621
|
+
staleDays: z
|
|
622
|
+
.number()
|
|
623
|
+
.int()
|
|
624
|
+
.positive()
|
|
625
|
+
.optional()
|
|
626
|
+
.describe('Memory staleness threshold in days'),
|
|
627
|
+
})
|
|
628
|
+
.partial();
|
|
629
|
+
/** Workspace config Zod schema. */
|
|
630
|
+
const workspaceConfigSchema = z.object({
|
|
631
|
+
/** JSON Schema pointer for IDE autocomplete. */
|
|
632
|
+
$schema: z.string().optional().describe('JSON Schema pointer'),
|
|
633
|
+
/** Core shared defaults. */
|
|
634
|
+
core: workspaceCoreConfigSchema.optional(),
|
|
635
|
+
/** Memory hygiene shared defaults. */
|
|
636
|
+
memory: workspaceMemoryConfigSchema.optional(),
|
|
637
|
+
});
|
|
638
|
+
/** Built-in workspace config defaults. */
|
|
639
|
+
const WORKSPACE_CONFIG_DEFAULTS = {
|
|
640
|
+
core: {
|
|
641
|
+
workspace: '.',
|
|
642
|
+
configRoot: './config',
|
|
643
|
+
gatewayUrl: 'http://127.0.0.1:3000',
|
|
644
|
+
},
|
|
645
|
+
memory: {
|
|
646
|
+
budget: 20_000,
|
|
647
|
+
warningThreshold: 0.8,
|
|
648
|
+
staleDays: 30,
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
/**
|
|
652
|
+
* Load workspace config from `jeeves.config.json` at a given path.
|
|
653
|
+
*
|
|
654
|
+
* @param workspacePath - Workspace root directory.
|
|
655
|
+
* @returns Parsed config or undefined if missing or invalid.
|
|
656
|
+
*/
|
|
657
|
+
function loadWorkspaceConfig(workspacePath) {
|
|
658
|
+
const configPath = join(workspacePath, WORKSPACE_CONFIG_FILE);
|
|
659
|
+
if (!existsSync(configPath))
|
|
660
|
+
return undefined;
|
|
661
|
+
try {
|
|
662
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
663
|
+
const parsed = JSON.parse(raw);
|
|
664
|
+
return workspaceConfigSchema.parse(parsed);
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
668
|
+
console.warn(`jeeves-core: failed to load ${configPath}: ${msg}`);
|
|
669
|
+
return undefined;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Resolve a config value with four-tier precedence.
|
|
674
|
+
*
|
|
675
|
+
* @param flagValue - CLI flag value (highest priority).
|
|
676
|
+
* @param envValue - Environment variable value.
|
|
677
|
+
* @param fileValue - Value from jeeves.config.json.
|
|
678
|
+
* @param defaultValue - Built-in default (lowest priority).
|
|
679
|
+
* @returns The resolved value with provenance annotation.
|
|
680
|
+
*/
|
|
681
|
+
function resolveConfigValue(flagValue, envValue, fileValue, defaultValue) {
|
|
682
|
+
if (flagValue !== undefined)
|
|
683
|
+
return { value: flagValue, provenance: 'flag' };
|
|
684
|
+
if (envValue !== undefined)
|
|
685
|
+
return { value: envValue, provenance: 'env' };
|
|
686
|
+
if (fileValue !== undefined)
|
|
687
|
+
return { value: fileValue, provenance: 'file' };
|
|
688
|
+
return { value: defaultValue, provenance: 'default' };
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Generate a JSON Schema for the workspace config.
|
|
692
|
+
*
|
|
693
|
+
* @returns A JSON Schema object.
|
|
694
|
+
*/
|
|
695
|
+
function generateWorkspaceJsonSchema() {
|
|
696
|
+
return {
|
|
697
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
698
|
+
title: 'Jeeves Workspace Configuration',
|
|
699
|
+
type: 'object',
|
|
700
|
+
properties: {
|
|
701
|
+
$schema: { type: 'string' },
|
|
702
|
+
core: {
|
|
703
|
+
type: 'object',
|
|
704
|
+
properties: {
|
|
705
|
+
workspace: {
|
|
706
|
+
type: 'string',
|
|
707
|
+
default: WORKSPACE_CONFIG_DEFAULTS.core.workspace,
|
|
708
|
+
},
|
|
709
|
+
configRoot: {
|
|
710
|
+
type: 'string',
|
|
711
|
+
default: WORKSPACE_CONFIG_DEFAULTS.core.configRoot,
|
|
712
|
+
},
|
|
713
|
+
gatewayUrl: {
|
|
714
|
+
type: 'string',
|
|
715
|
+
default: WORKSPACE_CONFIG_DEFAULTS.core.gatewayUrl,
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
memory: {
|
|
720
|
+
type: 'object',
|
|
721
|
+
properties: {
|
|
722
|
+
budget: {
|
|
723
|
+
type: 'integer',
|
|
724
|
+
minimum: 1,
|
|
725
|
+
default: WORKSPACE_CONFIG_DEFAULTS.memory.budget,
|
|
726
|
+
},
|
|
727
|
+
warningThreshold: {
|
|
728
|
+
type: 'number',
|
|
729
|
+
minimum: 0,
|
|
730
|
+
maximum: 1,
|
|
731
|
+
default: WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold,
|
|
732
|
+
},
|
|
733
|
+
staleDays: {
|
|
734
|
+
type: 'integer',
|
|
735
|
+
minimum: 1,
|
|
736
|
+
default: WORKSPACE_CONFIG_DEFAULTS.memory.staleDays,
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Shared CLI defaults and resolution for Jeeves CLI commands.
|
|
746
|
+
*
|
|
747
|
+
* @remarks
|
|
748
|
+
* All root CLI commands share workspace/config-root resolution. Values follow
|
|
749
|
+
* the shared precedence model: flags → env → jeeves.config.json → defaults.
|
|
750
|
+
*/
|
|
751
|
+
/** Default workspace path. */
|
|
752
|
+
const DEFAULT_WORKSPACE = WORKSPACE_CONFIG_DEFAULTS.core.workspace;
|
|
753
|
+
/** Default config root path. */
|
|
754
|
+
const DEFAULT_CONFIG_ROOT = WORKSPACE_CONFIG_DEFAULTS.core.configRoot;
|
|
755
|
+
/** Read a numeric env var or return undefined if missing, empty, or invalid. */
|
|
756
|
+
function readNumericEnv(name) {
|
|
757
|
+
const raw = process.env[name];
|
|
758
|
+
if (raw === undefined || raw.trim() === '')
|
|
759
|
+
return undefined;
|
|
760
|
+
const value = Number(raw);
|
|
761
|
+
return Number.isFinite(value) ? value : undefined;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Resolve shared CLI config using flags, env, file, and defaults.
|
|
765
|
+
*
|
|
766
|
+
* @param opts - Parsed CLI workspace/config-root options.
|
|
767
|
+
* @returns Resolved config tree with provenance on every leaf.
|
|
768
|
+
*/
|
|
769
|
+
function resolveCliConfig(opts) {
|
|
770
|
+
const workspaceSeed = resolveConfigValue(opts.workspace, process.env['JEEVES_WORKSPACE'], undefined, DEFAULT_WORKSPACE);
|
|
771
|
+
const fileConfig = loadWorkspaceConfig(workspaceSeed.value);
|
|
772
|
+
return {
|
|
773
|
+
core: {
|
|
774
|
+
workspace: resolveConfigValue(opts.workspace, process.env['JEEVES_WORKSPACE'], fileConfig?.core?.workspace, DEFAULT_WORKSPACE),
|
|
775
|
+
configRoot: resolveConfigValue(opts.configRoot, process.env['JEEVES_CONFIG_ROOT'], fileConfig?.core?.configRoot, DEFAULT_CONFIG_ROOT),
|
|
776
|
+
gatewayUrl: resolveConfigValue(undefined, process.env['JEEVES_GATEWAY_URL'], fileConfig?.core?.gatewayUrl, WORKSPACE_CONFIG_DEFAULTS.core.gatewayUrl),
|
|
777
|
+
},
|
|
778
|
+
memory: {
|
|
779
|
+
budget: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_BUDGET'), fileConfig?.memory?.budget, WORKSPACE_CONFIG_DEFAULTS.memory.budget),
|
|
780
|
+
warningThreshold: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_WARNING_THRESHOLD'), fileConfig?.memory?.warningThreshold, WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold),
|
|
781
|
+
staleDays: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_STALE_DAYS'), fileConfig?.memory?.staleDays, WORKSPACE_CONFIG_DEFAULTS.memory.staleDays),
|
|
782
|
+
},
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Initialize core from standard CLI options after resolving shared defaults.
|
|
787
|
+
*
|
|
788
|
+
* @param opts - Parsed Commander options with workspace and configRoot.
|
|
789
|
+
* @returns Resolved CLI config.
|
|
790
|
+
*/
|
|
791
|
+
function initFromOptions(opts) {
|
|
792
|
+
const resolved = resolveCliConfig(opts);
|
|
793
|
+
init({
|
|
794
|
+
workspacePath: resolve(resolved.core.workspace.value),
|
|
795
|
+
configRoot: resolve(resolved.core.configRoot.value),
|
|
796
|
+
});
|
|
797
|
+
return resolved;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* `jeeves config [jsonpath]` — inspect effective shared CLI configuration.
|
|
802
|
+
*
|
|
803
|
+
* @remarks
|
|
804
|
+
* Shows effective values and provenance using the shared precedence model.
|
|
805
|
+
* Optional JSONPath filters the resolved config tree.
|
|
806
|
+
*/
|
|
807
|
+
/**
|
|
808
|
+
* Build the effective shared CLI config tree.
|
|
809
|
+
*
|
|
810
|
+
* @param opts - Parsed CLI workspace/config-root options.
|
|
811
|
+
* @returns Effective config tree with provenance on each leaf.
|
|
812
|
+
*/
|
|
813
|
+
function buildEffectiveConfig(opts) {
|
|
814
|
+
return initFromOptions(opts);
|
|
815
|
+
}
|
|
816
|
+
|
|
560
817
|
function getDefaultExportFromCjs (x) {
|
|
561
818
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
562
819
|
}
|
|
@@ -750,7 +1007,7 @@ function parseHeartbeat(fileContent) {
|
|
|
750
1007
|
const userContent = fileContent.slice(0, headingIndex).trim();
|
|
751
1008
|
const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
|
|
752
1009
|
const entries = [];
|
|
753
|
-
const h2Re = /^## (jeeves-\S
|
|
1010
|
+
const h2Re = /^## (jeeves-\S+?|MEMORY\.md)(?:: declined)?$/gm;
|
|
754
1011
|
let match;
|
|
755
1012
|
const h2Positions = [];
|
|
756
1013
|
while ((match = h2Re.exec(sectionContent)) !== null) {
|
|
@@ -1121,6 +1378,135 @@ function buildWithSections(beforeContent, userContent, sections, markers, coreVe
|
|
|
1121
1378
|
return parts.join('\n');
|
|
1122
1379
|
}
|
|
1123
1380
|
|
|
1381
|
+
var skillContent = `---
|
|
1382
|
+
name: jeeves
|
|
1383
|
+
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.
|
|
1384
|
+
---
|
|
1385
|
+
|
|
1386
|
+
# Jeeves Platform Skill
|
|
1387
|
+
|
|
1388
|
+
## Platform Architecture
|
|
1389
|
+
|
|
1390
|
+
Jeeves is a four-component platform coordinated by a shared library (\`@karmaniverous/jeeves\`):
|
|
1391
|
+
|
|
1392
|
+
| Component | Role | Port |
|
|
1393
|
+
|-----------|------|------|
|
|
1394
|
+
| **jeeves-runner** | Execute: scheduled jobs, SQLite state, HTTP API | 1937 |
|
|
1395
|
+
| **jeeves-watcher** | Index: file→Qdrant semantic indexing, inference rules | 1936 |
|
|
1396
|
+
| **jeeves-server** | Present: web UI, file browser, doc render, export | 1934 |
|
|
1397
|
+
| **jeeves-meta** | Distill: LLM synthesis, .meta/ directories, scheduling | 1938 |
|
|
1398
|
+
|
|
1399
|
+
Core (\`@karmaniverous/jeeves\`) is a **library + CLI**, not a service. No port.
|
|
1400
|
+
|
|
1401
|
+
## Data Flow
|
|
1402
|
+
|
|
1403
|
+
\`\`\`
|
|
1404
|
+
Files → Watcher (index) → Qdrant → Meta (synthesize) → .meta/ → Watcher (re-index)
|
|
1405
|
+
↓
|
|
1406
|
+
Runner (schedule) → Scripts → Services ← Server (present) ← Browser
|
|
1407
|
+
\`\`\`
|
|
1408
|
+
|
|
1409
|
+
## Component Interaction
|
|
1410
|
+
|
|
1411
|
+
- **Watcher** indexes files into Qdrant with inference rules and enrichments.
|
|
1412
|
+
- **Meta** reads from Qdrant, synthesizes \`.meta/\` directories, which watcher re-indexes.
|
|
1413
|
+
- **Runner** executes scheduled scripts that may call any service's HTTP API.
|
|
1414
|
+
- **Server** presents files, renders documents, and provides the event gateway.
|
|
1415
|
+
- **Core** provides shared content management (TOOLS.md, SOUL.md, AGENTS.md), service discovery, config resolution, and the component SDK.
|
|
1416
|
+
|
|
1417
|
+
## Service Discovery
|
|
1418
|
+
|
|
1419
|
+
Services find each other via config resolution:
|
|
1420
|
+
1. Component's own config file (\`{configRoot}/jeeves-{name}/config.json\`)
|
|
1421
|
+
2. Core config file (\`{configRoot}/jeeves-core/config.json\`)
|
|
1422
|
+
3. Default port constants
|
|
1423
|
+
|
|
1424
|
+
## Scripts Repo
|
|
1425
|
+
|
|
1426
|
+
Location: \`{configRoot}/jeeves-core/scripts/\`
|
|
1427
|
+
Template: \`@karmaniverous/jeeves-scripts-template\`
|
|
1428
|
+
|
|
1429
|
+
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.
|
|
1430
|
+
|
|
1431
|
+
## Managed Content System
|
|
1432
|
+
|
|
1433
|
+
Core maintains managed sections in workspace files using comment markers:
|
|
1434
|
+
- **TOOLS.md** — Component sections (section mode) + Platform section
|
|
1435
|
+
- **SOUL.md** — Professional discipline and behavioral foundations (block mode)
|
|
1436
|
+
- **AGENTS.md** — Operational protocols and memory architecture (block mode)
|
|
1437
|
+
- **HEARTBEAT.md** — Platform health status (heading-based)
|
|
1438
|
+
|
|
1439
|
+
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.
|
|
1440
|
+
|
|
1441
|
+
## Workspace Configuration
|
|
1442
|
+
|
|
1443
|
+
\`jeeves.config.json\` at workspace root provides shared defaults:
|
|
1444
|
+
- Precedence: CLI flags → env vars → file → defaults
|
|
1445
|
+
- Namespaced: \`core.*\` (workspace, configRoot, gatewayUrl) and \`memory.*\` (budget, warningThreshold, staleDays)
|
|
1446
|
+
- Inspect with \`jeeves config [jsonpath]\`
|
|
1447
|
+
|
|
1448
|
+
## HEARTBEAT Protocol
|
|
1449
|
+
|
|
1450
|
+
The HEARTBEAT system uses a state machine per component:
|
|
1451
|
+
\`not_installed → deps_missing → config_missing → service_not_installed → service_stopped → healthy\`
|
|
1452
|
+
|
|
1453
|
+
Dependency-aware: hard deps block alerts, soft deps add informational notes. Declined components are tracked via heading suffix.
|
|
1454
|
+
|
|
1455
|
+
## Plugin Lifecycle
|
|
1456
|
+
|
|
1457
|
+
\`\`\`bash
|
|
1458
|
+
# Core install (seed workspace content)
|
|
1459
|
+
npx @karmaniverous/jeeves install
|
|
1460
|
+
|
|
1461
|
+
# Component plugin install
|
|
1462
|
+
npx @karmaniverous/jeeves-{component}-openclaw install
|
|
1463
|
+
|
|
1464
|
+
# Component plugin uninstall
|
|
1465
|
+
npx @karmaniverous/jeeves-{component}-openclaw uninstall
|
|
1466
|
+
|
|
1467
|
+
# Core uninstall (remove managed sections)
|
|
1468
|
+
npx @karmaniverous/jeeves uninstall
|
|
1469
|
+
\`\`\`
|
|
1470
|
+
|
|
1471
|
+
## Memory Hygiene
|
|
1472
|
+
|
|
1473
|
+
MEMORY.md has a character budget (default 20,000). Core tracks:
|
|
1474
|
+
- Character count and usage percentage
|
|
1475
|
+
- Warning at 80% of budget
|
|
1476
|
+
- Stale section candidates (H2 sections whose most recent ISO date exceeds the staleness threshold)
|
|
1477
|
+
- Evergreen sections (no dates) are never flagged
|
|
1478
|
+
|
|
1479
|
+
Review is human/agent-mediated — core does not auto-delete.
|
|
1480
|
+
|
|
1481
|
+
### HEARTBEAT Integration
|
|
1482
|
+
|
|
1483
|
+
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.
|
|
1484
|
+
|
|
1485
|
+
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\`.
|
|
1486
|
+
`;
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* Skill seeding: write the `jeeves` workspace skill unconditionally.
|
|
1490
|
+
*
|
|
1491
|
+
* @remarks
|
|
1492
|
+
* The skill file is entirely generated — no user-authored content (Decision 48).
|
|
1493
|
+
* Every installer (core CLI and component plugins) writes it unconditionally.
|
|
1494
|
+
* Content is inlined at build time via `rollup-plugin-md.ts`.
|
|
1495
|
+
*/
|
|
1496
|
+
/**
|
|
1497
|
+
* Seed the jeeves workspace skill file.
|
|
1498
|
+
*
|
|
1499
|
+
* @param workspacePath - Workspace root directory.
|
|
1500
|
+
*/
|
|
1501
|
+
function seedSkill(workspacePath) {
|
|
1502
|
+
const skillDir = join(workspacePath, SKILLS_DIR, JEEVES_SKILL_DIR);
|
|
1503
|
+
if (!existsSync(skillDir)) {
|
|
1504
|
+
mkdirSync(skillDir, { recursive: true });
|
|
1505
|
+
}
|
|
1506
|
+
const skillPath = join(skillDir, 'SKILL.md');
|
|
1507
|
+
writeFileSync(skillPath, skillContent, 'utf-8');
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1124
1510
|
/**
|
|
1125
1511
|
* OpenClaw configuration helpers for plugin CLI installers.
|
|
1126
1512
|
*
|
|
@@ -1360,7 +1746,7 @@ function createPluginCli(options) {
|
|
|
1360
1746
|
for (const msg of messages) {
|
|
1361
1747
|
console.log(` ✓ ${msg}`);
|
|
1362
1748
|
}
|
|
1363
|
-
// 4. Write initial HEARTBEAT entry
|
|
1749
|
+
// 4. Write initial HEARTBEAT entry and seed jeeves skill
|
|
1364
1750
|
try {
|
|
1365
1751
|
const cfgRoot = opts.configRoot;
|
|
1366
1752
|
const agents = config.agents;
|
|
@@ -1390,10 +1776,17 @@ function createPluginCli(options) {
|
|
|
1390
1776
|
catch {
|
|
1391
1777
|
console.log(' ⚠ Could not write HEARTBEAT entry');
|
|
1392
1778
|
}
|
|
1779
|
+
try {
|
|
1780
|
+
seedSkill(ws);
|
|
1781
|
+
console.log(' ✓ Jeeves skill seeded');
|
|
1782
|
+
}
|
|
1783
|
+
catch {
|
|
1784
|
+
console.log(' ⚠ Could not seed Jeeves skill');
|
|
1785
|
+
}
|
|
1393
1786
|
}
|
|
1394
1787
|
}
|
|
1395
1788
|
catch {
|
|
1396
|
-
// HEARTBEAT
|
|
1789
|
+
// HEARTBEAT + skill seeding are best-effort during install
|
|
1397
1790
|
}
|
|
1398
1791
|
// 5. Write component version
|
|
1399
1792
|
try {
|
|
@@ -2045,19 +2438,6 @@ function createServiceManager(descriptor) {
|
|
|
2045
2438
|
}
|
|
2046
2439
|
}
|
|
2047
2440
|
|
|
2048
|
-
/**
|
|
2049
|
-
* Shared CLI defaults and option registration for Jeeves CLI commands.
|
|
2050
|
-
*
|
|
2051
|
-
* @remarks
|
|
2052
|
-
* All three CLI commands (install, uninstall, status) share the same
|
|
2053
|
-
* `--workspace` and `--config-root` options with the same defaults.
|
|
2054
|
-
* This module centralizes them to eliminate duplication.
|
|
2055
|
-
*/
|
|
2056
|
-
/** Default workspace path (current directory). */
|
|
2057
|
-
const DEFAULT_WORKSPACE = '.';
|
|
2058
|
-
/** Default config root path. */
|
|
2059
|
-
const DEFAULT_CONFIG_ROOT = './config';
|
|
2060
|
-
|
|
2061
2441
|
/**
|
|
2062
2442
|
* Factory for the standard Jeeves service CLI.
|
|
2063
2443
|
*
|
|
@@ -2486,10 +2866,10 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
2486
2866
|
? `# ${markers.title}\n\n${sectionText}`
|
|
2487
2867
|
: sectionText;
|
|
2488
2868
|
}
|
|
2489
|
-
//
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
//
|
|
2869
|
+
// Build the full managed block
|
|
2870
|
+
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
2871
|
+
const endLine = formatEndMarker(markers.end);
|
|
2872
|
+
// Combine all user content for cleanup detection
|
|
2493
2873
|
const rawUserContent = [parsed.beforeContent, parsed.userContent]
|
|
2494
2874
|
.filter(Boolean)
|
|
2495
2875
|
.join('\n\n')
|
|
@@ -2497,9 +2877,6 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
2497
2877
|
// Strip foreign managed blocks from user content (cross-contamination fix)
|
|
2498
2878
|
const userContent = stripForeignMarkers(rawUserContent, markers);
|
|
2499
2879
|
const cleanupNeeded = needsCleanup(newManagedBody, userContent);
|
|
2500
|
-
// Build the full managed block
|
|
2501
|
-
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
2502
|
-
const endLine = formatEndMarker(markers.end);
|
|
2503
2880
|
const managedParts = [];
|
|
2504
2881
|
managedParts.push(beginLine);
|
|
2505
2882
|
if (cleanupNeeded) {
|
|
@@ -2511,26 +2888,54 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
2511
2888
|
managedParts.push('');
|
|
2512
2889
|
managedParts.push(endLine);
|
|
2513
2890
|
const managedBlock = managedParts.join('\n');
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
//
|
|
2518
|
-
|
|
2519
|
-
|
|
2891
|
+
let newFileContent;
|
|
2892
|
+
if (parsed.found) {
|
|
2893
|
+
// Existing block: update in place — preserve position, don't move.
|
|
2894
|
+
// Strip foreign managed blocks from both content zones (cross-contamination fix).
|
|
2895
|
+
const cleanBefore = stripForeignMarkers(parsed.beforeContent, markers);
|
|
2896
|
+
const cleanAfter = stripForeignMarkers(parsed.userContent, markers);
|
|
2897
|
+
const fileParts = [];
|
|
2898
|
+
if (cleanBefore) {
|
|
2899
|
+
fileParts.push(cleanBefore);
|
|
2520
2900
|
fileParts.push('');
|
|
2521
2901
|
}
|
|
2522
2902
|
fileParts.push(managedBlock);
|
|
2903
|
+
if (cleanAfter) {
|
|
2904
|
+
fileParts.push('');
|
|
2905
|
+
fileParts.push(cleanAfter);
|
|
2906
|
+
}
|
|
2907
|
+
fileParts.push('');
|
|
2908
|
+
newFileContent = fileParts.join('\n');
|
|
2523
2909
|
}
|
|
2524
2910
|
else {
|
|
2525
|
-
//
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2911
|
+
// No existing block: insert new block using the configured position.
|
|
2912
|
+
// Strip orphaned same-type BEGIN markers from user content to prevent
|
|
2913
|
+
// the parser from pairing them with the new END marker on the next cycle.
|
|
2914
|
+
const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2915
|
+
const orphanedBeginRe = new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$\\n?`, 'gm');
|
|
2916
|
+
const cleanUserContent = userContent
|
|
2917
|
+
.replace(orphanedBeginRe, '')
|
|
2918
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
2919
|
+
.trim();
|
|
2920
|
+
const position = markers.position ?? 'top';
|
|
2921
|
+
const fileParts = [];
|
|
2922
|
+
if (position === 'bottom') {
|
|
2923
|
+
if (cleanUserContent) {
|
|
2924
|
+
fileParts.push(cleanUserContent);
|
|
2925
|
+
fileParts.push('');
|
|
2926
|
+
}
|
|
2927
|
+
fileParts.push(managedBlock);
|
|
2928
|
+
}
|
|
2929
|
+
else {
|
|
2930
|
+
fileParts.push(managedBlock);
|
|
2931
|
+
if (cleanUserContent) {
|
|
2932
|
+
fileParts.push('');
|
|
2933
|
+
fileParts.push(cleanUserContent);
|
|
2934
|
+
}
|
|
2530
2935
|
}
|
|
2936
|
+
fileParts.push('');
|
|
2937
|
+
newFileContent = fileParts.join('\n');
|
|
2531
2938
|
}
|
|
2532
|
-
fileParts.push('');
|
|
2533
|
-
const newFileContent = fileParts.join('\n');
|
|
2534
2939
|
atomicWrite(filePath, newFileContent);
|
|
2535
2940
|
});
|
|
2536
2941
|
}
|
|
@@ -3024,6 +3429,229 @@ async function refreshPlatformContent(options) {
|
|
|
3024
3429
|
copyTemplates(coreConfigDir);
|
|
3025
3430
|
}
|
|
3026
3431
|
|
|
3432
|
+
/**
|
|
3433
|
+
* Cleanup-session escalation for managed files with orphaned duplicated content.
|
|
3434
|
+
*
|
|
3435
|
+
* @remarks
|
|
3436
|
+
* When a managed file contains the cleanup flag, the writer can ask the
|
|
3437
|
+
* OpenClaw gateway to spawn a background session to remove orphaned content.
|
|
3438
|
+
* The request is best-effort: accepted requests return `true`; any transport
|
|
3439
|
+
* or HTTP failure returns `false` so the file warning remains the fallback.
|
|
3440
|
+
*/
|
|
3441
|
+
/** Timeout for cleanup-session spawn requests. */
|
|
3442
|
+
const CLEANUP_REQUEST_TIMEOUT_MS = 5_000;
|
|
3443
|
+
/**
|
|
3444
|
+
* Build the cleanup task prompt sent to the gateway session API.
|
|
3445
|
+
*
|
|
3446
|
+
* @param filePath - Managed file requiring cleanup.
|
|
3447
|
+
* @param markerIdentity - Marker identity for the file.
|
|
3448
|
+
* @returns Cleanup instructions for the spawned session.
|
|
3449
|
+
*/
|
|
3450
|
+
function buildCleanupTask(filePath, markerIdentity) {
|
|
3451
|
+
return [
|
|
3452
|
+
`Clean up orphaned managed content in ${filePath}.`,
|
|
3453
|
+
`The file uses ${markerIdentity} managed comment markers.`,
|
|
3454
|
+
'Review content outside the managed block and remove only duplicated managed content.',
|
|
3455
|
+
'Preserve any unique user-authored content outside the managed block.',
|
|
3456
|
+
'Do not modify content inside the managed block unless required to preserve valid marker structure.',
|
|
3457
|
+
].join(' ');
|
|
3458
|
+
}
|
|
3459
|
+
/**
|
|
3460
|
+
* Request a cleanup session from the OpenClaw gateway.
|
|
3461
|
+
*
|
|
3462
|
+
* @remarks
|
|
3463
|
+
* Fire-and-forget. A 200-class response means the request was accepted.
|
|
3464
|
+
* Any HTTP or transport failure returns `false` so the file-level cleanup
|
|
3465
|
+
* warning remains the only signal.
|
|
3466
|
+
*
|
|
3467
|
+
* @param options - Cleanup request configuration.
|
|
3468
|
+
* @returns Whether the gateway accepted the cleanup request.
|
|
3469
|
+
*/
|
|
3470
|
+
async function requestCleanupSession(options) {
|
|
3471
|
+
const { gatewayUrl, filePath, markerIdentity } = options;
|
|
3472
|
+
const url = `${gatewayUrl.replace(/\/$/, '')}/sessions/spawn`;
|
|
3473
|
+
const label = `cleanup:${basename(filePath)}`;
|
|
3474
|
+
const body = {
|
|
3475
|
+
task: buildCleanupTask(filePath, markerIdentity),
|
|
3476
|
+
label,
|
|
3477
|
+
};
|
|
3478
|
+
try {
|
|
3479
|
+
const response = await fetchWithTimeout(url, CLEANUP_REQUEST_TIMEOUT_MS, {
|
|
3480
|
+
method: 'POST',
|
|
3481
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3482
|
+
body: JSON.stringify(body),
|
|
3483
|
+
});
|
|
3484
|
+
return response.ok;
|
|
3485
|
+
}
|
|
3486
|
+
catch {
|
|
3487
|
+
return false;
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
/**
|
|
3492
|
+
* Cleanup flag scanning extracted from ComponentWriter.cycle().
|
|
3493
|
+
*
|
|
3494
|
+
* @remarks
|
|
3495
|
+
* After writing managed files, scans each for the cleanup flag and
|
|
3496
|
+
* fires a best-effort escalation request when a gateway URL is configured.
|
|
3497
|
+
* Uses a `pendingCleanups` set to deduplicate in-flight requests.
|
|
3498
|
+
*/
|
|
3499
|
+
/**
|
|
3500
|
+
* Scan managed files for the cleanup flag and escalate when detected.
|
|
3501
|
+
*
|
|
3502
|
+
* @param targets - Managed files to scan.
|
|
3503
|
+
* @param gatewayUrl - Gateway URL for session spawn.
|
|
3504
|
+
* @param pendingCleanups - Set tracking in-flight requests (mutated).
|
|
3505
|
+
*/
|
|
3506
|
+
function scanAndEscalateCleanup(targets, gatewayUrl, pendingCleanups) {
|
|
3507
|
+
for (const target of targets) {
|
|
3508
|
+
try {
|
|
3509
|
+
if (pendingCleanups.has(target.filePath))
|
|
3510
|
+
continue;
|
|
3511
|
+
const fileContent = readFileSync(target.filePath, 'utf-8');
|
|
3512
|
+
if (fileContent.includes(CLEANUP_FLAG)) {
|
|
3513
|
+
pendingCleanups.add(target.filePath);
|
|
3514
|
+
void requestCleanupSession({
|
|
3515
|
+
gatewayUrl,
|
|
3516
|
+
filePath: target.filePath,
|
|
3517
|
+
markerIdentity: target.markerIdentity,
|
|
3518
|
+
}).finally(() => {
|
|
3519
|
+
pendingCleanups.delete(target.filePath);
|
|
3520
|
+
});
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
catch {
|
|
3524
|
+
// Best-effort: don't fail the cycle for escalation issues.
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
/**
|
|
3530
|
+
* Memory budget accounting and staleness detection for MEMORY.md.
|
|
3531
|
+
*
|
|
3532
|
+
* @remarks
|
|
3533
|
+
* Scans MEMORY.md for ISO date patterns in H2/H3 headings and bullet items.
|
|
3534
|
+
* Reports character count against a configured budget, warning threshold state,
|
|
3535
|
+
* and stale section candidates. Does not auto-delete: review remains
|
|
3536
|
+
* human- or agent-mediated (Decision 42).
|
|
3537
|
+
*/
|
|
3538
|
+
/** ISO date pattern: YYYY-MM-DD. */
|
|
3539
|
+
const ISO_DATE_RE = /\b(\d{4}-\d{2}-\d{2})\b/g;
|
|
3540
|
+
/** H2 heading pattern used to split sections. */
|
|
3541
|
+
const H2_RE = /^## /m;
|
|
3542
|
+
/**
|
|
3543
|
+
* Extract the most recent ISO date from a string.
|
|
3544
|
+
*
|
|
3545
|
+
* @param text - Text to scan for dates.
|
|
3546
|
+
* @returns The most recent date found, or undefined.
|
|
3547
|
+
*/
|
|
3548
|
+
function extractMostRecentDate(text) {
|
|
3549
|
+
const matches = text.match(ISO_DATE_RE);
|
|
3550
|
+
if (!matches)
|
|
3551
|
+
return undefined;
|
|
3552
|
+
let latest;
|
|
3553
|
+
for (const match of matches) {
|
|
3554
|
+
const d = new Date(match + 'T00:00:00Z');
|
|
3555
|
+
if (!Number.isNaN(d.getTime())) {
|
|
3556
|
+
if (!latest || d > latest)
|
|
3557
|
+
latest = d;
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
return latest;
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
3563
|
+
* Analyze MEMORY.md for budget and staleness.
|
|
3564
|
+
*
|
|
3565
|
+
* @param options - Analysis configuration.
|
|
3566
|
+
* @returns Memory hygiene result.
|
|
3567
|
+
*/
|
|
3568
|
+
function analyzeMemory(options) {
|
|
3569
|
+
const { workspacePath, budget, warningThreshold, staleDays } = options;
|
|
3570
|
+
const memoryPath = join(workspacePath, WORKSPACE_FILES.memory);
|
|
3571
|
+
if (!existsSync(memoryPath)) {
|
|
3572
|
+
return {
|
|
3573
|
+
exists: false,
|
|
3574
|
+
charCount: 0,
|
|
3575
|
+
budget,
|
|
3576
|
+
usage: 0,
|
|
3577
|
+
warning: false,
|
|
3578
|
+
overBudget: false,
|
|
3579
|
+
staleCandidates: 0,
|
|
3580
|
+
staleSectionNames: [],
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
const content = readFileSync(memoryPath, 'utf-8');
|
|
3584
|
+
const charCount = content.length;
|
|
3585
|
+
const usage = budget > 0 ? charCount / budget : charCount > 0 ? Infinity : 0;
|
|
3586
|
+
const warning = usage >= warningThreshold;
|
|
3587
|
+
const overBudget = usage > 1;
|
|
3588
|
+
// Split into H2 sections and scan for staleness
|
|
3589
|
+
const sections = content.split(H2_RE).slice(1); // skip content before first H2
|
|
3590
|
+
const now = Date.now();
|
|
3591
|
+
const thresholdMs = staleDays * 24 * 60 * 60 * 1000;
|
|
3592
|
+
const staleSectionNames = [];
|
|
3593
|
+
for (const section of sections) {
|
|
3594
|
+
const sectionName = section.split('\n')[0]?.trim() ?? '';
|
|
3595
|
+
const recentDate = extractMostRecentDate(section);
|
|
3596
|
+
// Sections without dates are evergreen — never flagged (Decision 47)
|
|
3597
|
+
if (!recentDate)
|
|
3598
|
+
continue;
|
|
3599
|
+
if (now - recentDate.getTime() > thresholdMs) {
|
|
3600
|
+
staleSectionNames.push(sectionName);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
return {
|
|
3604
|
+
exists: true,
|
|
3605
|
+
charCount,
|
|
3606
|
+
budget,
|
|
3607
|
+
usage,
|
|
3608
|
+
warning,
|
|
3609
|
+
overBudget,
|
|
3610
|
+
staleCandidates: staleSectionNames.length,
|
|
3611
|
+
staleSectionNames,
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
/**
|
|
3616
|
+
* HEARTBEAT integration for memory hygiene.
|
|
3617
|
+
*
|
|
3618
|
+
* @remarks
|
|
3619
|
+
* Calls `analyzeMemory()` and converts the result into a `HeartbeatEntry`
|
|
3620
|
+
* suitable for inclusion in the HEARTBEAT.md platform status section.
|
|
3621
|
+
* Returns `undefined` when MEMORY.md is healthy (no alert needed).
|
|
3622
|
+
*
|
|
3623
|
+
* Uses the `## MEMORY.md` heading (Decision 50) to distinguish memory
|
|
3624
|
+
* alerts from component alerts (`## jeeves-{name}`).
|
|
3625
|
+
*/
|
|
3626
|
+
/** The HEARTBEAT heading name for memory alerts. */
|
|
3627
|
+
const MEMORY_HEARTBEAT_NAME = 'MEMORY.md';
|
|
3628
|
+
/**
|
|
3629
|
+
* Check memory health and return a HEARTBEAT entry if unhealthy.
|
|
3630
|
+
*
|
|
3631
|
+
* @param options - Memory hygiene options (workspacePath, budget, etc.).
|
|
3632
|
+
* @returns A `HeartbeatEntry` when memory needs attention, `undefined` when healthy.
|
|
3633
|
+
*/
|
|
3634
|
+
function checkMemoryHealth(options) {
|
|
3635
|
+
const result = analyzeMemory(options);
|
|
3636
|
+
if (!result.exists)
|
|
3637
|
+
return undefined;
|
|
3638
|
+
if (!result.warning && result.staleCandidates === 0)
|
|
3639
|
+
return undefined;
|
|
3640
|
+
const lines = [];
|
|
3641
|
+
if (result.warning) {
|
|
3642
|
+
const pct = Math.round(result.usage * 100);
|
|
3643
|
+
lines.push(`- Budget: ${result.charCount.toLocaleString()} / ${result.budget.toLocaleString()} chars (${String(pct)}%).${result.overBudget ? ' **Over budget.**' : ' Consider reviewing.'}`);
|
|
3644
|
+
}
|
|
3645
|
+
if (result.staleCandidates > 0) {
|
|
3646
|
+
lines.push(`- ${String(result.staleCandidates)} stale section${result.staleCandidates === 1 ? '' : 's'}: ${result.staleSectionNames.join(', ')}`);
|
|
3647
|
+
}
|
|
3648
|
+
return {
|
|
3649
|
+
name: MEMORY_HEARTBEAT_NAME,
|
|
3650
|
+
declined: false,
|
|
3651
|
+
content: lines.join('\n'),
|
|
3652
|
+
};
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3027
3655
|
/**
|
|
3028
3656
|
* Core configuration schema and resolution.
|
|
3029
3657
|
*
|
|
@@ -3468,29 +4096,97 @@ async function orchestrateHeartbeat(options) {
|
|
|
3468
4096
|
}
|
|
3469
4097
|
|
|
3470
4098
|
/**
|
|
3471
|
-
*
|
|
4099
|
+
* HEARTBEAT orchestration extracted from ComponentWriter.cycle().
|
|
3472
4100
|
*
|
|
3473
4101
|
* @remarks
|
|
3474
|
-
*
|
|
3475
|
-
*
|
|
3476
|
-
*
|
|
4102
|
+
* Reads existing HEARTBEAT.md, resolves declined components, runs the
|
|
4103
|
+
* heartbeat state machine, and writes the result. Best-effort: failures
|
|
4104
|
+
* are logged but do not propagate.
|
|
4105
|
+
*/
|
|
4106
|
+
/**
|
|
4107
|
+
* Read a file's content, returning empty string if the file does not exist.
|
|
4108
|
+
*
|
|
4109
|
+
* @param filePath - Absolute file path.
|
|
4110
|
+
* @returns File content or empty string.
|
|
3477
4111
|
*/
|
|
4112
|
+
function readFileOrEmpty(filePath) {
|
|
4113
|
+
try {
|
|
4114
|
+
return readFileSync(filePath, 'utf-8');
|
|
4115
|
+
}
|
|
4116
|
+
catch (err) {
|
|
4117
|
+
if (err instanceof Error &&
|
|
4118
|
+
'code' in err &&
|
|
4119
|
+
err.code === 'ENOENT') {
|
|
4120
|
+
return '';
|
|
4121
|
+
}
|
|
4122
|
+
throw err;
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
3478
4125
|
/**
|
|
3479
|
-
*
|
|
4126
|
+
* Run a single HEARTBEAT orchestration cycle.
|
|
4127
|
+
*
|
|
4128
|
+
* @param options - Heartbeat cycle configuration.
|
|
4129
|
+
*/
|
|
4130
|
+
async function runHeartbeatCycle(options) {
|
|
4131
|
+
const { workspacePath, coreConfigDir, configRoot } = options;
|
|
4132
|
+
const heartbeatPath = join(workspacePath, WORKSPACE_FILES.heartbeat);
|
|
4133
|
+
try {
|
|
4134
|
+
const existingContent = readFileOrEmpty(heartbeatPath);
|
|
4135
|
+
const parsed = parseHeartbeat(existingContent);
|
|
4136
|
+
const declinedNames = new Set(parsed.entries.filter((e) => e.declined).map((e) => e.name));
|
|
4137
|
+
const entries = await orchestrateHeartbeat({
|
|
4138
|
+
coreConfigDir,
|
|
4139
|
+
configRoot,
|
|
4140
|
+
declinedNames,
|
|
4141
|
+
});
|
|
4142
|
+
// Memory hygiene check (Decision 49)
|
|
4143
|
+
if (!declinedNames.has(MEMORY_HEARTBEAT_NAME)) {
|
|
4144
|
+
const wsConfig = loadWorkspaceConfig(workspacePath);
|
|
4145
|
+
const memoryEntry = checkMemoryHealth({
|
|
4146
|
+
workspacePath,
|
|
4147
|
+
budget: wsConfig?.memory?.budget ?? WORKSPACE_CONFIG_DEFAULTS.memory.budget,
|
|
4148
|
+
warningThreshold: wsConfig?.memory?.warningThreshold ??
|
|
4149
|
+
WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold,
|
|
4150
|
+
staleDays: wsConfig?.memory?.staleDays ??
|
|
4151
|
+
WORKSPACE_CONFIG_DEFAULTS.memory.staleDays,
|
|
4152
|
+
});
|
|
4153
|
+
if (memoryEntry)
|
|
4154
|
+
entries.push(memoryEntry);
|
|
4155
|
+
}
|
|
4156
|
+
else {
|
|
4157
|
+
entries.push({
|
|
4158
|
+
name: MEMORY_HEARTBEAT_NAME,
|
|
4159
|
+
declined: true,
|
|
4160
|
+
content: '',
|
|
4161
|
+
});
|
|
4162
|
+
}
|
|
4163
|
+
await writeHeartbeatSection(heartbeatPath, entries);
|
|
4164
|
+
}
|
|
4165
|
+
catch (err) {
|
|
4166
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4167
|
+
console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${msg}`);
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
/**
|
|
4172
|
+
* Timer-based orchestrator for managed content writing.
|
|
3480
4173
|
*
|
|
3481
4174
|
* @remarks
|
|
3482
|
-
*
|
|
3483
|
-
*
|
|
3484
|
-
*
|
|
4175
|
+
* `ComponentWriter` manages a component's TOOLS.md section writes
|
|
4176
|
+
* and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
|
|
4177
|
+
* on a configurable prime-interval timer cycle.
|
|
3485
4178
|
*/
|
|
3486
4179
|
class ComponentWriter {
|
|
3487
4180
|
timer;
|
|
3488
4181
|
component;
|
|
3489
4182
|
configDir;
|
|
4183
|
+
gatewayUrl;
|
|
4184
|
+
pendingCleanups = new Set();
|
|
3490
4185
|
/** @internal */
|
|
3491
|
-
constructor(component) {
|
|
4186
|
+
constructor(component, options) {
|
|
3492
4187
|
this.component = component;
|
|
3493
4188
|
this.configDir = getComponentConfigDir(component.name);
|
|
4189
|
+
this.gatewayUrl = options?.gatewayUrl;
|
|
3494
4190
|
}
|
|
3495
4191
|
/** The component's config directory path. */
|
|
3496
4192
|
get componentConfigDir() {
|
|
@@ -3524,15 +4220,16 @@ class ComponentWriter {
|
|
|
3524
4220
|
* Execute a single write cycle.
|
|
3525
4221
|
*
|
|
3526
4222
|
* @remarks
|
|
3527
|
-
*
|
|
3528
|
-
*
|
|
3529
|
-
*
|
|
4223
|
+
* 1. Write the component's TOOLS.md section.
|
|
4224
|
+
* 2. Refresh shared platform content (SOUL.md, AGENTS.md, Platform section).
|
|
4225
|
+
* 3. Scan for cleanup flags and escalate if a gateway URL is configured.
|
|
4226
|
+
* 4. Run HEARTBEAT health orchestration.
|
|
3530
4227
|
*/
|
|
3531
4228
|
async cycle() {
|
|
3532
4229
|
try {
|
|
3533
4230
|
const workspacePath = getWorkspacePath();
|
|
3534
4231
|
const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
|
|
3535
|
-
// Write the component's TOOLS.md section
|
|
4232
|
+
// 1. Write the component's TOOLS.md section
|
|
3536
4233
|
const toolsContent = this.component.generateToolsContent();
|
|
3537
4234
|
await updateManagedSection(toolsPath, toolsContent, {
|
|
3538
4235
|
mode: 'section',
|
|
@@ -3540,7 +4237,7 @@ class ComponentWriter {
|
|
|
3540
4237
|
markers: TOOLS_MARKERS,
|
|
3541
4238
|
coreVersion: CORE_VERSION,
|
|
3542
4239
|
});
|
|
3543
|
-
// Platform content maintenance
|
|
4240
|
+
// 2. Platform content maintenance
|
|
3544
4241
|
await refreshPlatformContent({
|
|
3545
4242
|
coreVersion: CORE_VERSION,
|
|
3546
4243
|
componentName: this.component.name,
|
|
@@ -3548,36 +4245,26 @@ class ComponentWriter {
|
|
|
3548
4245
|
servicePackage: this.component.servicePackage,
|
|
3549
4246
|
pluginPackage: this.component.pluginPackage,
|
|
3550
4247
|
});
|
|
3551
|
-
//
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
}
|
|
3565
|
-
throw err;
|
|
3566
|
-
}
|
|
3567
|
-
})();
|
|
3568
|
-
const parsed = parseHeartbeat(existingContent);
|
|
3569
|
-
const declinedNames = new Set(parsed.entries.filter((e) => e.declined).map((e) => e.name));
|
|
3570
|
-
const entries = await orchestrateHeartbeat({
|
|
3571
|
-
coreConfigDir: getCoreConfigDir(),
|
|
3572
|
-
configRoot: getConfigRoot(),
|
|
3573
|
-
declinedNames,
|
|
3574
|
-
});
|
|
3575
|
-
await writeHeartbeatSection(heartbeatPath, entries);
|
|
3576
|
-
}
|
|
3577
|
-
catch (err) {
|
|
3578
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
3579
|
-
console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${msg}`);
|
|
4248
|
+
// 3. Cleanup escalation
|
|
4249
|
+
if (this.gatewayUrl) {
|
|
4250
|
+
scanAndEscalateCleanup([
|
|
4251
|
+
{ filePath: toolsPath, markerIdentity: 'TOOLS' },
|
|
4252
|
+
{
|
|
4253
|
+
filePath: join(workspacePath, WORKSPACE_FILES.soul),
|
|
4254
|
+
markerIdentity: 'SOUL',
|
|
4255
|
+
},
|
|
4256
|
+
{
|
|
4257
|
+
filePath: join(workspacePath, WORKSPACE_FILES.agents),
|
|
4258
|
+
markerIdentity: 'AGENTS',
|
|
4259
|
+
},
|
|
4260
|
+
], this.gatewayUrl, this.pendingCleanups);
|
|
3580
4261
|
}
|
|
4262
|
+
// 4. HEARTBEAT orchestration
|
|
4263
|
+
await runHeartbeatCycle({
|
|
4264
|
+
workspacePath,
|
|
4265
|
+
coreConfigDir: getCoreConfigDir(),
|
|
4266
|
+
configRoot: getConfigRoot(),
|
|
4267
|
+
});
|
|
3581
4268
|
}
|
|
3582
4269
|
catch (err) {
|
|
3583
4270
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -3660,13 +4347,14 @@ function createAsyncContentCache(options) {
|
|
|
3660
4347
|
* This replaces the v0.4.0 `createComponentWriter(JeevesComponent)`.
|
|
3661
4348
|
*
|
|
3662
4349
|
* @param descriptor - The component descriptor to validate and wrap.
|
|
4350
|
+
* @param options - Optional writer configuration (e.g., gatewayUrl for cleanup escalation).
|
|
3663
4351
|
* @returns A new `ComponentWriter` instance.
|
|
3664
4352
|
* @throws ZodError if the descriptor is invalid.
|
|
3665
4353
|
*/
|
|
3666
|
-
function createComponentWriter(descriptor) {
|
|
4354
|
+
function createComponentWriter(descriptor, options) {
|
|
3667
4355
|
// Validate via Zod — throws ZodError with detailed messages on failure
|
|
3668
4356
|
jeevesComponentDescriptorSchema.parse(descriptor);
|
|
3669
|
-
return new ComponentWriter(descriptor);
|
|
4357
|
+
return new ComponentWriter(descriptor, options);
|
|
3670
4358
|
}
|
|
3671
4359
|
|
|
3672
4360
|
/**
|
|
@@ -3765,6 +4453,8 @@ async function seedContent(options) {
|
|
|
3765
4453
|
content: `- ${NOT_INSTALLED_ALERTS[name]}`,
|
|
3766
4454
|
}));
|
|
3767
4455
|
await writeHeartbeatSection(heartbeatPath, entries);
|
|
4456
|
+
// Seed jeeves workspace skill (Decision 48: overwrite-on-install)
|
|
4457
|
+
seedSkill(getWorkspacePath());
|
|
3768
4458
|
}
|
|
3769
4459
|
|
|
3770
4460
|
/**
|
|
@@ -4468,4 +5158,4 @@ async function getChannelWorkspace(channelId, token, options) {
|
|
|
4468
5158
|
return teamId;
|
|
4469
5159
|
}
|
|
4470
5160
|
|
|
4471
|
-
export { AGENTS_MARKERS, CLEANUP_FLAG, COMPONENT_CONFIG_PREFIX, COMPONENT_VERSIONS_FILE, CONFIG_FILE, CORE_CONFIG_DIR, CORE_VERSION, ComponentWriter, DEFAULT_BIND_ADDRESS, DEFAULT_CORE_VERSION, DEFAULT_PORTS, HEARTBEAT_HEADING, META_PORT, PLATFORM_COMPONENTS, 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, appendJsonl, atomicWrite, buildHeartbeatSection, checkRegistryVersion, connectionFail, coreConfigSchema, createAsyncContentCache, createComponentWriter, createConfigApplyHandler, createConfigQueryHandler, createGoogleAuth, createPluginCli, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, ensureDir, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isPrime, jaccard, jeevesComponentDescriptorSchema, loadEnvFile, needsCleanup, nowIso, ok, orchestrateHeartbeat, parseArgs, parseHeartbeat, parseManaged, patchConfig, postJson, readComponentVersions, readJson, readJsonl, refreshPlatformContent, removeComponentVersion, removeManagedSection, resetInit, resolveConfigPath, resolveOpenClawHome, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, run, runScript, runWithRetry, saveCache, seedContent, shingles, shouldWrite, sleepAsync, sleepMs, updateManagedSection, uuid, withFileLock, writeComponentVersion, writeHeartbeatSection, writeJsonAtomic, writeJsonl };
|
|
5161
|
+
export { AGENTS_MARKERS, CLEANUP_FLAG, COMPONENT_CONFIG_PREFIX, COMPONENT_VERSIONS_FILE, CONFIG_FILE, CORE_CONFIG_DIR, CORE_VERSION, ComponentWriter, DEFAULT_BIND_ADDRESS, DEFAULT_CORE_VERSION, DEFAULT_PORTS, HEARTBEAT_HEADING, JEEVES_SKILL_DIR, MEMORY_HEARTBEAT_NAME, META_PORT, PLATFORM_COMPONENTS, REGISTRY_CACHE_FILE, RUNNER_PORT, SECTION_IDS, SECTION_ORDER, SERVER_PORT, SKILLS_DIR, SOUL_MARKERS, STALENESS_THRESHOLD_MS, STALE_LOCK_MS, TEMPLATES_DIR, TOOLS_MARKERS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_CONFIG_DEFAULTS, WORKSPACE_CONFIG_FILE, WORKSPACE_FILES, analyzeMemory, appendJsonl, atomicWrite, buildEffectiveConfig, buildHeartbeatSection, checkMemoryHealth, checkNodeVersion, checkRegistryVersion, connectionFail, coreConfigSchema, createAsyncContentCache, createComponentWriter, createConfigApplyHandler, createConfigQueryHandler, createGoogleAuth, createPluginCli, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, ensureDir, extractMostRecentDate, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, generateWorkspaceJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isPrime, jaccard, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, needsCleanup, nowIso, ok, orchestrateHeartbeat, parseArgs, parseHeartbeat, parseManaged, patchConfig, postJson, readComponentVersions, readJson, readJsonl, refreshPlatformContent, removeComponentVersion, removeManagedSection, resetInit, resolveConfigPath, resolveConfigValue, resolveOpenClawHome, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, run, runScript, runWithRetry, saveCache, seedContent, seedSkill, shingles, shouldWrite, sleepAsync, sleepMs, updateManagedSection, uuid, withFileLock, workspaceConfigSchema, writeComponentVersion, writeHeartbeatSection, writeJsonAtomic, writeJsonl };
|