@karmaniverous/jeeves 0.4.6 → 0.4.7

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.
@@ -261,14 +261,14 @@ const PLATFORM_COMPONENTS = [
261
261
  * Core library version, inlined at build time.
262
262
  *
263
263
  * @remarks
264
- * The `0.4.5` placeholder is replaced by
264
+ * The `0.4.6` placeholder is replaced by
265
265
  * `@rollup/plugin-replace` during the build with the actual version
266
266
  * from `package.json`. This ensures the correct version survives
267
267
  * when consumers bundle core into their own dist (where runtime
268
268
  * `import.meta.url`-based resolution would find the wrong package.json).
269
269
  */
270
270
  /** The core library version from package.json (inlined at build time). */
271
- const CORE_VERSION = '0.4.5';
271
+ const CORE_VERSION = '0.4.6';
272
272
 
273
273
  /**
274
274
  * Dynamic discovery of installed Jeeves component CLIs.
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { writeFileSync, renameSync, unlinkSync, existsSync, readFileSync, rmSync, mkdirSync, readdirSync, copyFileSync } from 'node:fs';
2
+ import { writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync, readFileSync, readdirSync, copyFileSync, rmSync } from 'node:fs';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import * as commander from 'commander';
5
+ import { packageDirectorySync } from 'package-directory';
5
6
  import { lock } from 'proper-lockfile';
6
7
  import 'semver';
7
8
  import { homedir } from 'node:os';
@@ -119,14 +120,14 @@ const COMPONENT_VERSIONS_FILE = 'component-versions.json';
119
120
  * Core library version, inlined at build time.
120
121
  *
121
122
  * @remarks
122
- * The `0.4.5` placeholder is replaced by
123
+ * The `0.4.6` placeholder is replaced by
123
124
  * `@rollup/plugin-replace` during the build with the actual version
124
125
  * from `package.json`. This ensures the correct version survives
125
126
  * when consumers bundle core into their own dist (where runtime
126
127
  * `import.meta.url`-based resolution would find the wrong package.json).
127
128
  */
128
129
  /** The core library version from package.json (inlined at build time). */
129
- const CORE_VERSION = '0.4.5';
130
+ const CORE_VERSION = '0.4.6';
130
131
 
131
132
  /**
132
133
  * Shared file I/O helpers for managed section operations.
@@ -224,6 +225,30 @@ function readComponentVersions(coreConfigDir) {
224
225
  return {};
225
226
  }
226
227
  }
228
+ /**
229
+ * Write a component's version entry to the shared state file.
230
+ *
231
+ * @remarks
232
+ * Reads the existing file, merges the new entry, and writes atomically.
233
+ *
234
+ * @param coreConfigDir - Path to the core config directory.
235
+ * @param options - Component version data to write.
236
+ */
237
+ function writeComponentVersion(coreConfigDir, options) {
238
+ const existing = readComponentVersions(coreConfigDir);
239
+ existing[options.componentName] = {
240
+ pluginVersion: options.pluginVersion,
241
+ servicePackage: options.servicePackage,
242
+ pluginPackage: options.pluginPackage,
243
+ updatedAt: new Date().toISOString(),
244
+ };
245
+ const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
246
+ const dir = dirname(filePath);
247
+ if (!existsSync(dir)) {
248
+ mkdirSync(dir, { recursive: true });
249
+ }
250
+ atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
251
+ }
227
252
  /**
228
253
  * Remove a component's version entry from the shared state file.
229
254
  *
@@ -802,13 +827,9 @@ function patchConfig(config, pluginId, mode) {
802
827
  }
803
828
 
804
829
  /**
805
- * Factory for the standard `-openclaw` plugin installer CLI.
830
+ * Internal helpers for the plugin installer CLI.
806
831
  *
807
- * @remarks
808
- * Produces a Commander program with `install` and `uninstall` commands
809
- * that handle the full plugin lifecycle: copy dist to extensions,
810
- * patch OpenClaw config, manage HEARTBEAT entries, and clean up
811
- * managed sections on uninstall.
832
+ * @module
812
833
  */
813
834
  /**
814
835
  * Derive a component name from a plugin ID.
@@ -823,7 +844,7 @@ function deriveComponentName(pluginId) {
823
844
  return pluginId.replace(/^jeeves-/, '').replace(/-openclaw$/, '');
824
845
  }
825
846
  /**
826
- * Copy all files from source directory to destination.
847
+ * Copy all files from source directory to destination, recursively.
827
848
  *
828
849
  * @param srcDir - Source directory.
829
850
  * @param destDir - Destination directory.
@@ -857,6 +878,12 @@ function readJsonFile(filePath) {
857
878
  return {};
858
879
  }
859
880
  }
881
+
882
+ /**
883
+ * Factory for the standard `-openclaw` plugin installer CLI.
884
+ *
885
+ * @module
886
+ */
860
887
  /**
861
888
  * Create a standard plugin installer CLI program.
862
889
  *
@@ -882,6 +909,16 @@ function createPluginCli(options) {
882
909
  const extensionsDir = join(openClawHome, 'extensions', pluginId);
883
910
  console.log(`Copying dist to ${extensionsDir}...`);
884
911
  copyDistFiles(distDir, extensionsDir);
912
+ // Copy package.json and openclaw.plugin.json from package root
913
+ const pkgRoot = packageDirectorySync({ cwd: distDir });
914
+ if (pkgRoot) {
915
+ for (const file of ['package.json', 'openclaw.plugin.json']) {
916
+ const src = join(pkgRoot, file);
917
+ if (existsSync(src)) {
918
+ copyFileSync(src, join(extensionsDir, file));
919
+ }
920
+ }
921
+ }
885
922
  console.log(' ✓ Dist files copied');
886
923
  // 2. Patch openclaw.json
887
924
  console.log('Patching OpenClaw config...');
@@ -925,7 +962,6 @@ function createPluginCli(options) {
925
962
  : '';
926
963
  const parsed = parseHeartbeat(existing);
927
964
  const fullName = `jeeves-${componentName}`;
928
- // Only add if not already present
929
965
  const hasEntry = parsed.entries.some((e) => e.name === fullName);
930
966
  if (!hasEntry) {
931
967
  parsed.entries.push({
@@ -946,6 +982,25 @@ function createPluginCli(options) {
946
982
  catch {
947
983
  // HEARTBEAT is best-effort during install
948
984
  }
985
+ // 5. Write component version
986
+ try {
987
+ init({
988
+ workspacePath: opts.workspace ?? '.',
989
+ configRoot: opts.configRoot,
990
+ });
991
+ const pkgJsonPath = join(extensionsDir, 'package.json');
992
+ const pkgJson = readJsonFile(pkgJsonPath);
993
+ const pluginVersion = typeof pkgJson.version === 'string' ? pkgJson.version : undefined;
994
+ writeComponentVersion(getCoreConfigDir(), {
995
+ componentName,
996
+ pluginPackage,
997
+ pluginVersion,
998
+ });
999
+ console.log(' ✓ Component version written');
1000
+ }
1001
+ catch {
1002
+ console.log(' ⚠ Could not write component version');
1003
+ }
949
1004
  console.log();
950
1005
  console.log(`✅ ${pluginPackage} installed.`);
951
1006
  });
@@ -974,10 +1029,9 @@ function createPluginCli(options) {
974
1029
  }
975
1030
  // 3. Remove TOOLS.md section
976
1031
  try {
977
- const cfgRoot = opts.configRoot;
978
1032
  const ws = opts.workspace;
979
1033
  if (ws) {
980
- init({ workspacePath: ws, configRoot: cfgRoot });
1034
+ init({ workspacePath: ws, configRoot: opts.configRoot });
981
1035
  const sectionId = componentName.charAt(0).toUpperCase() + componentName.slice(1);
982
1036
  const toolsPath = join(ws, WORKSPACE_FILES.tools);
983
1037
  if (existsSync(toolsPath)) {
@@ -994,10 +1048,9 @@ function createPluginCli(options) {
994
1048
  }
995
1049
  // 4. Remove component-versions.json entry
996
1050
  try {
997
- const cfgRoot = opts.configRoot;
998
1051
  init({
999
1052
  workspacePath: opts.workspace ?? '.',
1000
- configRoot: cfgRoot,
1053
+ configRoot: opts.configRoot,
1001
1054
  });
1002
1055
  removeComponentVersion(getCoreConfigDir(), componentName);
1003
1056
  console.log(' ✓ Component version entry removed');
package/dist/index.d.ts CHANGED
@@ -267,11 +267,7 @@ declare function createStatusHandler(options: CreateStatusHandlerOptions): Statu
267
267
  /**
268
268
  * Factory for the standard `-openclaw` plugin installer CLI.
269
269
  *
270
- * @remarks
271
- * Produces a Commander program with `install` and `uninstall` commands
272
- * that handle the full plugin lifecycle: copy dist to extensions,
273
- * patch OpenClaw config, manage HEARTBEAT entries, and clean up
274
- * managed sections on uninstall.
270
+ * @module
275
271
  */
276
272
 
277
273
  /** Options for creating a plugin installer CLI. */
@@ -1230,6 +1226,20 @@ declare function seedContent(options: SeedContentOptions): Promise<void>;
1230
1226
  */
1231
1227
  declare function createPluginToolset(descriptor: JeevesComponentDescriptor): ToolDescriptor[];
1232
1228
 
1229
+ /**
1230
+ * Resolve the version of a package from its `import.meta.url`.
1231
+ *
1232
+ * @module
1233
+ */
1234
+ /**
1235
+ * Get the version string from the nearest `package.json` relative to the
1236
+ * caller's module URL.
1237
+ *
1238
+ * @param importMetaUrl - The `import.meta.url` of the calling module.
1239
+ * @returns The `version` field, or `'unknown'` on any error.
1240
+ */
1241
+ declare function getPackageVersion(importMetaUrl: string): string;
1242
+
1233
1243
  /**
1234
1244
  * HTTP helpers for the OpenClaw plugin SDK.
1235
1245
  *
@@ -1394,6 +1404,153 @@ declare function fail(error: unknown): ToolResult;
1394
1404
  */
1395
1405
  declare function connectionFail(error: unknown, baseUrl: string, pluginId: string): ToolResult;
1396
1406
 
1407
+ /**
1408
+ * Shared filesystem utilities for runner job scripts.
1409
+ *
1410
+ * @module
1411
+ */
1412
+ /** Return current time as ISO 8601 string. */
1413
+ declare function nowIso(): string;
1414
+ /** Generate a random UUID v4. */
1415
+ declare function uuid(): string;
1416
+ /** Create a directory and any missing parents. */
1417
+ declare function ensureDir(p: string): void;
1418
+ /** Read and parse a JSON file, returning `fallback` on any error. */
1419
+ declare function readJson<T>(p: string, fallback: T): T;
1420
+ /** Atomically write a JSON file (write to .tmp, then rename). */
1421
+ declare function writeJsonAtomic(p: string, obj: unknown): void;
1422
+ /** Append a single JSON object as a JSONL line. */
1423
+ declare function appendJsonl(p: string, obj: unknown): void;
1424
+ /** Read a JSONL file into an array of parsed objects. */
1425
+ declare function readJsonl<T = unknown>(p: string): T[];
1426
+ /** Overwrite a file with an array of objects as JSONL. */
1427
+ declare function writeJsonl(p: string, entries: unknown[]): void;
1428
+ /** Synchronous sleep using Atomics.wait. */
1429
+ declare function sleepMs(ms: number): void;
1430
+ /** Async sleep via setTimeout. */
1431
+ declare function sleepAsync(ms: number): Promise<void>;
1432
+ /** Load a .env-style key=value file into process.env. */
1433
+ declare function loadEnvFile(envPath: string): void;
1434
+ /** Parse --key=value arguments from argv into a record. */
1435
+ declare function parseArgs(argv?: string[]): Record<string, string>;
1436
+ /** Get the value following a named flag, or a default. */
1437
+ declare function getArg(argv: string[], name: string, defaultValue: string): string;
1438
+
1439
+ /**
1440
+ * Google API auth helpers for runner job scripts.
1441
+ * Supports OAuth refresh tokens and service account impersonation.
1442
+ *
1443
+ * @module
1444
+ */
1445
+ /** Service account config object specifying a key file path. */
1446
+ interface ServiceAccountFileConfig {
1447
+ /** Path to the service account JSON key file. */
1448
+ file: string;
1449
+ }
1450
+ /** Configuration for a Google account's auth method. */
1451
+ interface AccountConfig {
1452
+ /** Google account email address. */
1453
+ email: string;
1454
+ /** Path to refresh token file (relative to credentialsDir). */
1455
+ tokenFile?: string;
1456
+ /** Service account key file path or config object. */
1457
+ serviceAccount?: string | ServiceAccountFileConfig;
1458
+ }
1459
+ /** Options for the Google auth helper. */
1460
+ interface GoogleAuthOptions {
1461
+ /** Path to the OAuth client credentials JSON file. */
1462
+ clientCredentialsPath: string;
1463
+ /** Base directory for credential files. */
1464
+ credentialsDir: string;
1465
+ /** Directory containing service account JSON files. */
1466
+ serviceAccountDir?: string;
1467
+ }
1468
+ /**
1469
+ * Create a Google auth helper with the given configuration.
1470
+ * Returns a function that resolves an access token for a given account and scopes.
1471
+ */
1472
+ declare function createGoogleAuth(options: GoogleAuthOptions): {
1473
+ /** Get an access token for the given account and scopes. */
1474
+ getAccessToken: (account: AccountConfig, scopes: string[]) => Promise<string>;
1475
+ };
1476
+
1477
+ /**
1478
+ * Shared crash-handler wrapper for runner job scripts.
1479
+ * Catches uncaught errors, logs them, and exits with code 1.
1480
+ *
1481
+ * @module
1482
+ */
1483
+ /**
1484
+ * Wrap a script's main function with crash handling.
1485
+ * On uncaught errors, appends to `_crash.log` in `crashDir` and exits.
1486
+ *
1487
+ * @param name - Script identifier for the crash log.
1488
+ * @param fn - Main function to execute (sync or async).
1489
+ * @param crashDir - Directory for crash logs (default: current working directory).
1490
+ */
1491
+ declare function runScript(name: string, fn: () => void | Promise<void>, crashDir?: string): void;
1492
+
1493
+ /**
1494
+ * Shell execution utilities for runner job scripts.
1495
+ *
1496
+ * @module
1497
+ */
1498
+ /** Options for synchronous command execution. */
1499
+ interface RunOptions {
1500
+ /** Output encoding (default utf8). */
1501
+ encoding?: BufferEncoding;
1502
+ /** Max stdout/stderr buffer size in bytes. */
1503
+ maxBuffer?: number;
1504
+ /** Execution timeout in milliseconds. */
1505
+ timeout?: number;
1506
+ /** Run command through the shell. */
1507
+ shell?: boolean;
1508
+ }
1509
+ /**
1510
+ * Run a command synchronously and return trimmed stdout.
1511
+ * Throws on non-zero exit code.
1512
+ */
1513
+ declare function run(cmd: string, args: string[], opts?: RunOptions): string;
1514
+ /** Options for retry-enabled command execution. */
1515
+ interface RetryOptions {
1516
+ /** Number of retries (default 2). */
1517
+ retries?: number;
1518
+ /** Base backoff interval in milliseconds (default 5000). */
1519
+ backoffMs?: number;
1520
+ /** Custom predicate to determine if an error is retryable. */
1521
+ isRetryable?: (error: unknown) => boolean;
1522
+ }
1523
+ /**
1524
+ * Run a command with automatic retries on transient failures.
1525
+ * Uses exponential backoff between attempts.
1526
+ */
1527
+ declare function runWithRetry(cmd: string, args: string[], opts?: RetryOptions & RunOptions): string;
1528
+
1529
+ /**
1530
+ * Slack channel → workspace mapping cache.
1531
+ * Resolves which Slack workspace owns a given channel.
1532
+ *
1533
+ * @module
1534
+ */
1535
+ /** Options for the Slack workspace resolver. */
1536
+ interface SlackWorkspaceOptions {
1537
+ /** Path to the channel→workspace cache JSON file. */
1538
+ cachePath: string;
1539
+ /** Default workspace team ID when resolution fails. */
1540
+ defaultWorkspace: string;
1541
+ }
1542
+ /** Flush pending cache changes to disk. */
1543
+ declare function saveCache(): void;
1544
+ /**
1545
+ * Resolve the workspace team ID that owns a Slack channel.
1546
+ * Results are cached to disk.
1547
+ *
1548
+ * @param channelId - Slack channel ID.
1549
+ * @param token - Slack bot token for API calls.
1550
+ * @param options - Cache path and default workspace.
1551
+ */
1552
+ declare function getChannelWorkspace(channelId: string, token: string, options: SlackWorkspaceOptions): Promise<string>;
1553
+
1397
1554
  /**
1398
1555
  * Factory for platform-aware service lifecycle management.
1399
1556
  *
@@ -1437,5 +1594,5 @@ interface ServiceManager {
1437
1594
  */
1438
1595
  declare function createServiceManager(descriptor: JeevesComponentDescriptor): ServiceManager;
1439
1596
 
1440
- 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 };
1441
- export type { AsyncContentCacheOptions, ComponentDependencies, ComponentState, ComponentVersionEntry, ComponentVersionsState, ConfigApplyHandler, ConfigApplyRequest, ConfigApplyResult, ConfigQueryHandler, ConfigQueryResponse, CoreConfig, CreatePluginCliOptions, CreateStatusHandlerOptions, HeartbeatEntry, InitOptions, JeevesComponentDescriptor, ManagedMarkers, ManagedSection, OrchestrateHeartbeatOptions, ParseManagedResult, ParsedHeartbeat, PlatformComponent, PluginApi, RefreshPlatformContentOptions, RemoveManagedSectionOptions, SectionId, SeedContentOptions, ServiceManager, ServiceManagerOptions, ServiceState, StatusHandler, StatusHandlerResult, StatusResponse, ToolDescriptor, ToolRegistrationOptions, ToolResult, UpdateManagedSectionOptions, VersionStamp, WriteComponentVersionOptions };
1597
+ 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 };
1598
+ export type { AccountConfig, AsyncContentCacheOptions, ComponentDependencies, ComponentState, ComponentVersionEntry, ComponentVersionsState, ConfigApplyHandler, ConfigApplyRequest, ConfigApplyResult, ConfigQueryHandler, ConfigQueryResponse, CoreConfig, CreatePluginCliOptions, CreateStatusHandlerOptions, GoogleAuthOptions, HeartbeatEntry, InitOptions, JeevesComponentDescriptor, ManagedMarkers, ManagedSection, OrchestrateHeartbeatOptions, ParseManagedResult, ParsedHeartbeat, PlatformComponent, PluginApi, RefreshPlatformContentOptions, RemoveManagedSectionOptions, RetryOptions, RunOptions, SectionId, SeedContentOptions, ServiceAccountFileConfig, ServiceManager, ServiceManagerOptions, ServiceState, SlackWorkspaceOptions, StatusHandler, StatusHandlerResult, StatusResponse, ToolDescriptor, ToolRegistrationOptions, ToolResult, UpdateManagedSectionOptions, VersionStamp, WriteComponentVersionOptions };
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 } from 'node:path';
3
3
  import { lock } from 'proper-lockfile';
4
4
  import { JSONPath } from 'jsonpath-plus';
5
5
  import * as commander from 'commander';
6
+ import { packageDirectorySync } from 'package-directory';
6
7
  import { valid, gte, gt } from 'semver';
7
8
  import { homedir } from 'node:os';
8
9
  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.
@@ -176,14 +177,14 @@ const PLATFORM_COMPONENTS = [
176
177
  * Core library version, inlined at build time.
177
178
  *
178
179
  * @remarks
179
- * The `0.4.5` placeholder is replaced by
180
+ * The `0.4.6` placeholder is replaced by
180
181
  * `@rollup/plugin-replace` during the build with the actual version
181
182
  * from `package.json`. This ensures the correct version survives
182
183
  * when consumers bundle core into their own dist (where runtime
183
184
  * `import.meta.url`-based resolution would find the wrong package.json).
184
185
  */
185
186
  /** The core library version from package.json (inlined at build time). */
186
- const CORE_VERSION = '0.4.5';
187
+ const CORE_VERSION = '0.4.6';
187
188
 
188
189
  /**
189
190
  * Workspace and config root initialization.
@@ -1239,13 +1240,9 @@ function patchConfig(config, pluginId, mode) {
1239
1240
  }
1240
1241
 
1241
1242
  /**
1242
- * Factory for the standard `-openclaw` plugin installer CLI.
1243
+ * Internal helpers for the plugin installer CLI.
1243
1244
  *
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.
1245
+ * @module
1249
1246
  */
1250
1247
  /**
1251
1248
  * Derive a component name from a plugin ID.
@@ -1260,7 +1257,7 @@ function deriveComponentName(pluginId) {
1260
1257
  return pluginId.replace(/^jeeves-/, '').replace(/-openclaw$/, '');
1261
1258
  }
1262
1259
  /**
1263
- * Copy all files from source directory to destination.
1260
+ * Copy all files from source directory to destination, recursively.
1264
1261
  *
1265
1262
  * @param srcDir - Source directory.
1266
1263
  * @param destDir - Destination directory.
@@ -1294,6 +1291,12 @@ function readJsonFile(filePath) {
1294
1291
  return {};
1295
1292
  }
1296
1293
  }
1294
+
1295
+ /**
1296
+ * Factory for the standard `-openclaw` plugin installer CLI.
1297
+ *
1298
+ * @module
1299
+ */
1297
1300
  /**
1298
1301
  * Create a standard plugin installer CLI program.
1299
1302
  *
@@ -1319,6 +1322,16 @@ function createPluginCli(options) {
1319
1322
  const extensionsDir = join(openClawHome, 'extensions', pluginId);
1320
1323
  console.log(`Copying dist to ${extensionsDir}...`);
1321
1324
  copyDistFiles(distDir, extensionsDir);
1325
+ // Copy package.json and openclaw.plugin.json from package root
1326
+ const pkgRoot = packageDirectorySync({ cwd: distDir });
1327
+ if (pkgRoot) {
1328
+ for (const file of ['package.json', 'openclaw.plugin.json']) {
1329
+ const src = join(pkgRoot, file);
1330
+ if (existsSync(src)) {
1331
+ copyFileSync(src, join(extensionsDir, file));
1332
+ }
1333
+ }
1334
+ }
1322
1335
  console.log(' ✓ Dist files copied');
1323
1336
  // 2. Patch openclaw.json
1324
1337
  console.log('Patching OpenClaw config...');
@@ -1362,7 +1375,6 @@ function createPluginCli(options) {
1362
1375
  : '';
1363
1376
  const parsed = parseHeartbeat(existing);
1364
1377
  const fullName = `jeeves-${componentName}`;
1365
- // Only add if not already present
1366
1378
  const hasEntry = parsed.entries.some((e) => e.name === fullName);
1367
1379
  if (!hasEntry) {
1368
1380
  parsed.entries.push({
@@ -1383,6 +1395,25 @@ function createPluginCli(options) {
1383
1395
  catch {
1384
1396
  // HEARTBEAT is best-effort during install
1385
1397
  }
1398
+ // 5. Write component version
1399
+ try {
1400
+ init({
1401
+ workspacePath: opts.workspace ?? '.',
1402
+ configRoot: opts.configRoot,
1403
+ });
1404
+ const pkgJsonPath = join(extensionsDir, 'package.json');
1405
+ const pkgJson = readJsonFile(pkgJsonPath);
1406
+ const pluginVersion = typeof pkgJson.version === 'string' ? pkgJson.version : undefined;
1407
+ writeComponentVersion(getCoreConfigDir(), {
1408
+ componentName,
1409
+ pluginPackage,
1410
+ pluginVersion,
1411
+ });
1412
+ console.log(' ✓ Component version written');
1413
+ }
1414
+ catch {
1415
+ console.log(' ⚠ Could not write component version');
1416
+ }
1386
1417
  console.log();
1387
1418
  console.log(`✅ ${pluginPackage} installed.`);
1388
1419
  });
@@ -1411,10 +1442,9 @@ function createPluginCli(options) {
1411
1442
  }
1412
1443
  // 3. Remove TOOLS.md section
1413
1444
  try {
1414
- const cfgRoot = opts.configRoot;
1415
1445
  const ws = opts.workspace;
1416
1446
  if (ws) {
1417
- init({ workspacePath: ws, configRoot: cfgRoot });
1447
+ init({ workspacePath: ws, configRoot: opts.configRoot });
1418
1448
  const sectionId = componentName.charAt(0).toUpperCase() + componentName.slice(1);
1419
1449
  const toolsPath = join(ws, WORKSPACE_FILES.tools);
1420
1450
  if (existsSync(toolsPath)) {
@@ -1431,10 +1461,9 @@ function createPluginCli(options) {
1431
1461
  }
1432
1462
  // 4. Remove component-versions.json entry
1433
1463
  try {
1434
- const cfgRoot = opts.configRoot;
1435
1464
  init({
1436
1465
  workspacePath: opts.workspace ?? '.',
1437
- configRoot: cfgRoot,
1466
+ configRoot: opts.configRoot,
1438
1467
  });
1439
1468
  removeComponentVersion(getCoreConfigDir(), componentName);
1440
1469
  console.log(' ✓ Component version entry removed');
@@ -1770,7 +1799,7 @@ function isExecError(err) {
1770
1799
  * (Windows), systemd (Linux), or launchd (macOS) based on platform.
1771
1800
  */
1772
1801
  /** Exec helper that returns stdout. */
1773
- function run(cmd) {
1802
+ function run$1(cmd) {
1774
1803
  return execSync(cmd, {
1775
1804
  encoding: 'utf-8',
1776
1805
  timeout: 30_000,
@@ -1823,31 +1852,31 @@ function createWindowsManager(descriptor) {
1823
1852
  const cmdArgs = descriptor.startCommand(cfgPath);
1824
1853
  const appPath = cmdArgs[0];
1825
1854
  const appArgs = cmdArgs.slice(1).join(' ');
1826
- run(`nssm install ${svcName} ${appPath}`);
1855
+ run$1(`nssm install ${svcName} ${appPath}`);
1827
1856
  if (appArgs) {
1828
- run(`nssm set ${svcName} AppParameters ${appArgs}`);
1857
+ run$1(`nssm set ${svcName} AppParameters ${appArgs}`);
1829
1858
  }
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`);
1859
+ run$1(`nssm set ${svcName} AppStdout ${join(homedir(), `${svcName}.log`)}`);
1860
+ run$1(`nssm set ${svcName} AppStderr ${join(homedir(), `${svcName}.log`)}`);
1861
+ run$1(`nssm set ${svcName} AppRotateFiles 1`);
1862
+ run$1(`nssm set ${svcName} AppRotateBytes 1048576`);
1834
1863
  },
1835
1864
  uninstall(options) {
1836
1865
  const svcName = resolveServiceName(descriptor, options);
1837
1866
  runQuiet(`nssm stop ${svcName}`);
1838
- run(`nssm remove ${svcName} confirm`);
1867
+ run$1(`nssm remove ${svcName} confirm`);
1839
1868
  },
1840
1869
  start(options) {
1841
1870
  const svcName = resolveServiceName(descriptor, options);
1842
- run(`nssm start ${svcName}`);
1871
+ run$1(`nssm start ${svcName}`);
1843
1872
  },
1844
1873
  stop(options) {
1845
1874
  const svcName = resolveServiceName(descriptor, options);
1846
- run(`nssm stop ${svcName}`);
1875
+ run$1(`nssm stop ${svcName}`);
1847
1876
  },
1848
1877
  restart(options) {
1849
1878
  const svcName = resolveServiceName(descriptor, options);
1850
- run(`nssm restart ${svcName}`);
1879
+ run$1(`nssm restart ${svcName}`);
1851
1880
  },
1852
1881
  status(options) {
1853
1882
  const svcName = resolveServiceName(descriptor, options);
@@ -1892,8 +1921,8 @@ function createLinuxManager(descriptor) {
1892
1921
  const cmdArgs = descriptor.startCommand(cfgPath);
1893
1922
  mkdirSync(unitDir, { recursive: true });
1894
1923
  writeFileSync(unitPath(svcName), buildSystemdUnit(svcName, cmdArgs));
1895
- run('systemctl --user daemon-reload');
1896
- run(`systemctl --user enable ${svcName}.service`);
1924
+ run$1('systemctl --user daemon-reload');
1925
+ run$1(`systemctl --user enable ${svcName}.service`);
1897
1926
  },
1898
1927
  uninstall(options) {
1899
1928
  const svcName = resolveServiceName(descriptor, options);
@@ -1906,15 +1935,15 @@ function createLinuxManager(descriptor) {
1906
1935
  },
1907
1936
  start(options) {
1908
1937
  const svcName = resolveServiceName(descriptor, options);
1909
- run(`systemctl --user start ${svcName}.service`);
1938
+ run$1(`systemctl --user start ${svcName}.service`);
1910
1939
  },
1911
1940
  stop(options) {
1912
1941
  const svcName = resolveServiceName(descriptor, options);
1913
- run(`systemctl --user stop ${svcName}.service`);
1942
+ run$1(`systemctl --user stop ${svcName}.service`);
1914
1943
  },
1915
1944
  restart(options) {
1916
1945
  const svcName = resolveServiceName(descriptor, options);
1917
- run(`systemctl --user restart ${svcName}.service`);
1946
+ run$1(`systemctl --user restart ${svcName}.service`);
1918
1947
  },
1919
1948
  status(options) {
1920
1949
  const svcName = resolveServiceName(descriptor, options);
@@ -1978,16 +2007,16 @@ function createMacOSManager(descriptor) {
1978
2007
  },
1979
2008
  start(options) {
1980
2009
  const svcName = resolveServiceName(descriptor, options);
1981
- run(`launchctl load ${plistPath(svcName)}`);
2010
+ run$1(`launchctl load ${plistPath(svcName)}`);
1982
2011
  },
1983
2012
  stop(options) {
1984
2013
  const svcName = resolveServiceName(descriptor, options);
1985
- run(`launchctl unload ${plistPath(svcName)}`);
2014
+ run$1(`launchctl unload ${plistPath(svcName)}`);
1986
2015
  },
1987
2016
  restart(options) {
1988
2017
  const svcName = resolveServiceName(descriptor, options);
1989
2018
  runQuiet(`launchctl unload ${plistPath(svcName)}`);
1990
- run(`launchctl load ${plistPath(svcName)}`);
2019
+ run$1(`launchctl load ${plistPath(svcName)}`);
1991
2020
  },
1992
2021
  status(options) {
1993
2022
  const svcName = resolveServiceName(descriptor, options);
@@ -3967,6 +3996,33 @@ function createPluginToolset(descriptor) {
3967
3996
  return [statusTool, configTool, configApplyTool, serviceTool];
3968
3997
  }
3969
3998
 
3999
+ /**
4000
+ * Resolve the version of a package from its `import.meta.url`.
4001
+ *
4002
+ * @module
4003
+ */
4004
+ /**
4005
+ * Get the version string from the nearest `package.json` relative to the
4006
+ * caller's module URL.
4007
+ *
4008
+ * @param importMetaUrl - The `import.meta.url` of the calling module.
4009
+ * @returns The `version` field, or `'unknown'` on any error.
4010
+ */
4011
+ function getPackageVersion(importMetaUrl) {
4012
+ try {
4013
+ const dir = fileURLToPath(importMetaUrl);
4014
+ const pkgRoot = packageDirectorySync({ cwd: dir });
4015
+ if (!pkgRoot)
4016
+ return 'unknown';
4017
+ const raw = readFileSync(join(pkgRoot, 'package.json'), 'utf-8');
4018
+ const pkg = JSON.parse(raw);
4019
+ return typeof pkg.version === 'string' ? pkg.version : 'unknown';
4020
+ }
4021
+ catch {
4022
+ return 'unknown';
4023
+ }
4024
+ }
4025
+
3970
4026
  /**
3971
4027
  * Plugin resolution helpers for the OpenClaw plugin SDK.
3972
4028
  *
@@ -4038,4 +4094,378 @@ function resolveOptionalPluginSetting(api, pluginId, key, envVar) {
4038
4094
  return undefined;
4039
4095
  }
4040
4096
 
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 };
4097
+ /**
4098
+ * Shared filesystem utilities for runner job scripts.
4099
+ *
4100
+ * @module
4101
+ */
4102
+ // ========== Time & UUID ==========
4103
+ /** Return current time as ISO 8601 string. */
4104
+ function nowIso() {
4105
+ return new Date().toISOString();
4106
+ }
4107
+ /** Generate a random UUID v4. */
4108
+ function uuid() {
4109
+ return crypto.randomUUID();
4110
+ }
4111
+ // ========== File System ==========
4112
+ /** Create a directory and any missing parents. */
4113
+ function ensureDir(p) {
4114
+ fs.mkdirSync(p, { recursive: true });
4115
+ }
4116
+ /** Read and parse a JSON file, returning `fallback` on any error. */
4117
+ function readJson(p, fallback) {
4118
+ try {
4119
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
4120
+ }
4121
+ catch {
4122
+ return fallback;
4123
+ }
4124
+ }
4125
+ /** Atomically write a JSON file (write to .tmp, then rename). */
4126
+ function writeJsonAtomic(p, obj) {
4127
+ ensureDir(path.dirname(p));
4128
+ const tmp = p + '.tmp';
4129
+ fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
4130
+ fs.renameSync(tmp, p);
4131
+ }
4132
+ /** Append a single JSON object as a JSONL line. */
4133
+ function appendJsonl(p, obj) {
4134
+ ensureDir(path.dirname(p));
4135
+ fs.appendFileSync(p, JSON.stringify(obj) + '\n', 'utf8');
4136
+ }
4137
+ /** Read a JSONL file into an array of parsed objects. */
4138
+ function readJsonl(p) {
4139
+ try {
4140
+ const content = fs.readFileSync(p, 'utf8');
4141
+ return content
4142
+ .split('\n')
4143
+ .filter((line) => line.trim())
4144
+ .map((line) => JSON.parse(line));
4145
+ }
4146
+ catch {
4147
+ return [];
4148
+ }
4149
+ }
4150
+ /** Overwrite a file with an array of objects as JSONL. */
4151
+ function writeJsonl(p, entries) {
4152
+ ensureDir(path.dirname(p));
4153
+ const content = entries.map((e) => JSON.stringify(e)).join('\n') + '\n';
4154
+ fs.writeFileSync(p, content, 'utf8');
4155
+ }
4156
+ // ========== Process Control ==========
4157
+ /** Synchronous sleep using Atomics.wait. */
4158
+ function sleepMs(ms) {
4159
+ const sab = new SharedArrayBuffer(4);
4160
+ const ia = new Int32Array(sab);
4161
+ Atomics.wait(ia, 0, 0, ms);
4162
+ }
4163
+ /** Async sleep via setTimeout. */
4164
+ function sleepAsync(ms) {
4165
+ return new Promise((r) => setTimeout(r, ms));
4166
+ }
4167
+ // ========== Env File Loader ==========
4168
+ /** Load a .env-style key=value file into process.env. */
4169
+ function loadEnvFile(envPath) {
4170
+ if (!fs.existsSync(envPath))
4171
+ throw new Error(`Missing secret file: ${envPath}`);
4172
+ for (const line of fs.readFileSync(envPath, 'utf8').split(/\r?\n/)) {
4173
+ const trimmed = line.trim();
4174
+ if (!trimmed || trimmed.startsWith('#'))
4175
+ continue;
4176
+ const idx = trimmed.indexOf('=');
4177
+ if (idx > 0) {
4178
+ process.env[trimmed.slice(0, idx).trim()] = trimmed.slice(idx + 1).trim();
4179
+ }
4180
+ }
4181
+ }
4182
+ // ========== CLI Arg Parsing ==========
4183
+ /** Parse --key=value arguments from argv into a record. */
4184
+ function parseArgs(argv = process.argv.slice(2)) {
4185
+ const args = {};
4186
+ for (const arg of argv) {
4187
+ const match = /^--([^=]+)=(.*)$/.exec(arg);
4188
+ if (match) {
4189
+ args[match[1]] = match[2];
4190
+ }
4191
+ }
4192
+ return args;
4193
+ }
4194
+ /** Get the value following a named flag, or a default. */
4195
+ function getArg(argv, name, defaultValue) {
4196
+ const i = argv.indexOf(name);
4197
+ if (i >= 0 && i + 1 < argv.length)
4198
+ return argv[i + 1];
4199
+ return defaultValue;
4200
+ }
4201
+
4202
+ /**
4203
+ * Google API auth helpers for runner job scripts.
4204
+ * Supports OAuth refresh tokens and service account impersonation.
4205
+ *
4206
+ * @module
4207
+ */
4208
+ // ========== JWT / Service Account ==========
4209
+ function base64url(buf) {
4210
+ return buf
4211
+ .toString('base64')
4212
+ .replace(/=/g, '')
4213
+ .replace(/\+/g, '-')
4214
+ .replace(/\//g, '_');
4215
+ }
4216
+ function createJwt(serviceAccount, scopes, subject) {
4217
+ const now = Math.floor(Date.now() / 1000);
4218
+ const header = { alg: 'RS256', typ: 'JWT' };
4219
+ const payload = {
4220
+ iss: serviceAccount.client_email,
4221
+ sub: subject,
4222
+ scope: scopes.join(' '),
4223
+ aud: serviceAccount.token_uri,
4224
+ iat: now,
4225
+ exp: now + 3600,
4226
+ };
4227
+ const headerB64 = base64url(Buffer.from(JSON.stringify(header)));
4228
+ const payloadB64 = base64url(Buffer.from(JSON.stringify(payload)));
4229
+ const unsigned = `${headerB64}.${payloadB64}`;
4230
+ const sign = crypto.createSign('RSA-SHA256');
4231
+ sign.update(unsigned);
4232
+ const signature = base64url(sign.sign(serviceAccount.private_key));
4233
+ return `${unsigned}.${signature}`;
4234
+ }
4235
+ async function getServiceAccountToken(serviceAccount, scopes, subject) {
4236
+ const jwt = createJwt(serviceAccount, scopes, subject);
4237
+ const resp = await fetch(serviceAccount.token_uri, {
4238
+ method: 'POST',
4239
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
4240
+ body: new URLSearchParams({
4241
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
4242
+ assertion: jwt,
4243
+ }),
4244
+ });
4245
+ if (!resp.ok) {
4246
+ const body = await resp.text();
4247
+ throw new Error(`Service account token failed (${String(resp.status)}): ${body}`);
4248
+ }
4249
+ const data = (await resp.json());
4250
+ return data.access_token;
4251
+ }
4252
+ // ========== OAuth Refresh ==========
4253
+ async function getOAuthToken(refreshToken, client) {
4254
+ const resp = await fetch('https://oauth2.googleapis.com/token', {
4255
+ method: 'POST',
4256
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
4257
+ body: new URLSearchParams({
4258
+ client_id: client.client_id,
4259
+ client_secret: client.client_secret,
4260
+ refresh_token: refreshToken,
4261
+ grant_type: 'refresh_token',
4262
+ }),
4263
+ });
4264
+ if (!resp.ok) {
4265
+ const body = await resp.text();
4266
+ throw new Error(`OAuth token refresh failed (${String(resp.status)}): ${body}`);
4267
+ }
4268
+ const data = (await resp.json());
4269
+ return data.access_token;
4270
+ }
4271
+ // ========== Unified Auth ==========
4272
+ /**
4273
+ * Create a Google auth helper with the given configuration.
4274
+ * Returns a function that resolves an access token for a given account and scopes.
4275
+ */
4276
+ function createGoogleAuth(options) {
4277
+ const { clientCredentialsPath, credentialsDir, serviceAccountDir } = options;
4278
+ let _oauthClient = null;
4279
+ function getOAuthClient() {
4280
+ if (!_oauthClient) {
4281
+ _oauthClient = JSON.parse(fs.readFileSync(clientCredentialsPath, 'utf8'));
4282
+ }
4283
+ return _oauthClient;
4284
+ }
4285
+ /**
4286
+ * Get an access token for the given account and scopes.
4287
+ */
4288
+ async function getAccessToken(account, scopes) {
4289
+ if (account.serviceAccount) {
4290
+ const saDir = serviceAccountDir ?? credentialsDir;
4291
+ const saPath = typeof account.serviceAccount === 'string'
4292
+ ? account.serviceAccount
4293
+ : path.join(saDir, account.serviceAccount.file);
4294
+ const sa = JSON.parse(fs.readFileSync(saPath, 'utf8'));
4295
+ return getServiceAccountToken(sa, scopes, account.email);
4296
+ }
4297
+ if (account.tokenFile) {
4298
+ const tokenPath = path.join(credentialsDir, account.tokenFile);
4299
+ const tokenData = JSON.parse(fs.readFileSync(tokenPath, 'utf8'));
4300
+ return getOAuthToken(tokenData.refresh_token, getOAuthClient());
4301
+ }
4302
+ throw new Error(`No auth method configured for ${account.email}`);
4303
+ }
4304
+ return {
4305
+ /** Get an access token for the given account and scopes. */
4306
+ getAccessToken,
4307
+ };
4308
+ }
4309
+
4310
+ /**
4311
+ * Shared crash-handler wrapper for runner job scripts.
4312
+ * Catches uncaught errors, logs them, and exits with code 1.
4313
+ *
4314
+ * @module
4315
+ */
4316
+ /**
4317
+ * Wrap a script's main function with crash handling.
4318
+ * On uncaught errors, appends to `_crash.log` in `crashDir` and exits.
4319
+ *
4320
+ * @param name - Script identifier for the crash log.
4321
+ * @param fn - Main function to execute (sync or async).
4322
+ * @param crashDir - Directory for crash logs (default: current working directory).
4323
+ */
4324
+ function runScript(name, fn, crashDir = process.cwd()) {
4325
+ const execute = () => {
4326
+ const result = fn();
4327
+ if (result instanceof Promise) {
4328
+ result.catch((err) => {
4329
+ handleCrash(name, err, crashDir);
4330
+ });
4331
+ }
4332
+ };
4333
+ try {
4334
+ execute();
4335
+ }
4336
+ catch (err) {
4337
+ handleCrash(name, err, crashDir);
4338
+ }
4339
+ }
4340
+ function handleCrash(name, err, crashDir) {
4341
+ const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
4342
+ const entry = `[${new Date().toISOString()}] CRASH (${name}): ${message}\n`;
4343
+ try {
4344
+ fs.mkdirSync(crashDir, { recursive: true });
4345
+ fs.appendFileSync(path.join(crashDir, '_crash.log'), entry);
4346
+ }
4347
+ catch {
4348
+ // Best effort — don't crash the crash handler.
4349
+ }
4350
+ console.error(entry);
4351
+ process.exit(1);
4352
+ }
4353
+
4354
+ /**
4355
+ * Shell execution utilities for runner job scripts.
4356
+ *
4357
+ * @module
4358
+ */
4359
+ /**
4360
+ * Run a command synchronously and return trimmed stdout.
4361
+ * Throws on non-zero exit code.
4362
+ */
4363
+ function run(cmd, args, opts = {}) {
4364
+ const r = cp.spawnSync(cmd, args, {
4365
+ encoding: 'utf8',
4366
+ maxBuffer: 50 * 1024 * 1024,
4367
+ ...opts,
4368
+ });
4369
+ if (r.error)
4370
+ throw r.error;
4371
+ const stdout = r.stdout || '';
4372
+ const stderr = r.stderr || '';
4373
+ if (r.status !== 0) {
4374
+ const msg = (stderr || stdout).trim();
4375
+ throw new Error(`${cmd} ${args.join(' ')} failed (exit ${String(r.status)}): ${msg}`);
4376
+ }
4377
+ return stdout.trim();
4378
+ }
4379
+ /**
4380
+ * Run a command with automatic retries on transient failures.
4381
+ * Uses exponential backoff between attempts.
4382
+ */
4383
+ function runWithRetry(cmd, args, opts = {}) {
4384
+ const { retries = 2, backoffMs = 5000, isRetryable, ...runOpts } = opts;
4385
+ let lastErr;
4386
+ for (let attempt = 0; attempt <= retries; attempt++) {
4387
+ try {
4388
+ return run(cmd, args, runOpts);
4389
+ }
4390
+ catch (e) {
4391
+ lastErr = e;
4392
+ const msg = e instanceof Error ? e.message : String(e);
4393
+ const retryable = isRetryable
4394
+ ? isRetryable(e)
4395
+ : /context deadline exceeded|timed out|timeout/i.test(msg);
4396
+ if (!retryable || attempt === retries)
4397
+ throw lastErr;
4398
+ sleepMs(backoffMs * Math.pow(2, attempt));
4399
+ }
4400
+ }
4401
+ throw lastErr;
4402
+ }
4403
+
4404
+ /**
4405
+ * Slack channel → workspace mapping cache.
4406
+ * Resolves which Slack workspace owns a given channel.
4407
+ *
4408
+ * @module
4409
+ */
4410
+ let _cache = null;
4411
+ let _dirty = false;
4412
+ let _cachePath = '';
4413
+ function loadCache(cachePath) {
4414
+ if (!_cache || _cachePath !== cachePath) {
4415
+ _cachePath = cachePath;
4416
+ try {
4417
+ _cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
4418
+ }
4419
+ catch {
4420
+ _cache = {};
4421
+ }
4422
+ }
4423
+ return _cache;
4424
+ }
4425
+ /** Flush pending cache changes to disk. */
4426
+ function saveCache() {
4427
+ if (_dirty && _cache && _cachePath) {
4428
+ fs.writeFileSync(_cachePath, JSON.stringify(_cache, null, 2) + '\n');
4429
+ _dirty = false;
4430
+ }
4431
+ }
4432
+ async function queryChannelWorkspace(channelId, token, defaultWorkspace) {
4433
+ const url = `https://slack.com/api/conversations.info?channel=${channelId}`;
4434
+ try {
4435
+ const res = await fetch(url, {
4436
+ headers: { Authorization: `Bearer ${token}` },
4437
+ });
4438
+ if (!res.ok)
4439
+ return defaultWorkspace;
4440
+ const j = (await res.json());
4441
+ if (!j.ok)
4442
+ return defaultWorkspace;
4443
+ const shared = j.channel?.shared_team_ids ?? [];
4444
+ if (shared.length > 0 && !shared.includes(defaultWorkspace)) {
4445
+ return shared[0];
4446
+ }
4447
+ return defaultWorkspace;
4448
+ }
4449
+ catch {
4450
+ return defaultWorkspace;
4451
+ }
4452
+ }
4453
+ /**
4454
+ * Resolve the workspace team ID that owns a Slack channel.
4455
+ * Results are cached to disk.
4456
+ *
4457
+ * @param channelId - Slack channel ID.
4458
+ * @param token - Slack bot token for API calls.
4459
+ * @param options - Cache path and default workspace.
4460
+ */
4461
+ async function getChannelWorkspace(channelId, token, options) {
4462
+ const cache = loadCache(options.cachePath);
4463
+ if (cache[channelId])
4464
+ return cache[channelId];
4465
+ const teamId = await queryChannelWorkspace(channelId, token, options.defaultWorkspace);
4466
+ cache[channelId] = teamId;
4467
+ _dirty = true;
4468
+ return teamId;
4469
+ }
4470
+
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 };
package/package.json CHANGED
@@ -139,5 +139,5 @@
139
139
  },
140
140
  "type": "module",
141
141
  "types": "dist/index.d.ts",
142
- "version": "0.4.6"
142
+ "version": "0.4.7"
143
143
  }