@karmaniverous/jeeves 0.4.6 → 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/dist/index.js CHANGED
@@ -1,14 +1,15 @@
1
- import { writeFileSync, renameSync, unlinkSync, existsSync, readFileSync, mkdirSync, rmSync, readdirSync, copyFileSync, cpSync } from 'node:fs';
2
- import { join, dirname, resolve } from 'node:path';
1
+ import fs, { writeFileSync, renameSync, unlinkSync, existsSync, readFileSync, mkdirSync, readdirSync, copyFileSync, rmSync, cpSync } from 'node:fs';
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
- import { valid, gte, gt } from 'semver';
8
+ import { packageDirectorySync } from 'package-directory';
7
9
  import { homedir } from 'node:os';
8
- import { z } from 'zod';
9
- import { execSync } from 'node:child_process';
10
+ import cp, { execSync } from 'node:child_process';
10
11
  import { fileURLToPath } from 'node:url';
11
- import { packageDirectorySync } from 'package-directory';
12
+ import crypto from 'node:crypto';
12
13
 
13
14
  /**
14
15
  * Comment markers for managed content blocks.
@@ -68,8 +69,8 @@ const ALL_MARKERS = [
68
69
  const VERSION_STAMP_PATTERN = /<!--\s*(.+?)\s*\|\s*core:(\S+)\s*\|\s*(\S+)\s*-->/;
69
70
  /** Staleness threshold for version-stamp convergence in milliseconds. */
70
71
  const STALENESS_THRESHOLD_MS = 5 * 60 * 1000;
71
- /** Warning text prepended inside managed block when cleanup is needed. */
72
- const CLEANUP_FLAG = '> ⚠️ CLEANUP NEEDED: Orphaned Jeeves content may exist below this managed section. Review everything after the END marker and remove any content that duplicates what appears above.';
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.';
73
74
 
74
75
  /**
75
76
  * Directory and file path conventions for the Jeeves platform.
@@ -88,7 +89,13 @@ const WORKSPACE_FILES = {
88
89
  agents: 'AGENTS.md',
89
90
  /** HEARTBEAT.md — platform status and health alerts. */
90
91
  heartbeat: 'HEARTBEAT.md',
92
+ /** MEMORY.md — curated long-term memory. */
93
+ memory: 'MEMORY.md',
91
94
  };
95
+ /** Skill directory name within workspace. */
96
+ const SKILLS_DIR = 'skills';
97
+ /** Jeeves skill directory name. */
98
+ const JEEVES_SKILL_DIR = 'jeeves';
92
99
  /** Templates directory name within core config. */
93
100
  const TEMPLATES_DIR = 'templates';
94
101
  /** Registry cache file name. */
@@ -176,14 +183,14 @@ const PLATFORM_COMPONENTS = [
176
183
  * Core library version, inlined at build time.
177
184
  *
178
185
  * @remarks
179
- * The `0.4.5` placeholder is replaced by
186
+ * The `0.4.7` placeholder is replaced by
180
187
  * `@rollup/plugin-replace` during the build with the actual version
181
188
  * from `package.json`. This ensures the correct version survives
182
189
  * when consumers bundle core into their own dist (where runtime
183
190
  * `import.meta.url`-based resolution would find the wrong package.json).
184
191
  */
185
192
  /** The core library version from package.json (inlined at build time). */
186
- const CORE_VERSION = '0.4.5';
193
+ const CORE_VERSION = '0.4.7';
187
194
 
188
195
  /**
189
196
  * Workspace and config root initialization.
@@ -556,6 +563,257 @@ function createStatusHandler(options) {
556
563
  };
557
564
  }
558
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
+
559
817
  function getDefaultExportFromCjs (x) {
560
818
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
561
819
  }
@@ -1120,6 +1378,129 @@ function buildWithSections(beforeContent, userContent, sections, markers, coreVe
1120
1378
  return parts.join('\n');
1121
1379
  }
1122
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
+
1123
1504
  /**
1124
1505
  * OpenClaw configuration helpers for plugin CLI installers.
1125
1506
  *
@@ -1239,13 +1620,9 @@ function patchConfig(config, pluginId, mode) {
1239
1620
  }
1240
1621
 
1241
1622
  /**
1242
- * Factory for the standard `-openclaw` plugin installer CLI.
1623
+ * Internal helpers for the plugin installer CLI.
1243
1624
  *
1244
- * @remarks
1245
- * Produces a Commander program with `install` and `uninstall` commands
1246
- * that handle the full plugin lifecycle: copy dist to extensions,
1247
- * patch OpenClaw config, manage HEARTBEAT entries, and clean up
1248
- * managed sections on uninstall.
1625
+ * @module
1249
1626
  */
1250
1627
  /**
1251
1628
  * Derive a component name from a plugin ID.
@@ -1260,7 +1637,7 @@ function deriveComponentName(pluginId) {
1260
1637
  return pluginId.replace(/^jeeves-/, '').replace(/-openclaw$/, '');
1261
1638
  }
1262
1639
  /**
1263
- * Copy all files from source directory to destination.
1640
+ * Copy all files from source directory to destination, recursively.
1264
1641
  *
1265
1642
  * @param srcDir - Source directory.
1266
1643
  * @param destDir - Destination directory.
@@ -1294,6 +1671,12 @@ function readJsonFile(filePath) {
1294
1671
  return {};
1295
1672
  }
1296
1673
  }
1674
+
1675
+ /**
1676
+ * Factory for the standard `-openclaw` plugin installer CLI.
1677
+ *
1678
+ * @module
1679
+ */
1297
1680
  /**
1298
1681
  * Create a standard plugin installer CLI program.
1299
1682
  *
@@ -1319,6 +1702,16 @@ function createPluginCli(options) {
1319
1702
  const extensionsDir = join(openClawHome, 'extensions', pluginId);
1320
1703
  console.log(`Copying dist to ${extensionsDir}...`);
1321
1704
  copyDistFiles(distDir, extensionsDir);
1705
+ // Copy package.json and openclaw.plugin.json from package root
1706
+ const pkgRoot = packageDirectorySync({ cwd: distDir });
1707
+ if (pkgRoot) {
1708
+ for (const file of ['package.json', 'openclaw.plugin.json']) {
1709
+ const src = join(pkgRoot, file);
1710
+ if (existsSync(src)) {
1711
+ copyFileSync(src, join(extensionsDir, file));
1712
+ }
1713
+ }
1714
+ }
1322
1715
  console.log(' ✓ Dist files copied');
1323
1716
  // 2. Patch openclaw.json
1324
1717
  console.log('Patching OpenClaw config...');
@@ -1347,7 +1740,7 @@ function createPluginCli(options) {
1347
1740
  for (const msg of messages) {
1348
1741
  console.log(` ✓ ${msg}`);
1349
1742
  }
1350
- // 4. Write initial HEARTBEAT entry
1743
+ // 4. Write initial HEARTBEAT entry and seed jeeves skill
1351
1744
  try {
1352
1745
  const cfgRoot = opts.configRoot;
1353
1746
  const agents = config.agents;
@@ -1362,7 +1755,6 @@ function createPluginCli(options) {
1362
1755
  : '';
1363
1756
  const parsed = parseHeartbeat(existing);
1364
1757
  const fullName = `jeeves-${componentName}`;
1365
- // Only add if not already present
1366
1758
  const hasEntry = parsed.entries.some((e) => e.name === fullName);
1367
1759
  if (!hasEntry) {
1368
1760
  parsed.entries.push({
@@ -1378,10 +1770,36 @@ function createPluginCli(options) {
1378
1770
  catch {
1379
1771
  console.log(' ⚠ Could not write HEARTBEAT entry');
1380
1772
  }
1773
+ try {
1774
+ seedSkill(ws);
1775
+ console.log(' ✓ Jeeves skill seeded');
1776
+ }
1777
+ catch {
1778
+ console.log(' ⚠ Could not seed Jeeves skill');
1779
+ }
1381
1780
  }
1382
1781
  }
1383
1782
  catch {
1384
- // HEARTBEAT is best-effort during install
1783
+ // HEARTBEAT + skill seeding are best-effort during install
1784
+ }
1785
+ // 5. Write component version
1786
+ try {
1787
+ init({
1788
+ workspacePath: opts.workspace ?? '.',
1789
+ configRoot: opts.configRoot,
1790
+ });
1791
+ const pkgJsonPath = join(extensionsDir, 'package.json');
1792
+ const pkgJson = readJsonFile(pkgJsonPath);
1793
+ const pluginVersion = typeof pkgJson.version === 'string' ? pkgJson.version : undefined;
1794
+ writeComponentVersion(getCoreConfigDir(), {
1795
+ componentName,
1796
+ pluginPackage,
1797
+ pluginVersion,
1798
+ });
1799
+ console.log(' ✓ Component version written');
1800
+ }
1801
+ catch {
1802
+ console.log(' ⚠ Could not write component version');
1385
1803
  }
1386
1804
  console.log();
1387
1805
  console.log(`✅ ${pluginPackage} installed.`);
@@ -1411,10 +1829,9 @@ function createPluginCli(options) {
1411
1829
  }
1412
1830
  // 3. Remove TOOLS.md section
1413
1831
  try {
1414
- const cfgRoot = opts.configRoot;
1415
1832
  const ws = opts.workspace;
1416
1833
  if (ws) {
1417
- init({ workspacePath: ws, configRoot: cfgRoot });
1834
+ init({ workspacePath: ws, configRoot: opts.configRoot });
1418
1835
  const sectionId = componentName.charAt(0).toUpperCase() + componentName.slice(1);
1419
1836
  const toolsPath = join(ws, WORKSPACE_FILES.tools);
1420
1837
  if (existsSync(toolsPath)) {
@@ -1431,10 +1848,9 @@ function createPluginCli(options) {
1431
1848
  }
1432
1849
  // 4. Remove component-versions.json entry
1433
1850
  try {
1434
- const cfgRoot = opts.configRoot;
1435
1851
  init({
1436
1852
  workspacePath: opts.workspace ?? '.',
1437
- configRoot: cfgRoot,
1853
+ configRoot: opts.configRoot,
1438
1854
  });
1439
1855
  removeComponentVersion(getCoreConfigDir(), componentName);
1440
1856
  console.log(' ✓ Component version entry removed');
@@ -1770,7 +2186,7 @@ function isExecError(err) {
1770
2186
  * (Windows), systemd (Linux), or launchd (macOS) based on platform.
1771
2187
  */
1772
2188
  /** Exec helper that returns stdout. */
1773
- function run(cmd) {
2189
+ function run$1(cmd) {
1774
2190
  return execSync(cmd, {
1775
2191
  encoding: 'utf-8',
1776
2192
  timeout: 30_000,
@@ -1823,31 +2239,31 @@ function createWindowsManager(descriptor) {
1823
2239
  const cmdArgs = descriptor.startCommand(cfgPath);
1824
2240
  const appPath = cmdArgs[0];
1825
2241
  const appArgs = cmdArgs.slice(1).join(' ');
1826
- run(`nssm install ${svcName} ${appPath}`);
2242
+ run$1(`nssm install ${svcName} ${appPath}`);
1827
2243
  if (appArgs) {
1828
- run(`nssm set ${svcName} AppParameters ${appArgs}`);
2244
+ run$1(`nssm set ${svcName} AppParameters ${appArgs}`);
1829
2245
  }
1830
- run(`nssm set ${svcName} AppStdout ${join(homedir(), `${svcName}.log`)}`);
1831
- run(`nssm set ${svcName} AppStderr ${join(homedir(), `${svcName}.log`)}`);
1832
- run(`nssm set ${svcName} AppRotateFiles 1`);
1833
- run(`nssm set ${svcName} AppRotateBytes 1048576`);
2246
+ run$1(`nssm set ${svcName} AppStdout ${join(homedir(), `${svcName}.log`)}`);
2247
+ run$1(`nssm set ${svcName} AppStderr ${join(homedir(), `${svcName}.log`)}`);
2248
+ run$1(`nssm set ${svcName} AppRotateFiles 1`);
2249
+ run$1(`nssm set ${svcName} AppRotateBytes 1048576`);
1834
2250
  },
1835
2251
  uninstall(options) {
1836
2252
  const svcName = resolveServiceName(descriptor, options);
1837
2253
  runQuiet(`nssm stop ${svcName}`);
1838
- run(`nssm remove ${svcName} confirm`);
2254
+ run$1(`nssm remove ${svcName} confirm`);
1839
2255
  },
1840
2256
  start(options) {
1841
2257
  const svcName = resolveServiceName(descriptor, options);
1842
- run(`nssm start ${svcName}`);
2258
+ run$1(`nssm start ${svcName}`);
1843
2259
  },
1844
2260
  stop(options) {
1845
2261
  const svcName = resolveServiceName(descriptor, options);
1846
- run(`nssm stop ${svcName}`);
2262
+ run$1(`nssm stop ${svcName}`);
1847
2263
  },
1848
2264
  restart(options) {
1849
2265
  const svcName = resolveServiceName(descriptor, options);
1850
- run(`nssm restart ${svcName}`);
2266
+ run$1(`nssm restart ${svcName}`);
1851
2267
  },
1852
2268
  status(options) {
1853
2269
  const svcName = resolveServiceName(descriptor, options);
@@ -1892,8 +2308,8 @@ function createLinuxManager(descriptor) {
1892
2308
  const cmdArgs = descriptor.startCommand(cfgPath);
1893
2309
  mkdirSync(unitDir, { recursive: true });
1894
2310
  writeFileSync(unitPath(svcName), buildSystemdUnit(svcName, cmdArgs));
1895
- run('systemctl --user daemon-reload');
1896
- run(`systemctl --user enable ${svcName}.service`);
2311
+ run$1('systemctl --user daemon-reload');
2312
+ run$1(`systemctl --user enable ${svcName}.service`);
1897
2313
  },
1898
2314
  uninstall(options) {
1899
2315
  const svcName = resolveServiceName(descriptor, options);
@@ -1906,15 +2322,15 @@ function createLinuxManager(descriptor) {
1906
2322
  },
1907
2323
  start(options) {
1908
2324
  const svcName = resolveServiceName(descriptor, options);
1909
- run(`systemctl --user start ${svcName}.service`);
2325
+ run$1(`systemctl --user start ${svcName}.service`);
1910
2326
  },
1911
2327
  stop(options) {
1912
2328
  const svcName = resolveServiceName(descriptor, options);
1913
- run(`systemctl --user stop ${svcName}.service`);
2329
+ run$1(`systemctl --user stop ${svcName}.service`);
1914
2330
  },
1915
2331
  restart(options) {
1916
2332
  const svcName = resolveServiceName(descriptor, options);
1917
- run(`systemctl --user restart ${svcName}.service`);
2333
+ run$1(`systemctl --user restart ${svcName}.service`);
1918
2334
  },
1919
2335
  status(options) {
1920
2336
  const svcName = resolveServiceName(descriptor, options);
@@ -1978,16 +2394,16 @@ function createMacOSManager(descriptor) {
1978
2394
  },
1979
2395
  start(options) {
1980
2396
  const svcName = resolveServiceName(descriptor, options);
1981
- run(`launchctl load ${plistPath(svcName)}`);
2397
+ run$1(`launchctl load ${plistPath(svcName)}`);
1982
2398
  },
1983
2399
  stop(options) {
1984
2400
  const svcName = resolveServiceName(descriptor, options);
1985
- run(`launchctl unload ${plistPath(svcName)}`);
2401
+ run$1(`launchctl unload ${plistPath(svcName)}`);
1986
2402
  },
1987
2403
  restart(options) {
1988
2404
  const svcName = resolveServiceName(descriptor, options);
1989
2405
  runQuiet(`launchctl unload ${plistPath(svcName)}`);
1990
- run(`launchctl load ${plistPath(svcName)}`);
2406
+ run$1(`launchctl load ${plistPath(svcName)}`);
1991
2407
  },
1992
2408
  status(options) {
1993
2409
  const svcName = resolveServiceName(descriptor, options);
@@ -2016,19 +2432,6 @@ function createServiceManager(descriptor) {
2016
2432
  }
2017
2433
  }
2018
2434
 
2019
- /**
2020
- * Shared CLI defaults and option registration for Jeeves CLI commands.
2021
- *
2022
- * @remarks
2023
- * All three CLI commands (install, uninstall, status) share the same
2024
- * `--workspace` and `--config-root` options with the same defaults.
2025
- * This module centralizes them to eliminate duplication.
2026
- */
2027
- /** Default workspace path (current directory). */
2028
- const DEFAULT_WORKSPACE = '.';
2029
- /** Default config root path. */
2030
- const DEFAULT_CONFIG_ROOT = './config';
2031
-
2032
2435
  /**
2033
2436
  * Factory for the standard Jeeves service CLI.
2034
2437
  *
@@ -2457,10 +2860,10 @@ async function updateManagedSection(filePath, content, options = {}) {
2457
2860
  ? `# ${markers.title}\n\n${sectionText}`
2458
2861
  : sectionText;
2459
2862
  }
2460
- // Combine beforeContent + userContent for the user zone.
2461
- // When migrating from top→bottom, beforeContent is empty and
2462
- // userContent has the real content. When already at bottom,
2463
- // beforeContent has the user content and userContent is empty.
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
2464
2867
  const rawUserContent = [parsed.beforeContent, parsed.userContent]
2465
2868
  .filter(Boolean)
2466
2869
  .join('\n\n')
@@ -2468,9 +2871,6 @@ async function updateManagedSection(filePath, content, options = {}) {
2468
2871
  // Strip foreign managed blocks from user content (cross-contamination fix)
2469
2872
  const userContent = stripForeignMarkers(rawUserContent, markers);
2470
2873
  const cleanupNeeded = needsCleanup(newManagedBody, userContent);
2471
- // Build the full managed block
2472
- const beginLine = formatBeginMarker(markers.begin, coreVersion);
2473
- const endLine = formatEndMarker(markers.end);
2474
2874
  const managedParts = [];
2475
2875
  managedParts.push(beginLine);
2476
2876
  if (cleanupNeeded) {
@@ -2482,26 +2882,54 @@ async function updateManagedSection(filePath, content, options = {}) {
2482
2882
  managedParts.push('');
2483
2883
  managedParts.push(endLine);
2484
2884
  const managedBlock = managedParts.join('\n');
2485
- const position = markers.position ?? 'top';
2486
- const fileParts = [];
2487
- if (position === 'bottom') {
2488
- // User content first, managed block at end
2489
- if (userContent) {
2490
- fileParts.push(userContent);
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);
2491
2894
  fileParts.push('');
2492
2895
  }
2493
2896
  fileParts.push(managedBlock);
2897
+ if (cleanAfter) {
2898
+ fileParts.push('');
2899
+ fileParts.push(cleanAfter);
2900
+ }
2901
+ fileParts.push('');
2902
+ newFileContent = fileParts.join('\n');
2494
2903
  }
2495
2904
  else {
2496
- // Managed block first (legacy default), user content below
2497
- fileParts.push(managedBlock);
2498
- if (userContent) {
2499
- fileParts.push('');
2500
- fileParts.push(userContent);
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);
2501
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');
2502
2932
  }
2503
- fileParts.push('');
2504
- const newFileContent = fileParts.join('\n');
2505
2933
  atomicWrite(filePath, newFileContent);
2506
2934
  });
2507
2935
  }
@@ -2995,6 +3423,103 @@ async function refreshPlatformContent(options) {
2995
3423
  copyTemplates(coreConfigDir);
2996
3424
  }
2997
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
+
2998
3523
  /**
2999
3524
  * Core configuration schema and resolution.
3000
3525
  *
@@ -3439,29 +3964,76 @@ async function orchestrateHeartbeat(options) {
3439
3964
  }
3440
3965
 
3441
3966
  /**
3442
- * Timer-based orchestrator for managed content writing.
3967
+ * HEARTBEAT orchestration extracted from ComponentWriter.cycle().
3443
3968
  *
3444
3969
  * @remarks
3445
- * `ComponentWriter` manages a component's TOOLS.md section writes
3446
- * and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
3447
- * on a configurable prime-interval timer cycle.
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.
3448
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
+ }
3449
3993
  /**
3450
- * Orchestrates managed content writing for a single Jeeves component.
3994
+ * Run a single HEARTBEAT orchestration cycle.
3995
+ *
3996
+ * @param options - Heartbeat cycle configuration.
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
+
4018
+ /**
4019
+ * Timer-based orchestrator for managed content writing.
3451
4020
  *
3452
4021
  * @remarks
3453
- * Created via `createComponentWriter()`. Manages a timer that fires
3454
- * at the component's prime-interval, calling `generateToolsContent()`
3455
- * and `refreshPlatformContent()` on each cycle.
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.
3456
4025
  */
3457
4026
  class ComponentWriter {
3458
4027
  timer;
3459
4028
  component;
3460
4029
  configDir;
4030
+ gatewayUrl;
4031
+ pendingCleanups = new Set();
3461
4032
  /** @internal */
3462
- constructor(component) {
4033
+ constructor(component, options) {
3463
4034
  this.component = component;
3464
4035
  this.configDir = getComponentConfigDir(component.name);
4036
+ this.gatewayUrl = options?.gatewayUrl;
3465
4037
  }
3466
4038
  /** The component's config directory path. */
3467
4039
  get componentConfigDir() {
@@ -3495,15 +4067,16 @@ class ComponentWriter {
3495
4067
  * Execute a single write cycle.
3496
4068
  *
3497
4069
  * @remarks
3498
- * Calls `generateToolsContent()` and writes the component's
3499
- * TOOLS.md section via `updateManagedSection()`. Also calls
3500
- * `refreshPlatformContent()` for shared content maintenance.
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.
3501
4074
  */
3502
4075
  async cycle() {
3503
4076
  try {
3504
4077
  const workspacePath = getWorkspacePath();
3505
4078
  const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
3506
- // Write the component's TOOLS.md section
4079
+ // 1. Write the component's TOOLS.md section
3507
4080
  const toolsContent = this.component.generateToolsContent();
3508
4081
  await updateManagedSection(toolsPath, toolsContent, {
3509
4082
  mode: 'section',
@@ -3511,7 +4084,7 @@ class ComponentWriter {
3511
4084
  markers: TOOLS_MARKERS,
3512
4085
  coreVersion: CORE_VERSION,
3513
4086
  });
3514
- // Platform content maintenance: SOUL.md, AGENTS.md, Platform section
4087
+ // 2. Platform content maintenance
3515
4088
  await refreshPlatformContent({
3516
4089
  coreVersion: CORE_VERSION,
3517
4090
  componentName: this.component.name,
@@ -3519,36 +4092,26 @@ class ComponentWriter {
3519
4092
  servicePackage: this.component.servicePackage,
3520
4093
  pluginPackage: this.component.pluginPackage,
3521
4094
  });
3522
- // HEARTBEAT health orchestration
3523
- const heartbeatPath = join(workspacePath, WORKSPACE_FILES.heartbeat);
3524
- try {
3525
- const existingContent = (() => {
3526
- try {
3527
- return readFileSync(heartbeatPath, 'utf-8');
3528
- }
3529
- catch (err) {
3530
- // Only swallow "file not found" — let permission errors propagate
3531
- if (err instanceof Error &&
3532
- 'code' in err &&
3533
- err.code === 'ENOENT') {
3534
- return '';
3535
- }
3536
- throw err;
3537
- }
3538
- })();
3539
- const parsed = parseHeartbeat(existingContent);
3540
- const declinedNames = new Set(parsed.entries.filter((e) => e.declined).map((e) => e.name));
3541
- const entries = await orchestrateHeartbeat({
3542
- coreConfigDir: getCoreConfigDir(),
3543
- configRoot: getConfigRoot(),
3544
- declinedNames,
3545
- });
3546
- await writeHeartbeatSection(heartbeatPath, entries);
3547
- }
3548
- catch (err) {
3549
- const msg = err instanceof Error ? err.message : String(err);
3550
- 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);
3551
4108
  }
4109
+ // 4. HEARTBEAT orchestration
4110
+ await runHeartbeatCycle({
4111
+ workspacePath,
4112
+ coreConfigDir: getCoreConfigDir(),
4113
+ configRoot: getConfigRoot(),
4114
+ });
3552
4115
  }
3553
4116
  catch (err) {
3554
4117
  const message = err instanceof Error ? err.message : String(err);
@@ -3631,13 +4194,14 @@ function createAsyncContentCache(options) {
3631
4194
  * This replaces the v0.4.0 `createComponentWriter(JeevesComponent)`.
3632
4195
  *
3633
4196
  * @param descriptor - The component descriptor to validate and wrap.
4197
+ * @param options - Optional writer configuration (e.g., gatewayUrl for cleanup escalation).
3634
4198
  * @returns A new `ComponentWriter` instance.
3635
4199
  * @throws ZodError if the descriptor is invalid.
3636
4200
  */
3637
- function createComponentWriter(descriptor) {
4201
+ function createComponentWriter(descriptor, options) {
3638
4202
  // Validate via Zod — throws ZodError with detailed messages on failure
3639
4203
  jeevesComponentDescriptorSchema.parse(descriptor);
3640
- return new ComponentWriter(descriptor);
4204
+ return new ComponentWriter(descriptor, options);
3641
4205
  }
3642
4206
 
3643
4207
  /**
@@ -3678,6 +4242,92 @@ function getBindAddress(componentName) {
3678
4242
  return DEFAULT_BIND_ADDRESS;
3679
4243
  }
3680
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
+
3681
4331
  /**
3682
4332
  * One-shot content seeding used by the CLI install command.
3683
4333
  *
@@ -3736,6 +4386,8 @@ async function seedContent(options) {
3736
4386
  content: `- ${NOT_INSTALLED_ALERTS[name]}`,
3737
4387
  }));
3738
4388
  await writeHeartbeatSection(heartbeatPath, entries);
4389
+ // Seed jeeves workspace skill (Decision 48: overwrite-on-install)
4390
+ seedSkill(getWorkspacePath());
3739
4391
  }
3740
4392
 
3741
4393
  /**
@@ -3967,6 +4619,33 @@ function createPluginToolset(descriptor) {
3967
4619
  return [statusTool, configTool, configApplyTool, serviceTool];
3968
4620
  }
3969
4621
 
4622
+ /**
4623
+ * Resolve the version of a package from its `import.meta.url`.
4624
+ *
4625
+ * @module
4626
+ */
4627
+ /**
4628
+ * Get the version string from the nearest `package.json` relative to the
4629
+ * caller's module URL.
4630
+ *
4631
+ * @param importMetaUrl - The `import.meta.url` of the calling module.
4632
+ * @returns The `version` field, or `'unknown'` on any error.
4633
+ */
4634
+ function getPackageVersion(importMetaUrl) {
4635
+ try {
4636
+ const dir = fileURLToPath(importMetaUrl);
4637
+ const pkgRoot = packageDirectorySync({ cwd: dir });
4638
+ if (!pkgRoot)
4639
+ return 'unknown';
4640
+ const raw = readFileSync(join(pkgRoot, 'package.json'), 'utf-8');
4641
+ const pkg = JSON.parse(raw);
4642
+ return typeof pkg.version === 'string' ? pkg.version : 'unknown';
4643
+ }
4644
+ catch {
4645
+ return 'unknown';
4646
+ }
4647
+ }
4648
+
3970
4649
  /**
3971
4650
  * Plugin resolution helpers for the OpenClaw plugin SDK.
3972
4651
  *
@@ -4038,4 +4717,378 @@ function resolveOptionalPluginSetting(api, pluginId, key, envVar) {
4038
4717
  return undefined;
4039
4718
  }
4040
4719
 
4041
- 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, atomicWrite, buildHeartbeatSection, checkRegistryVersion, connectionFail, coreConfigSchema, createAsyncContentCache, createComponentWriter, createConfigApplyHandler, createConfigQueryHandler, createPluginCli, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, getBindAddress, getComponentConfigDir, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getServiceState, getServiceUrl, getWorkspacePath, init, isPrime, jaccard, jeevesComponentDescriptorSchema, needsCleanup, ok, orchestrateHeartbeat, parseHeartbeat, parseManaged, patchConfig, postJson, readComponentVersions, refreshPlatformContent, removeComponentVersion, removeManagedSection, resetInit, resolveConfigPath, resolveOpenClawHome, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, seedContent, shingles, shouldWrite, updateManagedSection, withFileLock, writeComponentVersion, writeHeartbeatSection };
4720
+ /**
4721
+ * Shared filesystem utilities for runner job scripts.
4722
+ *
4723
+ * @module
4724
+ */
4725
+ // ========== Time & UUID ==========
4726
+ /** Return current time as ISO 8601 string. */
4727
+ function nowIso() {
4728
+ return new Date().toISOString();
4729
+ }
4730
+ /** Generate a random UUID v4. */
4731
+ function uuid() {
4732
+ return crypto.randomUUID();
4733
+ }
4734
+ // ========== File System ==========
4735
+ /** Create a directory and any missing parents. */
4736
+ function ensureDir(p) {
4737
+ fs.mkdirSync(p, { recursive: true });
4738
+ }
4739
+ /** Read and parse a JSON file, returning `fallback` on any error. */
4740
+ function readJson(p, fallback) {
4741
+ try {
4742
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
4743
+ }
4744
+ catch {
4745
+ return fallback;
4746
+ }
4747
+ }
4748
+ /** Atomically write a JSON file (write to .tmp, then rename). */
4749
+ function writeJsonAtomic(p, obj) {
4750
+ ensureDir(path.dirname(p));
4751
+ const tmp = p + '.tmp';
4752
+ fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
4753
+ fs.renameSync(tmp, p);
4754
+ }
4755
+ /** Append a single JSON object as a JSONL line. */
4756
+ function appendJsonl(p, obj) {
4757
+ ensureDir(path.dirname(p));
4758
+ fs.appendFileSync(p, JSON.stringify(obj) + '\n', 'utf8');
4759
+ }
4760
+ /** Read a JSONL file into an array of parsed objects. */
4761
+ function readJsonl(p) {
4762
+ try {
4763
+ const content = fs.readFileSync(p, 'utf8');
4764
+ return content
4765
+ .split('\n')
4766
+ .filter((line) => line.trim())
4767
+ .map((line) => JSON.parse(line));
4768
+ }
4769
+ catch {
4770
+ return [];
4771
+ }
4772
+ }
4773
+ /** Overwrite a file with an array of objects as JSONL. */
4774
+ function writeJsonl(p, entries) {
4775
+ ensureDir(path.dirname(p));
4776
+ const content = entries.map((e) => JSON.stringify(e)).join('\n') + '\n';
4777
+ fs.writeFileSync(p, content, 'utf8');
4778
+ }
4779
+ // ========== Process Control ==========
4780
+ /** Synchronous sleep using Atomics.wait. */
4781
+ function sleepMs(ms) {
4782
+ const sab = new SharedArrayBuffer(4);
4783
+ const ia = new Int32Array(sab);
4784
+ Atomics.wait(ia, 0, 0, ms);
4785
+ }
4786
+ /** Async sleep via setTimeout. */
4787
+ function sleepAsync(ms) {
4788
+ return new Promise((r) => setTimeout(r, ms));
4789
+ }
4790
+ // ========== Env File Loader ==========
4791
+ /** Load a .env-style key=value file into process.env. */
4792
+ function loadEnvFile(envPath) {
4793
+ if (!fs.existsSync(envPath))
4794
+ throw new Error(`Missing secret file: ${envPath}`);
4795
+ for (const line of fs.readFileSync(envPath, 'utf8').split(/\r?\n/)) {
4796
+ const trimmed = line.trim();
4797
+ if (!trimmed || trimmed.startsWith('#'))
4798
+ continue;
4799
+ const idx = trimmed.indexOf('=');
4800
+ if (idx > 0) {
4801
+ process.env[trimmed.slice(0, idx).trim()] = trimmed.slice(idx + 1).trim();
4802
+ }
4803
+ }
4804
+ }
4805
+ // ========== CLI Arg Parsing ==========
4806
+ /** Parse --key=value arguments from argv into a record. */
4807
+ function parseArgs(argv = process.argv.slice(2)) {
4808
+ const args = {};
4809
+ for (const arg of argv) {
4810
+ const match = /^--([^=]+)=(.*)$/.exec(arg);
4811
+ if (match) {
4812
+ args[match[1]] = match[2];
4813
+ }
4814
+ }
4815
+ return args;
4816
+ }
4817
+ /** Get the value following a named flag, or a default. */
4818
+ function getArg(argv, name, defaultValue) {
4819
+ const i = argv.indexOf(name);
4820
+ if (i >= 0 && i + 1 < argv.length)
4821
+ return argv[i + 1];
4822
+ return defaultValue;
4823
+ }
4824
+
4825
+ /**
4826
+ * Google API auth helpers for runner job scripts.
4827
+ * Supports OAuth refresh tokens and service account impersonation.
4828
+ *
4829
+ * @module
4830
+ */
4831
+ // ========== JWT / Service Account ==========
4832
+ function base64url(buf) {
4833
+ return buf
4834
+ .toString('base64')
4835
+ .replace(/=/g, '')
4836
+ .replace(/\+/g, '-')
4837
+ .replace(/\//g, '_');
4838
+ }
4839
+ function createJwt(serviceAccount, scopes, subject) {
4840
+ const now = Math.floor(Date.now() / 1000);
4841
+ const header = { alg: 'RS256', typ: 'JWT' };
4842
+ const payload = {
4843
+ iss: serviceAccount.client_email,
4844
+ sub: subject,
4845
+ scope: scopes.join(' '),
4846
+ aud: serviceAccount.token_uri,
4847
+ iat: now,
4848
+ exp: now + 3600,
4849
+ };
4850
+ const headerB64 = base64url(Buffer.from(JSON.stringify(header)));
4851
+ const payloadB64 = base64url(Buffer.from(JSON.stringify(payload)));
4852
+ const unsigned = `${headerB64}.${payloadB64}`;
4853
+ const sign = crypto.createSign('RSA-SHA256');
4854
+ sign.update(unsigned);
4855
+ const signature = base64url(sign.sign(serviceAccount.private_key));
4856
+ return `${unsigned}.${signature}`;
4857
+ }
4858
+ async function getServiceAccountToken(serviceAccount, scopes, subject) {
4859
+ const jwt = createJwt(serviceAccount, scopes, subject);
4860
+ const resp = await fetch(serviceAccount.token_uri, {
4861
+ method: 'POST',
4862
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
4863
+ body: new URLSearchParams({
4864
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
4865
+ assertion: jwt,
4866
+ }),
4867
+ });
4868
+ if (!resp.ok) {
4869
+ const body = await resp.text();
4870
+ throw new Error(`Service account token failed (${String(resp.status)}): ${body}`);
4871
+ }
4872
+ const data = (await resp.json());
4873
+ return data.access_token;
4874
+ }
4875
+ // ========== OAuth Refresh ==========
4876
+ async function getOAuthToken(refreshToken, client) {
4877
+ const resp = await fetch('https://oauth2.googleapis.com/token', {
4878
+ method: 'POST',
4879
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
4880
+ body: new URLSearchParams({
4881
+ client_id: client.client_id,
4882
+ client_secret: client.client_secret,
4883
+ refresh_token: refreshToken,
4884
+ grant_type: 'refresh_token',
4885
+ }),
4886
+ });
4887
+ if (!resp.ok) {
4888
+ const body = await resp.text();
4889
+ throw new Error(`OAuth token refresh failed (${String(resp.status)}): ${body}`);
4890
+ }
4891
+ const data = (await resp.json());
4892
+ return data.access_token;
4893
+ }
4894
+ // ========== Unified Auth ==========
4895
+ /**
4896
+ * Create a Google auth helper with the given configuration.
4897
+ * Returns a function that resolves an access token for a given account and scopes.
4898
+ */
4899
+ function createGoogleAuth(options) {
4900
+ const { clientCredentialsPath, credentialsDir, serviceAccountDir } = options;
4901
+ let _oauthClient = null;
4902
+ function getOAuthClient() {
4903
+ if (!_oauthClient) {
4904
+ _oauthClient = JSON.parse(fs.readFileSync(clientCredentialsPath, 'utf8'));
4905
+ }
4906
+ return _oauthClient;
4907
+ }
4908
+ /**
4909
+ * Get an access token for the given account and scopes.
4910
+ */
4911
+ async function getAccessToken(account, scopes) {
4912
+ if (account.serviceAccount) {
4913
+ const saDir = serviceAccountDir ?? credentialsDir;
4914
+ const saPath = typeof account.serviceAccount === 'string'
4915
+ ? account.serviceAccount
4916
+ : path.join(saDir, account.serviceAccount.file);
4917
+ const sa = JSON.parse(fs.readFileSync(saPath, 'utf8'));
4918
+ return getServiceAccountToken(sa, scopes, account.email);
4919
+ }
4920
+ if (account.tokenFile) {
4921
+ const tokenPath = path.join(credentialsDir, account.tokenFile);
4922
+ const tokenData = JSON.parse(fs.readFileSync(tokenPath, 'utf8'));
4923
+ return getOAuthToken(tokenData.refresh_token, getOAuthClient());
4924
+ }
4925
+ throw new Error(`No auth method configured for ${account.email}`);
4926
+ }
4927
+ return {
4928
+ /** Get an access token for the given account and scopes. */
4929
+ getAccessToken,
4930
+ };
4931
+ }
4932
+
4933
+ /**
4934
+ * Shared crash-handler wrapper for runner job scripts.
4935
+ * Catches uncaught errors, logs them, and exits with code 1.
4936
+ *
4937
+ * @module
4938
+ */
4939
+ /**
4940
+ * Wrap a script's main function with crash handling.
4941
+ * On uncaught errors, appends to `_crash.log` in `crashDir` and exits.
4942
+ *
4943
+ * @param name - Script identifier for the crash log.
4944
+ * @param fn - Main function to execute (sync or async).
4945
+ * @param crashDir - Directory for crash logs (default: current working directory).
4946
+ */
4947
+ function runScript(name, fn, crashDir = process.cwd()) {
4948
+ const execute = () => {
4949
+ const result = fn();
4950
+ if (result instanceof Promise) {
4951
+ result.catch((err) => {
4952
+ handleCrash(name, err, crashDir);
4953
+ });
4954
+ }
4955
+ };
4956
+ try {
4957
+ execute();
4958
+ }
4959
+ catch (err) {
4960
+ handleCrash(name, err, crashDir);
4961
+ }
4962
+ }
4963
+ function handleCrash(name, err, crashDir) {
4964
+ const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
4965
+ const entry = `[${new Date().toISOString()}] CRASH (${name}): ${message}\n`;
4966
+ try {
4967
+ fs.mkdirSync(crashDir, { recursive: true });
4968
+ fs.appendFileSync(path.join(crashDir, '_crash.log'), entry);
4969
+ }
4970
+ catch {
4971
+ // Best effort — don't crash the crash handler.
4972
+ }
4973
+ console.error(entry);
4974
+ process.exit(1);
4975
+ }
4976
+
4977
+ /**
4978
+ * Shell execution utilities for runner job scripts.
4979
+ *
4980
+ * @module
4981
+ */
4982
+ /**
4983
+ * Run a command synchronously and return trimmed stdout.
4984
+ * Throws on non-zero exit code.
4985
+ */
4986
+ function run(cmd, args, opts = {}) {
4987
+ const r = cp.spawnSync(cmd, args, {
4988
+ encoding: 'utf8',
4989
+ maxBuffer: 50 * 1024 * 1024,
4990
+ ...opts,
4991
+ });
4992
+ if (r.error)
4993
+ throw r.error;
4994
+ const stdout = r.stdout || '';
4995
+ const stderr = r.stderr || '';
4996
+ if (r.status !== 0) {
4997
+ const msg = (stderr || stdout).trim();
4998
+ throw new Error(`${cmd} ${args.join(' ')} failed (exit ${String(r.status)}): ${msg}`);
4999
+ }
5000
+ return stdout.trim();
5001
+ }
5002
+ /**
5003
+ * Run a command with automatic retries on transient failures.
5004
+ * Uses exponential backoff between attempts.
5005
+ */
5006
+ function runWithRetry(cmd, args, opts = {}) {
5007
+ const { retries = 2, backoffMs = 5000, isRetryable, ...runOpts } = opts;
5008
+ let lastErr;
5009
+ for (let attempt = 0; attempt <= retries; attempt++) {
5010
+ try {
5011
+ return run(cmd, args, runOpts);
5012
+ }
5013
+ catch (e) {
5014
+ lastErr = e;
5015
+ const msg = e instanceof Error ? e.message : String(e);
5016
+ const retryable = isRetryable
5017
+ ? isRetryable(e)
5018
+ : /context deadline exceeded|timed out|timeout/i.test(msg);
5019
+ if (!retryable || attempt === retries)
5020
+ throw lastErr;
5021
+ sleepMs(backoffMs * Math.pow(2, attempt));
5022
+ }
5023
+ }
5024
+ throw lastErr;
5025
+ }
5026
+
5027
+ /**
5028
+ * Slack channel → workspace mapping cache.
5029
+ * Resolves which Slack workspace owns a given channel.
5030
+ *
5031
+ * @module
5032
+ */
5033
+ let _cache = null;
5034
+ let _dirty = false;
5035
+ let _cachePath = '';
5036
+ function loadCache(cachePath) {
5037
+ if (!_cache || _cachePath !== cachePath) {
5038
+ _cachePath = cachePath;
5039
+ try {
5040
+ _cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
5041
+ }
5042
+ catch {
5043
+ _cache = {};
5044
+ }
5045
+ }
5046
+ return _cache;
5047
+ }
5048
+ /** Flush pending cache changes to disk. */
5049
+ function saveCache() {
5050
+ if (_dirty && _cache && _cachePath) {
5051
+ fs.writeFileSync(_cachePath, JSON.stringify(_cache, null, 2) + '\n');
5052
+ _dirty = false;
5053
+ }
5054
+ }
5055
+ async function queryChannelWorkspace(channelId, token, defaultWorkspace) {
5056
+ const url = `https://slack.com/api/conversations.info?channel=${channelId}`;
5057
+ try {
5058
+ const res = await fetch(url, {
5059
+ headers: { Authorization: `Bearer ${token}` },
5060
+ });
5061
+ if (!res.ok)
5062
+ return defaultWorkspace;
5063
+ const j = (await res.json());
5064
+ if (!j.ok)
5065
+ return defaultWorkspace;
5066
+ const shared = j.channel?.shared_team_ids ?? [];
5067
+ if (shared.length > 0 && !shared.includes(defaultWorkspace)) {
5068
+ return shared[0];
5069
+ }
5070
+ return defaultWorkspace;
5071
+ }
5072
+ catch {
5073
+ return defaultWorkspace;
5074
+ }
5075
+ }
5076
+ /**
5077
+ * Resolve the workspace team ID that owns a Slack channel.
5078
+ * Results are cached to disk.
5079
+ *
5080
+ * @param channelId - Slack channel ID.
5081
+ * @param token - Slack bot token for API calls.
5082
+ * @param options - Cache path and default workspace.
5083
+ */
5084
+ async function getChannelWorkspace(channelId, token, options) {
5085
+ const cache = loadCache(options.cachePath);
5086
+ if (cache[channelId])
5087
+ return cache[channelId];
5088
+ const teamId = await queryChannelWorkspace(channelId, token, options.defaultWorkspace);
5089
+ cache[channelId] = teamId;
5090
+ _dirty = true;
5091
+ return teamId;
5092
+ }
5093
+
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 };