@karmaniverous/jeeves 0.4.7 → 0.5.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/README.md +56 -3
- package/content/skill.md +99 -0
- package/dist/cli/jeeves/index.js +660 -158
- package/dist/cli/plugin/index.js +140 -4
- package/dist/cli/service/index.js +63 -7
- package/dist/index.d.ts +231 -9
- package/dist/index.js +711 -88
- 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.4.
|
|
186
|
+
* The `0.4.7` 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.4.
|
|
193
|
+
const CORE_VERSION = '0.4.7';
|
|
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
|
}
|
|
@@ -1121,6 +1378,129 @@ 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
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* Skill seeding: write the `jeeves` workspace skill unconditionally.
|
|
1484
|
+
*
|
|
1485
|
+
* @remarks
|
|
1486
|
+
* The skill file is entirely generated — no user-authored content (Decision 48).
|
|
1487
|
+
* Every installer (core CLI and component plugins) writes it unconditionally.
|
|
1488
|
+
* Content is inlined at build time via `rollup-plugin-md.ts`.
|
|
1489
|
+
*/
|
|
1490
|
+
/**
|
|
1491
|
+
* Seed the jeeves workspace skill file.
|
|
1492
|
+
*
|
|
1493
|
+
* @param workspacePath - Workspace root directory.
|
|
1494
|
+
*/
|
|
1495
|
+
function seedSkill(workspacePath) {
|
|
1496
|
+
const skillDir = join(workspacePath, SKILLS_DIR, JEEVES_SKILL_DIR);
|
|
1497
|
+
if (!existsSync(skillDir)) {
|
|
1498
|
+
mkdirSync(skillDir, { recursive: true });
|
|
1499
|
+
}
|
|
1500
|
+
const skillPath = join(skillDir, 'SKILL.md');
|
|
1501
|
+
writeFileSync(skillPath, skillContent, 'utf-8');
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1124
1504
|
/**
|
|
1125
1505
|
* OpenClaw configuration helpers for plugin CLI installers.
|
|
1126
1506
|
*
|
|
@@ -1360,7 +1740,7 @@ function createPluginCli(options) {
|
|
|
1360
1740
|
for (const msg of messages) {
|
|
1361
1741
|
console.log(` ✓ ${msg}`);
|
|
1362
1742
|
}
|
|
1363
|
-
// 4. Write initial HEARTBEAT entry
|
|
1743
|
+
// 4. Write initial HEARTBEAT entry and seed jeeves skill
|
|
1364
1744
|
try {
|
|
1365
1745
|
const cfgRoot = opts.configRoot;
|
|
1366
1746
|
const agents = config.agents;
|
|
@@ -1390,10 +1770,17 @@ function createPluginCli(options) {
|
|
|
1390
1770
|
catch {
|
|
1391
1771
|
console.log(' ⚠ Could not write HEARTBEAT entry');
|
|
1392
1772
|
}
|
|
1773
|
+
try {
|
|
1774
|
+
seedSkill(ws);
|
|
1775
|
+
console.log(' ✓ Jeeves skill seeded');
|
|
1776
|
+
}
|
|
1777
|
+
catch {
|
|
1778
|
+
console.log(' ⚠ Could not seed Jeeves skill');
|
|
1779
|
+
}
|
|
1393
1780
|
}
|
|
1394
1781
|
}
|
|
1395
1782
|
catch {
|
|
1396
|
-
// HEARTBEAT
|
|
1783
|
+
// HEARTBEAT + skill seeding are best-effort during install
|
|
1397
1784
|
}
|
|
1398
1785
|
// 5. Write component version
|
|
1399
1786
|
try {
|
|
@@ -2045,19 +2432,6 @@ function createServiceManager(descriptor) {
|
|
|
2045
2432
|
}
|
|
2046
2433
|
}
|
|
2047
2434
|
|
|
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
2435
|
/**
|
|
2062
2436
|
* Factory for the standard Jeeves service CLI.
|
|
2063
2437
|
*
|
|
@@ -2486,10 +2860,10 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
2486
2860
|
? `# ${markers.title}\n\n${sectionText}`
|
|
2487
2861
|
: sectionText;
|
|
2488
2862
|
}
|
|
2489
|
-
//
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
//
|
|
2863
|
+
// Build the full managed block
|
|
2864
|
+
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
2865
|
+
const endLine = formatEndMarker(markers.end);
|
|
2866
|
+
// Combine all user content for cleanup detection
|
|
2493
2867
|
const rawUserContent = [parsed.beforeContent, parsed.userContent]
|
|
2494
2868
|
.filter(Boolean)
|
|
2495
2869
|
.join('\n\n')
|
|
@@ -2497,9 +2871,6 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
2497
2871
|
// Strip foreign managed blocks from user content (cross-contamination fix)
|
|
2498
2872
|
const userContent = stripForeignMarkers(rawUserContent, markers);
|
|
2499
2873
|
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
2874
|
const managedParts = [];
|
|
2504
2875
|
managedParts.push(beginLine);
|
|
2505
2876
|
if (cleanupNeeded) {
|
|
@@ -2511,26 +2882,54 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
2511
2882
|
managedParts.push('');
|
|
2512
2883
|
managedParts.push(endLine);
|
|
2513
2884
|
const managedBlock = managedParts.join('\n');
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
//
|
|
2518
|
-
|
|
2519
|
-
|
|
2885
|
+
let newFileContent;
|
|
2886
|
+
if (parsed.found) {
|
|
2887
|
+
// Existing block: update in place — preserve position, don't move.
|
|
2888
|
+
// Strip foreign managed blocks from both content zones (cross-contamination fix).
|
|
2889
|
+
const cleanBefore = stripForeignMarkers(parsed.beforeContent, markers);
|
|
2890
|
+
const cleanAfter = stripForeignMarkers(parsed.userContent, markers);
|
|
2891
|
+
const fileParts = [];
|
|
2892
|
+
if (cleanBefore) {
|
|
2893
|
+
fileParts.push(cleanBefore);
|
|
2520
2894
|
fileParts.push('');
|
|
2521
2895
|
}
|
|
2522
2896
|
fileParts.push(managedBlock);
|
|
2897
|
+
if (cleanAfter) {
|
|
2898
|
+
fileParts.push('');
|
|
2899
|
+
fileParts.push(cleanAfter);
|
|
2900
|
+
}
|
|
2901
|
+
fileParts.push('');
|
|
2902
|
+
newFileContent = fileParts.join('\n');
|
|
2523
2903
|
}
|
|
2524
2904
|
else {
|
|
2525
|
-
//
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2905
|
+
// No existing block: insert new block using the configured position.
|
|
2906
|
+
// Strip orphaned same-type BEGIN markers from user content to prevent
|
|
2907
|
+
// the parser from pairing them with the new END marker on the next cycle.
|
|
2908
|
+
const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2909
|
+
const orphanedBeginRe = new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$\\n?`, 'gm');
|
|
2910
|
+
const cleanUserContent = userContent
|
|
2911
|
+
.replace(orphanedBeginRe, '')
|
|
2912
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
2913
|
+
.trim();
|
|
2914
|
+
const position = markers.position ?? 'top';
|
|
2915
|
+
const fileParts = [];
|
|
2916
|
+
if (position === 'bottom') {
|
|
2917
|
+
if (cleanUserContent) {
|
|
2918
|
+
fileParts.push(cleanUserContent);
|
|
2919
|
+
fileParts.push('');
|
|
2920
|
+
}
|
|
2921
|
+
fileParts.push(managedBlock);
|
|
2530
2922
|
}
|
|
2923
|
+
else {
|
|
2924
|
+
fileParts.push(managedBlock);
|
|
2925
|
+
if (cleanUserContent) {
|
|
2926
|
+
fileParts.push('');
|
|
2927
|
+
fileParts.push(cleanUserContent);
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
fileParts.push('');
|
|
2931
|
+
newFileContent = fileParts.join('\n');
|
|
2531
2932
|
}
|
|
2532
|
-
fileParts.push('');
|
|
2533
|
-
const newFileContent = fileParts.join('\n');
|
|
2534
2933
|
atomicWrite(filePath, newFileContent);
|
|
2535
2934
|
});
|
|
2536
2935
|
}
|
|
@@ -3024,6 +3423,103 @@ async function refreshPlatformContent(options) {
|
|
|
3024
3423
|
copyTemplates(coreConfigDir);
|
|
3025
3424
|
}
|
|
3026
3425
|
|
|
3426
|
+
/**
|
|
3427
|
+
* Cleanup-session escalation for managed files with orphaned duplicated content.
|
|
3428
|
+
*
|
|
3429
|
+
* @remarks
|
|
3430
|
+
* When a managed file contains the cleanup flag, the writer can ask the
|
|
3431
|
+
* OpenClaw gateway to spawn a background session to remove orphaned content.
|
|
3432
|
+
* The request is best-effort: accepted requests return `true`; any transport
|
|
3433
|
+
* or HTTP failure returns `false` so the file warning remains the fallback.
|
|
3434
|
+
*/
|
|
3435
|
+
/** Timeout for cleanup-session spawn requests. */
|
|
3436
|
+
const CLEANUP_REQUEST_TIMEOUT_MS = 5_000;
|
|
3437
|
+
/**
|
|
3438
|
+
* Build the cleanup task prompt sent to the gateway session API.
|
|
3439
|
+
*
|
|
3440
|
+
* @param filePath - Managed file requiring cleanup.
|
|
3441
|
+
* @param markerIdentity - Marker identity for the file.
|
|
3442
|
+
* @returns Cleanup instructions for the spawned session.
|
|
3443
|
+
*/
|
|
3444
|
+
function buildCleanupTask(filePath, markerIdentity) {
|
|
3445
|
+
return [
|
|
3446
|
+
`Clean up orphaned managed content in ${filePath}.`,
|
|
3447
|
+
`The file uses ${markerIdentity} managed comment markers.`,
|
|
3448
|
+
'Review content outside the managed block and remove only duplicated managed content.',
|
|
3449
|
+
'Preserve any unique user-authored content outside the managed block.',
|
|
3450
|
+
'Do not modify content inside the managed block unless required to preserve valid marker structure.',
|
|
3451
|
+
].join(' ');
|
|
3452
|
+
}
|
|
3453
|
+
/**
|
|
3454
|
+
* Request a cleanup session from the OpenClaw gateway.
|
|
3455
|
+
*
|
|
3456
|
+
* @remarks
|
|
3457
|
+
* Fire-and-forget. A 200-class response means the request was accepted.
|
|
3458
|
+
* Any HTTP or transport failure returns `false` so the file-level cleanup
|
|
3459
|
+
* warning remains the only signal.
|
|
3460
|
+
*
|
|
3461
|
+
* @param options - Cleanup request configuration.
|
|
3462
|
+
* @returns Whether the gateway accepted the cleanup request.
|
|
3463
|
+
*/
|
|
3464
|
+
async function requestCleanupSession(options) {
|
|
3465
|
+
const { gatewayUrl, filePath, markerIdentity } = options;
|
|
3466
|
+
const url = `${gatewayUrl.replace(/\/$/, '')}/sessions/spawn`;
|
|
3467
|
+
const label = `cleanup:${basename(filePath)}`;
|
|
3468
|
+
const body = {
|
|
3469
|
+
task: buildCleanupTask(filePath, markerIdentity),
|
|
3470
|
+
label,
|
|
3471
|
+
};
|
|
3472
|
+
try {
|
|
3473
|
+
const response = await fetchWithTimeout(url, CLEANUP_REQUEST_TIMEOUT_MS, {
|
|
3474
|
+
method: 'POST',
|
|
3475
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3476
|
+
body: JSON.stringify(body),
|
|
3477
|
+
});
|
|
3478
|
+
return response.ok;
|
|
3479
|
+
}
|
|
3480
|
+
catch {
|
|
3481
|
+
return false;
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
/**
|
|
3486
|
+
* Cleanup flag scanning extracted from ComponentWriter.cycle().
|
|
3487
|
+
*
|
|
3488
|
+
* @remarks
|
|
3489
|
+
* After writing managed files, scans each for the cleanup flag and
|
|
3490
|
+
* fires a best-effort escalation request when a gateway URL is configured.
|
|
3491
|
+
* Uses a `pendingCleanups` set to deduplicate in-flight requests.
|
|
3492
|
+
*/
|
|
3493
|
+
/**
|
|
3494
|
+
* Scan managed files for the cleanup flag and escalate when detected.
|
|
3495
|
+
*
|
|
3496
|
+
* @param targets - Managed files to scan.
|
|
3497
|
+
* @param gatewayUrl - Gateway URL for session spawn.
|
|
3498
|
+
* @param pendingCleanups - Set tracking in-flight requests (mutated).
|
|
3499
|
+
*/
|
|
3500
|
+
function scanAndEscalateCleanup(targets, gatewayUrl, pendingCleanups) {
|
|
3501
|
+
for (const target of targets) {
|
|
3502
|
+
try {
|
|
3503
|
+
if (pendingCleanups.has(target.filePath))
|
|
3504
|
+
continue;
|
|
3505
|
+
const fileContent = readFileSync(target.filePath, 'utf-8');
|
|
3506
|
+
if (fileContent.includes(CLEANUP_FLAG)) {
|
|
3507
|
+
pendingCleanups.add(target.filePath);
|
|
3508
|
+
void requestCleanupSession({
|
|
3509
|
+
gatewayUrl,
|
|
3510
|
+
filePath: target.filePath,
|
|
3511
|
+
markerIdentity: target.markerIdentity,
|
|
3512
|
+
}).finally(() => {
|
|
3513
|
+
pendingCleanups.delete(target.filePath);
|
|
3514
|
+
});
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
catch {
|
|
3518
|
+
// Best-effort: don't fail the cycle for escalation issues.
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3027
3523
|
/**
|
|
3028
3524
|
* Core configuration schema and resolution.
|
|
3029
3525
|
*
|
|
@@ -3468,29 +3964,76 @@ async function orchestrateHeartbeat(options) {
|
|
|
3468
3964
|
}
|
|
3469
3965
|
|
|
3470
3966
|
/**
|
|
3471
|
-
*
|
|
3967
|
+
* HEARTBEAT orchestration extracted from ComponentWriter.cycle().
|
|
3472
3968
|
*
|
|
3473
3969
|
* @remarks
|
|
3474
|
-
*
|
|
3475
|
-
*
|
|
3476
|
-
*
|
|
3970
|
+
* Reads existing HEARTBEAT.md, resolves declined components, runs the
|
|
3971
|
+
* heartbeat state machine, and writes the result. Best-effort: failures
|
|
3972
|
+
* are logged but do not propagate.
|
|
3973
|
+
*/
|
|
3974
|
+
/**
|
|
3975
|
+
* Read a file's content, returning empty string if the file does not exist.
|
|
3976
|
+
*
|
|
3977
|
+
* @param filePath - Absolute file path.
|
|
3978
|
+
* @returns File content or empty string.
|
|
3979
|
+
*/
|
|
3980
|
+
function readFileOrEmpty(filePath) {
|
|
3981
|
+
try {
|
|
3982
|
+
return readFileSync(filePath, 'utf-8');
|
|
3983
|
+
}
|
|
3984
|
+
catch (err) {
|
|
3985
|
+
if (err instanceof Error &&
|
|
3986
|
+
'code' in err &&
|
|
3987
|
+
err.code === 'ENOENT') {
|
|
3988
|
+
return '';
|
|
3989
|
+
}
|
|
3990
|
+
throw err;
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
/**
|
|
3994
|
+
* Run a single HEARTBEAT orchestration cycle.
|
|
3995
|
+
*
|
|
3996
|
+
* @param options - Heartbeat cycle configuration.
|
|
3477
3997
|
*/
|
|
3998
|
+
async function runHeartbeatCycle(options) {
|
|
3999
|
+
const { workspacePath, coreConfigDir, configRoot } = options;
|
|
4000
|
+
const heartbeatPath = join(workspacePath, WORKSPACE_FILES.heartbeat);
|
|
4001
|
+
try {
|
|
4002
|
+
const existingContent = readFileOrEmpty(heartbeatPath);
|
|
4003
|
+
const parsed = parseHeartbeat(existingContent);
|
|
4004
|
+
const declinedNames = new Set(parsed.entries.filter((e) => e.declined).map((e) => e.name));
|
|
4005
|
+
const entries = await orchestrateHeartbeat({
|
|
4006
|
+
coreConfigDir,
|
|
4007
|
+
configRoot,
|
|
4008
|
+
declinedNames,
|
|
4009
|
+
});
|
|
4010
|
+
await writeHeartbeatSection(heartbeatPath, entries);
|
|
4011
|
+
}
|
|
4012
|
+
catch (err) {
|
|
4013
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4014
|
+
console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${msg}`);
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
|
|
3478
4018
|
/**
|
|
3479
|
-
*
|
|
4019
|
+
* Timer-based orchestrator for managed content writing.
|
|
3480
4020
|
*
|
|
3481
4021
|
* @remarks
|
|
3482
|
-
*
|
|
3483
|
-
*
|
|
3484
|
-
*
|
|
4022
|
+
* `ComponentWriter` manages a component's TOOLS.md section writes
|
|
4023
|
+
* and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
|
|
4024
|
+
* on a configurable prime-interval timer cycle.
|
|
3485
4025
|
*/
|
|
3486
4026
|
class ComponentWriter {
|
|
3487
4027
|
timer;
|
|
3488
4028
|
component;
|
|
3489
4029
|
configDir;
|
|
4030
|
+
gatewayUrl;
|
|
4031
|
+
pendingCleanups = new Set();
|
|
3490
4032
|
/** @internal */
|
|
3491
|
-
constructor(component) {
|
|
4033
|
+
constructor(component, options) {
|
|
3492
4034
|
this.component = component;
|
|
3493
4035
|
this.configDir = getComponentConfigDir(component.name);
|
|
4036
|
+
this.gatewayUrl = options?.gatewayUrl;
|
|
3494
4037
|
}
|
|
3495
4038
|
/** The component's config directory path. */
|
|
3496
4039
|
get componentConfigDir() {
|
|
@@ -3524,15 +4067,16 @@ class ComponentWriter {
|
|
|
3524
4067
|
* Execute a single write cycle.
|
|
3525
4068
|
*
|
|
3526
4069
|
* @remarks
|
|
3527
|
-
*
|
|
3528
|
-
*
|
|
3529
|
-
*
|
|
4070
|
+
* 1. Write the component's TOOLS.md section.
|
|
4071
|
+
* 2. Refresh shared platform content (SOUL.md, AGENTS.md, Platform section).
|
|
4072
|
+
* 3. Scan for cleanup flags and escalate if a gateway URL is configured.
|
|
4073
|
+
* 4. Run HEARTBEAT health orchestration.
|
|
3530
4074
|
*/
|
|
3531
4075
|
async cycle() {
|
|
3532
4076
|
try {
|
|
3533
4077
|
const workspacePath = getWorkspacePath();
|
|
3534
4078
|
const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
|
|
3535
|
-
// Write the component's TOOLS.md section
|
|
4079
|
+
// 1. Write the component's TOOLS.md section
|
|
3536
4080
|
const toolsContent = this.component.generateToolsContent();
|
|
3537
4081
|
await updateManagedSection(toolsPath, toolsContent, {
|
|
3538
4082
|
mode: 'section',
|
|
@@ -3540,7 +4084,7 @@ class ComponentWriter {
|
|
|
3540
4084
|
markers: TOOLS_MARKERS,
|
|
3541
4085
|
coreVersion: CORE_VERSION,
|
|
3542
4086
|
});
|
|
3543
|
-
// Platform content maintenance
|
|
4087
|
+
// 2. Platform content maintenance
|
|
3544
4088
|
await refreshPlatformContent({
|
|
3545
4089
|
coreVersion: CORE_VERSION,
|
|
3546
4090
|
componentName: this.component.name,
|
|
@@ -3548,36 +4092,26 @@ class ComponentWriter {
|
|
|
3548
4092
|
servicePackage: this.component.servicePackage,
|
|
3549
4093
|
pluginPackage: this.component.pluginPackage,
|
|
3550
4094
|
});
|
|
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}`);
|
|
4095
|
+
// 3. Cleanup escalation
|
|
4096
|
+
if (this.gatewayUrl) {
|
|
4097
|
+
scanAndEscalateCleanup([
|
|
4098
|
+
{ filePath: toolsPath, markerIdentity: 'TOOLS' },
|
|
4099
|
+
{
|
|
4100
|
+
filePath: join(workspacePath, WORKSPACE_FILES.soul),
|
|
4101
|
+
markerIdentity: 'SOUL',
|
|
4102
|
+
},
|
|
4103
|
+
{
|
|
4104
|
+
filePath: join(workspacePath, WORKSPACE_FILES.agents),
|
|
4105
|
+
markerIdentity: 'AGENTS',
|
|
4106
|
+
},
|
|
4107
|
+
], this.gatewayUrl, this.pendingCleanups);
|
|
3580
4108
|
}
|
|
4109
|
+
// 4. HEARTBEAT orchestration
|
|
4110
|
+
await runHeartbeatCycle({
|
|
4111
|
+
workspacePath,
|
|
4112
|
+
coreConfigDir: getCoreConfigDir(),
|
|
4113
|
+
configRoot: getConfigRoot(),
|
|
4114
|
+
});
|
|
3581
4115
|
}
|
|
3582
4116
|
catch (err) {
|
|
3583
4117
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -3660,13 +4194,14 @@ function createAsyncContentCache(options) {
|
|
|
3660
4194
|
* This replaces the v0.4.0 `createComponentWriter(JeevesComponent)`.
|
|
3661
4195
|
*
|
|
3662
4196
|
* @param descriptor - The component descriptor to validate and wrap.
|
|
4197
|
+
* @param options - Optional writer configuration (e.g., gatewayUrl for cleanup escalation).
|
|
3663
4198
|
* @returns A new `ComponentWriter` instance.
|
|
3664
4199
|
* @throws ZodError if the descriptor is invalid.
|
|
3665
4200
|
*/
|
|
3666
|
-
function createComponentWriter(descriptor) {
|
|
4201
|
+
function createComponentWriter(descriptor, options) {
|
|
3667
4202
|
// Validate via Zod — throws ZodError with detailed messages on failure
|
|
3668
4203
|
jeevesComponentDescriptorSchema.parse(descriptor);
|
|
3669
|
-
return new ComponentWriter(descriptor);
|
|
4204
|
+
return new ComponentWriter(descriptor, options);
|
|
3670
4205
|
}
|
|
3671
4206
|
|
|
3672
4207
|
/**
|
|
@@ -3707,6 +4242,92 @@ function getBindAddress(componentName) {
|
|
|
3707
4242
|
return DEFAULT_BIND_ADDRESS;
|
|
3708
4243
|
}
|
|
3709
4244
|
|
|
4245
|
+
/**
|
|
4246
|
+
* Memory budget accounting and staleness detection for MEMORY.md.
|
|
4247
|
+
*
|
|
4248
|
+
* @remarks
|
|
4249
|
+
* Scans MEMORY.md for ISO date patterns in H2/H3 headings and bullet items.
|
|
4250
|
+
* Reports character count against a configured budget, warning threshold state,
|
|
4251
|
+
* and stale section candidates. Does not auto-delete: review remains
|
|
4252
|
+
* human- or agent-mediated (Decision 42).
|
|
4253
|
+
*/
|
|
4254
|
+
/** ISO date pattern: YYYY-MM-DD. */
|
|
4255
|
+
const ISO_DATE_RE = /\b(\d{4}-\d{2}-\d{2})\b/g;
|
|
4256
|
+
/** H2 heading pattern used to split sections. */
|
|
4257
|
+
const H2_RE = /^## /m;
|
|
4258
|
+
/**
|
|
4259
|
+
* Extract the most recent ISO date from a string.
|
|
4260
|
+
*
|
|
4261
|
+
* @param text - Text to scan for dates.
|
|
4262
|
+
* @returns The most recent date found, or undefined.
|
|
4263
|
+
*/
|
|
4264
|
+
function extractMostRecentDate(text) {
|
|
4265
|
+
const matches = text.match(ISO_DATE_RE);
|
|
4266
|
+
if (!matches)
|
|
4267
|
+
return undefined;
|
|
4268
|
+
let latest;
|
|
4269
|
+
for (const match of matches) {
|
|
4270
|
+
const d = new Date(match + 'T00:00:00Z');
|
|
4271
|
+
if (!Number.isNaN(d.getTime())) {
|
|
4272
|
+
if (!latest || d > latest)
|
|
4273
|
+
latest = d;
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
return latest;
|
|
4277
|
+
}
|
|
4278
|
+
/**
|
|
4279
|
+
* Analyze MEMORY.md for budget and staleness.
|
|
4280
|
+
*
|
|
4281
|
+
* @param options - Analysis configuration.
|
|
4282
|
+
* @returns Memory hygiene result.
|
|
4283
|
+
*/
|
|
4284
|
+
function analyzeMemory(options) {
|
|
4285
|
+
const { workspacePath, budget, warningThreshold, staleDays } = options;
|
|
4286
|
+
const memoryPath = join(workspacePath, WORKSPACE_FILES.memory);
|
|
4287
|
+
if (!existsSync(memoryPath)) {
|
|
4288
|
+
return {
|
|
4289
|
+
exists: false,
|
|
4290
|
+
charCount: 0,
|
|
4291
|
+
budget,
|
|
4292
|
+
usage: 0,
|
|
4293
|
+
warning: false,
|
|
4294
|
+
overBudget: false,
|
|
4295
|
+
staleCandidates: 0,
|
|
4296
|
+
staleSectionNames: [],
|
|
4297
|
+
};
|
|
4298
|
+
}
|
|
4299
|
+
const content = readFileSync(memoryPath, 'utf-8');
|
|
4300
|
+
const charCount = content.length;
|
|
4301
|
+
const usage = budget > 0 ? charCount / budget : charCount > 0 ? Infinity : 0;
|
|
4302
|
+
const warning = usage >= warningThreshold;
|
|
4303
|
+
const overBudget = usage > 1;
|
|
4304
|
+
// Split into H2 sections and scan for staleness
|
|
4305
|
+
const sections = content.split(H2_RE).slice(1); // skip content before first H2
|
|
4306
|
+
const now = Date.now();
|
|
4307
|
+
const thresholdMs = staleDays * 24 * 60 * 60 * 1000;
|
|
4308
|
+
const staleSectionNames = [];
|
|
4309
|
+
for (const section of sections) {
|
|
4310
|
+
const sectionName = section.split('\n')[0]?.trim() ?? '';
|
|
4311
|
+
const recentDate = extractMostRecentDate(section);
|
|
4312
|
+
// Sections without dates are evergreen — never flagged (Decision 47)
|
|
4313
|
+
if (!recentDate)
|
|
4314
|
+
continue;
|
|
4315
|
+
if (now - recentDate.getTime() > thresholdMs) {
|
|
4316
|
+
staleSectionNames.push(sectionName);
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
return {
|
|
4320
|
+
exists: true,
|
|
4321
|
+
charCount,
|
|
4322
|
+
budget,
|
|
4323
|
+
usage,
|
|
4324
|
+
warning,
|
|
4325
|
+
overBudget,
|
|
4326
|
+
staleCandidates: staleSectionNames.length,
|
|
4327
|
+
staleSectionNames,
|
|
4328
|
+
};
|
|
4329
|
+
}
|
|
4330
|
+
|
|
3710
4331
|
/**
|
|
3711
4332
|
* One-shot content seeding used by the CLI install command.
|
|
3712
4333
|
*
|
|
@@ -3765,6 +4386,8 @@ async function seedContent(options) {
|
|
|
3765
4386
|
content: `- ${NOT_INSTALLED_ALERTS[name]}`,
|
|
3766
4387
|
}));
|
|
3767
4388
|
await writeHeartbeatSection(heartbeatPath, entries);
|
|
4389
|
+
// Seed jeeves workspace skill (Decision 48: overwrite-on-install)
|
|
4390
|
+
seedSkill(getWorkspacePath());
|
|
3768
4391
|
}
|
|
3769
4392
|
|
|
3770
4393
|
/**
|
|
@@ -4468,4 +5091,4 @@ async function getChannelWorkspace(channelId, token, options) {
|
|
|
4468
5091
|
return teamId;
|
|
4469
5092
|
}
|
|
4470
5093
|
|
|
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 };
|
|
5094
|
+
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, 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, 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 };
|