@karmaniverous/jeeves-meta 0.12.4 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import fs, { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, statSync, readdirSync, watchFile } from 'node:fs';
3
- import path, { join, dirname, relative, posix, resolve } from 'node:path';
3
+ import path, { join, dirname, basename, relative, posix, resolve } from 'node:path';
4
+ import { randomUUID, createHash } from 'node:crypto';
4
5
  import require$$0$4 from 'path';
5
6
  import require$$0$3 from 'fs';
6
7
  import require$$0$1 from 'constants';
@@ -9,13 +10,12 @@ import require$$4 from 'util';
9
10
  import require$$5 from 'assert';
10
11
  import require$$2 from 'events';
11
12
  import vm from 'vm';
13
+ import { z } from 'zod';
12
14
  import * as commander from 'commander';
13
15
  import { homedir, tmpdir } from 'node:os';
14
- import { z } from 'zod';
15
16
  import { execSync } from 'node:child_process';
16
17
  import { fileURLToPath } from 'node:url';
17
18
  import { readFile, unlink, mkdir, writeFile, copyFile } from 'node:fs/promises';
18
- import { randomUUID, createHash } from 'node:crypto';
19
19
  import pino from 'pino';
20
20
  import Handlebars from 'handlebars';
21
21
  import { Cron } from 'croner';
@@ -7017,6 +7017,32 @@ const COMPONENT_CONFIG_PREFIX = 'jeeves-';
7017
7017
  /** Core config file name. */
7018
7018
  const CONFIG_FILE = 'config.json';
7019
7019
 
7020
+ /**
7021
+ * Default port assignments for Jeeves platform services.
7022
+ *
7023
+ * @remarks
7024
+ * Each port number is a historical reference:
7025
+ * - 1934: Wodehouse's *Thank You, Jeeves*; Popper's *Logic of Scientific Discovery*
7026
+ * - 1936: Turing's "On Computable Numbers"; Church's lambda calculus
7027
+ * - 1937: Turing's paper in *Proceedings of the London Mathematical Society*
7028
+ * - 1938: Wodehouse's *The Code of the Woosters*; Shannon's relay/switching paper
7029
+ */
7030
+ /** Default port for jeeves-server. */
7031
+ const SERVER_PORT = 1934;
7032
+ /** Default port for jeeves-watcher. */
7033
+ const WATCHER_PORT = 1936;
7034
+ /** Default port for jeeves-runner. */
7035
+ const RUNNER_PORT = 1937;
7036
+ /** Default port for jeeves-meta. */
7037
+ const META_PORT = 1938;
7038
+ /** Map of service names to their default ports. */
7039
+ const DEFAULT_PORTS = {
7040
+ server: SERVER_PORT,
7041
+ watcher: WATCHER_PORT,
7042
+ runner: RUNNER_PORT,
7043
+ meta: META_PORT,
7044
+ };
7045
+
7020
7046
  /**
7021
7047
  * Workspace and config root initialization.
7022
7048
  *
@@ -7062,30 +7088,64 @@ function getComponentConfigDir(componentName) {
7062
7088
  throw new Error('jeeves-core: init() must be called first');
7063
7089
  return join(state.configRoot, `${COMPONENT_CONFIG_PREFIX}${componentName}`);
7064
7090
  }
7091
+ /** Maximum rename retry attempts on EPERM. */
7092
+ const ATOMIC_WRITE_MAX_RETRIES = 3;
7093
+ /** Delay between EPERM retries in milliseconds. */
7094
+ const ATOMIC_WRITE_RETRY_DELAY_MS = 100;
7065
7095
  /**
7066
7096
  * Write content to a file atomically via a temp file + rename.
7067
7097
  *
7098
+ * @remarks
7099
+ * Retries the rename up to three times on EPERM (Windows file-handle
7100
+ * contention) with a 100 ms synchronous delay between attempts.
7101
+ *
7068
7102
  * @param filePath - Absolute path to the target file.
7069
7103
  * @param content - Content to write.
7070
7104
  */
7071
7105
  function atomicWrite(filePath, content) {
7072
7106
  const dir = dirname(filePath);
7073
- const tempPath = join(dir, `.${String(Date.now())}.tmp`);
7107
+ const base = basename(filePath, '.md');
7108
+ const tempPath = join(dir, `.${base}.${String(Date.now())}.${randomUUID().slice(0, 8)}.tmp`);
7074
7109
  writeFileSync(tempPath, content, 'utf-8');
7075
- try {
7076
- renameSync(tempPath, filePath);
7077
- }
7078
- catch (err) {
7110
+ for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
7079
7111
  try {
7080
- unlinkSync(tempPath);
7112
+ renameSync(tempPath, filePath);
7113
+ return;
7081
7114
  }
7082
- catch {
7083
- /* best-effort cleanup */
7115
+ catch (err) {
7116
+ const isEperm = err instanceof Error &&
7117
+ 'code' in err &&
7118
+ err.code === 'EPERM';
7119
+ if (!isEperm || attempt === ATOMIC_WRITE_MAX_RETRIES - 1) {
7120
+ try {
7121
+ unlinkSync(tempPath);
7122
+ }
7123
+ catch {
7124
+ /* best-effort cleanup */
7125
+ }
7126
+ throw err;
7127
+ }
7128
+ // Synchronous sleep before retry (acceptable in atomic write context)
7129
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ATOMIC_WRITE_RETRY_DELAY_MS);
7084
7130
  }
7085
- throw err;
7086
7131
  }
7087
7132
  }
7088
7133
 
7134
+ /**
7135
+ * Shared internal utility functions.
7136
+ *
7137
+ * @packageDocumentation
7138
+ */
7139
+ /**
7140
+ * Extract a human-readable message from an unknown caught value.
7141
+ *
7142
+ * @param err - The caught value (typically `unknown`).
7143
+ * @returns The error message string.
7144
+ */
7145
+ function getErrorMessage(err) {
7146
+ return err instanceof Error ? err.message : String(err);
7147
+ }
7148
+
7089
7149
  /**
7090
7150
  * Factory for a framework-agnostic config apply HTTP handler.
7091
7151
  *
@@ -7138,8 +7198,7 @@ function readConfigFile(filePath) {
7138
7198
  return JSON.parse(raw);
7139
7199
  }
7140
7200
  catch (err) {
7141
- const msg = err instanceof Error ? err.message : String(err);
7142
- console.warn(`jeeves-core: Could not read config file ${filePath}: ${msg}`);
7201
+ console.warn(`jeeves-core: Could not read config file ${filePath}: ${getErrorMessage(err)}`);
7143
7202
  return {};
7144
7203
  }
7145
7204
  }
@@ -7188,10 +7247,9 @@ function createConfigApplyHandler(descriptor) {
7188
7247
  atomicWrite(configPath, json);
7189
7248
  }
7190
7249
  catch (err) {
7191
- const message = err instanceof Error ? err.message : String(err);
7192
7250
  return {
7193
7251
  status: 500,
7194
- body: { error: `Failed to write config: ${message}` },
7252
+ body: { error: `Failed to write config: ${getErrorMessage(err)}` },
7195
7253
  };
7196
7254
  }
7197
7255
  // Call onConfigApply callback if defined
@@ -7200,12 +7258,11 @@ function createConfigApplyHandler(descriptor) {
7200
7258
  await descriptor.onConfigApply(validatedConfig);
7201
7259
  }
7202
7260
  catch (err) {
7203
- const message = err instanceof Error ? err.message : String(err);
7204
7261
  return {
7205
7262
  status: 200,
7206
7263
  body: {
7207
7264
  applied: true,
7208
- warning: `Config written but callback failed: ${message}`,
7265
+ warning: `Config written but callback failed: ${getErrorMessage(err)}`,
7209
7266
  config: validatedConfig,
7210
7267
  },
7211
7268
  };
@@ -7288,8 +7345,7 @@ function createStatusHandler(options) {
7288
7345
  health = await options.getHealth();
7289
7346
  }
7290
7347
  catch (err) {
7291
- const message = err instanceof Error ? err.message : String(err);
7292
- health = { error: message };
7348
+ health = { error: getErrorMessage(err) };
7293
7349
  overallStatus = 'degraded';
7294
7350
  }
7295
7351
  }
@@ -7305,6 +7361,70 @@ function createStatusHandler(options) {
7305
7361
  };
7306
7362
  };
7307
7363
  }
7364
+ /** Core shared config section. */
7365
+ const workspaceCoreConfigSchema = z
7366
+ .object({
7367
+ /** Workspace root path. */
7368
+ workspace: z.string().optional().describe('Workspace root path'),
7369
+ /** Platform config root path. */
7370
+ configRoot: z.string().optional().describe('Platform config root path'),
7371
+ /** OpenClaw gateway URL. */
7372
+ gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
7373
+ })
7374
+ .partial();
7375
+ /** Memory shared config section. */
7376
+ const workspaceMemoryConfigSchema = z
7377
+ .object({
7378
+ /** MEMORY.md character budget. */
7379
+ budget: z.number().int().positive().optional().describe('Memory budget'),
7380
+ /** Warning threshold as a fraction of budget. */
7381
+ warningThreshold: z
7382
+ .number()
7383
+ .min(0)
7384
+ .max(1)
7385
+ .optional()
7386
+ .describe('Memory warning threshold'),
7387
+ /** Staleness threshold in days. */
7388
+ staleDays: z
7389
+ .number()
7390
+ .int()
7391
+ .positive()
7392
+ .optional()
7393
+ .describe('Memory staleness threshold in days'),
7394
+ })
7395
+ .partial();
7396
+ /** Workspace config Zod schema. */
7397
+ z.object({
7398
+ /** JSON Schema pointer for IDE autocomplete. */
7399
+ $schema: z.string().optional().describe('JSON Schema pointer'),
7400
+ /** Core shared defaults. */
7401
+ core: workspaceCoreConfigSchema.optional(),
7402
+ /** Memory hygiene shared defaults. */
7403
+ memory: workspaceMemoryConfigSchema.optional(),
7404
+ });
7405
+ /**
7406
+ * Built-in workspace config defaults.
7407
+ *
7408
+ * @remarks
7409
+ * These defaults are used as the lowest-priority tier in config resolution
7410
+ * (below CLI flags, env vars, and `jeeves.config.json` values).
7411
+ */
7412
+ const WORKSPACE_CONFIG_DEFAULTS = {
7413
+ core: {
7414
+ workspace: '.',
7415
+ configRoot: './config'}};
7416
+
7417
+ /**
7418
+ * Shared CLI defaults and resolution for Jeeves CLI commands.
7419
+ *
7420
+ * @remarks
7421
+ * All root CLI commands share workspace/config-root resolution. Values follow
7422
+ * the shared precedence model: flags → env → jeeves.config.json → defaults.
7423
+ */
7424
+ /** Default workspace path. */
7425
+ const DEFAULT_WORKSPACE = WORKSPACE_CONFIG_DEFAULTS.core.workspace;
7426
+ /** Default config root path. */
7427
+ const DEFAULT_CONFIG_ROOT = WORKSPACE_CONFIG_DEFAULTS.core.configRoot;
7308
7428
 
7309
7429
  function getDefaultExportFromCjs (x) {
7310
7430
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
@@ -7690,7 +7810,7 @@ function isExecError(err) {
7690
7810
  * (Windows), systemd (Linux), or launchd (macOS) based on platform.
7691
7811
  */
7692
7812
  /** Exec helper that returns stdout. */
7693
- function run(cmd) {
7813
+ function run$1(cmd) {
7694
7814
  return execSync(cmd, {
7695
7815
  encoding: 'utf-8',
7696
7816
  timeout: 30_000,
@@ -7743,31 +7863,31 @@ function createWindowsManager(descriptor) {
7743
7863
  const cmdArgs = descriptor.startCommand(cfgPath);
7744
7864
  const appPath = cmdArgs[0];
7745
7865
  const appArgs = cmdArgs.slice(1).join(' ');
7746
- run(`nssm install ${svcName} ${appPath}`);
7866
+ run$1(`nssm install ${svcName} ${appPath}`);
7747
7867
  if (appArgs) {
7748
- run(`nssm set ${svcName} AppParameters ${appArgs}`);
7868
+ run$1(`nssm set ${svcName} AppParameters ${appArgs}`);
7749
7869
  }
7750
- run(`nssm set ${svcName} AppStdout ${join(homedir(), `${svcName}.log`)}`);
7751
- run(`nssm set ${svcName} AppStderr ${join(homedir(), `${svcName}.log`)}`);
7752
- run(`nssm set ${svcName} AppRotateFiles 1`);
7753
- run(`nssm set ${svcName} AppRotateBytes 1048576`);
7870
+ run$1(`nssm set ${svcName} AppStdout ${join(homedir(), `${svcName}.log`)}`);
7871
+ run$1(`nssm set ${svcName} AppStderr ${join(homedir(), `${svcName}.log`)}`);
7872
+ run$1(`nssm set ${svcName} AppRotateFiles 1`);
7873
+ run$1(`nssm set ${svcName} AppRotateBytes 1048576`);
7754
7874
  },
7755
7875
  uninstall(options) {
7756
7876
  const svcName = resolveServiceName(descriptor, options);
7757
7877
  runQuiet(`nssm stop ${svcName}`);
7758
- run(`nssm remove ${svcName} confirm`);
7878
+ run$1(`nssm remove ${svcName} confirm`);
7759
7879
  },
7760
7880
  start(options) {
7761
7881
  const svcName = resolveServiceName(descriptor, options);
7762
- run(`nssm start ${svcName}`);
7882
+ run$1(`nssm start ${svcName}`);
7763
7883
  },
7764
7884
  stop(options) {
7765
7885
  const svcName = resolveServiceName(descriptor, options);
7766
- run(`nssm stop ${svcName}`);
7886
+ run$1(`nssm stop ${svcName}`);
7767
7887
  },
7768
7888
  restart(options) {
7769
7889
  const svcName = resolveServiceName(descriptor, options);
7770
- run(`nssm restart ${svcName}`);
7890
+ run$1(`nssm restart ${svcName}`);
7771
7891
  },
7772
7892
  status(options) {
7773
7893
  const svcName = resolveServiceName(descriptor, options);
@@ -7812,8 +7932,8 @@ function createLinuxManager(descriptor) {
7812
7932
  const cmdArgs = descriptor.startCommand(cfgPath);
7813
7933
  mkdirSync(unitDir, { recursive: true });
7814
7934
  writeFileSync(unitPath(svcName), buildSystemdUnit(svcName, cmdArgs));
7815
- run('systemctl --user daemon-reload');
7816
- run(`systemctl --user enable ${svcName}.service`);
7935
+ run$1('systemctl --user daemon-reload');
7936
+ run$1(`systemctl --user enable ${svcName}.service`);
7817
7937
  },
7818
7938
  uninstall(options) {
7819
7939
  const svcName = resolveServiceName(descriptor, options);
@@ -7826,15 +7946,15 @@ function createLinuxManager(descriptor) {
7826
7946
  },
7827
7947
  start(options) {
7828
7948
  const svcName = resolveServiceName(descriptor, options);
7829
- run(`systemctl --user start ${svcName}.service`);
7949
+ run$1(`systemctl --user start ${svcName}.service`);
7830
7950
  },
7831
7951
  stop(options) {
7832
7952
  const svcName = resolveServiceName(descriptor, options);
7833
- run(`systemctl --user stop ${svcName}.service`);
7953
+ run$1(`systemctl --user stop ${svcName}.service`);
7834
7954
  },
7835
7955
  restart(options) {
7836
7956
  const svcName = resolveServiceName(descriptor, options);
7837
- run(`systemctl --user restart ${svcName}.service`);
7957
+ run$1(`systemctl --user restart ${svcName}.service`);
7838
7958
  },
7839
7959
  status(options) {
7840
7960
  const svcName = resolveServiceName(descriptor, options);
@@ -7898,16 +8018,16 @@ function createMacOSManager(descriptor) {
7898
8018
  },
7899
8019
  start(options) {
7900
8020
  const svcName = resolveServiceName(descriptor, options);
7901
- run(`launchctl load ${plistPath(svcName)}`);
8021
+ run$1(`launchctl load ${plistPath(svcName)}`);
7902
8022
  },
7903
8023
  stop(options) {
7904
8024
  const svcName = resolveServiceName(descriptor, options);
7905
- run(`launchctl unload ${plistPath(svcName)}`);
8025
+ run$1(`launchctl unload ${plistPath(svcName)}`);
7906
8026
  },
7907
8027
  restart(options) {
7908
8028
  const svcName = resolveServiceName(descriptor, options);
7909
8029
  runQuiet(`launchctl unload ${plistPath(svcName)}`);
7910
- run(`launchctl load ${plistPath(svcName)}`);
8030
+ run$1(`launchctl load ${plistPath(svcName)}`);
7911
8031
  },
7912
8032
  status(options) {
7913
8033
  const svcName = resolveServiceName(descriptor, options);
@@ -7936,19 +8056,6 @@ function createServiceManager(descriptor) {
7936
8056
  }
7937
8057
  }
7938
8058
 
7939
- /**
7940
- * Shared CLI defaults and option registration for Jeeves CLI commands.
7941
- *
7942
- * @remarks
7943
- * All three CLI commands (install, uninstall, status) share the same
7944
- * `--workspace` and `--config-root` options with the same defaults.
7945
- * This module centralizes them to eliminate duplication.
7946
- */
7947
- /** Default workspace path (current directory). */
7948
- const DEFAULT_WORKSPACE = '.';
7949
- /** Default config root path. */
7950
- const DEFAULT_CONFIG_ROOT = './config';
7951
-
7952
8059
  /**
7953
8060
  * Factory for the standard Jeeves service CLI.
7954
8061
  *
@@ -7957,6 +8064,10 @@ const DEFAULT_CONFIG_ROOT = './config';
7957
8064
  * a component descriptor. Components add domain-specific commands
7958
8065
  * via `descriptor.customCliCommands`.
7959
8066
  */
8067
+ function handleCommandError(action, err) {
8068
+ console.error(`${action} failed: ${getErrorMessage(err)}`);
8069
+ process.exitCode = 1;
8070
+ }
7960
8071
  /**
7961
8072
  * Create a standard service CLI program from a component descriptor.
7962
8073
  *
@@ -8009,8 +8120,7 @@ function createServiceCli(descriptor) {
8009
8120
  console.log(JSON.stringify(result, null, 2));
8010
8121
  }
8011
8122
  catch (err) {
8012
- const msg = err instanceof Error ? err.message : String(err);
8013
- console.error(`Service unreachable: ${msg}`);
8123
+ console.error(`Service unreachable: ${getErrorMessage(err)}`);
8014
8124
  process.exitCode = 1;
8015
8125
  }
8016
8126
  });
@@ -8028,8 +8138,7 @@ function createServiceCli(descriptor) {
8028
8138
  console.log(JSON.stringify(result, null, 2));
8029
8139
  }
8030
8140
  catch (err) {
8031
- const msg = err instanceof Error ? err.message : String(err);
8032
- console.error(`Config query failed: ${msg}`);
8141
+ console.error(`Config query failed: ${getErrorMessage(err)}`);
8033
8142
  process.exitCode = 1;
8034
8143
  }
8035
8144
  });
@@ -8045,8 +8154,7 @@ function createServiceCli(descriptor) {
8045
8154
  console.log('Config is valid.');
8046
8155
  }
8047
8156
  catch (err) {
8048
- const msg = err instanceof Error ? err.message : String(err);
8049
- console.error(`Validation failed: ${msg}`);
8157
+ console.error(`Validation failed: ${getErrorMessage(err)}`);
8050
8158
  process.exitCode = 1;
8051
8159
  }
8052
8160
  });
@@ -8083,8 +8191,7 @@ function createServiceCli(descriptor) {
8083
8191
  console.log(JSON.stringify(result, null, 2));
8084
8192
  }
8085
8193
  catch (err) {
8086
- const msg = err instanceof Error ? err.message : String(err);
8087
- console.error(`Config apply failed: ${msg}`);
8194
+ console.error(`Config apply failed: ${getErrorMessage(err)}`);
8088
8195
  process.exitCode = 1;
8089
8196
  }
8090
8197
  });
@@ -8121,9 +8228,7 @@ function createServiceCli(descriptor) {
8121
8228
  console.log(`Service "${opts.name}" installed.`);
8122
8229
  }
8123
8230
  catch (err) {
8124
- const msg = err instanceof Error ? err.message : String(err);
8125
- console.error(`Install failed: ${msg}`);
8126
- process.exitCode = 1;
8231
+ handleCommandError('Install', err);
8127
8232
  }
8128
8233
  });
8129
8234
  serviceCmd
@@ -8136,9 +8241,7 @@ function createServiceCli(descriptor) {
8136
8241
  console.log(`Service "${opts.name}" uninstalled.`);
8137
8242
  }
8138
8243
  catch (err) {
8139
- const msg = err instanceof Error ? err.message : String(err);
8140
- console.error(`Uninstall failed: ${msg}`);
8141
- process.exitCode = 1;
8244
+ handleCommandError('Uninstall', err);
8142
8245
  }
8143
8246
  });
8144
8247
  serviceCmd
@@ -8151,9 +8254,7 @@ function createServiceCli(descriptor) {
8151
8254
  console.log(`Service "${opts.name}" started.`);
8152
8255
  }
8153
8256
  catch (err) {
8154
- const msg = err instanceof Error ? err.message : String(err);
8155
- console.error(`Start failed: ${msg}`);
8156
- process.exitCode = 1;
8257
+ handleCommandError('Start', err);
8157
8258
  }
8158
8259
  });
8159
8260
  serviceCmd
@@ -8166,9 +8267,7 @@ function createServiceCli(descriptor) {
8166
8267
  console.log(`Service "${opts.name}" stopped.`);
8167
8268
  }
8168
8269
  catch (err) {
8169
- const msg = err instanceof Error ? err.message : String(err);
8170
- console.error(`Stop failed: ${msg}`);
8171
- process.exitCode = 1;
8270
+ handleCommandError('Stop', err);
8172
8271
  }
8173
8272
  });
8174
8273
  serviceCmd
@@ -8181,9 +8280,7 @@ function createServiceCli(descriptor) {
8181
8280
  console.log(`Service "${opts.name}" restarted.`);
8182
8281
  }
8183
8282
  catch (err) {
8184
- const msg = err instanceof Error ? err.message : String(err);
8185
- console.error(`Restart failed: ${msg}`);
8186
- process.exitCode = 1;
8283
+ handleCommandError('Restart', err);
8187
8284
  }
8188
8285
  });
8189
8286
  serviceCmd
@@ -8196,9 +8293,7 @@ function createServiceCli(descriptor) {
8196
8293
  console.log(`Service "${opts.name}": ${state}`);
8197
8294
  }
8198
8295
  catch (err) {
8199
- const msg = err instanceof Error ? err.message : String(err);
8200
- console.error(`Status failed: ${msg}`);
8201
- process.exitCode = 1;
8296
+ handleCommandError('Status', err);
8202
8297
  }
8203
8298
  });
8204
8299
  // Apply custom CLI commands if provided
@@ -8283,6 +8378,38 @@ function loadConfig(configDir) {
8283
8378
  }
8284
8379
  }
8285
8380
 
8381
+ /**
8382
+ * Service URL resolution.
8383
+ *
8384
+ * @remarks
8385
+ * Resolves the URL for a named Jeeves service using the following
8386
+ * resolution order:
8387
+ * 1. Consumer's own component config
8388
+ * 2. Core config (`{configRoot}/jeeves-core/config.json`)
8389
+ * 3. Default port constants
8390
+ */
8391
+ /**
8392
+ * Resolve the URL for a named Jeeves service.
8393
+ *
8394
+ * @param serviceName - The service name (e.g., 'watcher', 'runner').
8395
+ * @param consumerName - Optional consumer component name for config override.
8396
+ * @returns The resolved service URL.
8397
+ * @throws Error if `init()` has not been called or the service is unknown.
8398
+ */
8399
+ function getServiceUrl(serviceName, consumerName) {
8400
+ // 2. Check core config
8401
+ const coreDir = getCoreConfigDir();
8402
+ const coreConfig = loadConfig(coreDir);
8403
+ const coreUrl = coreConfig?.services[serviceName]?.url;
8404
+ if (coreUrl)
8405
+ return coreUrl;
8406
+ // 3. Fall back to port constants
8407
+ const port = DEFAULT_PORTS[serviceName];
8408
+ {
8409
+ return `http://127.0.0.1:${String(port)}`;
8410
+ }
8411
+ }
8412
+
8286
8413
  /**
8287
8414
  * Resolve the bind address for a Jeeves service.
8288
8415
  *
@@ -8320,6 +8447,10 @@ function getBindAddress(componentName) {
8320
8447
  // Tier 4: Default
8321
8448
  return DEFAULT_BIND_ADDRESS;
8322
8449
  }
8450
+ /** Async sleep via setTimeout. */
8451
+ function sleepAsync(ms) {
8452
+ return new Promise((r) => setTimeout(r, ms));
8453
+ }
8323
8454
 
8324
8455
  /**
8325
8456
  * Shared live config hot-reload support.
@@ -8337,7 +8468,6 @@ function getBindAddress(componentName) {
8337
8468
  */
8338
8469
  const RESTART_REQUIRED_FIELDS = [
8339
8470
  'port',
8340
- 'host',
8341
8471
  'watcherUrl',
8342
8472
  'gatewayUrl',
8343
8473
  'gatewayApiKey',
@@ -8449,8 +8579,6 @@ const autoSeedRuleSchema = z.object({
8449
8579
  const serviceConfigSchema = metaConfigSchema.extend({
8450
8580
  /** HTTP port for the service (default: 1938). */
8451
8581
  port: z.number().int().min(1).max(65535).default(1938),
8452
- /** Bind address for the HTTP server (default: 127.0.0.1). */
8453
- host: z.string().default('127.0.0.1'),
8454
8582
  /** Cron schedule for synthesis cycles (default: every 30 min). */
8455
8583
  schedule: z.string().default('*/30 * * * *'),
8456
8584
  /** Optional channel identifier for reporting. */
@@ -8810,6 +8938,174 @@ async function readMetaJson(metaPath) {
8810
8938
  return JSON.parse(raw);
8811
8939
  }
8812
8940
 
8941
+ /**
8942
+ * Escape special glob characters in a path so it can be used as a literal
8943
+ * prefix in glob patterns.
8944
+ *
8945
+ * Glob metacharacters `* ? [ ] { } ( ) !` are escaped with a backslash so
8946
+ * that paths containing parentheses (e.g. Slack channel IDs) or other
8947
+ * special characters are matched literally by the watcher's walk endpoint.
8948
+ *
8949
+ * @module escapeGlob
8950
+ */
8951
+ /**
8952
+ * Escape glob metacharacters in a string using character-class wrapping.
8953
+ *
8954
+ * Backslash escaping (`\(`) does not work reliably on Windows where `\` is
8955
+ * the path separator. Instead, each metacharacter is wrapped in a character
8956
+ * class (e.g. `(` → `[(]`) which is universally supported by glob libraries.
8957
+ *
8958
+ * Square brackets themselves are escaped as `[[]` and `[]]`.
8959
+ *
8960
+ * @param s - Raw path string.
8961
+ * @returns String with glob metacharacters wrapped in character classes.
8962
+ */
8963
+ function escapeGlob(s) {
8964
+ return s.replace(/[*?[\]{}()!]/g, (ch) => `[${ch}]`);
8965
+ }
8966
+
8967
+ /**
8968
+ * Filter file paths by modification time.
8969
+ *
8970
+ * Shared utility for staleness detection and delta file enumeration.
8971
+ * Uses `fs.statSync` for fast local mtime checks on known paths.
8972
+ *
8973
+ * @module mtimeFilter
8974
+ */
8975
+ /**
8976
+ * Check if any file in the list was modified after the given timestamp.
8977
+ *
8978
+ * Short-circuits on first match for efficiency (staleness checks).
8979
+ *
8980
+ * @param files - Array of file paths to check.
8981
+ * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` match.
8982
+ * @returns True if any file was modified after the timestamp.
8983
+ */
8984
+ function hasModifiedAfter(files, afterMs) {
8985
+ for (const filePath of files) {
8986
+ try {
8987
+ if (statSync(filePath).mtimeMs > afterMs)
8988
+ return true;
8989
+ }
8990
+ catch {
8991
+ // Unreadable file — skip
8992
+ }
8993
+ }
8994
+ return false;
8995
+ }
8996
+ /**
8997
+ * Filter files to only those modified after the given timestamp.
8998
+ *
8999
+ * @param files - Array of file paths to filter.
9000
+ * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` are included.
9001
+ * @returns Filtered array of file paths.
9002
+ */
9003
+ function filterModifiedAfter(files, afterMs) {
9004
+ return files.filter((filePath) => {
9005
+ try {
9006
+ return statSync(filePath).mtimeMs > afterMs;
9007
+ }
9008
+ catch {
9009
+ return false;
9010
+ }
9011
+ });
9012
+ }
9013
+
9014
+ /**
9015
+ * Staleness detection via watcher walk.
9016
+ *
9017
+ * A meta is stale when any watched file in its scope was modified after
9018
+ * `_generatedAt`.
9019
+ *
9020
+ * @module scheduling/staleness
9021
+ */
9022
+ /**
9023
+ * Check if a meta is stale.
9024
+ *
9025
+ * Uses watcher `/walk` to enumerate watched files under the scope prefix,
9026
+ * then applies a local mtime check (fast) to detect any modifications since
9027
+ * `_generatedAt`. Short-circuits on first match.
9028
+ *
9029
+ * @param scopePrefix - Path prefix for this meta's scope.
9030
+ * @param meta - Current meta.json content.
9031
+ * @param watcher - WatcherClient instance.
9032
+ * @returns True if any file in scope was modified after `_generatedAt`.
9033
+ */
9034
+ async function isStale(scopePrefix, meta, watcher) {
9035
+ if (!meta._generatedAt)
9036
+ return true; // Never synthesized = stale
9037
+ const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
9038
+ // Exclude .meta/ subtree — synthesis outputs must not trigger staleness.
9039
+ // Handle both forward and back slashes for cross-platform compatibility.
9040
+ const metaSep = /[/\\]\.meta(?:[/\\]|$)/;
9041
+ const filtered = files.filter((f) => !metaSep.test(f));
9042
+ return hasModifiedAfter(filtered, new Date(meta._generatedAt).getTime());
9043
+ }
9044
+ /** Maximum staleness for never-synthesized metas (1 year in seconds). */
9045
+ const MAX_STALENESS_SECONDS = 365 * 86_400;
9046
+ /**
9047
+ * Compute actual staleness in seconds (now minus _generatedAt).
9048
+ *
9049
+ * Never-synthesized metas are capped at {@link MAX_STALENESS_SECONDS}
9050
+ * (1 year) so that depth weighting can differentiate them. Without
9051
+ * bounding, `Infinity * depthFactor` = `Infinity` for all depths.
9052
+ *
9053
+ * @param meta - Current meta.json content.
9054
+ * @returns Staleness in seconds, capped at 1 year for never-synthesized metas.
9055
+ */
9056
+ function actualStaleness(meta) {
9057
+ if (!meta._generatedAt)
9058
+ return MAX_STALENESS_SECONDS;
9059
+ const generatedMs = new Date(meta._generatedAt).getTime();
9060
+ return Math.min((Date.now() - generatedMs) / 1000, MAX_STALENESS_SECONDS);
9061
+ }
9062
+ /**
9063
+ * Check whether the architect step should be triggered.
9064
+ *
9065
+ * @param meta - Current meta.json.
9066
+ * @param structureChanged - Whether the structure hash changed.
9067
+ * @param steerChanged - Whether the steer directive changed.
9068
+ * @param architectEvery - Config: run architect every N cycles.
9069
+ * @returns True if the architect step should run.
9070
+ */
9071
+ function isArchitectTriggered(meta, structureChanged, steerChanged, architectEvery) {
9072
+ return (!meta._builder ||
9073
+ structureChanged ||
9074
+ steerChanged ||
9075
+ (meta._synthesisCount ?? 0) >= architectEvery);
9076
+ }
9077
+ /**
9078
+ * Detect whether the steer directive changed since the last archive.
9079
+ *
9080
+ * @param currentSteer - Current _steer value (or undefined).
9081
+ * @param archiveSteer - Archive _steer value (or undefined).
9082
+ * @param hasArchive - Whether an archive snapshot exists.
9083
+ * @returns True if steer changed.
9084
+ */
9085
+ function hasSteerChanged(currentSteer, archiveSteer, hasArchive) {
9086
+ if (!hasArchive)
9087
+ return Boolean(currentSteer);
9088
+ return currentSteer !== archiveSteer;
9089
+ }
9090
+ /**
9091
+ * Compute a normalized staleness score (0–1) for display purposes.
9092
+ *
9093
+ * Uses the same depth/emphasis weighting as candidate selection,
9094
+ * normalized to a 30-day window.
9095
+ *
9096
+ * @param stalenessSeconds - Raw staleness in seconds (null = never synthesized).
9097
+ * @param depth - Meta tree depth.
9098
+ * @param emphasis - Scheduling emphasis multiplier.
9099
+ * @param depthWeight - Depth weighting exponent from config.
9100
+ * @returns Normalized score between 0 and 1.
9101
+ */
9102
+ function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
9103
+ if (stalenessSeconds === null)
9104
+ return 1;
9105
+ const depthFactor = Math.pow(1 + depthWeight, depth);
9106
+ return Math.min(1, (stalenessSeconds * depthFactor * emphasis) / (30 * 86400));
9107
+ }
9108
+
8813
9109
  /**
8814
9110
  * Build the ownership tree from discovered .meta/ paths.
8815
9111
  *
@@ -8892,8 +9188,6 @@ function findNode(tree, targetPath) {
8892
9188
  *
8893
9189
  * @module discovery/listMetas
8894
9190
  */
8895
- /** Maximum staleness for never-synthesized metas (1 year in seconds). */
8896
- const MAX_STALENESS_SECONDS$1 = 365 * 86_400;
8897
9191
  /**
8898
9192
  * Discover, deduplicate, and enrich all metas.
8899
9193
  *
@@ -8929,7 +9223,7 @@ async function listMetas(config, watcher) {
8929
9223
  // Compute staleness
8930
9224
  let stalenessSeconds;
8931
9225
  if (neverSynth) {
8932
- stalenessSeconds = MAX_STALENESS_SECONDS$1;
9226
+ stalenessSeconds = MAX_STALENESS_SECONDS;
8933
9227
  }
8934
9228
  else {
8935
9229
  const genAt = new Date(meta._generatedAt).getTime();
@@ -8962,79 +9256,6 @@ async function listMetas(config, watcher) {
8962
9256
  };
8963
9257
  }
8964
9258
 
8965
- /**
8966
- * Escape special glob characters in a path so it can be used as a literal
8967
- * prefix in glob patterns.
8968
- *
8969
- * Glob metacharacters `* ? [ ] { } ( ) !` are escaped with a backslash so
8970
- * that paths containing parentheses (e.g. Slack channel IDs) or other
8971
- * special characters are matched literally by the watcher's walk endpoint.
8972
- *
8973
- * @module escapeGlob
8974
- */
8975
- /**
8976
- * Escape glob metacharacters in a string using character-class wrapping.
8977
- *
8978
- * Backslash escaping (`\(`) does not work reliably on Windows where `\` is
8979
- * the path separator. Instead, each metacharacter is wrapped in a character
8980
- * class (e.g. `(` → `[(]`) which is universally supported by glob libraries.
8981
- *
8982
- * Square brackets themselves are escaped as `[[]` and `[]]`.
8983
- *
8984
- * @param s - Raw path string.
8985
- * @returns String with glob metacharacters wrapped in character classes.
8986
- */
8987
- function escapeGlob(s) {
8988
- return s.replace(/[*?[\]{}()!]/g, (ch) => `[${ch}]`);
8989
- }
8990
-
8991
- /**
8992
- * Filter file paths by modification time.
8993
- *
8994
- * Shared utility for staleness detection and delta file enumeration.
8995
- * Uses `fs.statSync` for fast local mtime checks on known paths.
8996
- *
8997
- * @module mtimeFilter
8998
- */
8999
- /**
9000
- * Check if any file in the list was modified after the given timestamp.
9001
- *
9002
- * Short-circuits on first match for efficiency (staleness checks).
9003
- *
9004
- * @param files - Array of file paths to check.
9005
- * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` match.
9006
- * @returns True if any file was modified after the timestamp.
9007
- */
9008
- function hasModifiedAfter(files, afterMs) {
9009
- for (const filePath of files) {
9010
- try {
9011
- if (statSync(filePath).mtimeMs > afterMs)
9012
- return true;
9013
- }
9014
- catch {
9015
- // Unreadable file — skip
9016
- }
9017
- }
9018
- return false;
9019
- }
9020
- /**
9021
- * Filter files to only those modified after the given timestamp.
9022
- *
9023
- * @param files - Array of file paths to filter.
9024
- * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` are included.
9025
- * @returns Filtered array of file paths.
9026
- */
9027
- function filterModifiedAfter(files, afterMs) {
9028
- return files.filter((filePath) => {
9029
- try {
9030
- return statSync(filePath).mtimeMs > afterMs;
9031
- }
9032
- catch {
9033
- return false;
9034
- }
9035
- });
9036
- }
9037
-
9038
9259
  /**
9039
9260
  * Compute the file scope owned by a meta node.
9040
9261
  *
@@ -9111,11 +9332,6 @@ function getDeltaFiles(generatedAt, scopeFiles) {
9111
9332
  return filterModifiedAfter(scopeFiles, new Date(generatedAt).getTime());
9112
9333
  }
9113
9334
 
9114
- /** Sleep for a given number of milliseconds. */
9115
- function sleep(ms) {
9116
- return new Promise((resolve) => setTimeout(resolve, ms));
9117
- }
9118
-
9119
9335
  /**
9120
9336
  * Error thrown when a spawned subprocess is aborted via AbortController.
9121
9337
  *
@@ -9270,7 +9486,7 @@ class GatewayExecutor {
9270
9486
  JSON.stringify(spawnResult));
9271
9487
  }
9272
9488
  // Step 2: Poll for completion via sessions_history
9273
- await sleep(3000);
9489
+ await sleepAsync(3000);
9274
9490
  while (Date.now() < deadline) {
9275
9491
  // Check for abort before each poll iteration
9276
9492
  if (this.controller.signal.aborted) {
@@ -9334,7 +9550,7 @@ class GatewayExecutor {
9334
9550
  catch {
9335
9551
  // Transient poll failure — keep trying
9336
9552
  }
9337
- await sleep(this.pollIntervalMs);
9553
+ await sleepAsync(this.pollIntervalMs);
9338
9554
  }
9339
9555
  throw new SpawnTimeoutError('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms', outputPath);
9340
9556
  }
@@ -10195,101 +10411,6 @@ function discoverStalestPath(candidates, depthWeight) {
10195
10411
  return winner?.node.metaPath ?? null;
10196
10412
  }
10197
10413
 
10198
- /**
10199
- * Staleness detection via watcher walk.
10200
- *
10201
- * A meta is stale when any watched file in its scope was modified after
10202
- * `_generatedAt`.
10203
- *
10204
- * @module scheduling/staleness
10205
- */
10206
- /**
10207
- * Check if a meta is stale.
10208
- *
10209
- * Uses watcher `/walk` to enumerate watched files under the scope prefix,
10210
- * then applies a local mtime check (fast) to detect any modifications since
10211
- * `_generatedAt`. Short-circuits on first match.
10212
- *
10213
- * @param scopePrefix - Path prefix for this meta's scope.
10214
- * @param meta - Current meta.json content.
10215
- * @param watcher - WatcherClient instance.
10216
- * @returns True if any file in scope was modified after `_generatedAt`.
10217
- */
10218
- async function isStale(scopePrefix, meta, watcher) {
10219
- if (!meta._generatedAt)
10220
- return true; // Never synthesized = stale
10221
- const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
10222
- // Exclude .meta/ subtree — synthesis outputs must not trigger staleness.
10223
- // Handle both forward and back slashes for cross-platform compatibility.
10224
- const metaSep = /[/\\]\.meta(?:[/\\]|$)/;
10225
- const filtered = files.filter((f) => !metaSep.test(f));
10226
- return hasModifiedAfter(filtered, new Date(meta._generatedAt).getTime());
10227
- }
10228
- /** Maximum staleness for never-synthesized metas (1 year in seconds). */
10229
- const MAX_STALENESS_SECONDS = 365 * 86_400;
10230
- /**
10231
- * Compute actual staleness in seconds (now minus _generatedAt).
10232
- *
10233
- * Never-synthesized metas are capped at {@link MAX_STALENESS_SECONDS}
10234
- * (1 year) so that depth weighting can differentiate them. Without
10235
- * bounding, `Infinity * depthFactor` = `Infinity` for all depths.
10236
- *
10237
- * @param meta - Current meta.json content.
10238
- * @returns Staleness in seconds, capped at 1 year for never-synthesized metas.
10239
- */
10240
- function actualStaleness(meta) {
10241
- if (!meta._generatedAt)
10242
- return MAX_STALENESS_SECONDS;
10243
- const generatedMs = new Date(meta._generatedAt).getTime();
10244
- return Math.min((Date.now() - generatedMs) / 1000, MAX_STALENESS_SECONDS);
10245
- }
10246
- /**
10247
- * Check whether the architect step should be triggered.
10248
- *
10249
- * @param meta - Current meta.json.
10250
- * @param structureChanged - Whether the structure hash changed.
10251
- * @param steerChanged - Whether the steer directive changed.
10252
- * @param architectEvery - Config: run architect every N cycles.
10253
- * @returns True if the architect step should run.
10254
- */
10255
- function isArchitectTriggered(meta, structureChanged, steerChanged, architectEvery) {
10256
- return (!meta._builder ||
10257
- structureChanged ||
10258
- steerChanged ||
10259
- (meta._synthesisCount ?? 0) >= architectEvery);
10260
- }
10261
- /**
10262
- * Detect whether the steer directive changed since the last archive.
10263
- *
10264
- * @param currentSteer - Current _steer value (or undefined).
10265
- * @param archiveSteer - Archive _steer value (or undefined).
10266
- * @param hasArchive - Whether an archive snapshot exists.
10267
- * @returns True if steer changed.
10268
- */
10269
- function hasSteerChanged(currentSteer, archiveSteer, hasArchive) {
10270
- if (!hasArchive)
10271
- return Boolean(currentSteer);
10272
- return currentSteer !== archiveSteer;
10273
- }
10274
- /**
10275
- * Compute a normalized staleness score (0–1) for display purposes.
10276
- *
10277
- * Uses the same depth/emphasis weighting as candidate selection,
10278
- * normalized to a 30-day window.
10279
- *
10280
- * @param stalenessSeconds - Raw staleness in seconds (null = never synthesized).
10281
- * @param depth - Meta tree depth.
10282
- * @param emphasis - Scheduling emphasis multiplier.
10283
- * @param depthWeight - Depth weighting exponent from config.
10284
- * @returns Normalized score between 0 and 1.
10285
- */
10286
- function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
10287
- if (stalenessSeconds === null)
10288
- return 1;
10289
- const depthFactor = Math.pow(1 + depthWeight, depth);
10290
- return Math.min(1, (stalenessSeconds * depthFactor * emphasis) / (30 * 86400));
10291
- }
10292
-
10293
10414
  /**
10294
10415
  * Shared error utilities.
10295
10416
  *
@@ -11350,7 +11471,10 @@ function buildMetaRules(config) {
11350
11471
  properties: {
11351
11472
  file: {
11352
11473
  properties: {
11353
- path: { type: 'string', glob: '**/jeeves-meta.config.json' },
11474
+ path: {
11475
+ type: 'string',
11476
+ glob: '**/jeeves-meta{.config.json,/config.json}',
11477
+ },
11354
11478
  },
11355
11479
  },
11356
11480
  },
@@ -12511,7 +12635,7 @@ class HttpWatcherClient {
12511
12635
  }
12512
12636
  // Exponential backoff
12513
12637
  const delayMs = this.backoffBaseMs * Math.pow(this.backoffFactor, attempt);
12514
- await sleep(delayMs);
12638
+ await sleepAsync(delayMs);
12515
12639
  }
12516
12640
  // Unreachable, but TypeScript needs it
12517
12641
  throw new Error('Retry exhausted');
@@ -12730,7 +12854,11 @@ async function startService(config, configPath) {
12730
12854
  */
12731
12855
  /** Build the full API URL for a given port string and path. */
12732
12856
  function apiUrl(port, apiPath) {
12733
- return `http://127.0.0.1:${port}${apiPath}`;
12857
+ const url = new URL(apiPath, getServiceUrl('meta'));
12858
+ if (port !== DEFAULT_PORT_STR) {
12859
+ url.port = port;
12860
+ }
12861
+ return url.toString();
12734
12862
  }
12735
12863
  /** Wrap an async CLI action with consistent error handling. */
12736
12864
  function withErrorHandling(fn, label) {