@karmaniverous/jeeves-meta 0.18.0 → 0.19.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.
@@ -5,7 +5,7 @@ import fs, { unlinkSync, writeFileSync, existsSync, readFileSync, statSync, mkdi
5
5
  import path, { join, dirname, relative, posix, sep, resolve } from 'node:path';
6
6
  import { readFile, unlink, mkdir, writeFile, copyFile } from 'node:fs/promises';
7
7
  import { z } from 'zod';
8
- import { randomUUID, createHash } from 'node:crypto';
8
+ import { randomUUID, createHash, createHmac } from 'node:crypto';
9
9
  import { tmpdir } from 'node:os';
10
10
  import pino from 'pino';
11
11
  import { fileURLToPath } from 'node:url';
@@ -763,11 +763,14 @@ function applyHotReloadedConfig(newConfig) {
763
763
  *
764
764
  * @module schema/config
765
765
  */
766
- /** Default Handlebars template strings for progress reporting. Single source of truth. */
766
+ /**
767
+ * Default Handlebars template strings for progress reporting. Single source of truth.
768
+ * Links are empty when jeeves-server refuses `serverKey`, so each is guarded.
769
+ */
767
770
  const DEFAULT_TEMPLATE_STRINGS = {
768
- phaseStart: ':gear: Started meta synthesis {{phase}} phase of <{{dirLink}}>',
769
- phaseEnd: ':white_check_mark: Completed meta synthesis {{phase}} phase ({{tokens}} tokens / {{seconds}}s) at <{{metaLink}}>',
770
- phaseError: ':x: Meta synthesis {{phase}} phase failed at <{{dirLink}}>\n Error: {{error}}',
771
+ phaseStart: ':gear: Started meta synthesis {{phase}} phase{{#if dirLink}} of <{{dirLink}}>{{/if}}',
772
+ phaseEnd: ':white_check_mark: Completed meta synthesis {{phase}} phase ({{tokens}} tokens / {{seconds}}s){{#if metaLink}} at <{{metaLink}}>{{/if}}',
773
+ phaseError: ':x: Meta synthesis {{phase}} phase failed{{#if dirLink}} at <{{dirLink}}>{{/if}}\n Error: {{error}}',
771
774
  };
772
775
  /** Zod schema for logging configuration. */
773
776
  const loggingSchema = z.object({
@@ -803,6 +806,13 @@ const serviceConfigSchema = metaConfigSchema.extend({
803
806
  * Default: http://127.0.0.1:1934
804
807
  */
805
808
  serverUrl: z.string().default('http://127.0.0.1:1934'),
809
+ /**
810
+ * jeeves-server machine key seed (a `keys` entry in jeeves-server config).
811
+ * The seed is never sent: the resolve-path API receives only the key
812
+ * derived from it, as the `k` parameter. Required from jeeves-server 3.15.0.
813
+ * Supports `${ENV}` substitution. Redacted in `GET /config`.
814
+ */
815
+ serverKey: z.string().optional(),
806
816
  /**
807
817
  * Handlebars templates for progress reporting messages.
808
818
  * Each template receives a standard set of data keys (dirLink, metaLink,
@@ -3018,13 +3028,82 @@ async function executePhase(node, currentMeta, phaseState, phase, config, execut
3018
3028
  }
3019
3029
 
3020
3030
  /**
3021
- * Progress reporting via OpenClaw gateway `/tools/invoke` → `message` tool.
3031
+ * Resolve filesystem paths to jeeves-server browse links via
3032
+ * `GET /api/resolve-path`, authenticated with a jeeves-server machine key.
3033
+ * Network I/O only; no logging (callers decide how to report auth failures).
3034
+ *
3035
+ * @module progress/resolveLink
3036
+ */
3037
+ /** Timeout for resolve-path API calls (ms). */
3038
+ const RESOLVE_PATH_TIMEOUT_MS = 3000;
3039
+ /** The fields of a resolve-path 200 response this module uses. */
3040
+ const resolvePathResponseSchema = z.object({
3041
+ browseUrl: z.string().optional(),
3042
+ publicUrl: z.string().optional(),
3043
+ });
3044
+ /**
3045
+ * Derive the URL key jeeves-server accepts for a machine key seed.
3022
3046
  *
3023
- * Progress events are rendered using Handlebars templates. Owner-path links
3024
- * are resolved via the jeeves-server resolve-path API, with graceful
3025
- * fallback to raw filesystem paths when the server is unreachable.
3047
+ * Mirrors `computeInsiderKey` in `@karmaniverous/jeeves-server-core`:
3048
+ * the first 32 hex chars of HMAC-SHA256(seed, 'insider'). Kept local so this
3049
+ * service does not pull in a second copy of `@karmaniverous/jeeves`.
3026
3050
  *
3027
- * @module progress
3051
+ * @param seed - Machine key seed, as configured under `keys` in jeeves-server.
3052
+ * @returns The derived URL key.
3053
+ */
3054
+ function deriveServerUrlKey(seed) {
3055
+ return createHmac('sha256', seed)
3056
+ .update('insider')
3057
+ .digest('hex')
3058
+ .substring(0, 32);
3059
+ }
3060
+ /**
3061
+ * Resolve a filesystem path to a browse URL.
3062
+ *
3063
+ * Sends the derived machine key as the `k` query parameter when `serverKey`
3064
+ * is set. On HTTP 200 uses `publicUrl` if present, otherwise prefixes
3065
+ * `serverUrl` to `browseUrl`. A malformed 200 response falls back.
3066
+ *
3067
+ * @param fsPath - Absolute filesystem path to resolve.
3068
+ * @param serverUrl - jeeves-server base URL.
3069
+ * @param serverKey - Optional jeeves-server machine key seed.
3070
+ * @returns The lookup outcome.
3071
+ */
3072
+ async function resolveLink(fsPath, serverUrl, serverKey) {
3073
+ const fallback = { kind: 'fallback', link: fsPath };
3074
+ try {
3075
+ const url = new URL('/api/resolve-path', serverUrl);
3076
+ url.searchParams.set('fsPath', fsPath);
3077
+ if (serverKey)
3078
+ url.searchParams.set('k', deriveServerUrlKey(serverKey));
3079
+ const res = await fetchWithTimeout(url.toString(), RESOLVE_PATH_TIMEOUT_MS);
3080
+ if (res.status === 401 || res.status === 403)
3081
+ return { kind: 'unauthorized', status: res.status };
3082
+ if (!res.ok)
3083
+ return fallback;
3084
+ const parsed = resolvePathResponseSchema.safeParse(await res.json());
3085
+ if (!parsed.success)
3086
+ return fallback;
3087
+ const data = parsed.data;
3088
+ if (data.publicUrl)
3089
+ return { kind: 'resolved', link: data.publicUrl };
3090
+ if (data.browseUrl)
3091
+ return {
3092
+ kind: 'resolved',
3093
+ link: serverUrl.replace(/\/+$/, '') + data.browseUrl,
3094
+ };
3095
+ return fallback;
3096
+ }
3097
+ catch {
3098
+ return fallback;
3099
+ }
3100
+ }
3101
+
3102
+ /**
3103
+ * Render progress events with Handlebars templates, resolving owner-path
3104
+ * browse links through jeeves-server. Network I/O via resolveLink only.
3105
+ *
3106
+ * @module progress/render
3028
3107
  */
3029
3108
  /** Compile raw template strings into Handlebars delegates. */
3030
3109
  function compileTemplates(raw) {
@@ -3043,40 +3122,24 @@ function formatNumber(n) {
3043
3122
  return n.toLocaleString('en-US');
3044
3123
  }
3045
3124
  /**
3046
- * Resolve a filesystem path to a browse URL via the jeeves-server
3047
- * resolve-path API. Returns the raw path as fallback on any failure.
3048
- *
3049
- * On HTTP 200, uses publicUrl if present, otherwise prefixes serverUrl
3050
- * to browseUrl. On any error or non-200 response, returns fsPath as-is.
3125
+ * Resolve the path and link fields for an owner directory and its meta.json.
3126
+ * When the server refuses the key, every field is empty: no raw filesystem
3127
+ * path reaches any template, default or custom.
3051
3128
  */
3052
- /** Timeout for resolve-path API calls (ms). */
3053
- const RESOLVE_PATH_TIMEOUT_MS = 3000;
3054
- async function resolveLink(fsPath, serverUrl) {
3055
- try {
3056
- const url = new URL('/api/resolve-path', serverUrl);
3057
- url.searchParams.set('fsPath', fsPath);
3058
- const res = await fetchWithTimeout(url.toString(), RESOLVE_PATH_TIMEOUT_MS);
3059
- if (!res.ok)
3060
- return fsPath;
3061
- const data = (await res.json());
3062
- if (data.publicUrl)
3063
- return data.publicUrl;
3064
- if (data.browseUrl) {
3065
- const base = serverUrl.replace(/\/+$/, '');
3066
- return base + data.browseUrl;
3067
- }
3068
- return fsPath;
3129
+ async function resolvePathFields(ownerPath, serverUrl, options) {
3130
+ const result = await resolveLink(ownerPath, serverUrl, options.serverKey);
3131
+ if (result.kind === 'unauthorized') {
3132
+ options.onUnauthorized?.(result.status);
3133
+ return { dirPath: '', metaPath: '', dirLink: '', metaLink: '' };
3069
3134
  }
3070
- catch {
3071
- return fsPath;
3072
- }
3073
- }
3074
- /** Resolve browse links for both the owner directory and its meta.json. */
3075
- async function resolveLinks(ownerPath, serverUrl) {
3076
- const dirLink = await resolveLink(ownerPath, serverUrl);
3077
- // Derive metaLink by appending /.meta/meta.json to the resolved dir link
3078
- const metaLink = dirLink.replace(/\/+$/, '') + '/.meta/meta.json';
3079
- return { dirLink, metaLink };
3135
+ const dirLink = result.link;
3136
+ return {
3137
+ dirPath: ownerPath,
3138
+ metaPath: ownerPath.replace(/\/+$/, '') + '/.meta/meta.json',
3139
+ dirLink,
3140
+ // Derive metaLink by appending /.meta/meta.json to the resolved dir link
3141
+ metaLink: dirLink.replace(/\/+$/, '') + '/.meta/meta.json',
3142
+ };
3080
3143
  }
3081
3144
  /**
3082
3145
  * Render the appropriate template for a progress event.
@@ -3084,18 +3147,13 @@ async function resolveLinks(ownerPath, serverUrl) {
3084
3147
  * @param event - The progress event to render.
3085
3148
  * @param templates - Compiled Handlebars templates.
3086
3149
  * @param serverUrl - jeeves-server URL for resolve-path API.
3150
+ * @param options - Machine key and unauthorized-response callback.
3087
3151
  * @returns Rendered message string.
3088
3152
  */
3089
- async function renderProgressEvent(event, templates, serverUrl) {
3090
- const ownerPath = event.path;
3091
- const metaPath = ownerPath.replace(/\/+$/, '') + '/.meta/meta.json';
3153
+ async function renderProgressEvent(event, templates, serverUrl, options = {}) {
3092
3154
  const phase = (event.phase ?? 'unknown').toUpperCase();
3093
- const { dirLink, metaLink } = await resolveLinks(ownerPath, serverUrl);
3094
3155
  const base = {
3095
- dirPath: ownerPath,
3096
- metaPath,
3097
- dirLink,
3098
- metaLink,
3156
+ ...(await resolvePathFields(event.path, serverUrl, options)),
3099
3157
  phase,
3100
3158
  };
3101
3159
  switch (event.type) {
@@ -3119,6 +3177,18 @@ async function renderProgressEvent(event, templates, serverUrl) {
3119
3177
  return 'Unknown progress event';
3120
3178
  }
3121
3179
  }
3180
+
3181
+ /**
3182
+ * Progress reporting via OpenClaw gateway `/tools/invoke` → `message` tool.
3183
+ *
3184
+ * Progress events are rendered using Handlebars templates (see
3185
+ * `progress/render`). Owner-path links are resolved via the jeeves-server
3186
+ * resolve-path API (authenticated with `serverKey`), falling back to raw
3187
+ * filesystem paths when the server is unreachable and omitting links when
3188
+ * the server refuses the key.
3189
+ *
3190
+ * @module progress
3191
+ */
3122
3192
  class ProgressReporter {
3123
3193
  config;
3124
3194
  logger;
@@ -3126,6 +3196,8 @@ class ProgressReporter {
3126
3196
  /** Snapshot of template source strings used to compile `this.templates`. */
3127
3197
  lastTemplateSource;
3128
3198
  serverUrl;
3199
+ /** Unauthorized causes already warned about; each is logged once. */
3200
+ reportedAuthCauses = new Set();
3129
3201
  constructor(config, logger) {
3130
3202
  this.config = config;
3131
3203
  this.logger = logger;
@@ -3154,7 +3226,12 @@ class ProgressReporter {
3154
3226
  }
3155
3227
  let message;
3156
3228
  try {
3157
- message = await renderProgressEvent(event, this.templates, this.serverUrl);
3229
+ message = await renderProgressEvent(event, this.templates, this.serverUrl, {
3230
+ serverKey: this.config.serverKey,
3231
+ onUnauthorized: (status) => {
3232
+ this.warnUnauthorized(status);
3233
+ },
3234
+ });
3158
3235
  }
3159
3236
  catch (err) {
3160
3237
  this.logger.warn({ err }, 'Progress event rendering failed');
@@ -3191,6 +3268,16 @@ class ProgressReporter {
3191
3268
  this.logger.warn({ err }, 'Progress reporting threw');
3192
3269
  }
3193
3270
  }
3271
+ /** Warn once per distinct cause when jeeves-server refuses resolve-path. */
3272
+ warnUnauthorized(status) {
3273
+ const reason = this.config.serverKey ? 'rejected' : 'missing';
3274
+ if (this.reportedAuthCauses.has(reason))
3275
+ return;
3276
+ this.reportedAuthCauses.add(reason);
3277
+ this.logger.warn({ status, serverUrl: this.serverUrl }, reason === 'missing'
3278
+ ? 'jeeves-server requires a machine key for resolve-path; set serverKey in meta config. Progress reports omit links until then.'
3279
+ : 'jeeves-server rejected serverKey for resolve-path; check the machine key. Progress reports omit links until then.');
3280
+ }
3194
3281
  }
3195
3282
 
3196
3283
  /**
@@ -4001,11 +4088,12 @@ class Scheduler {
4001
4088
  *
4002
4089
  * @module routes/config
4003
4090
  */
4004
- /** Return a sanitized copy of the config (redact gatewayApiKey). */
4091
+ /** Return a sanitized copy of the config (redact gatewayApiKey and serverKey). */
4005
4092
  function sanitizeConfig(config) {
4006
4093
  return {
4007
4094
  ...config,
4008
4095
  gatewayApiKey: config.gatewayApiKey ? '[REDACTED]' : undefined,
4096
+ serverKey: config.serverKey ? '[REDACTED]' : undefined,
4009
4097
  };
4010
4098
  }
4011
4099
  function registerConfigRoute(app, deps) {
package/package.json CHANGED
@@ -7,31 +7,31 @@
7
7
  "url": "https://github.com/karmaniverous/jeeves-meta/issues"
8
8
  },
9
9
  "dependencies": {
10
- "@karmaniverous/jeeves": "0.6.0",
11
- "@karmaniverous/jeeves-meta-core": "^0.2.2",
10
+ "@karmaniverous/jeeves": "0.7.0",
11
+ "@karmaniverous/jeeves-meta-core": "^0.3.0",
12
12
  "commander": "^15",
13
13
  "croner": "^10",
14
14
  "fastify": "^5.12",
15
- "handlebars": "^4.7.9",
15
+ "handlebars": "^4.7.10",
16
16
  "package-directory": "^8.2.0",
17
17
  "pino": "^10",
18
18
  "zod": "^4.6"
19
19
  },
20
20
  "description": "Fastify HTTP service for the Jeeves Meta synthesis engine",
21
21
  "devDependencies": {
22
- "@dotenvx/dotenvx": "^2.30.0",
22
+ "@dotenvx/dotenvx": "^2.35.0",
23
23
  "@rollup/plugin-commonjs": "^29.0.3",
24
24
  "@rollup/plugin-json": "^6.1.0",
25
25
  "@rollup/plugin-node-resolve": "^16.0.3",
26
26
  "@rollup/plugin-typescript": "^12.3.0",
27
- "@types/node": "^26.6.3",
28
- "@vitest/coverage-v8": "^5.0.2",
27
+ "@types/node": "^26.6.5",
28
+ "@vitest/coverage-v8": "^5.0.4",
29
29
  "cross-env": "^10.1.0",
30
- "release-it": "^21.1.0",
31
- "rollup": "^4.63.5",
30
+ "release-it": "^21.1.1",
31
+ "rollup": "^4.64.6",
32
32
  "rollup-plugin-copy": "^3.5.0",
33
33
  "tslib": "^2.8.1",
34
- "vitest": "^5.0.2"
34
+ "vitest": "^5.0.4"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=22"
@@ -62,7 +62,9 @@
62
62
  "tagName": "service/${version}"
63
63
  },
64
64
  "github": {
65
- "release": true
65
+ "release": true,
66
+ "releaseName": "${name.split('/').pop()}@${version}",
67
+ "releaseNotes": "npx git-cliff --tag service/${version} --unreleased --strip header"
66
68
  },
67
69
  "hooks": {
68
70
  "after:bump": [
@@ -81,7 +83,7 @@
81
83
  "git switch ${branchName}"
82
84
  ],
83
85
  "before:npm:release": [
84
- "npx git-cliff -o CHANGELOG.md",
86
+ "npx git-cliff --tag service/${version} -o CHANGELOG.md",
85
87
  "git add -A"
86
88
  ]
87
89
  },
@@ -106,5 +108,5 @@
106
108
  "typecheck": "tsc"
107
109
  },
108
110
  "type": "module",
109
- "version": "0.18.0"
111
+ "version": "0.19.0"
110
112
  }