@karmaniverous/jeeves-watcher-openclaw 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DEFAULT_PORTS, resolvePluginSetting, resolveOptionalPluginSetting, fail, init, resolveWorkspacePath, registerPromptContext, jeevesComponentDescriptorSchema, postJson, fetchJson, ok, connectionFail, getPackageVersion, createPluginToolset } from '@karmaniverous/jeeves';
1
+ import { DEFAULT_PORTS, resolvePluginSetting, resolveOptionalPluginSetting, ok, connectionFail, fail, init, resolveWorkspacePath, registerPromptContext, jeevesComponentDescriptorSchema, getPackageVersion, createPluginToolset } from '@karmaniverous/jeeves';
2
2
  import 'node:crypto';
3
3
 
4
4
  function getEnumValues$1(entries) {
@@ -165,7 +165,7 @@ const allowsEval$1 = /* @__PURE__*/ cached$1(() => {
165
165
  return false;
166
166
  }
167
167
  });
168
- function isPlainObject$1(o) {
168
+ function isPlainObject$2(o) {
169
169
  if (isObject$1(o) === false)
170
170
  return false;
171
171
  // modified constructor
@@ -185,7 +185,7 @@ function isPlainObject$1(o) {
185
185
  return true;
186
186
  }
187
187
  function shallowClone$1(o) {
188
- if (isPlainObject$1(o))
188
+ if (isPlainObject$2(o))
189
189
  return { ...o };
190
190
  if (Array.isArray(o))
191
191
  return [...o];
@@ -284,7 +284,7 @@ function omit$1(schema, mask) {
284
284
  return clone$1(schema, mergeDefs$1(currDef, { shape: newShape, checks: [] }));
285
285
  }
286
286
  function extend$1(schema, shape) {
287
- if (!isPlainObject$1(shape)) {
287
+ if (!isPlainObject$2(shape)) {
288
288
  throw new Error("Invalid input to extend: expected a plain object");
289
289
  }
290
290
  const checks = schema._zod.def.checks;
@@ -308,7 +308,7 @@ function extended(schema, shape) {
308
308
  return newShape;
309
309
  }
310
310
  function safeExtend$1(schema, shape) {
311
- if (!isPlainObject$1(shape)) {
311
+ if (!isPlainObject$2(shape)) {
312
312
  throw new Error("Invalid input to safeExtend: expected a plain object");
313
313
  }
314
314
  return clone$1(schema, mergeDefs$1(schema._zod.def, { shape: extended(schema, shape) }));
@@ -387,6 +387,20 @@ function unwrapMessage$1(message) {
387
387
  return typeof message === "string" ? message : message?.message;
388
388
  }
389
389
  /* A check holds no link back to the schema it is attached to — the same check instance is shared by every clone of that schema — so the owner is stamped onto the issues a check just raised, at the only point where both are in scope. Runs on the failure path only; `start` is the issue count from before the check ran. */
390
+ /** The leading size checks of `defChecks` that set an upper bound, each paired with it. */
391
+ function sizeCaps(defChecks) {
392
+ const caps = [];
393
+ for (const ch of defChecks ?? []) {
394
+ const d = ch._zod.def;
395
+ // a later check may rewrite the value
396
+ if (!/_(length|size)$|_equals$/.test(d.check))
397
+ break;
398
+ const max = d.maximum ?? d.length ?? d.size;
399
+ if (max >= 0)
400
+ caps.push([max, ch]);
401
+ }
402
+ return caps;
403
+ }
390
404
  function attachSchema(issues, start, inst) {
391
405
  var _a;
392
406
  for (let i = start; i < issues.length; i++) {
@@ -1152,6 +1166,7 @@ const anyString = /^[\s\S]{0,}$/;
1152
1166
  const integer$1 = /^-?\d+$/;
1153
1167
  const number$3 = /^-?\d+(?:\.\d+)?$/;
1154
1168
  const boolean$3 = /^(?:true|false)$/i;
1169
+ const _null$5 = /^null$/i;
1155
1170
  // regex for string with no uppercase letters
1156
1171
  const lowercase$1 = /^[^A-Z]*$/;
1157
1172
  // regex for string with no lowercase letters
@@ -1550,7 +1565,7 @@ let Doc$1 = class Doc {
1550
1565
  const version$1 = {
1551
1566
  major: 4,
1552
1567
  minor: 6,
1553
- patch: 5,
1568
+ patch: 7,
1554
1569
  };
1555
1570
 
1556
1571
  const $ZodType$1 = /*@__PURE__*/ $constructor$1("$ZodType", (inst, def) => {
@@ -2142,6 +2157,23 @@ const $ZodBoolean$1 = /*@__PURE__*/ $constructor$1("$ZodBoolean", (inst, def) =>
2142
2157
  return payload;
2143
2158
  };
2144
2159
  });
2160
+ const $ZodNull$1 = /*@__PURE__*/ $constructor$1("$ZodNull", (inst, def) => {
2161
+ $ZodType$1.init(inst, def);
2162
+ inst._zod.pattern = _null$5;
2163
+ inst._zod.values = new Set([null]);
2164
+ inst._zod.parse = (payload, _ctx) => {
2165
+ const input = payload.value;
2166
+ if (input === null)
2167
+ return payload;
2168
+ payload.issues.push({
2169
+ expected: "null",
2170
+ code: "invalid_type",
2171
+ input,
2172
+ inst,
2173
+ });
2174
+ return payload;
2175
+ };
2176
+ });
2145
2177
  const $ZodUnknown$1 = /*@__PURE__*/ $constructor$1("$ZodUnknown", (inst, def) => {
2146
2178
  $ZodType$1.init(inst, def);
2147
2179
  inst._zod.parse = (payload) => payload;
@@ -2158,6 +2190,31 @@ const $ZodNever$1 = /*@__PURE__*/ $constructor$1("$ZodNever", (inst, def) => {
2158
2190
  return payload;
2159
2191
  };
2160
2192
  });
2193
+ // an oversized container fails its leading size checks before any element is parsed
2194
+ function capSize(inst, size) {
2195
+ const caps = sizeCaps(inst._zod.def.checks);
2196
+ if (!caps.length)
2197
+ return;
2198
+ const cap = Math.min(...caps.map((c) => c[0]));
2199
+ const parse = inst._zod.parse;
2200
+ inst._zod.parse = (payload, ctx) => {
2201
+ const n = size(payload.value);
2202
+ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
2203
+ for (const [max, ch] of sizeCaps(inst._zod.def.checks)) {
2204
+ if (n > max && ch._zod.def.when(payload)) {
2205
+ const start = payload.issues.length;
2206
+ ch._zod.check(payload);
2207
+ // the value is still the raw input, so no later check may read it
2208
+ payload.issues[start].continue = false;
2209
+ attachSchema(payload.issues, start, inst);
2210
+ return payload;
2211
+ }
2212
+ }
2213
+ }
2214
+ return parse(payload, ctx);
2215
+ };
2216
+ }
2217
+ const arraySize = (input) => (Array.isArray(input) ? input.length : -1);
2161
2218
  function handleArrayResult$1(result, final, index) {
2162
2219
  if (result.issues.length) {
2163
2220
  final.issues.push(...prefixIssues$1(index, result.issues));
@@ -2203,6 +2260,7 @@ const $ZodArray$1 = /*@__PURE__*/ $constructor$1("$ZodArray", (inst, def) => {
2203
2260
  }
2204
2261
  return payload; //handleArrayResultsAsync(parseResults, final);
2205
2262
  };
2263
+ capSize(inst, arraySize);
2206
2264
  });
2207
2265
  function handlePropertyResult$1(result, final, key, input, optin, optout) {
2208
2266
  const isPresent = key in input;
@@ -2628,7 +2686,7 @@ function mergeValues$1(a, b) {
2628
2686
  if (a instanceof Date && b instanceof Date && +a === +b) {
2629
2687
  return { valid: true, data: a };
2630
2688
  }
2631
- if (isPlainObject$1(a) && isPlainObject$1(b)) {
2689
+ if (isPlainObject$2(a) && isPlainObject$2(b)) {
2632
2690
  const bKeys = Object.keys(b);
2633
2691
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
2634
2692
  const newObj = { ...a, ...b };
@@ -2730,7 +2788,7 @@ const $ZodRecord$1 = /*@__PURE__*/ $constructor$1("$ZodRecord", (inst, def) => {
2730
2788
  memo?.attach(inst);
2731
2789
  inst._zod.parse = (payload, ctx) => {
2732
2790
  const input = payload.value;
2733
- if (!isPlainObject$1(input)) {
2791
+ if (!isPlainObject$2(input)) {
2734
2792
  payload.issues.push({
2735
2793
  expected: "record",
2736
2794
  code: "invalid_type",
@@ -3166,6 +3224,60 @@ function handlePipeResult$1(left, next, ctx) {
3166
3224
  }
3167
3225
  return next._zod.run({ value: left.value, issues: left.issues }, ctx);
3168
3226
  }
3227
+ const $ZodCodec = /*@__PURE__*/ $constructor$1("$ZodCodec", (inst, def) => {
3228
+ $ZodType$1.init(inst, def);
3229
+ defineLazyInternal(inst, "values", (zod) => zod.def.in._zod.values);
3230
+ defineLazyInternal(inst, "optin", (zod) => zod.def.in._zod.optin);
3231
+ defineLazyInternal(inst, "optout", (zod) => zod.def.out._zod.optout);
3232
+ defineLazyInternal(inst, "propValues", (zod) => zod.def.in._zod.propValues);
3233
+ inst._zod.parse = (payload, ctx) => {
3234
+ const direction = ctx.direction || "forward";
3235
+ if (direction === "forward") {
3236
+ const left = def.in._zod.run(payload, ctx);
3237
+ if (left instanceof Promise) {
3238
+ return left.then((left) => handleCodecAResult(left, def, ctx));
3239
+ }
3240
+ return handleCodecAResult(left, def, ctx);
3241
+ }
3242
+ else {
3243
+ const right = def.out._zod.run(payload, ctx);
3244
+ if (right instanceof Promise) {
3245
+ return right.then((right) => handleCodecAResult(right, def, ctx));
3246
+ }
3247
+ return handleCodecAResult(right, def, ctx);
3248
+ }
3249
+ };
3250
+ });
3251
+ function handleCodecAResult(result, def, ctx) {
3252
+ if (result.issues.length) {
3253
+ // prevent further checks
3254
+ result.aborted = true;
3255
+ return result;
3256
+ }
3257
+ const direction = ctx.direction || "forward";
3258
+ if (direction === "forward") {
3259
+ const transformed = def.transform(result.value, result);
3260
+ if (transformed instanceof Promise) {
3261
+ return transformed.then((value) => handleCodecTxResult(result, value, def.out, ctx));
3262
+ }
3263
+ return handleCodecTxResult(result, transformed, def.out, ctx);
3264
+ }
3265
+ else {
3266
+ const transformed = def.reverseTransform(result.value, result);
3267
+ if (transformed instanceof Promise) {
3268
+ return transformed.then((value) => handleCodecTxResult(result, value, def.in, ctx));
3269
+ }
3270
+ return handleCodecTxResult(result, transformed, def.in, ctx);
3271
+ }
3272
+ }
3273
+ function handleCodecTxResult(left, value, nextSchema, ctx) {
3274
+ // Check if transform added any issues
3275
+ if (left.issues.length) {
3276
+ left.aborted = true;
3277
+ return left;
3278
+ }
3279
+ return nextSchema._zod.run({ value, issues: left.issues }, ctx);
3280
+ }
3169
3281
  const $ZodReadonly$1 = /*@__PURE__*/ $constructor$1("$ZodReadonly", (inst, def) => {
3170
3282
  $ZodType$1.init(inst, def);
3171
3283
  defineLazyInternal(inst, "propValues", (zod) => zod.def.innerType._zod.propValues);
@@ -3994,6 +4106,13 @@ function _boolean$1(Class, params) {
3994
4106
  });
3995
4107
  }
3996
4108
  // @__NO_SIDE_EFFECTS__
4109
+ function _null$4(Class, params) {
4110
+ return new Class({
4111
+ type: "null",
4112
+ ...normalizeParams$1(params),
4113
+ });
4114
+ }
4115
+ // @__NO_SIDE_EFFECTS__
3997
4116
  function _unknown$1(Class) {
3998
4117
  return new Class({
3999
4118
  type: "unknown",
@@ -4214,6 +4333,63 @@ function _check$1(fn, params) {
4214
4333
  ch._zod.check = fn;
4215
4334
  return ch;
4216
4335
  }
4336
+ // @__NO_SIDE_EFFECTS__
4337
+ function _stringbool(Classes, _params) {
4338
+ const params = normalizeParams$1(_params);
4339
+ let truthyArray = params.truthy ?? ["true", "1", "yes", "on", "y", "enabled"];
4340
+ let falsyArray = params.falsy ?? ["false", "0", "no", "off", "n", "disabled"];
4341
+ if (params.case !== "sensitive") {
4342
+ truthyArray = truthyArray.map((v) => (typeof v === "string" ? v.toLowerCase() : v));
4343
+ falsyArray = falsyArray.map((v) => (typeof v === "string" ? v.toLowerCase() : v));
4344
+ }
4345
+ const truthySet = new Set(truthyArray);
4346
+ const falsySet = new Set(falsyArray);
4347
+ const _Codec = Classes.Codec ?? $ZodCodec;
4348
+ const _Boolean = Classes.Boolean ?? $ZodBoolean$1;
4349
+ const _String = Classes.String ?? $ZodString$1;
4350
+ const stringSchema = new _String({ type: "string", error: params.error });
4351
+ const booleanSchema = new _Boolean({ type: "boolean", error: params.error });
4352
+ const codec = new _Codec({
4353
+ type: "pipe",
4354
+ in: stringSchema,
4355
+ out: booleanSchema,
4356
+ transform: ((input, payload) => {
4357
+ let data = input;
4358
+ if (params.case !== "sensitive")
4359
+ data = data.toLowerCase();
4360
+ if (truthySet.has(data)) {
4361
+ return true;
4362
+ }
4363
+ else if (falsySet.has(data)) {
4364
+ return false;
4365
+ }
4366
+ else {
4367
+ payload.issues.push({
4368
+ code: "invalid_value",
4369
+ expected: "stringbool",
4370
+ values: [...truthySet, ...falsySet],
4371
+ input: payload.value,
4372
+ inst: codec,
4373
+ continue: false,
4374
+ });
4375
+ return {};
4376
+ }
4377
+ }),
4378
+ reverseTransform: ((input, _payload) => {
4379
+ if (input === true) {
4380
+ return truthyArray[0] || "true";
4381
+ }
4382
+ else {
4383
+ return falsyArray[0] || "false";
4384
+ }
4385
+ }),
4386
+ error: params.error,
4387
+ });
4388
+ codec._zod.bag.truthy = truthyArray;
4389
+ codec._zod.bag.falsy = falsyArray;
4390
+ codec._zod.bag.case = params.case ?? "insensitive";
4391
+ return codec;
4392
+ }
4217
4393
 
4218
4394
  function assignProps(target, ...sources) {
4219
4395
  for (const source of sources) {
@@ -5053,6 +5229,16 @@ const numberProcessor$1 = (schema, ctx, _json, params) => {
5053
5229
  const booleanProcessor$1 = (_schema, _ctx, json, _params) => {
5054
5230
  json.type = "boolean";
5055
5231
  };
5232
+ const nullProcessor$1 = (_schema, ctx, json, _params) => {
5233
+ if (ctx.target === "openapi-3.0") {
5234
+ json.type = "string";
5235
+ json.nullable = true;
5236
+ json.enum = [null];
5237
+ }
5238
+ else {
5239
+ json.type = "null";
5240
+ }
5241
+ };
5056
5242
  const neverProcessor$1 = (_schema, _ctx, json, _params) => {
5057
5243
  json.not = {};
5058
5244
  };
@@ -6063,6 +6249,14 @@ const ZodBoolean$1 = /*@__PURE__*/ $constructor$1("ZodBoolean", (inst, def) => {
6063
6249
  function boolean$2(params) {
6064
6250
  return _boolean$1(ZodBoolean$1, params);
6065
6251
  }
6252
+ const ZodNull$1 = /*@__PURE__*/ $constructor$1("ZodNull", (inst, def) => {
6253
+ $ZodNull$1.init(inst, def);
6254
+ ZodType$1.init(inst, def);
6255
+ inst._zod.processJSONSchema = (ctx, json, params) => nullProcessor$1(inst, ctx, json);
6256
+ });
6257
+ function _null$3(params) {
6258
+ return _null$4(ZodNull$1, params);
6259
+ }
6066
6260
  const ZodUnknown$1 = /*@__PURE__*/ $constructor$1("ZodUnknown", (inst, def) => {
6067
6261
  $ZodUnknown$1.init(inst, def);
6068
6262
  ZodType$1.init(inst, def);
@@ -6164,6 +6358,24 @@ function object$1(shape, params) {
6164
6358
  };
6165
6359
  return new ZodObject$1(def);
6166
6360
  }
6361
+ // strictObject
6362
+ function strictObject(shape, params) {
6363
+ return new ZodObject$1({
6364
+ type: "object",
6365
+ shape,
6366
+ catchall: never$1(),
6367
+ ...normalizeParams$1(params),
6368
+ });
6369
+ }
6370
+ // looseObject
6371
+ function looseObject(shape, params) {
6372
+ return new ZodObject$1({
6373
+ type: "object",
6374
+ shape,
6375
+ catchall: unknown$1(),
6376
+ ...normalizeParams$1(params),
6377
+ });
6378
+ }
6167
6379
  const ZodUnion$1 = /*@__PURE__*/ $constructor$1("ZodUnion", (inst, def) => {
6168
6380
  $ZodUnion$1.init(inst, def);
6169
6381
  ZodType$1.init(inst, def);
@@ -6436,6 +6648,10 @@ function pipe$1(in_, out) {
6436
6648
  // ...util.normalizeParams(params),
6437
6649
  });
6438
6650
  }
6651
+ const ZodCodec = /*@__PURE__*/ $constructor$1("ZodCodec", (inst, def) => {
6652
+ ZodPipe$1.init(inst, def);
6653
+ $ZodCodec.init(inst, def);
6654
+ });
6439
6655
  const ZodReadonly$1 = /*@__PURE__*/ $constructor$1("ZodReadonly", (inst, def) => {
6440
6656
  $ZodReadonly$1.init(inst, def);
6441
6657
  ZodType$1.init(inst, def);
@@ -6460,6 +6676,12 @@ function refine$1(fn, _params = {}) {
6460
6676
  function superRefine$1(fn, params) {
6461
6677
  return _superRefine$1(fn, params);
6462
6678
  }
6679
+ // stringbool
6680
+ const stringbool = (...args) => _stringbool({
6681
+ Codec: ZodCodec,
6682
+ Boolean: ZodBoolean$1,
6683
+ String: ZodString$1,
6684
+ }, ...args);
6463
6685
 
6464
6686
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
6465
6687
 
@@ -23900,7 +24122,7 @@ const allowsEval = cached(() => {
23900
24122
  return false;
23901
24123
  }
23902
24124
  });
23903
- function isPlainObject(o) {
24125
+ function isPlainObject$1(o) {
23904
24126
  if (isObject(o) === false)
23905
24127
  return false;
23906
24128
  // modified constructor
@@ -23920,7 +24142,7 @@ function isPlainObject(o) {
23920
24142
  return true;
23921
24143
  }
23922
24144
  function shallowClone(o) {
23923
- if (isPlainObject(o))
24145
+ if (isPlainObject$1(o))
23924
24146
  return { ...o };
23925
24147
  if (Array.isArray(o))
23926
24148
  return [...o];
@@ -24016,7 +24238,7 @@ function omit(schema, mask) {
24016
24238
  return clone(schema, def);
24017
24239
  }
24018
24240
  function extend(schema, shape) {
24019
- if (!isPlainObject(shape)) {
24241
+ if (!isPlainObject$1(shape)) {
24020
24242
  throw new Error("Invalid input to extend: expected a plain object");
24021
24243
  }
24022
24244
  const checks = schema._zod.def.checks;
@@ -24041,7 +24263,7 @@ function extend(schema, shape) {
24041
24263
  return clone(schema, def);
24042
24264
  }
24043
24265
  function safeExtend(schema, shape) {
24044
- if (!isPlainObject(shape)) {
24266
+ if (!isPlainObject$1(shape)) {
24045
24267
  throw new Error("Invalid input to safeExtend: expected a plain object");
24046
24268
  }
24047
24269
  const def = mergeDefs(schema._zod.def, {
@@ -25782,7 +26004,7 @@ function mergeValues(a, b) {
25782
26004
  if (a instanceof Date && b instanceof Date && +a === +b) {
25783
26005
  return { valid: true, data: a };
25784
26006
  }
25785
- if (isPlainObject(a) && isPlainObject(b)) {
26007
+ if (isPlainObject$1(a) && isPlainObject$1(b)) {
25786
26008
  const bKeys = Object.keys(b);
25787
26009
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
25788
26010
  const newObj = { ...a, ...b };
@@ -25866,7 +26088,7 @@ const $ZodRecord = /*@__PURE__*/ $constructor("$ZodRecord", (inst, def) => {
25866
26088
  $ZodType.init(inst, def);
25867
26089
  inst._zod.parse = (payload, ctx) => {
25868
26090
  const input = payload.value;
25869
- if (!isPlainObject(input)) {
26091
+ if (!isPlainObject$1(input)) {
25870
26092
  payload.issues.push({
25871
26093
  expected: "record",
25872
26094
  code: "invalid_type",
@@ -28439,144 +28661,871 @@ object({
28439
28661
  });
28440
28662
 
28441
28663
  /**
28442
- * @module constants
28443
- * Shared identity constants for the jeeves-watcher ecosystem.
28444
- * Consumed by both the service and plugin packages.
28664
+ * @module schemas/sources
28665
+ * Zod schemas for configured `sources` (named callback endpoints for non-file documents), source references and the callback contract. No I/O.
28445
28666
  */
28446
- /** Component name shared across service and plugin descriptors. */
28447
- const COMPONENT_NAME = 'watcher';
28448
- /** npm package name for the service. */
28449
- const SERVICE_PACKAGE = '@karmaniverous/jeeves-watcher';
28450
- /** npm package name for the plugin. */
28451
- const PLUGIN_PACKAGE = '@karmaniverous/jeeves-watcher-openclaw';
28452
- /** Default watcher API port. */
28453
- const DEFAULT_PORT = DEFAULT_PORTS.watcher;
28454
- /** Default API values. */
28455
- ({
28456
- port: DEFAULT_PORTS.watcher});
28667
+ /**
28668
+ * Body of a source name pattern (shared by the name and reference
28669
+ * patterns): a lowercase letter, then 1-31 of `[a-z0-9-]`.
28670
+ */
28671
+ const SOURCE_NAME_BODY = '[a-z][a-z0-9-]{1,31}';
28672
+ /** Pattern every source name must match (≥ 2 chars, so never a drive letter). */
28673
+ const SOURCE_NAME_PATTERN = new RegExp(`^${SOURCE_NAME_BODY}$`);
28674
+ /**
28675
+ * Pattern of a source reference: `<name>://<id>`, where `<id>` is 1–512
28676
+ * non-whitespace characters (`/`-separated segments allowed).
28677
+ */
28678
+ const SOURCE_REFERENCE_PATTERN = new RegExp(`^${SOURCE_NAME_BODY}://\\S{1,512}$`);
28679
+ /** Placeholder in a source `url` template that is replaced with the document id. */
28680
+ const SOURCE_ID_PLACEHOLDER = '{id}';
28681
+ /** A source name, e.g. `app`. */
28682
+ const sourceNameSchema = string$2()
28683
+ .regex(SOURCE_NAME_PATTERN, `Source name must match ${SOURCE_NAME_PATTERN.source}`)
28684
+ .describe('Source name: lowercase letter, then 1-31 of [a-z0-9-].');
28685
+ /** A source reference, e.g. `app://chat/thread/123`. */
28686
+ const sourceReferenceSchema = string$2()
28687
+ .regex(SOURCE_REFERENCE_PATTERN, `Source reference must match <name>://<id> (name ${SOURCE_NAME_PATTERN.source}, id 1-512 non-whitespace characters)`)
28688
+ .describe('Source document reference: <name>://<id>.');
28689
+ /** Configuration of a single named source. */
28690
+ const sourceConfigSchema = object$1({
28691
+ /** Callback URL template; must contain `{id}`. */
28692
+ url: string$2()
28693
+ .min(1)
28694
+ .refine((url) => url.includes(SOURCE_ID_PLACEHOLDER), {
28695
+ message: `url must contain the ${SOURCE_ID_PLACEHOLDER} placeholder`,
28696
+ })
28697
+ .describe('Callback URL template. {id} is replaced with the document id (each /-segment URL-encoded, slashes kept). Supports ${ENV_VAR} substitution.'),
28698
+ /** Request headers sent with every callback (e.g. Authorization). */
28699
+ headers: record$1(string$2(), string$2())
28700
+ .optional()
28701
+ .describe('Headers sent with every callback request (supports ${ENV_VAR} substitution).'),
28702
+ /** Callback timeout in milliseconds. */
28703
+ timeoutMs: number$2()
28704
+ .int()
28705
+ .positive()
28706
+ .default(10000)
28707
+ .describe('Callback timeout in milliseconds. Default: 10000.'),
28708
+ /** Maximum parallel callbacks for this source. */
28709
+ concurrency: number$2()
28710
+ .int()
28711
+ .positive()
28712
+ .default(8)
28713
+ .describe('Maximum parallel callbacks for this source (e.g. during reindex). Default: 8.'),
28714
+ });
28715
+ /** The `sources` config section: source name → source configuration. */
28716
+ const sourcesConfigSchema = record$1(sourceNameSchema, sourceConfigSchema)
28717
+ .describe('Named sources for non-file documents, retrieved by callback. Keys are source names used in <name>://<id> references. Read at startup only.');
28718
+ /**
28719
+ * A source document as returned by a source callback: a `GET` to the
28720
+ * configured URL answered with `200` and an `application/json` body. The body
28721
+ * is the record itself, as the owner stores it, with no envelope: any JSON
28722
+ * object. A `404` means the document no longer exists (see
28723
+ * {@link SOURCE_DOCUMENT_GONE_STATUS}); any other status is a transient failure.
28724
+ */
28725
+ record$1(string$2(), unknown$1())
28726
+ .describe('A source document: any JSON object returned by the callback.');
28457
28727
 
28458
28728
  /**
28459
- * Shared endpoint catalog — single source of truth for the jeeves-watcher API.
28729
+ * @module api/common
28730
+ * Shared Zod building blocks for API request/response schemas: Qdrant filters, error bodies and path-or-source document targets. No I/O.
28731
+ */
28732
+ /**
28733
+ * A native Qdrant filter object schema (passed through to Qdrant
28734
+ * unchanged), optionally with the message reported for a missing or
28735
+ * non-object value.
28460
28736
  *
28461
- * Both the CLI service and the OpenClaw plugin derive their registrations
28462
- * from this declarative catalog, eliminating drift between the two.
28737
+ * @param error - Message for a missing or non-object filter.
28738
+ * @returns The filter schema.
28739
+ */
28740
+ const qdrantFilter = (error) => record$1(string$2(), unknown$1(), error === undefined ? undefined : { error })
28741
+ .describe('Native Qdrant filter object.');
28742
+ /** A native Qdrant filter object (passed through to Qdrant unchanged). */
28743
+ const qdrantFilterSchema = qdrantFilter();
28744
+ /** Message for a missing or non-object required `filter` (scan, points delete). */
28745
+ const FILTER_REQUIRED_MESSAGE = 'Missing required field: filter (object)';
28746
+ /** Boolean clauses of a Qdrant filter whose value is a condition list. */
28747
+ const FILTER_CLAUSES = ['must', 'should', 'must_not'];
28748
+ /** Every key a Qdrant filter object may carry (clauses plus `min_should`). */
28749
+ const FILTER_KEYS = new Set([...FILTER_CLAUSES, 'min_should']);
28750
+ /**
28751
+ * Whether a value is a plain object (not `null`, not an array). The one
28752
+ * definition shared by core and the service.
28463
28753
  *
28754
+ * @param value - Any value.
28755
+ * @returns `true` for a non-null, non-array object.
28464
28756
  */
28757
+ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
28465
28758
  /**
28466
- * Canonical endpoint catalog for the jeeves-watcher API.
28759
+ * Whether a condition restricts anything. `{}` (an empty nested filter)
28760
+ * does not; a nested filter (only clause keys) restricts when it has a
28761
+ * condition itself; anything else (field, `is_empty`, `has_id`, `nested`, …,
28762
+ * or a malformed value Qdrant will reject) counts as a condition.
28763
+ */
28764
+ function isEffectiveCondition(condition) {
28765
+ if (!isPlainObject(condition))
28766
+ return true;
28767
+ const keys = Object.keys(condition);
28768
+ if (keys.length === 0)
28769
+ return false;
28770
+ return keys.every((key) => FILTER_KEYS.has(key))
28771
+ ? hasFilterCondition(condition)
28772
+ : true;
28773
+ }
28774
+ /** Whether a clause value (a condition or a list of them) has a condition. */
28775
+ const clauseHasCondition = (value) => Array.isArray(value)
28776
+ ? value.some(isEffectiveCondition)
28777
+ : isPlainObject(value) && isEffectiveCondition(value);
28778
+ /**
28779
+ * Whether a Qdrant filter contains at least one condition. `{}`,
28780
+ * `{ must: [] }`, filters whose clauses are all empty (recursively) and
28781
+ * filters with only unknown keys are condition-free: they would match every
28782
+ * point.
28467
28783
  *
28468
- * Every entry describes a single HTTP endpoint exposed by the service.
28469
- * Route handlers, plugin tools, and HTTP clients should reference these
28470
- * descriptors rather than hard-coding paths and descriptions.
28784
+ * @param filter - A Qdrant filter object.
28785
+ * @returns True when the filter restricts the points it matches.
28471
28786
  */
28472
- const WATCHER_ENDPOINTS = [
28473
- {
28474
- name: 'status',
28475
- method: 'GET',
28476
- path: '/status',
28477
- description: 'Service health and status overview.',
28478
- },
28479
- {
28480
- name: 'metadata',
28481
- method: 'POST',
28482
- path: '/metadata',
28483
- description: 'Set or update metadata on a document by file path.',
28484
- },
28485
- {
28486
- name: 'render',
28487
- method: 'POST',
28488
- path: '/render',
28489
- description: 'Render a document through the template pipeline.',
28490
- },
28491
- {
28492
- name: 'searchFacets',
28493
- method: 'GET',
28494
- path: '/search/facets',
28495
- description: 'List available search facets and their values.',
28496
- },
28497
- {
28498
- name: 'scan',
28499
- method: 'POST',
28500
- path: '/scan',
28501
- description: 'Filter-only point query without vector search. Returns metadata for points matching a Qdrant filter.',
28502
- },
28503
- {
28504
- name: 'walk',
28505
- method: 'POST',
28506
- path: '/walk',
28507
- description: 'Walk watched filesystem paths with glob intersection. Returns matching file paths.',
28508
- },
28509
- {
28510
- name: 'search',
28511
- method: 'POST',
28512
- path: '/search',
28513
- description: 'Semantic search over indexed documents. Supports Qdrant filters.',
28514
- },
28515
- {
28516
- name: 'rebuildMetadata',
28517
- method: 'POST',
28518
- path: '/rebuild-metadata',
28519
- description: 'Rebuild metadata from enrichment store into vector points.',
28520
- },
28521
- {
28522
- name: 'reindex',
28523
- method: 'POST',
28524
- path: '/reindex',
28525
- description: 'Trigger a scoped reindex of watched files.',
28526
- },
28527
- {
28528
- name: 'issues',
28529
- method: 'GET',
28530
- path: '/issues',
28531
- description: 'Get runtime embedding failures. Shows files that failed processing and why.',
28532
- },
28533
- {
28534
- name: 'configSchema',
28535
- method: 'GET',
28536
- path: '/config/schema',
28537
- description: 'Get the JSON Schema for the configuration file.',
28538
- },
28539
- {
28540
- name: 'configMatch',
28541
- method: 'POST',
28542
- path: '/config/match',
28543
- description: 'Test file paths against config inference rules.',
28544
- },
28545
- {
28546
- name: 'config',
28547
- method: 'GET',
28548
- path: '/config',
28549
- description: 'Query service configuration with optional JSONPath.',
28550
- },
28551
- {
28552
- name: 'configValidate',
28553
- method: 'POST',
28554
- path: '/config/validate',
28555
- description: 'Validate a candidate config. Optionally test file paths against the config.',
28556
- },
28557
- {
28558
- name: 'configApply',
28559
- method: 'POST',
28560
- path: '/config/apply',
28561
- description: 'Apply a configuration patch.',
28562
- },
28563
- {
28564
- name: 'rulesRegister',
28565
- method: 'POST',
28566
- path: '/rules/register',
28567
- description: 'Register external inference rules.',
28568
- },
28569
- {
28570
- name: 'rulesUnregister',
28571
- method: 'DELETE',
28572
- path: '/rules/unregister',
28573
- description: 'Unregister external inference rules.',
28574
- },
28575
- {
28576
- name: 'rulesUnregisterBySource',
28577
- method: 'DELETE',
28578
- path: '/rules/unregister/:source',
28579
- description: 'Unregister all external inference rules from a source.',
28787
+ function hasFilterCondition(filter) {
28788
+ if (FILTER_CLAUSES.some((clause) => clauseHasCondition(filter[clause]))) {
28789
+ return true;
28790
+ }
28791
+ const minShould = filter.min_should;
28792
+ return isPlainObject(minShould) && clauseHasCondition(minShould.conditions);
28793
+ }
28794
+ /**
28795
+ * A Qdrant filter that must contain at least one condition (see
28796
+ * {@link hasFilterCondition}); used where a match-everything filter would be
28797
+ * destructive (`POST /documents/delete`).
28798
+ */
28799
+ const nonEmptyQdrantFilterSchema = qdrantFilterSchema.refine(hasFilterCondition, { message: 'filter must contain at least one condition' });
28800
+ /**
28801
+ * Error body shape common to every watcher version: `error` plus optional
28802
+ * `message` and `details`, no `code`. Responses from 0.20.0+ also carry a
28803
+ * machine-readable `code`; `apiErrorBodySchema` (module `api/errors`)
28804
+ * extends this schema with it.
28805
+ */
28806
+ const apiErrorResponseSchema = object$1({
28807
+ /** Short error description. */
28808
+ error: string$2(),
28809
+ /** Optional longer explanation. */
28810
+ message: string$2().optional(),
28811
+ /** Optional structured details (e.g. Zod issues for a 400). */
28812
+ details: unknown$1().optional(),
28813
+ });
28814
+ /**
28815
+ * Error detail attached to a per-document result or rejection (`code` is a
28816
+ * machine-readable error code, e.g. an issue type).
28817
+ */
28818
+ const documentErrorSchema = object$1({
28819
+ /** Machine-readable error code. */
28820
+ code: string$2(),
28821
+ /** Human-readable error message. */
28822
+ message: string$2(),
28823
+ /** Optional structured details (e.g. AJV errors). */
28824
+ details: unknown$1().optional(),
28825
+ });
28826
+ /** A file path target (non-empty). */
28827
+ const filePathSchema = string$2()
28828
+ .min(1, 'path must be a non-empty string')
28829
+ .describe('Absolute or watched file path.');
28830
+ /** Fields addressing a single document by file path or source reference. */
28831
+ const documentTargetShape = {
28832
+ /** File path of a file document. */
28833
+ path: filePathSchema.optional(),
28834
+ /** Source reference of a source document (`<name>://<id>`). */
28835
+ source: sourceReferenceSchema.optional(),
28836
+ };
28837
+ /**
28838
+ * Refinement requiring exactly one of `path` or `source`.
28839
+ *
28840
+ * @param val - The object being validated.
28841
+ * @param ctx - Zod refinement context.
28842
+ */
28843
+ function requireExactlyOneTarget(val, ctx) {
28844
+ if ((val.path === undefined) === (val.source === undefined)) {
28845
+ ctx.addIssue({
28846
+ code: 'custom',
28847
+ path: val.path === undefined ? ['path'] : [],
28848
+ message: 'Exactly one of path or source is required',
28849
+ });
28850
+ }
28851
+ }
28852
+
28853
+ /**
28854
+ * @module api/admin
28855
+ * Request schemas of the administrative endpoints: POST /points/delete, POST /rules/register, DELETE /rules/unregister, POST /rules/reapply. The service parses their bodies with these, like every other route. No I/O.
28856
+ */
28857
+ /** POST /points/delete request body. */
28858
+ object$1({
28859
+ /** Native Qdrant filter selecting the points to delete. */
28860
+ filter: qdrantFilter(FILTER_REQUIRED_MESSAGE),
28861
+ });
28862
+ /** A virtual-rule source name: a non-empty string. */
28863
+ const ruleSourceSchema = string$2({ error: 'Missing required field: source' })
28864
+ .min(1, { error: 'Missing required field: source' });
28865
+ const RULE_NAME_REQUIRED = 'Every rule must be an object with a name';
28866
+ /** POST /rules/register request body. */
28867
+ object$1({
28868
+ /** Registering source (e.g. a plugin name). */
28869
+ source: ruleSourceSchema,
28870
+ /** Inference rules; each needs a non-empty `name` (the rest is validated on registration). */
28871
+ rules: array$1(looseObject({
28872
+ name: string$2({ error: RULE_NAME_REQUIRED })
28873
+ .min(1, { error: RULE_NAME_REQUIRED }),
28874
+ }, { error: RULE_NAME_REQUIRED }), { error: 'Missing required field: rules (array)' }),
28875
+ });
28876
+ /** DELETE /rules/unregister request body. */
28877
+ object$1({
28878
+ /** Source whose rules are removed. */
28879
+ source: ruleSourceSchema,
28880
+ });
28881
+ /** POST /rules/reapply request body. */
28882
+ object$1({
28883
+ /** Globs selecting the indexed files to re-apply rules to. */
28884
+ globs: array$1(string$2().min(1), {
28885
+ error: 'Missing required field: globs (non-empty string array)',
28886
+ })
28887
+ .min(1, {
28888
+ error: 'Missing required field: globs (non-empty string array)',
28889
+ }),
28890
+ });
28891
+
28892
+ /**
28893
+ * @module api/documentOps
28894
+ * Zod request/response schemas for single-document operations POST /render and POST /metadata (path or source target). No I/O.
28895
+ */
28896
+ /** Request body for `POST /render`: exactly one of `path` or `source`. */
28897
+ object$1(documentTargetShape)
28898
+ .superRefine(requireExactlyOneTarget);
28899
+ /** Response body for `POST /render`. */
28900
+ object$1({
28901
+ /** Output format (file extension without dot). */
28902
+ renderAs: string$2(),
28903
+ /** Rendered (or passthrough) content. */
28904
+ content: string$2(),
28905
+ /** Names of the rules that matched. */
28906
+ rules: array$1(string$2()),
28907
+ /** Resulting metadata. */
28908
+ metadata: record$1(string$2(), unknown$1()),
28909
+ });
28910
+ /** Request body for `POST /metadata`: exactly one of `path` or `source`, plus metadata. */
28911
+ object$1({
28912
+ ...documentTargetShape,
28913
+ /** Enrichment metadata to merge into the document's points. */
28914
+ metadata: record$1(string$2(), unknown$1(), {
28915
+ error: 'Missing required field: metadata (object)',
28916
+ }),
28917
+ })
28918
+ .superRefine(requireExactlyOneTarget);
28919
+ /** Response body for `POST /metadata`. */
28920
+ object$1({
28921
+ /** Always true on success. */
28922
+ ok: literal(true),
28923
+ /** Rules whose schemas validated the metadata. */
28924
+ matched_rules: array$1(string$2()),
28925
+ });
28926
+
28927
+ /**
28928
+ * @module api/monitoring
28929
+ * Zod response schemas for the read-only monitoring endpoints GET /status and GET /issues, including the issue record. No I/O.
28930
+ */
28931
+ /** Response body for `GET /status` (standard Jeeves component status). */
28932
+ object$1({
28933
+ /** Component name (`watcher`). */
28934
+ name: string$2(),
28935
+ /** Service version. */
28936
+ version: string$2(),
28937
+ /** Whole seconds since the API server (its status handler) was created. */
28938
+ uptime: number$2(),
28939
+ /** Overall status. */
28940
+ status: _enum$1(['healthy', 'degraded', 'unhealthy']),
28941
+ /** Component health: collection, reindex and initial-scan state. */
28942
+ health: record$1(string$2(), unknown$1()),
28943
+ });
28944
+ /**
28945
+ * Canonical issue codes, in one list (spec Decision 16.4).
28946
+ *
28947
+ * `type_collision` and `interpolation_error` predate 0.20.0; the rest cover
28948
+ * processing failures (#255) and source documents.
28949
+ */
28950
+ const ISSUE_TYPES = [
28951
+ 'type_collision',
28952
+ 'interpolation_error',
28953
+ 'extraction_error',
28954
+ 'embedding_error',
28955
+ 'upsert_error',
28956
+ 'match_failed',
28957
+ 'rule_missing',
28958
+ 'no_content',
28959
+ 'source_unavailable',
28960
+ 'unsupported_content_type',
28961
+ ];
28962
+ /** Schema for the canonical issue code carried by every issue record. */
28963
+ const issueTypeSchema = _enum$1(ISSUE_TYPES)
28964
+ .describe('Issue code: type_collision (conflicting metadata types), interpolation_error (template rendering failure), extraction_error (reading, rule application or chunking failed), embedding_error (embedding provider failed), upsert_error (vector store write failed), match_failed (designated rule did not match), rule_missing (designated rule not found), no_content (nothing to embed), source_unavailable (source callback failed), unsupported_content_type (callback returned a non-JSON body).');
28965
+ /** Schema for a single issue record tracking a processing failure. */
28966
+ const issueRecordSchema = object$1({
28967
+ /** Canonical issue code. */
28968
+ type: issueTypeSchema,
28969
+ /** Operation that failed (e.g. `Failed to process file`). */
28970
+ operation: string$2().optional(),
28971
+ /** Property name where the issue occurred. */
28972
+ property: string$2().optional(),
28973
+ /** Name(s) of the rule(s) involved in the issue. */
28974
+ rules: array$1(string$2()).optional(),
28975
+ /**
28976
+ * Rule name for single-rule issues; for a source document, its designated
28977
+ * rule (so an `issues` reindex can retry a document that has no points).
28978
+ */
28979
+ rule: string$2().optional(),
28980
+ /** Declared types for type collision issues. */
28981
+ types: array$1(string$2()).optional(),
28982
+ /** Error message describing the failure. */
28983
+ message: string$2(),
28984
+ /** Optional structured details (e.g. the AJV errors of `match_failed`); JSON. */
28985
+ details: unknown$1().optional(),
28986
+ /** Unix timestamp (seconds) or ISO string of the last occurrence. */
28987
+ timestamp: union$1([number$2(), string$2()]),
28988
+ });
28989
+ /** Response body for `GET /issues`: issue records keyed by document key. */
28990
+ object$1({
28991
+ /** Number of documents with issues. */
28992
+ count: number$2().int().nonnegative(),
28993
+ /** Issue records keyed by document key (file path or source reference). */
28994
+ issues: record$1(string$2(), array$1(issueRecordSchema)),
28995
+ });
28996
+
28997
+ /**
28998
+ * @module api/documents
28999
+ * Zod request/response schemas for source-document notification (POST /documents, 202 or ?wait=true) and deletion (POST /documents/delete). No I/O.
29000
+ */
29001
+ /** Maximum number of documents in one `POST /documents` request. */
29002
+ const MAX_DOCUMENTS_PER_REQUEST = 100;
29003
+ /** Maximum number of references in one `POST /documents/delete` request. */
29004
+ const MAX_DELETE_SOURCES_PER_REQUEST = 1000;
29005
+ /**
29006
+ * The `documents` array of a notify request: 1 to
29007
+ * {@link MAX_DOCUMENTS_PER_REQUEST} items of the given schema.
29008
+ *
29009
+ * @param item - Schema of one document.
29010
+ * @returns The bounded array schema.
29011
+ */
29012
+ const documentsArray = (item) => array$1(item)
29013
+ .min(1, 'documents must contain at least one entry')
29014
+ .max(MAX_DOCUMENTS_PER_REQUEST, `documents must contain at most ${String(MAX_DOCUMENTS_PER_REQUEST)} entries`);
29015
+ /**
29016
+ * A document notification: the reference of a changed source document and
29017
+ * the rule that indexes it. Notify only: content is always retrieved through
29018
+ * the source callback, so unknown keys (e.g. inline content) are rejected.
29019
+ */
29020
+ const documentNotificationSchema = strictObject({
29021
+ /** Source reference (`<name>://<id>`). */
29022
+ source: sourceReferenceSchema,
29023
+ /** Name of the inference rule that indexes the document. */
29024
+ rule: string$2().min(1, 'rule must be a non-empty string'),
29025
+ });
29026
+ /** Request body for `POST /documents`. */
29027
+ object$1({
29028
+ /** Documents to (re)index: 1 to {@link MAX_DOCUMENTS_PER_REQUEST}. */
29029
+ documents: documentsArray(documentNotificationSchema),
29030
+ });
29031
+ /**
29032
+ * Envelope the service parses `POST /documents` with: the shape of
29033
+ * {@link documentsRequestSchema}, but `source` is any non-empty string, so a
29034
+ * malformed reference rejects only its own document (`invalid_reference` in
29035
+ * `rejected`) instead of the whole request. Shape errors (count, missing or
29036
+ * empty `rule`, unknown keys) still fail the request with `400`.
29037
+ */
29038
+ object$1({
29039
+ /** Documents to (re)index: 1 to {@link MAX_DOCUMENTS_PER_REQUEST}. */
29040
+ documents: documentsArray(documentNotificationSchema.extend({
29041
+ /** Reference as submitted; validated per document. */
29042
+ source: string$2().min(1, 'source must be a non-empty string'),
29043
+ })),
29044
+ });
29045
+ /**
29046
+ * Codes of notifications rejected by synchronous validation:
29047
+ * `invalid_reference` (not `<name>://<id>`), `unknown_source` (name not in
29048
+ * config `sources`), `invalid_id` (empty, `.` or `..` id segment),
29049
+ * `rule_missing` (no rule of that name in the merged rule set) and
29050
+ * `queue_stopped` (the service is shutting down).
29051
+ */
29052
+ const documentRejectionCodeSchema = _enum$1([
29053
+ 'invalid_reference',
29054
+ 'unknown_source',
29055
+ 'invalid_id',
29056
+ 'rule_missing',
29057
+ 'queue_stopped',
29058
+ ]);
29059
+ /**
29060
+ * Query string for `POST /documents`. `wait` accepts a boolean or the strings
29061
+ * `true`/`false`/`1`/`0`.
29062
+ */
29063
+ object$1({
29064
+ /** Process synchronously and return per-document results. */
29065
+ wait: union$1([
29066
+ boolean$2(),
29067
+ stringbool({ truthy: ['true', '1'], falsy: ['false', '0'] }),
29068
+ ])
29069
+ .optional(),
29070
+ });
29071
+ /** Why a notification was rejected: a typed code plus a message. */
29072
+ const documentRejectionErrorSchema = documentErrorSchema
29073
+ .omit({ details: true })
29074
+ .extend({
29075
+ /** Rejection code. */
29076
+ code: documentRejectionCodeSchema,
29077
+ });
29078
+ /** A notification rejected by synchronous validation. */
29079
+ const documentRejectionSchema = object$1({
29080
+ /** The reference as submitted. */
29081
+ source: string$2(),
29082
+ /** Why it was rejected. */
29083
+ error: documentRejectionErrorSchema,
29084
+ });
29085
+ /** `202` response body for `POST /documents` (asynchronous, the default). */
29086
+ object$1({
29087
+ /** References enqueued for processing. */
29088
+ accepted: array$1(string$2()),
29089
+ /** Notifications rejected by synchronous validation. */
29090
+ rejected: array$1(documentRejectionSchema),
29091
+ });
29092
+ /** Outcome of processing one document synchronously. */
29093
+ const documentResultStatusSchema = _enum$1([
29094
+ 'indexed',
29095
+ 'unchanged',
29096
+ 'deleted',
29097
+ 'failed',
29098
+ ]);
29099
+ /**
29100
+ * Code of a failed per-document result: the issue type of a processing
29101
+ * failure, or (with `?wait=true`) the rejection code of a notification that
29102
+ * failed synchronous validation.
29103
+ */
29104
+ const documentFailureCodeSchema = union$1([
29105
+ issueTypeSchema,
29106
+ documentRejectionCodeSchema,
29107
+ ]);
29108
+ /** Failure detail of a per-document result: typed code, message, optional details. */
29109
+ const documentFailureSchema = documentErrorSchema.extend({
29110
+ /** Issue type or rejection code. */
29111
+ code: documentFailureCodeSchema,
29112
+ });
29113
+ /** Per-document result of `POST /documents?wait=true`. */
29114
+ const documentResultSchema = object$1({
29115
+ /** The reference processed. */
29116
+ source: string$2(),
29117
+ /** Outcome. */
29118
+ status: documentResultStatusSchema,
29119
+ /** Number of chunks written (when indexed). */
29120
+ chunks: number$2().int().nonnegative().optional(),
29121
+ /** Rules that matched (when indexed or unchanged). */
29122
+ matched_rules: array$1(string$2()).optional(),
29123
+ /** Failure detail (when failed). */
29124
+ error: documentFailureSchema.optional(),
29125
+ });
29126
+ /** `200` response body for `POST /documents?wait=true`. */
29127
+ object$1({
29128
+ /** One result per submitted document. */
29129
+ results: array$1(documentResultSchema),
29130
+ });
29131
+ /**
29132
+ * Request body for `POST /documents/delete`: either explicit references or a
29133
+ * Qdrant filter with at least one condition (a condition-free filter such as
29134
+ * `{}` or `{ must: [] }` is rejected). The server constrains the filter to
29135
+ * points carrying `source` and no `file_path`.
29136
+ */
29137
+ union$1([
29138
+ strictObject({
29139
+ /** References of the documents to delete: 1 to {@link MAX_DELETE_SOURCES_PER_REQUEST}. */
29140
+ sources: array$1(sourceReferenceSchema)
29141
+ .min(1, 'sources must contain at least one reference')
29142
+ .max(MAX_DELETE_SOURCES_PER_REQUEST, `sources must contain at most ${String(MAX_DELETE_SOURCES_PER_REQUEST)} references`),
29143
+ }),
29144
+ strictObject({
29145
+ /** Qdrant filter selecting the documents to delete (at least one condition). */
29146
+ filter: nonEmptyQdrantFilterSchema,
29147
+ }),
29148
+ ]);
29149
+ /** Response body for `POST /documents/delete`. */
29150
+ object$1({
29151
+ /** Number of documents (not points) removed. */
29152
+ deleted: number$2().int().nonnegative(),
29153
+ });
29154
+
29155
+ /**
29156
+ * @module api/errors
29157
+ * Canonical machine-readable API error codes (`apiErrorCodeSchema`) and the error body every non-2xx watcher response carries (`apiErrorBodySchema`). No I/O.
29158
+ */
29159
+ /**
29160
+ * Every machine-readable code a non-2xx response can carry, in one list.
29161
+ * Codes are stable: new codes may be added, existing ones never change
29162
+ * meaning. The usual HTTP status is in {@link API_ERROR_STATUS}.
29163
+ *
29164
+ * - `invalid_request` (400): the body, query or parameters failed validation
29165
+ * (`details` holds the Zod issues when available), or the framework rejected
29166
+ * the request (malformed JSON, unsupported media type, body too large).
29167
+ * - `unauthorized` (401): `api.apiKey` is set and the bearer token is missing
29168
+ * or wrong.
29169
+ * - `path_not_watched` (403): the file path is outside the watched scope.
29170
+ * - `not_found` (404): no such route, file, VCS root, commit content or
29171
+ * `.gitignore` entry.
29172
+ * - `source_not_indexed` (404): no stored point for the source reference, so
29173
+ * no designated rule is known.
29174
+ * - `source_not_found` (404): the source callback answered `404`.
29175
+ * - `unsupported_content_type` (422): the source callback returned a non-JSON
29176
+ * body.
29177
+ * - `pipeline_error` (422): rendering or extraction failed; for a source
29178
+ * document `issueType` names the failing stage.
29179
+ * - `source_unavailable` (502): the source callback failed.
29180
+ * - `not_available` (503): the feature is not available on this instance
29181
+ * (no source support, no filesystem watcher). A prune reindex
29182
+ * (`POST /reindex`) without a vector store keeps its 0.19 status `400`.
29183
+ * - `scan_in_progress` (503): the initial filesystem scan is still running;
29184
+ * retry later.
29185
+ * - `embedding_unavailable` (503): the embedding provider's retry budget ran
29186
+ * out; honour `Retry-After`.
29187
+ * - `internal_error` (500): an unexpected server-side failure.
29188
+ */
29189
+ const API_ERROR_CODES = [
29190
+ 'invalid_request',
29191
+ 'unauthorized',
29192
+ 'path_not_watched',
29193
+ 'not_found',
29194
+ 'source_not_indexed',
29195
+ 'source_not_found',
29196
+ 'unsupported_content_type',
29197
+ 'pipeline_error',
29198
+ 'source_unavailable',
29199
+ 'not_available',
29200
+ 'scan_in_progress',
29201
+ 'embedding_unavailable',
29202
+ 'internal_error',
29203
+ ];
29204
+ /** Schema for the machine-readable code of an API error response. */
29205
+ const apiErrorCodeSchema = _enum$1(API_ERROR_CODES)
29206
+ .describe('Machine-readable API error code.');
29207
+ /**
29208
+ * Body of every non-2xx watcher response (0.20.0+): the version-independent
29209
+ * `apiErrorResponseSchema` (`error`, `message`, `details`) extended with
29210
+ * `code` and `issueType`. `error` is the human-readable text earlier
29211
+ * versions sent, kept unchanged for backward compatibility; branch on
29212
+ * `code`, never on `error`. Endpoints may add further fields (e.g. `issues`
29213
+ * on `POST /config/apply`).
29214
+ */
29215
+ apiErrorResponseSchema.extend({
29216
+ /** Machine-readable error code. */
29217
+ code: apiErrorCodeSchema,
29218
+ /** Issue type of the pipeline failure behind a source render error. */
29219
+ issueType: issueTypeSchema.optional(),
29220
+ });
29221
+ /** Largest `POST /scan` page size. */
29222
+ const SCAN_MAX_LIMIT = 1000;
29223
+ /**
29224
+ * Request body for `POST /search`. Unknown keys are rejected, so an
29225
+ * unsupported option (e.g. `collapse`) fails with `400` instead of being
29226
+ * silently ignored.
29227
+ */
29228
+ strictObject({
29229
+ /** Natural-language query text to embed. */
29230
+ query: string$2().min(1, 'query must be a non-empty string'),
29231
+ /** Maximum results to return. Default: {@link SEARCH_DEFAULT_LIMIT}. */
29232
+ limit: number$2().int().positive().optional(),
29233
+ /** Number of results to skip (pagination). */
29234
+ offset: number$2().int().nonnegative().optional(),
29235
+ /** Optional Qdrant filter. */
29236
+ filter: qdrantFilterSchema.optional(),
29237
+ });
29238
+ /** A single search hit. */
29239
+ object$1({
29240
+ /** Point ID. */
29241
+ id: string$2(),
29242
+ /** Similarity (or fused) score. */
29243
+ score: number$2(),
29244
+ /** Point payload (system fields plus rule metadata). */
29245
+ payload: record$1(string$2(), unknown$1()),
29246
+ });
29247
+ const SCAN_LIMIT_MESSAGE = `limit must be between 1 and ${String(SCAN_MAX_LIMIT)}`;
29248
+ /** Request body for `POST /scan`. */
29249
+ object$1({
29250
+ /** Qdrant filter (required). */
29251
+ filter: qdrantFilter(FILTER_REQUIRED_MESSAGE),
29252
+ /** Page size (1 to {@link SCAN_MAX_LIMIT}). Default: {@link SCAN_DEFAULT_LIMIT}. */
29253
+ limit: number$2({ error: SCAN_LIMIT_MESSAGE })
29254
+ .int('limit must be an integer')
29255
+ .min(1, SCAN_LIMIT_MESSAGE)
29256
+ .max(SCAN_MAX_LIMIT, SCAN_LIMIT_MESSAGE)
29257
+ .optional(),
29258
+ /** Opaque cursor from a previous page. */
29259
+ cursor: union$1([string$2(), number$2()]).optional(),
29260
+ /** Payload fields to return (projection). */
29261
+ fields: array$1(string$2()).optional(),
29262
+ /** Return only `{ count }`. */
29263
+ countOnly: boolean$2().optional(),
29264
+ });
29265
+ /** A scrolled point (one entry of the `POST /scan` `points` page). */
29266
+ const scanPointSchema = object$1({
29267
+ /** Point ID. */
29268
+ id: string$2(),
29269
+ /** Point payload (projected when `fields` is given). */
29270
+ payload: record$1(string$2(), unknown$1()),
29271
+ });
29272
+ /** Response body for `POST /scan`: a page of points, or a count when `countOnly`. */
29273
+ union$1([
29274
+ object$1({
29275
+ /** Matched points. */
29276
+ points: array$1(scanPointSchema),
29277
+ /** Cursor for the next page, or null when exhausted. */
29278
+ cursor: union$1([string$2(), number$2(), _null$3()]),
29279
+ }),
29280
+ object$1({
29281
+ /** Number of matching points. */
29282
+ count: number$2().int().nonnegative(),
29283
+ }),
29284
+ ]);
29285
+
29286
+ /**
29287
+ * @module api/reindex
29288
+ * Zod request/response schemas for POST /reindex (scopes issues, full, rules, path, prune; path scope targets file paths or source references). No I/O.
29289
+ */
29290
+ /** Reindex scopes, in documentation order. */
29291
+ const reindexScopeSchema = _enum$1(['issues', 'full', 'rules', 'path', 'prune'], {
29292
+ error: 'Scope must be one of: issues, full, rules, path, prune',
29293
+ })
29294
+ .describe('Reindex scope.');
29295
+ /**
29296
+ * Scopes `configWatch.reindex` may trigger on a config change: `issues`,
29297
+ * `full` and `rules` (`path` needs targets; auto-pruning is never
29298
+ * allowed).
29299
+ */
29300
+ const configWatchReindexScopeSchema = reindexScopeSchema.extract(['issues', 'full', 'rules'], { error: 'configWatch.reindex must be one of: issues, full, rules' });
29301
+ /** One or more file paths (a non-empty array or a single path). */
29302
+ const pathTargetsSchema = union$1([
29303
+ filePathSchema,
29304
+ array$1(filePathSchema).min(1, 'path must not be an empty array'),
29305
+ ]);
29306
+ /** Maximum number of source references in one `POST /reindex` request. */
29307
+ const MAX_REINDEX_SOURCES_PER_REQUEST = 1000;
29308
+ /**
29309
+ * One or more source references (a single reference, or an array of 1 to
29310
+ * {@link MAX_REINDEX_SOURCES_PER_REQUEST}).
29311
+ */
29312
+ const sourceTargetsSchema = union$1([
29313
+ sourceReferenceSchema,
29314
+ array$1(sourceReferenceSchema)
29315
+ .min(1, 'source must not be an empty array')
29316
+ .max(MAX_REINDEX_SOURCES_PER_REQUEST, `source must contain at most ${String(MAX_REINDEX_SOURCES_PER_REQUEST)} references`),
29317
+ ]);
29318
+ /**
29319
+ * Request body for `POST /reindex`.
29320
+ *
29321
+ * `scope` defaults to `issues`. Scope `path` requires exactly one of `path`
29322
+ * or `source`; `rules` optionally narrows to `path`; `source` is only valid
29323
+ * with scope `path`.
29324
+ */
29325
+ object$1({
29326
+ /** Reindex scope. Default: `issues`. */
29327
+ scope: reindexScopeSchema.optional(),
29328
+ /** File path(s) or globs for scopes `path` and `rules`. */
29329
+ path: pathTargetsSchema.optional(),
29330
+ /** Source reference(s) for scope `path`. */
29331
+ source: sourceTargetsSchema.optional(),
29332
+ /** Compute and return the plan without executing. */
29333
+ dryRun: boolean$2().optional(),
29334
+ })
29335
+ .superRefine((val, ctx) => {
29336
+ if (val.scope === 'path') {
29337
+ if ((val.path === undefined) === (val.source === undefined)) {
29338
+ ctx.addIssue({
29339
+ code: 'custom',
29340
+ path: val.path === undefined ? ['path'] : [],
29341
+ message: 'Exactly one of path or source is required when scope is "path"',
29342
+ });
29343
+ }
29344
+ }
29345
+ else if (val.source !== undefined) {
29346
+ ctx.addIssue({
29347
+ code: 'custom',
29348
+ path: ['source'],
29349
+ message: 'source is only valid when scope is "path"',
29350
+ });
29351
+ }
29352
+ });
29353
+ /** Blast-radius plan for a reindex operation. */
29354
+ const reindexPlanSchema = object$1({
29355
+ /** Total points (prune) or files and source documents (other scopes) examined. */
29356
+ total: number$2().int().nonnegative(),
29357
+ /** Number of items to process. */
29358
+ toProcess: number$2().int().nonnegative(),
29359
+ /** Number of points to delete (prune only). */
29360
+ toDelete: number$2().int().nonnegative(),
29361
+ /** Counts grouped by watch root (files) or source name (source documents). */
29362
+ byRoot: record$1(string$2(), number$2()),
29363
+ /** True when results are partial. */
29364
+ incomplete: boolean$2().optional(),
29365
+ });
29366
+ /** Response body for `POST /reindex`: started (async) or dry-run plan. */
29367
+ object$1({
29368
+ /** `started` (executing asynchronously) or `dry_run` (plan only). */
29369
+ status: _enum$1(['started', 'dry_run']),
29370
+ /** The scope requested. */
29371
+ scope: reindexScopeSchema,
29372
+ /** Plan (omitted for a live prune). */
29373
+ plan: reindexPlanSchema.optional(),
29374
+ });
29375
+
29376
+ /**
29377
+ * @module constants
29378
+ * Shared identity constants for the jeeves-watcher ecosystem.
29379
+ * Consumed by both the service and plugin packages.
29380
+ */
29381
+ /** Component name shared across service and plugin descriptors. */
29382
+ const COMPONENT_NAME = 'watcher';
29383
+ /** npm package name for the service. */
29384
+ const SERVICE_PACKAGE = '@karmaniverous/jeeves-watcher';
29385
+ /** npm package name for the plugin. */
29386
+ const PLUGIN_PACKAGE = '@karmaniverous/jeeves-watcher-openclaw';
29387
+ /** Default watcher API port. */
29388
+ const DEFAULT_PORT = DEFAULT_PORTS.watcher;
29389
+ /** Default API values. */
29390
+ ({
29391
+ port: DEFAULT_PORTS.watcher});
29392
+ /** Default reindex values. */
29393
+ const REINDEX_DEFAULTS = {
29394
+ /** Files (and source documents, and stored-rule lookups) in flight at once. */
29395
+ concurrency: 50,
29396
+ };
29397
+
29398
+ /**
29399
+ * @module endpoints
29400
+ * Declarative catalog of every jeeves-watcher HTTP endpoint (name, method, path, description) plus a typed lookup; the single source the service, plugin and SDK derive routes from. Pure.
29401
+ */
29402
+ /**
29403
+ * Canonical endpoint catalog for the jeeves-watcher API.
29404
+ *
29405
+ * Every entry describes a single HTTP endpoint exposed by the service.
29406
+ * Route handlers, plugin tools, and HTTP clients should reference these
29407
+ * descriptors rather than hard-coding paths and descriptions.
29408
+ */
29409
+ const WATCHER_ENDPOINTS = [
29410
+ {
29411
+ name: 'status',
29412
+ method: 'GET',
29413
+ path: '/status',
29414
+ description: 'Service health and status overview.',
29415
+ },
29416
+ {
29417
+ name: 'metadata',
29418
+ method: 'POST',
29419
+ path: '/metadata',
29420
+ description: 'Set or update metadata on a document by file path or source reference.',
29421
+ },
29422
+ {
29423
+ name: 'render',
29424
+ method: 'POST',
29425
+ path: '/render',
29426
+ description: 'Render a document (file path, or source reference retrieved through its callback) through the template pipeline.',
29427
+ },
29428
+ {
29429
+ name: 'searchFacets',
29430
+ method: 'GET',
29431
+ path: '/search/facets',
29432
+ description: 'List available search facets and their values.',
29433
+ },
29434
+ {
29435
+ name: 'scan',
29436
+ method: 'POST',
29437
+ path: '/scan',
29438
+ description: 'Filter-only point query without vector search. Returns metadata for points matching a Qdrant filter.',
29439
+ },
29440
+ {
29441
+ name: 'walk',
29442
+ method: 'POST',
29443
+ path: '/walk',
29444
+ description: 'Walk watched filesystem paths with glob intersection. Returns matching file paths.',
29445
+ },
29446
+ {
29447
+ name: 'search',
29448
+ method: 'POST',
29449
+ path: '/search',
29450
+ description: 'Semantic search over indexed documents. Supports Qdrant filters.',
29451
+ },
29452
+ {
29453
+ name: 'rebuildMetadata',
29454
+ method: 'POST',
29455
+ path: '/rebuild-metadata',
29456
+ description: 'Rebuild the enrichment store from vector point payloads (file points only).',
29457
+ },
29458
+ {
29459
+ name: 'reindex',
29460
+ method: 'POST',
29461
+ path: '/reindex',
29462
+ description: 'Trigger a scoped reindex of watched files (scope path also accepts source references).',
29463
+ },
29464
+ {
29465
+ name: 'documents',
29466
+ method: 'POST',
29467
+ path: '/documents',
29468
+ description: 'Notify changed source documents ({ source, rule }); queued and coalesced (202), or processed synchronously with ?wait=true.',
29469
+ },
29470
+ {
29471
+ name: 'documentsDelete',
29472
+ method: 'POST',
29473
+ path: '/documents/delete',
29474
+ description: 'Delete source documents by reference or by Qdrant filter (at least one condition; constrained to points carrying source and no file_path).',
29475
+ },
29476
+ {
29477
+ name: 'issues',
29478
+ method: 'GET',
29479
+ path: '/issues',
29480
+ description: 'Get runtime processing failures. Shows the documents (file path or source reference) that failed processing and why.',
29481
+ },
29482
+ {
29483
+ name: 'configSchema',
29484
+ method: 'GET',
29485
+ path: '/config/schema',
29486
+ description: 'Get the JSON Schema for the configuration file.',
29487
+ },
29488
+ {
29489
+ name: 'configMatch',
29490
+ method: 'POST',
29491
+ path: '/config/match',
29492
+ description: 'Test file paths against config inference rules.',
29493
+ },
29494
+ {
29495
+ name: 'config',
29496
+ method: 'GET',
29497
+ path: '/config',
29498
+ description: 'Query service configuration with optional JSONPath.',
29499
+ },
29500
+ {
29501
+ name: 'configValidate',
29502
+ method: 'POST',
29503
+ path: '/config/validate',
29504
+ description: 'Validate a candidate config. Optionally test file paths against the config.',
29505
+ },
29506
+ {
29507
+ name: 'configApply',
29508
+ method: 'POST',
29509
+ path: '/config/apply',
29510
+ description: 'Apply a configuration patch.',
29511
+ },
29512
+ {
29513
+ name: 'rulesRegister',
29514
+ method: 'POST',
29515
+ path: '/rules/register',
29516
+ description: 'Register external inference rules.',
29517
+ },
29518
+ {
29519
+ name: 'rulesUnregister',
29520
+ method: 'DELETE',
29521
+ path: '/rules/unregister',
29522
+ description: 'Unregister external inference rules.',
29523
+ },
29524
+ {
29525
+ name: 'rulesUnregisterBySource',
29526
+ method: 'DELETE',
29527
+ path: '/rules/unregister/:source',
29528
+ description: 'Unregister all external inference rules from a source.',
28580
29529
  },
28581
29530
  {
28582
29531
  name: 'pointsDelete',
@@ -28594,7 +29543,7 @@ const WATCHER_ENDPOINTS = [
28594
29543
  name: 'vcsStatus',
28595
29544
  method: 'GET',
28596
29545
  path: '/vcs/status',
28597
- description: 'VCS state for all roots: enabled state, tracked files, last commit, remote status.',
29546
+ description: 'VCS state for all roots: enabled state, tracked files, last commit, remote status, commit circuit breaker state.',
28598
29547
  },
28599
29548
  {
28600
29549
  name: 'vcsHistory',
@@ -28646,6 +29595,169 @@ function getEndpoint(name) {
28646
29595
  return ep;
28647
29596
  }
28648
29597
 
29598
+ /**
29599
+ * @module httpContract
29600
+ * The one HTTP client contract shared by the SDK, the CLI and the plugin: the API key env var, bearer header construction and the mapping of a non-2xx response (status + raw body) to `{ status, code, message, details }`. Pure.
29601
+ */
29602
+ /** Environment variable holding the watcher API bearer key for clients (CLI, plugin). */
29603
+ const API_KEY_ENV_VAR = 'JEEVES_WATCHER_API_KEY';
29604
+ /**
29605
+ * Request headers carrying the bearer key.
29606
+ *
29607
+ * @param apiKey - Bearer key; `undefined` means no key.
29608
+ * @returns `{ Authorization: 'Bearer <apiKey>' }`, or `{}` without a key.
29609
+ */
29610
+ function bearerHeaders(apiKey) {
29611
+ return apiKey === undefined ? {} : { Authorization: `Bearer ${apiKey}` };
29612
+ }
29613
+ /**
29614
+ * Parse text as JSON, returning `undefined` when it is empty or not JSON.
29615
+ *
29616
+ * @param text - Raw response body.
29617
+ * @returns The parsed value, or `undefined`.
29618
+ */
29619
+ function tryParseJson(text) {
29620
+ if (text.length === 0)
29621
+ return undefined;
29622
+ try {
29623
+ return JSON.parse(text);
29624
+ }
29625
+ catch {
29626
+ return undefined;
29627
+ }
29628
+ }
29629
+ const stringField = (body, key) => {
29630
+ if (typeof body !== 'object' || body === null)
29631
+ return undefined;
29632
+ const value = body[key];
29633
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
29634
+ };
29635
+ /**
29636
+ * Code and message of a 0.20.0+ error body: `code` is one of the canonical
29637
+ * API error codes and `error` is the human-readable text (`message`, when
29638
+ * present and different, is appended).
29639
+ *
29640
+ * @param body - Parsed response body.
29641
+ * @returns Code and message, or `undefined` for any other body.
29642
+ */
29643
+ function canonical(body) {
29644
+ const code = apiErrorCodeSchema.safeParse(stringField(body, 'code'));
29645
+ const error = stringField(body, 'error');
29646
+ if (!code.success || error === undefined)
29647
+ return undefined;
29648
+ const extra = stringField(body, 'message');
29649
+ return {
29650
+ code: code.data,
29651
+ message: extra !== undefined && extra !== error ? `${error}: ${extra}` : error,
29652
+ };
29653
+ }
29654
+ /**
29655
+ * Map a non-2xx response to its code, message and details.
29656
+ *
29657
+ * - Canonical body (`code` is an `ApiErrorCode`, `error` is a string): `code`
29658
+ * is `body.code`; `message` is `body.error`, plus `: <body.message>` when
29659
+ * present.
29660
+ * - Otherwise (older servers, proxies): `code` is `body.code`, else
29661
+ * `body.error`, else `http_<status>`; `message` is `body.message`, else
29662
+ * `body.error`, else `HTTP <status> <statusText>`.
29663
+ * - `details`: `body.details` when present, else the parsed body (or raw
29664
+ * text).
29665
+ *
29666
+ * @param status - HTTP status.
29667
+ * @param statusText - HTTP status text.
29668
+ * @param text - Raw response body.
29669
+ * @returns The mapped error parts.
29670
+ */
29671
+ function httpError(status, statusText, text) {
29672
+ const body = tryParseJson(text);
29673
+ const fallbackMessage = `HTTP ${String(status)}${statusText ? ` ${statusText}` : ''}`.trim();
29674
+ const hasDetails = typeof body === 'object' && body !== null && 'details' in body;
29675
+ const details = hasDetails
29676
+ ? body.details
29677
+ : (body ?? (text.length > 0 ? text : undefined));
29678
+ const mapped = canonical(body) ?? {
29679
+ code: stringField(body, 'code') ??
29680
+ stringField(body, 'error') ??
29681
+ `http_${String(status)}`,
29682
+ message: stringField(body, 'message') ??
29683
+ stringField(body, 'error') ??
29684
+ fallbackMessage,
29685
+ };
29686
+ return { status, ...mapped, details };
29687
+ }
29688
+ /**
29689
+ * One-line text for a mapped error, as the CLI and plugin report it.
29690
+ *
29691
+ * @param info - The mapped error.
29692
+ * @returns `HTTP <status> <code>: <message>`.
29693
+ */
29694
+ function formatHttpError(info) {
29695
+ return `HTTP ${String(info.status)} ${info.code}: ${info.message}`;
29696
+ }
29697
+ /**
29698
+ * Error text for a `401` response. Never contains the key.
29699
+ *
29700
+ * @param keySent - Whether the request carried a bearer key.
29701
+ * @param keySources - Where this client reads its key from (e.g. `--api-key or the JEEVES_WATCHER_API_KEY env var`).
29702
+ * @returns Guidance on supplying (or checking) the key.
29703
+ */
29704
+ function unauthorizedMessage(keySent, keySources) {
29705
+ return keySent
29706
+ ? `HTTP 401 unauthorized: the watcher API rejected the API key. Check ${keySources} against the service's api.apiKey.`
29707
+ : `HTTP 401 unauthorized: the watcher API requires an API key. Provide it via ${keySources}.`;
29708
+ }
29709
+
29710
+ /**
29711
+ * @module reservedFields
29712
+ * Reserved system payload field names (named constants plus the full list) that rule schema properties should not use, and the system payload indexes. Pure helpers, no I/O.
29713
+ */
29714
+ /**
29715
+ * Payload fields written by the watcher itself, by role. The single
29716
+ * definition of these names: the service's pipeline constants and
29717
+ * {@link RESERVED_PAYLOAD_FIELDS} derive from it.
29718
+ */
29719
+ const PAYLOAD_FIELD = {
29720
+ /** Normalized file path (file points only). */
29721
+ filePath: 'file_path',
29722
+ /** Source reference `<name>://<id>` (source points only). */
29723
+ source: 'source',
29724
+ /** Designated rule of a source document (source points only). */
29725
+ sourceRule: 'source_rule',
29726
+ /** Zero-based chunk index. */
29727
+ chunkIndex: 'chunk_index',
29728
+ /** The chunk text. */
29729
+ chunkText: 'chunk_text',
29730
+ /** Total number of chunks of the document. */
29731
+ totalChunks: 'total_chunks',
29732
+ /** SHA-256 hash of the embedded text. */
29733
+ contentHash: 'content_hash',
29734
+ /** Names of the rules that matched. */
29735
+ matchedRules: 'matched_rules',
29736
+ /** 1-indexed line where the chunk starts. */
29737
+ lineStart: 'line_start',
29738
+ /** 1-indexed line where the chunk ends. */
29739
+ lineEnd: 'line_end',
29740
+ /** Creation timestamp (unix seconds). */
29741
+ createdAt: 'created_at',
29742
+ /** Modification timestamp (unix seconds). */
29743
+ modifiedAt: 'modified_at',
29744
+ };
29745
+ /**
29746
+ * Payload fields written by the watcher itself. System fields overwrite rule
29747
+ * metadata on upsert, so a rule schema property with one of these names is
29748
+ * silently shadowed; config validation warns about it.
29749
+ */
29750
+ const RESERVED_PAYLOAD_FIELDS = Object.values(PAYLOAD_FIELD);
29751
+ /**
29752
+ * Fields the watcher always keeps a keyword payload index on. They cannot be
29753
+ * configured in `vectorStore.payloadIndexes`.
29754
+ */
29755
+ const SYSTEM_PAYLOAD_INDEX_FIELDS = [
29756
+ PAYLOAD_FIELD.source,
29757
+ PAYLOAD_FIELD.sourceRule,
29758
+ ];
29759
+ new Set(RESERVED_PAYLOAD_FIELDS);
29760
+
28649
29761
  /**
28650
29762
  * @module schemas/vcs
28651
29763
  * VCS (version control system) configuration schema for git-backed content versioning.
@@ -28749,12 +29861,23 @@ const vcsConfigSchema = object$1({
28749
29861
  .min(5000)
28750
29862
  .default(60000)
28751
29863
  .describe('Age in ms after which an index.lock is considered stale and force-removed. Default: 60000.'),
28752
- /** Circuit breaker: stop re-queuing after this many consecutive commit failures. */
29864
+ /** Circuit breaker: trip after this many consecutive commit failures. */
28753
29865
  maxConsecutiveFailures: number$2()
28754
29866
  .int()
28755
29867
  .min(1)
28756
29868
  .default(5)
28757
- .describe('Circuit breaker: stop re-queuing after this many consecutive commit failures. Default: 5.'),
29869
+ .describe('Circuit breaker: trip after this many consecutive commit failures. Default: 5.'),
29870
+ /**
29871
+ * Circuit breaker cooldown in ms. Once tripped, pending files are
29872
+ * retained (re-queued, never discarded) and a single half-open recovery
29873
+ * attempt is allowed once this cooldown elapses — without requiring a
29874
+ * new file-change event.
29875
+ */
29876
+ circuitBreakerCooldownMs: number$2()
29877
+ .int()
29878
+ .min(1000)
29879
+ .default(300000)
29880
+ .describe('Circuit breaker cooldown in ms before a half-open recovery attempt. Default: 300000 (5 minutes).'),
28758
29881
  /** Git branch name for VCS operations. SquashManager and startup recovery use this instead of dynamic detection. */
28759
29882
  branch: string$2()
28760
29883
  .min(1)
@@ -28853,13 +29976,7 @@ const configWatchConfigSchema = object$1({
28853
29976
  .optional()
28854
29977
  .describe('Debounce delay in milliseconds for config file change detection.'),
28855
29978
  /** Reindex scope triggered on config change. */
28856
- reindex: union$1([
28857
- literal('issues')
28858
- .describe('Re-process only files with recorded issues.'),
28859
- literal('full').describe('Full reindex of all watched files.'),
28860
- literal('rules')
28861
- .describe('Re-apply inference rules to existing points without re-embedding.'),
28862
- ])
29979
+ reindex: configWatchReindexScopeSchema
28863
29980
  .optional()
28864
29981
  .describe('Reindex scope triggered on config change. Default: issues.'),
28865
29982
  });
@@ -28877,6 +29994,11 @@ const apiConfigSchema = object$1({
28877
29994
  cacheTtlMs: number$2()
28878
29995
  .optional()
28879
29996
  .describe('TTL in milliseconds for caching read-heavy endpoints (e.g., /status, /config). Default: 30000.'),
29997
+ /** Bearer API key required on every route except GET /status. */
29998
+ apiKey: string$2()
29999
+ .min(1, 'api.apiKey must be a non-empty string (remove it to disable authentication)')
30000
+ .optional()
30001
+ .describe('Bearer API key required on every route except GET /status (supports ${ENV_VAR} substitution). Must be non-empty. Unset: no authentication. Read at startup only.'),
28880
30002
  });
28881
30003
  /**
28882
30004
  * Logging configuration.
@@ -29024,6 +30146,104 @@ const inferenceRuleSchema = object$1({
29024
30146
  }
29025
30147
  });
29026
30148
 
30149
+ /**
30150
+ * @module schemas/payloadIndexes
30151
+ * Zod schema for `vectorStore.payloadIndexes`: configured Qdrant payload indexes, including tenant and principal flags. No I/O.
30152
+ */
30153
+ /** Qdrant payload index types supported by `vectorStore.payloadIndexes`. */
30154
+ const payloadIndexTypeSchema = _enum$1(['keyword', 'integer', 'float', 'bool', 'datetime', 'uuid', 'text'])
30155
+ .describe('Qdrant payload index type.');
30156
+ /** Index types that may carry `isTenant: true` (Qdrant `is_tenant`). */
30157
+ const TENANT_INDEX_TYPES = ['keyword', 'uuid'];
30158
+ /** Index types that may carry `isPrincipal: true` (Qdrant `is_principal`). */
30159
+ const PRINCIPAL_INDEX_TYPES = [
30160
+ 'integer',
30161
+ 'float',
30162
+ 'datetime',
30163
+ ];
30164
+ /**
30165
+ * Whether an index type may carry `isTenant: true` (Qdrant `is_tenant`).
30166
+ *
30167
+ * @param type - Payload index type.
30168
+ * @returns `true` for `keyword` and `uuid`.
30169
+ */
30170
+ function isTenantIndexType(type) {
30171
+ return TENANT_INDEX_TYPES.includes(type);
30172
+ }
30173
+ /**
30174
+ * Whether an index type may carry `isPrincipal: true` (Qdrant
30175
+ * `is_principal`).
30176
+ *
30177
+ * @param type - Payload index type.
30178
+ * @returns `true` for `integer`, `float` and `datetime`.
30179
+ */
30180
+ function isPrincipalIndexType(type) {
30181
+ return PRINCIPAL_INDEX_TYPES.includes(type);
30182
+ }
30183
+ /**
30184
+ * A single configured payload index.
30185
+ *
30186
+ * `isTenant` is valid only for `keyword`/`uuid`; `isPrincipal` only for
30187
+ * `integer`/`float`/`datetime`. Both constraints are enforced by refinement.
30188
+ */
30189
+ const payloadIndexSchema = object$1({
30190
+ /** Payload field to index. */
30191
+ field: string$2().min(1).describe('Payload field name to index.'),
30192
+ /** Qdrant index type. */
30193
+ type: payloadIndexTypeSchema,
30194
+ /** Mark the index as a tenant index (keyword/uuid only). */
30195
+ isTenant: boolean$2()
30196
+ .optional()
30197
+ .describe('Qdrant is_tenant: optimise storage for per-tenant filtering (keyword/uuid only).'),
30198
+ /** Mark the index as a principal index (integer/float/datetime only). */
30199
+ isPrincipal: boolean$2()
30200
+ .optional()
30201
+ .describe('Qdrant is_principal: optimise storage for range filtering (integer/float/datetime only).'),
30202
+ })
30203
+ .superRefine((val, ctx) => {
30204
+ if (val.isTenant && !isTenantIndexType(val.type)) {
30205
+ ctx.addIssue({
30206
+ code: 'custom',
30207
+ path: ['isTenant'],
30208
+ message: `isTenant is only valid for ${TENANT_INDEX_TYPES.join('/')} indexes (got "${val.type}")`,
30209
+ });
30210
+ }
30211
+ if (val.isPrincipal && !isPrincipalIndexType(val.type)) {
30212
+ ctx.addIssue({
30213
+ code: 'custom',
30214
+ path: ['isPrincipal'],
30215
+ message: `isPrincipal is only valid for ${PRINCIPAL_INDEX_TYPES.join('/')} indexes (got "${val.type}")`,
30216
+ });
30217
+ }
30218
+ });
30219
+ const systemIndexFields = new Set(SYSTEM_PAYLOAD_INDEX_FIELDS);
30220
+ /**
30221
+ * The `vectorStore.payloadIndexes` list. Each `field` may appear once, and
30222
+ * the system-indexed fields (`source`, `source_rule`) are rejected: the
30223
+ * watcher always keeps a keyword index on them.
30224
+ */
30225
+ const payloadIndexesSchema = array$1(payloadIndexSchema)
30226
+ .superRefine((indexes, ctx) => {
30227
+ const seen = new Set();
30228
+ indexes.forEach(({ field }, i) => {
30229
+ if (systemIndexFields.has(field)) {
30230
+ ctx.addIssue({
30231
+ code: 'custom',
30232
+ path: [i, 'field'],
30233
+ message: `"${field}" is a system field the watcher always indexes (keyword); remove this entry`,
30234
+ });
30235
+ }
30236
+ else if (seen.has(field)) {
30237
+ ctx.addIssue({
30238
+ code: 'custom',
30239
+ path: [i, 'field'],
30240
+ message: `Duplicate payload index for field "${field}"; each field may be indexed once`,
30241
+ });
30242
+ }
30243
+ seen.add(field);
30244
+ });
30245
+ });
30246
+
29027
30247
  /**
29028
30248
  * @module schemas/services
29029
30249
  * Service configuration schemas: embedding and vector store.
@@ -29035,11 +30255,11 @@ const embeddingConfigSchema = object$1({
29035
30255
  /** The embedding model provider. */
29036
30256
  provider: string$2()
29037
30257
  .default('gemini')
29038
- .describe('Embedding provider name (e.g., "gemini", "openai").'),
30258
+ .describe('Embedding provider name. Default: "gemini".'),
29039
30259
  /** The embedding model name. */
29040
30260
  model: string$2()
29041
30261
  .default('gemini-embedding-001')
29042
- .describe('Embedding model identifier (e.g., "gemini-embedding-001", "text-embedding-3-small").'),
30262
+ .describe('Embedding model identifier for the provider. Default: "gemini-embedding-001".'),
29043
30263
  /** Maximum tokens per chunk for splitting. */
29044
30264
  chunkSize: number$2()
29045
30265
  .optional()
@@ -29079,6 +30299,10 @@ const vectorStoreConfigSchema = object$1({
29079
30299
  apiKey: string$2()
29080
30300
  .optional()
29081
30301
  .describe('Qdrant API key for authentication (supports ${ENV_VAR} substitution).'),
30302
+ /** Payload indexes created at startup when missing (never dropped). */
30303
+ payloadIndexes: payloadIndexesSchema
30304
+ .optional()
30305
+ .describe('Qdrant payload indexes created at startup when missing (never dropped). Each field at most once; source and source_rule are always indexed and cannot be listed. Default: none.'),
29082
30306
  });
29083
30307
 
29084
30308
  /**
@@ -29109,10 +30333,10 @@ object$1({
29109
30333
  vectorStore: vectorStoreConfigSchema.describe('Qdrant vector store configuration.'),
29110
30334
  /** API server configuration. */
29111
30335
  api: apiConfigSchema.optional().describe('API server configuration.'),
29112
- /** Directory for persistent state files (issues.json, values.json, enrichments.sqlite). */
30336
+ /** Directory for persistent state files (issues.json, values.v8, enrichments.sqlite). */
29113
30337
  stateDir: string$2()
29114
30338
  .optional()
29115
- .describe('Directory for persistent state files (issues.json, values.json, enrichments.sqlite). Defaults to .jeeves-metadata.'),
30339
+ .describe('Directory for persistent state files (issues.json, values.v8, enrichments.sqlite). Defaults to .jeeves-metadata.'),
29116
30340
  /** Rules for inferring metadata from document properties (inline objects or file paths). */
29117
30341
  inferenceRules: array$1(union$1([inferenceRuleSchema, string$2()]))
29118
30342
  .optional()
@@ -29168,8 +30392,8 @@ object$1({
29168
30392
  concurrency: number$2()
29169
30393
  .int()
29170
30394
  .min(1)
29171
- .default(50)
29172
- .describe('Maximum concurrent file operations during reindex (default 50).'),
30395
+ .default(REINDEX_DEFAULTS.concurrency)
30396
+ .describe(`Maximum concurrent file operations during reindex (default ${String(REINDEX_DEFAULTS.concurrency)}).`),
29173
30397
  })
29174
30398
  .optional()
29175
30399
  .describe('Reindex configuration.'),
@@ -29200,6 +30424,8 @@ object$1({
29200
30424
  vcs: vcsConfigSchema
29201
30425
  .optional()
29202
30426
  .describe('VCS configuration for git-backed version control of watched content.'),
30427
+ /** Named sources for non-file documents (callback retrieval). */
30428
+ sources: sourcesConfigSchema.optional(),
29203
30429
  /** Logging configuration. */
29204
30430
  logging: loggingConfigSchema.optional().describe('Logging configuration.'),
29205
30431
  /** Timeout in milliseconds for graceful shutdown. */
@@ -29241,9 +30467,10 @@ const CONFIG_ROOT_UNSET_WARNING = `[${PLUGIN_ID}] configRoot not configured yet
29241
30467
  * Watcher-specific convenience wrappers over `@karmaniverous/jeeves` core SDK.
29242
30468
  *
29243
30469
  * @remarks
29244
- * Only watcher-specific resolution logic lives here. Core SDK types and
29245
- * utilities (`PluginApi`, `ToolResult`, `ok`, `connectionFail`, `fetchJson`,
29246
- * `postJson`) should be imported directly from `@karmaniverous/jeeves`.
30470
+ * Only watcher-specific resolution logic (API URL, API key, config root)
30471
+ * lives here. Core SDK types and utilities (`PluginApi`, `ToolResult`, `ok`,
30472
+ * `connectionFail`) should be imported directly from `@karmaniverous/jeeves`;
30473
+ * watcher API requests go through `requestJson` (`./http.js`).
29247
30474
  *
29248
30475
  * @module plugin/helpers
29249
30476
  */
@@ -29261,6 +30488,23 @@ function getApiUrl(api) {
29261
30488
  return scoped;
29262
30489
  return resolvePluginSetting(api, PLUGIN_ID, 'apiUrl', 'JEEVES_WATCHER_URL', DEFAULT_API_URL);
29263
30490
  }
30491
+ /**
30492
+ * Resolve the watcher API bearer key, if configured.
30493
+ *
30494
+ * @remarks
30495
+ * Resolution order: plugin-scoped config (`api.pluginConfig.apiKey`), then
30496
+ * `plugins.entries.<id>.config.apiKey`, then `JEEVES_WATCHER_API_KEY`. Blank
30497
+ * values count as unset. Resolved per request; never logged.
30498
+ *
30499
+ * @returns The key, or `undefined` when unset (requests carry no header).
30500
+ */
30501
+ function getApiKey(api) {
30502
+ const scoped = api.pluginConfig?.apiKey;
30503
+ if (typeof scoped === 'string' && scoped.trim() !== '')
30504
+ return scoped;
30505
+ const resolved = resolveOptionalPluginSetting(api, PLUGIN_ID, 'apiKey', API_KEY_ENV_VAR);
30506
+ return resolved?.trim() ? resolved : undefined;
30507
+ }
29264
30508
  /**
29265
30509
  * Resolve the platform config root path, if configured.
29266
30510
  *
@@ -29281,6 +30525,164 @@ function getConfigRoot(api) {
29281
30525
  return resolved?.trim() ? resolved : undefined;
29282
30526
  }
29283
30527
 
30528
+ /**
30529
+ * @module plugin/http
30530
+ * The plugin's single HTTP helper for watcher API calls: JSON GET/POST via `fetch` with the core bearer header and the core error mapping (errors carry the server `code`; 401 adds key guidance). Never logs the key.
30531
+ */
30532
+ /** Where the plugin reads its API key from (named in 401 guidance). */
30533
+ const PLUGIN_KEY_SOURCES = `plugins.entries.${PLUGIN_ID}.config.apiKey or the ${API_KEY_ENV_VAR} env var`;
30534
+ /**
30535
+ * Build the `fetch` init for a request. With no key and no body this is
30536
+ * `undefined`, exactly as an unauthenticated GET has always been sent.
30537
+ *
30538
+ * @param body - JSON body; `undefined` for a GET.
30539
+ * @param apiKey - Bearer key, if any.
30540
+ * @returns The request init.
30541
+ */
30542
+ function buildInit(body, apiKey) {
30543
+ const auth = bearerHeaders(apiKey);
30544
+ if (body === undefined) {
30545
+ return apiKey === undefined ? undefined : { headers: auth };
30546
+ }
30547
+ return {
30548
+ method: 'POST',
30549
+ headers: { 'Content-Type': 'application/json', ...auth },
30550
+ body: JSON.stringify(body),
30551
+ };
30552
+ }
30553
+ /**
30554
+ * Call the watcher API and return the parsed JSON body.
30555
+ *
30556
+ * @param url - Full request URL.
30557
+ * @param body - JSON body; `undefined` sends a GET, anything else a POST.
30558
+ * @param apiKey - Bearer key; `undefined` sends no `Authorization` header.
30559
+ * @returns Parsed JSON response body.
30560
+ * @throws On `401` with core `unauthorizedMessage` (naming
30561
+ * {@link PLUGIN_KEY_SOURCES}); on any other non-OK status with
30562
+ * `HTTP <status> <code>: <message>` (core `httpError`); network errors
30563
+ * propagate unchanged.
30564
+ */
30565
+ async function requestJson(url, body, apiKey) {
30566
+ const res = await fetch(url, buildInit(body, apiKey));
30567
+ if (res.status === 401) {
30568
+ throw new Error(unauthorizedMessage(apiKey !== undefined, PLUGIN_KEY_SOURCES));
30569
+ }
30570
+ if (!res.ok) {
30571
+ const info = httpError(res.status, res.statusText, await res.text());
30572
+ throw new Error(formatHttpError(info));
30573
+ }
30574
+ return res.json();
30575
+ }
30576
+
30577
+ /**
30578
+ * @module plugin/apiTool
30579
+ * Generic HTTP-backed watcher tool registration: config shape, request helpers, and ok/connectionFail wrapping. Requests carry the bearer key (if configured) via `requestJson`.
30580
+ */
30581
+ /**
30582
+ * Register a single API tool with standard try/catch + ok/connectionFail.
30583
+ *
30584
+ * @remarks
30585
+ * The bearer key is resolved from `api` ({@link getApiKey}) on every call,
30586
+ * so a key set after registration takes effect without re-registering.
30587
+ */
30588
+ function registerApiTool(api, baseUrlSource, config) {
30589
+ const tool = {
30590
+ name: config.name,
30591
+ description: config.description,
30592
+ parameters: config.parameters,
30593
+ ...(config.catalogMode ? { catalogMode: config.catalogMode } : {}),
30594
+ execute: async (_id, params) => {
30595
+ const baseUrl = typeof baseUrlSource === 'function' ? baseUrlSource() : baseUrlSource;
30596
+ try {
30597
+ const [endpoint, body] = config.buildRequest(params);
30598
+ const data = await requestJson(`${baseUrl}${endpoint}`, body, getApiKey(api));
30599
+ return ok(data);
30600
+ }
30601
+ catch (error) {
30602
+ return connectionFail(error, baseUrl, PLUGIN_ID);
30603
+ }
30604
+ },
30605
+ };
30606
+ api.registerTool(tool, { optional: true });
30607
+ }
30608
+ /** Build a query string from defined params. */
30609
+ function buildQuery(params, keys) {
30610
+ const parts = [];
30611
+ for (const key of keys) {
30612
+ const val = params[key];
30613
+ if (val !== undefined) {
30614
+ const s = typeof val === 'string' ? val : JSON.stringify(val);
30615
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(s)}`);
30616
+ }
30617
+ }
30618
+ return parts.length > 0 ? `?${parts.join('&')}` : '';
30619
+ }
30620
+ /** Pick defined keys from params into a body object. */
30621
+ function pickDefined(params, keys) {
30622
+ const body = {};
30623
+ for (const key of keys) {
30624
+ if (params[key] !== undefined)
30625
+ body[key] = params[key];
30626
+ }
30627
+ return body;
30628
+ }
30629
+
30630
+ /**
30631
+ * @module plugin/configTools
30632
+ * Watcher-owned `watcher_config` and `watcher_config_apply` tools: same names, parameters and requests as the core toolset's, but sent through the plugin HTTP helper so they carry the bearer key.
30633
+ *
30634
+ * @remarks
30635
+ * Core's `createPluginToolset` cannot attach request headers, so its two
30636
+ * authenticated HTTP tools are replaced by these (see {@link CORE_TOOLS_REPLACED}).
30637
+ * `watcher_status` stays with core: `GET /status` is always open.
30638
+ */
30639
+ /** Core toolset tools that this module replaces. */
30640
+ const CORE_TOOLS_REPLACED = new Set([
30641
+ 'watcher_config',
30642
+ 'watcher_config_apply',
30643
+ ]);
30644
+ /** `watcher_config` and `watcher_config_apply`, as API tool configs. */
30645
+ const configToolConfigs = [
30646
+ {
30647
+ name: 'watcher_config',
30648
+ description: 'Query watcher running configuration. Optional JSONPath filter.',
30649
+ parameters: {
30650
+ type: 'object',
30651
+ properties: {
30652
+ path: {
30653
+ type: 'string',
30654
+ description: 'JSONPath expression (optional)',
30655
+ },
30656
+ },
30657
+ },
30658
+ buildRequest: (params) => {
30659
+ const path = params.path;
30660
+ const base = getEndpoint('config').path;
30661
+ return [path ? `${base}?path=${encodeURIComponent(path)}` : base];
30662
+ },
30663
+ },
30664
+ {
30665
+ name: 'watcher_config_apply',
30666
+ description: 'Apply a config patch to the running watcher service.',
30667
+ parameters: {
30668
+ type: 'object',
30669
+ properties: {
30670
+ config: {
30671
+ type: 'object',
30672
+ description: 'Config patch to apply',
30673
+ },
30674
+ },
30675
+ required: ['config'],
30676
+ },
30677
+ buildRequest: (params) => {
30678
+ const config = params.config;
30679
+ if (!config)
30680
+ throw new Error('Missing required parameter: config');
30681
+ return [getEndpoint('configApply').path, { patch: config }];
30682
+ },
30683
+ },
30684
+ ];
30685
+
29284
30686
  /**
29285
30687
  * @module plugin/toolGating
29286
30688
  * Which watcher tool invocations read `configRoot`, and so must be gated on it.
@@ -29290,16 +30692,20 @@ function getConfigRoot(api) {
29290
30692
  * error only when its implementation actually reads `configRoot`. Tools
29291
30693
  * that only call the service HTTP API keep working without it.
29292
30694
  *
29293
- * Audit of the core standard toolset (`createPluginToolset`, core 0.6.0-7):
29294
- * - `watcher_status`, `watcher_config`, `watcher_config_apply`: HTTP only
29295
- * (`{apiUrl}/…`, resolved per call).
29296
- * - `watcher_service`: only `install` reads `configRoot`
29297
- * (`createServiceManager` → `resolveConfigFilePath` →
30695
+ * Audit of the tools the plugin registers:
30696
+ * - `watcher_status` (core standard toolset, `createPluginToolset`): HTTP
30697
+ * only (`{apiUrl}/status`, resolved per call).
30698
+ * - `watcher_config`, `watcher_config_apply`: implemented by the plugin
30699
+ * (`configTools.ts`, replacing the core ones so the request carries the
30700
+ * `Authorization: Bearer` key); HTTP only.
30701
+ * - `watcher_service` (core standard toolset): only `install` reads
30702
+ * `configRoot` (`createServiceManager` → `resolveConfigFilePath` →
29298
30703
  * `getComponentConfigDir`); the other actions query or drive the OS
29299
30704
  * service manager by service name.
29300
30705
  *
29301
- * The 14 domain tools call `{apiUrl}/…` via `fetchJson`/`postJson` and are
29302
- * never gated.
30706
+ * The 14 domain tools (7 in `watcherTools.ts`, 7 VCS tools in
30707
+ * `vcsTools.ts`) call the service API via `registerApiTool` →
30708
+ * `requestJson` and are never gated.
29303
30709
  */
29304
30710
  /**
29305
30711
  * Tools whose implementation reads `configRoot`, keyed by tool name. A tool
@@ -29411,7 +30817,7 @@ const WATCHER_PROMPT_CONTEXT = [
29411
30817
  '- **Escalation rule:** Use `memory_search` for personal operational notes, decisions, and rules. Escalate to `watcher_search` when memory is thin, or when searching the broader archive (tickets, docs, code). ALWAYS use `watcher_search` BEFORE filesystem commands (exec, grep) when looking for information in indexed paths.',
29412
30818
  '- **Scan-first rule:** For structural queries (file enumeration, staleness checks, domain listing, counts), use `watcher_scan` instead of `watcher_search`. Scan does not use embeddings and does not accept a query string.',
29413
30819
  '- **Search-first rule:** When finding, reading, or modifying files in indexed paths, run `watcher_search` FIRST, even if you already know the file path. Direct filesystem access is for acting on search results, not bypassing them.',
29414
- '- **Scores:** strong / relevant / noise thresholds are configured at `$.search.scoreThresholds` (query with `watcher_config`; defaults 0.75 / 0.5 / 0.25). Discard noise; if all results are noise, broaden the query.',
30820
+ '- **Scores:** strong / relevant / noise thresholds apply only when configured at `$.search.scoreThresholds` (query with `watcher_config`); there are no defaults. When configured, discard noise; if all results are noise, broaden the query.',
29415
30821
  '- **Inventory on demand (`watcher_config`):** inference rules `$.inferenceRules[*]`, watched paths `$.watch.paths[*]`, ignored paths `$.watch.ignored[*]`. Version history: `watcher_vcs_*` tools. See the jeeves-watcher skill for detail.',
29416
30822
  ].join('\n');
29417
30823
  /**
@@ -29428,8 +30834,10 @@ function registerWatcherPromptContext(api) {
29428
30834
  * Jeeves component descriptor for the watcher OpenClaw plugin.
29429
30835
  *
29430
30836
  * @remarks
29431
- * Feeds `createPluginToolset()` (`watcher_status`, `watcher_config`,
29432
- * `watcher_config_apply`, `watcher_service`). The service descriptor
30837
+ * Feeds `createPluginToolset()`, of which the plugin registers
30838
+ * `watcher_status` and `watcher_service`; its `watcher_config` and
30839
+ * `watcher_config_apply` are replaced by the plugin's own (`configTools.ts`,
30840
+ * which send the API key). The service descriptor
29433
30841
  * (`packages/service/src/descriptor.ts`) is the canonical source for the
29434
30842
  * config schema and CLI commands.
29435
30843
  */
@@ -29464,53 +30872,6 @@ function createWatcherComponent(pluginVersion) {
29464
30872
  };
29465
30873
  }
29466
30874
 
29467
- /**
29468
- * @module plugin/apiTool
29469
- * Generic HTTP-backed watcher tool registration: config shape, request helpers, and ok/connectionFail wrapping.
29470
- */
29471
- /** Register a single API tool with standard try/catch + ok/connectionFail. */
29472
- function registerApiTool(api, baseUrl, config) {
29473
- const tool = {
29474
- name: config.name,
29475
- description: config.description,
29476
- parameters: config.parameters,
29477
- ...(config.catalogMode ? { catalogMode: config.catalogMode } : {}),
29478
- execute: async (_id, params) => {
29479
- try {
29480
- const [endpoint, body] = config.buildRequest(params);
29481
- const url = `${baseUrl}${endpoint}`;
29482
- const data = body !== undefined ? await postJson(url, body) : await fetchJson(url);
29483
- return ok(data);
29484
- }
29485
- catch (error) {
29486
- return connectionFail(error, baseUrl, PLUGIN_ID);
29487
- }
29488
- },
29489
- };
29490
- api.registerTool(tool, { optional: true });
29491
- }
29492
- /** Build a query string from defined params. */
29493
- function buildQuery(params, keys) {
29494
- const parts = [];
29495
- for (const key of keys) {
29496
- const val = params[key];
29497
- if (val !== undefined) {
29498
- const s = typeof val === 'string' ? val : JSON.stringify(val);
29499
- parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(s)}`);
29500
- }
29501
- }
29502
- return parts.length > 0 ? `?${parts.join('&')}` : '';
29503
- }
29504
- /** Pick defined keys from params into a body object. */
29505
- function pickDefined(params, keys) {
29506
- const body = {};
29507
- for (const key of keys) {
29508
- if (params[key] !== undefined)
29509
- body[key] = params[key];
29510
- }
29511
- return body;
29512
- }
29513
-
29514
30875
  /**
29515
30876
  * @module plugin/vcsTools
29516
30877
  * Version-tracking (watcher_vcs_*) tool configs for the OpenClaw plugin; each maps tool params to a watcher VCS endpoint.
@@ -29681,6 +31042,11 @@ const vcsToolConfigs = [
29681
31042
  * OpenClaw Tool Search keeps them model-visible: semantic search must be a
29682
31043
  * direct tool or the model reaches for grep first. All other tools remain
29683
31044
  * catalog-eligible.
31045
+ *
31046
+ * @param api - Plugin API.
31047
+ * @param baseUrl - API base URL, or a getter resolved on every call (so a
31048
+ * changed `apiUrl` takes effect without re-registering, as for the config
31049
+ * tools).
29684
31050
  */
29685
31051
  function registerWatcherTools(api, baseUrl) {
29686
31052
  const tools = [
@@ -29832,7 +31198,7 @@ function registerWatcherTools(api, baseUrl) {
29832
31198
  },
29833
31199
  {
29834
31200
  name: 'watcher_issues',
29835
- description: 'Get runtime embedding failures. Shows files that failed processing and why.',
31201
+ description: 'Get runtime processing failures. Shows the documents (file path or source reference) that failed processing and why.',
29836
31202
  parameters: { type: 'object', properties: {} },
29837
31203
  buildRequest: () => [getEndpoint('issues').path],
29838
31204
  },
@@ -29879,15 +31245,21 @@ const PLUGIN_VERSION = getPackageVersion(import.meta.url);
29879
31245
  function register(api) {
29880
31246
  warnIfConfigRootUnset(api);
29881
31247
  const toolApi = withGuardedTools(api, createLazyCore(api));
29882
- // 4 standard tools from core factory: watcher_status, watcher_config,
29883
- // watcher_config_apply, watcher_service. `apiUrl` is resolved lazily on
29884
- // every call, so the HTTP tools honour the configured URL (defaultPort is
29885
- // only core's fallback).
31248
+ // 4 standard tools: watcher_status and watcher_service from the core
31249
+ // factory; watcher_config and watcher_config_apply re-implemented here so
31250
+ // they send the bearer key (core cannot attach headers). `apiUrl` is
31251
+ // resolved lazily on every call by every tool (defaultPort is only core's
31252
+ // fallback).
29886
31253
  for (const tool of createPluginToolset(createWatcherComponent(PLUGIN_VERSION), { apiUrl: () => getApiUrl(api) })) {
29887
- toolApi.registerTool(tool, { optional: true });
31254
+ if (!CORE_TOOLS_REPLACED.has(tool.name)) {
31255
+ toolApi.registerTool(tool, { optional: true });
31256
+ }
31257
+ }
31258
+ for (const config of configToolConfigs) {
31259
+ registerApiTool(toolApi, () => getApiUrl(api), config);
29888
31260
  }
29889
31261
  // 14 domain-specific tools (search, enrich, scan, vcs, ...).
29890
- registerWatcherTools(toolApi, getApiUrl(api));
31262
+ registerWatcherTools(toolApi, () => getApiUrl(api));
29891
31263
  registerWatcherPromptContext(api);
29892
31264
  }
29893
31265