@karmaniverous/jeeves-watcher-openclaw 0.16.1 → 0.18.0-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/LICENSE +28 -0
- package/README.md +16 -13
- package/dist/index.js +2329 -708
- package/dist/scripts/build-skills.d.ts +1 -1
- package/dist/skills/jeeves-watcher/SKILL.md +230 -136
- package/dist/src/auth.test.d.ts +5 -0
- package/dist/src/catalogTools.d.ts +96 -0
- package/dist/src/catalogTools.test.d.ts +5 -0
- package/dist/src/constants.d.ts +1 -1
- package/dist/src/distSmoke.test.d.ts +5 -0
- package/dist/src/helpers.d.ts +15 -3
- package/dist/src/manifest.test.d.ts +1 -1
- package/dist/src/promptContext.d.ts +10 -10
- package/dist/src/skillFiles.d.ts +14 -0
- package/dist/src/test/surfaceDiff.d.ts +27 -0
- package/dist/src/test/toolRenames.d.ts +13 -0
- package/dist/src/toolGating.d.ts +8 -6
- package/dist/src/toolSurface.test.d.ts +5 -0
- package/dist/src/watcherComponent.d.ts +3 -2
- package/openclaw.plugin.json +20 -11
- package/package.json +18 -13
- package/dist/src/apiTool.d.ts +0 -37
- package/dist/src/toolTestHarness.d.ts +0 -23
- package/dist/src/vcsTools.d.ts +0 -7
- package/dist/src/vcsTools.test.d.ts +0 -1
- package/dist/src/watcherTools.d.ts +0 -15
- package/dist/src/watcherTools.test.d.ts +0 -1
- /package/dist/src/{apiTool.test.d.ts → skillFiles.test.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { DEFAULT_PORTS, resolvePluginSetting,
|
|
1
|
+
import { DEFAULT_PORTS, resolveOptionalPluginSetting, resolvePluginSetting, createPluginToolset, fail, init, resolveWorkspacePath, registerPromptContext, jeevesComponentDescriptorSchema, getPackageVersion } from '@karmaniverous/jeeves';
|
|
2
|
+
import { defineCatalog, standardEntries, toToolName } from '@karmaniverous/jeeves/catalog';
|
|
2
3
|
import 'node:crypto';
|
|
3
4
|
|
|
4
5
|
function getEnumValues$1(entries) {
|
|
@@ -165,7 +166,7 @@ const allowsEval$1 = /* @__PURE__*/ cached$1(() => {
|
|
|
165
166
|
return false;
|
|
166
167
|
}
|
|
167
168
|
});
|
|
168
|
-
function isPlainObject$
|
|
169
|
+
function isPlainObject$2(o) {
|
|
169
170
|
if (isObject$1(o) === false)
|
|
170
171
|
return false;
|
|
171
172
|
// modified constructor
|
|
@@ -185,7 +186,7 @@ function isPlainObject$1(o) {
|
|
|
185
186
|
return true;
|
|
186
187
|
}
|
|
187
188
|
function shallowClone$1(o) {
|
|
188
|
-
if (isPlainObject$
|
|
189
|
+
if (isPlainObject$2(o))
|
|
189
190
|
return { ...o };
|
|
190
191
|
if (Array.isArray(o))
|
|
191
192
|
return [...o];
|
|
@@ -284,7 +285,7 @@ function omit$1(schema, mask) {
|
|
|
284
285
|
return clone$1(schema, mergeDefs$1(currDef, { shape: newShape, checks: [] }));
|
|
285
286
|
}
|
|
286
287
|
function extend$1(schema, shape) {
|
|
287
|
-
if (!isPlainObject$
|
|
288
|
+
if (!isPlainObject$2(shape)) {
|
|
288
289
|
throw new Error("Invalid input to extend: expected a plain object");
|
|
289
290
|
}
|
|
290
291
|
const checks = schema._zod.def.checks;
|
|
@@ -308,7 +309,7 @@ function extended(schema, shape) {
|
|
|
308
309
|
return newShape;
|
|
309
310
|
}
|
|
310
311
|
function safeExtend$1(schema, shape) {
|
|
311
|
-
if (!isPlainObject$
|
|
312
|
+
if (!isPlainObject$2(shape)) {
|
|
312
313
|
throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
313
314
|
}
|
|
314
315
|
return clone$1(schema, mergeDefs$1(schema._zod.def, { shape: extended(schema, shape) }));
|
|
@@ -387,6 +388,20 @@ function unwrapMessage$1(message) {
|
|
|
387
388
|
return typeof message === "string" ? message : message?.message;
|
|
388
389
|
}
|
|
389
390
|
/* 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. */
|
|
391
|
+
/** The leading size checks of `defChecks` that set an upper bound, each paired with it. */
|
|
392
|
+
function sizeCaps(defChecks) {
|
|
393
|
+
const caps = [];
|
|
394
|
+
for (const ch of defChecks ?? []) {
|
|
395
|
+
const d = ch._zod.def;
|
|
396
|
+
// a later check may rewrite the value
|
|
397
|
+
if (!/_(length|size)$|_equals$/.test(d.check))
|
|
398
|
+
break;
|
|
399
|
+
const max = d.maximum ?? d.length ?? d.size;
|
|
400
|
+
if (max >= 0)
|
|
401
|
+
caps.push([max, ch]);
|
|
402
|
+
}
|
|
403
|
+
return caps;
|
|
404
|
+
}
|
|
390
405
|
function attachSchema(issues, start, inst) {
|
|
391
406
|
var _a;
|
|
392
407
|
for (let i = start; i < issues.length; i++) {
|
|
@@ -1152,6 +1167,7 @@ const anyString = /^[\s\S]{0,}$/;
|
|
|
1152
1167
|
const integer$1 = /^-?\d+$/;
|
|
1153
1168
|
const number$3 = /^-?\d+(?:\.\d+)?$/;
|
|
1154
1169
|
const boolean$3 = /^(?:true|false)$/i;
|
|
1170
|
+
const _null$5 = /^null$/i;
|
|
1155
1171
|
// regex for string with no uppercase letters
|
|
1156
1172
|
const lowercase$1 = /^[^A-Z]*$/;
|
|
1157
1173
|
// regex for string with no lowercase letters
|
|
@@ -1550,7 +1566,7 @@ let Doc$1 = class Doc {
|
|
|
1550
1566
|
const version$1 = {
|
|
1551
1567
|
major: 4,
|
|
1552
1568
|
minor: 6,
|
|
1553
|
-
patch:
|
|
1569
|
+
patch: 7,
|
|
1554
1570
|
};
|
|
1555
1571
|
|
|
1556
1572
|
const $ZodType$1 = /*@__PURE__*/ $constructor$1("$ZodType", (inst, def) => {
|
|
@@ -2142,6 +2158,23 @@ const $ZodBoolean$1 = /*@__PURE__*/ $constructor$1("$ZodBoolean", (inst, def) =>
|
|
|
2142
2158
|
return payload;
|
|
2143
2159
|
};
|
|
2144
2160
|
});
|
|
2161
|
+
const $ZodNull$1 = /*@__PURE__*/ $constructor$1("$ZodNull", (inst, def) => {
|
|
2162
|
+
$ZodType$1.init(inst, def);
|
|
2163
|
+
inst._zod.pattern = _null$5;
|
|
2164
|
+
inst._zod.values = new Set([null]);
|
|
2165
|
+
inst._zod.parse = (payload, _ctx) => {
|
|
2166
|
+
const input = payload.value;
|
|
2167
|
+
if (input === null)
|
|
2168
|
+
return payload;
|
|
2169
|
+
payload.issues.push({
|
|
2170
|
+
expected: "null",
|
|
2171
|
+
code: "invalid_type",
|
|
2172
|
+
input,
|
|
2173
|
+
inst,
|
|
2174
|
+
});
|
|
2175
|
+
return payload;
|
|
2176
|
+
};
|
|
2177
|
+
});
|
|
2145
2178
|
const $ZodUnknown$1 = /*@__PURE__*/ $constructor$1("$ZodUnknown", (inst, def) => {
|
|
2146
2179
|
$ZodType$1.init(inst, def);
|
|
2147
2180
|
inst._zod.parse = (payload) => payload;
|
|
@@ -2158,6 +2191,31 @@ const $ZodNever$1 = /*@__PURE__*/ $constructor$1("$ZodNever", (inst, def) => {
|
|
|
2158
2191
|
return payload;
|
|
2159
2192
|
};
|
|
2160
2193
|
});
|
|
2194
|
+
// an oversized container fails its leading size checks before any element is parsed
|
|
2195
|
+
function capSize(inst, size) {
|
|
2196
|
+
const caps = sizeCaps(inst._zod.def.checks);
|
|
2197
|
+
if (!caps.length)
|
|
2198
|
+
return;
|
|
2199
|
+
const cap = Math.min(...caps.map((c) => c[0]));
|
|
2200
|
+
const parse = inst._zod.parse;
|
|
2201
|
+
inst._zod.parse = (payload, ctx) => {
|
|
2202
|
+
const n = size(payload.value);
|
|
2203
|
+
if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
|
|
2204
|
+
for (const [max, ch] of sizeCaps(inst._zod.def.checks)) {
|
|
2205
|
+
if (n > max && ch._zod.def.when(payload)) {
|
|
2206
|
+
const start = payload.issues.length;
|
|
2207
|
+
ch._zod.check(payload);
|
|
2208
|
+
// the value is still the raw input, so no later check may read it
|
|
2209
|
+
payload.issues[start].continue = false;
|
|
2210
|
+
attachSchema(payload.issues, start, inst);
|
|
2211
|
+
return payload;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
return parse(payload, ctx);
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
const arraySize = (input) => (Array.isArray(input) ? input.length : -1);
|
|
2161
2219
|
function handleArrayResult$1(result, final, index) {
|
|
2162
2220
|
if (result.issues.length) {
|
|
2163
2221
|
final.issues.push(...prefixIssues$1(index, result.issues));
|
|
@@ -2203,6 +2261,7 @@ const $ZodArray$1 = /*@__PURE__*/ $constructor$1("$ZodArray", (inst, def) => {
|
|
|
2203
2261
|
}
|
|
2204
2262
|
return payload; //handleArrayResultsAsync(parseResults, final);
|
|
2205
2263
|
};
|
|
2264
|
+
capSize(inst, arraySize);
|
|
2206
2265
|
});
|
|
2207
2266
|
function handlePropertyResult$1(result, final, key, input, optin, optout) {
|
|
2208
2267
|
const isPresent = key in input;
|
|
@@ -2628,7 +2687,7 @@ function mergeValues$1(a, b) {
|
|
|
2628
2687
|
if (a instanceof Date && b instanceof Date && +a === +b) {
|
|
2629
2688
|
return { valid: true, data: a };
|
|
2630
2689
|
}
|
|
2631
|
-
if (isPlainObject$
|
|
2690
|
+
if (isPlainObject$2(a) && isPlainObject$2(b)) {
|
|
2632
2691
|
const bKeys = Object.keys(b);
|
|
2633
2692
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
2634
2693
|
const newObj = { ...a, ...b };
|
|
@@ -2730,7 +2789,7 @@ const $ZodRecord$1 = /*@__PURE__*/ $constructor$1("$ZodRecord", (inst, def) => {
|
|
|
2730
2789
|
memo?.attach(inst);
|
|
2731
2790
|
inst._zod.parse = (payload, ctx) => {
|
|
2732
2791
|
const input = payload.value;
|
|
2733
|
-
if (!isPlainObject$
|
|
2792
|
+
if (!isPlainObject$2(input)) {
|
|
2734
2793
|
payload.issues.push({
|
|
2735
2794
|
expected: "record",
|
|
2736
2795
|
code: "invalid_type",
|
|
@@ -3166,6 +3225,60 @@ function handlePipeResult$1(left, next, ctx) {
|
|
|
3166
3225
|
}
|
|
3167
3226
|
return next._zod.run({ value: left.value, issues: left.issues }, ctx);
|
|
3168
3227
|
}
|
|
3228
|
+
const $ZodCodec = /*@__PURE__*/ $constructor$1("$ZodCodec", (inst, def) => {
|
|
3229
|
+
$ZodType$1.init(inst, def);
|
|
3230
|
+
defineLazyInternal(inst, "values", (zod) => zod.def.in._zod.values);
|
|
3231
|
+
defineLazyInternal(inst, "optin", (zod) => zod.def.in._zod.optin);
|
|
3232
|
+
defineLazyInternal(inst, "optout", (zod) => zod.def.out._zod.optout);
|
|
3233
|
+
defineLazyInternal(inst, "propValues", (zod) => zod.def.in._zod.propValues);
|
|
3234
|
+
inst._zod.parse = (payload, ctx) => {
|
|
3235
|
+
const direction = ctx.direction || "forward";
|
|
3236
|
+
if (direction === "forward") {
|
|
3237
|
+
const left = def.in._zod.run(payload, ctx);
|
|
3238
|
+
if (left instanceof Promise) {
|
|
3239
|
+
return left.then((left) => handleCodecAResult(left, def, ctx));
|
|
3240
|
+
}
|
|
3241
|
+
return handleCodecAResult(left, def, ctx);
|
|
3242
|
+
}
|
|
3243
|
+
else {
|
|
3244
|
+
const right = def.out._zod.run(payload, ctx);
|
|
3245
|
+
if (right instanceof Promise) {
|
|
3246
|
+
return right.then((right) => handleCodecAResult(right, def, ctx));
|
|
3247
|
+
}
|
|
3248
|
+
return handleCodecAResult(right, def, ctx);
|
|
3249
|
+
}
|
|
3250
|
+
};
|
|
3251
|
+
});
|
|
3252
|
+
function handleCodecAResult(result, def, ctx) {
|
|
3253
|
+
if (result.issues.length) {
|
|
3254
|
+
// prevent further checks
|
|
3255
|
+
result.aborted = true;
|
|
3256
|
+
return result;
|
|
3257
|
+
}
|
|
3258
|
+
const direction = ctx.direction || "forward";
|
|
3259
|
+
if (direction === "forward") {
|
|
3260
|
+
const transformed = def.transform(result.value, result);
|
|
3261
|
+
if (transformed instanceof Promise) {
|
|
3262
|
+
return transformed.then((value) => handleCodecTxResult(result, value, def.out, ctx));
|
|
3263
|
+
}
|
|
3264
|
+
return handleCodecTxResult(result, transformed, def.out, ctx);
|
|
3265
|
+
}
|
|
3266
|
+
else {
|
|
3267
|
+
const transformed = def.reverseTransform(result.value, result);
|
|
3268
|
+
if (transformed instanceof Promise) {
|
|
3269
|
+
return transformed.then((value) => handleCodecTxResult(result, value, def.in, ctx));
|
|
3270
|
+
}
|
|
3271
|
+
return handleCodecTxResult(result, transformed, def.in, ctx);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
function handleCodecTxResult(left, value, nextSchema, ctx) {
|
|
3275
|
+
// Check if transform added any issues
|
|
3276
|
+
if (left.issues.length) {
|
|
3277
|
+
left.aborted = true;
|
|
3278
|
+
return left;
|
|
3279
|
+
}
|
|
3280
|
+
return nextSchema._zod.run({ value, issues: left.issues }, ctx);
|
|
3281
|
+
}
|
|
3169
3282
|
const $ZodReadonly$1 = /*@__PURE__*/ $constructor$1("$ZodReadonly", (inst, def) => {
|
|
3170
3283
|
$ZodType$1.init(inst, def);
|
|
3171
3284
|
defineLazyInternal(inst, "propValues", (zod) => zod.def.innerType._zod.propValues);
|
|
@@ -3994,6 +4107,13 @@ function _boolean$1(Class, params) {
|
|
|
3994
4107
|
});
|
|
3995
4108
|
}
|
|
3996
4109
|
// @__NO_SIDE_EFFECTS__
|
|
4110
|
+
function _null$4(Class, params) {
|
|
4111
|
+
return new Class({
|
|
4112
|
+
type: "null",
|
|
4113
|
+
...normalizeParams$1(params),
|
|
4114
|
+
});
|
|
4115
|
+
}
|
|
4116
|
+
// @__NO_SIDE_EFFECTS__
|
|
3997
4117
|
function _unknown$1(Class) {
|
|
3998
4118
|
return new Class({
|
|
3999
4119
|
type: "unknown",
|
|
@@ -4214,6 +4334,63 @@ function _check$1(fn, params) {
|
|
|
4214
4334
|
ch._zod.check = fn;
|
|
4215
4335
|
return ch;
|
|
4216
4336
|
}
|
|
4337
|
+
// @__NO_SIDE_EFFECTS__
|
|
4338
|
+
function _stringbool(Classes, _params) {
|
|
4339
|
+
const params = normalizeParams$1(_params);
|
|
4340
|
+
let truthyArray = params.truthy ?? ["true", "1", "yes", "on", "y", "enabled"];
|
|
4341
|
+
let falsyArray = params.falsy ?? ["false", "0", "no", "off", "n", "disabled"];
|
|
4342
|
+
if (params.case !== "sensitive") {
|
|
4343
|
+
truthyArray = truthyArray.map((v) => (typeof v === "string" ? v.toLowerCase() : v));
|
|
4344
|
+
falsyArray = falsyArray.map((v) => (typeof v === "string" ? v.toLowerCase() : v));
|
|
4345
|
+
}
|
|
4346
|
+
const truthySet = new Set(truthyArray);
|
|
4347
|
+
const falsySet = new Set(falsyArray);
|
|
4348
|
+
const _Codec = Classes.Codec ?? $ZodCodec;
|
|
4349
|
+
const _Boolean = Classes.Boolean ?? $ZodBoolean$1;
|
|
4350
|
+
const _String = Classes.String ?? $ZodString$1;
|
|
4351
|
+
const stringSchema = new _String({ type: "string", error: params.error });
|
|
4352
|
+
const booleanSchema = new _Boolean({ type: "boolean", error: params.error });
|
|
4353
|
+
const codec = new _Codec({
|
|
4354
|
+
type: "pipe",
|
|
4355
|
+
in: stringSchema,
|
|
4356
|
+
out: booleanSchema,
|
|
4357
|
+
transform: ((input, payload) => {
|
|
4358
|
+
let data = input;
|
|
4359
|
+
if (params.case !== "sensitive")
|
|
4360
|
+
data = data.toLowerCase();
|
|
4361
|
+
if (truthySet.has(data)) {
|
|
4362
|
+
return true;
|
|
4363
|
+
}
|
|
4364
|
+
else if (falsySet.has(data)) {
|
|
4365
|
+
return false;
|
|
4366
|
+
}
|
|
4367
|
+
else {
|
|
4368
|
+
payload.issues.push({
|
|
4369
|
+
code: "invalid_value",
|
|
4370
|
+
expected: "stringbool",
|
|
4371
|
+
values: [...truthySet, ...falsySet],
|
|
4372
|
+
input: payload.value,
|
|
4373
|
+
inst: codec,
|
|
4374
|
+
continue: false,
|
|
4375
|
+
});
|
|
4376
|
+
return {};
|
|
4377
|
+
}
|
|
4378
|
+
}),
|
|
4379
|
+
reverseTransform: ((input, _payload) => {
|
|
4380
|
+
if (input === true) {
|
|
4381
|
+
return truthyArray[0] || "true";
|
|
4382
|
+
}
|
|
4383
|
+
else {
|
|
4384
|
+
return falsyArray[0] || "false";
|
|
4385
|
+
}
|
|
4386
|
+
}),
|
|
4387
|
+
error: params.error,
|
|
4388
|
+
});
|
|
4389
|
+
codec._zod.bag.truthy = truthyArray;
|
|
4390
|
+
codec._zod.bag.falsy = falsyArray;
|
|
4391
|
+
codec._zod.bag.case = params.case ?? "insensitive";
|
|
4392
|
+
return codec;
|
|
4393
|
+
}
|
|
4217
4394
|
|
|
4218
4395
|
function assignProps(target, ...sources) {
|
|
4219
4396
|
for (const source of sources) {
|
|
@@ -5053,6 +5230,16 @@ const numberProcessor$1 = (schema, ctx, _json, params) => {
|
|
|
5053
5230
|
const booleanProcessor$1 = (_schema, _ctx, json, _params) => {
|
|
5054
5231
|
json.type = "boolean";
|
|
5055
5232
|
};
|
|
5233
|
+
const nullProcessor$1 = (_schema, ctx, json, _params) => {
|
|
5234
|
+
if (ctx.target === "openapi-3.0") {
|
|
5235
|
+
json.type = "string";
|
|
5236
|
+
json.nullable = true;
|
|
5237
|
+
json.enum = [null];
|
|
5238
|
+
}
|
|
5239
|
+
else {
|
|
5240
|
+
json.type = "null";
|
|
5241
|
+
}
|
|
5242
|
+
};
|
|
5056
5243
|
const neverProcessor$1 = (_schema, _ctx, json, _params) => {
|
|
5057
5244
|
json.not = {};
|
|
5058
5245
|
};
|
|
@@ -6063,6 +6250,14 @@ const ZodBoolean$1 = /*@__PURE__*/ $constructor$1("ZodBoolean", (inst, def) => {
|
|
|
6063
6250
|
function boolean$2(params) {
|
|
6064
6251
|
return _boolean$1(ZodBoolean$1, params);
|
|
6065
6252
|
}
|
|
6253
|
+
const ZodNull$1 = /*@__PURE__*/ $constructor$1("ZodNull", (inst, def) => {
|
|
6254
|
+
$ZodNull$1.init(inst, def);
|
|
6255
|
+
ZodType$1.init(inst, def);
|
|
6256
|
+
inst._zod.processJSONSchema = (ctx, json, params) => nullProcessor$1(inst, ctx, json);
|
|
6257
|
+
});
|
|
6258
|
+
function _null$3(params) {
|
|
6259
|
+
return _null$4(ZodNull$1, params);
|
|
6260
|
+
}
|
|
6066
6261
|
const ZodUnknown$1 = /*@__PURE__*/ $constructor$1("ZodUnknown", (inst, def) => {
|
|
6067
6262
|
$ZodUnknown$1.init(inst, def);
|
|
6068
6263
|
ZodType$1.init(inst, def);
|
|
@@ -6164,6 +6359,24 @@ function object$1(shape, params) {
|
|
|
6164
6359
|
};
|
|
6165
6360
|
return new ZodObject$1(def);
|
|
6166
6361
|
}
|
|
6362
|
+
// strictObject
|
|
6363
|
+
function strictObject(shape, params) {
|
|
6364
|
+
return new ZodObject$1({
|
|
6365
|
+
type: "object",
|
|
6366
|
+
shape,
|
|
6367
|
+
catchall: never$1(),
|
|
6368
|
+
...normalizeParams$1(params),
|
|
6369
|
+
});
|
|
6370
|
+
}
|
|
6371
|
+
// looseObject
|
|
6372
|
+
function looseObject(shape, params) {
|
|
6373
|
+
return new ZodObject$1({
|
|
6374
|
+
type: "object",
|
|
6375
|
+
shape,
|
|
6376
|
+
catchall: unknown$1(),
|
|
6377
|
+
...normalizeParams$1(params),
|
|
6378
|
+
});
|
|
6379
|
+
}
|
|
6167
6380
|
const ZodUnion$1 = /*@__PURE__*/ $constructor$1("ZodUnion", (inst, def) => {
|
|
6168
6381
|
$ZodUnion$1.init(inst, def);
|
|
6169
6382
|
ZodType$1.init(inst, def);
|
|
@@ -6436,6 +6649,10 @@ function pipe$1(in_, out) {
|
|
|
6436
6649
|
// ...util.normalizeParams(params),
|
|
6437
6650
|
});
|
|
6438
6651
|
}
|
|
6652
|
+
const ZodCodec = /*@__PURE__*/ $constructor$1("ZodCodec", (inst, def) => {
|
|
6653
|
+
ZodPipe$1.init(inst, def);
|
|
6654
|
+
$ZodCodec.init(inst, def);
|
|
6655
|
+
});
|
|
6439
6656
|
const ZodReadonly$1 = /*@__PURE__*/ $constructor$1("ZodReadonly", (inst, def) => {
|
|
6440
6657
|
$ZodReadonly$1.init(inst, def);
|
|
6441
6658
|
ZodType$1.init(inst, def);
|
|
@@ -6460,6 +6677,12 @@ function refine$1(fn, _params = {}) {
|
|
|
6460
6677
|
function superRefine$1(fn, params) {
|
|
6461
6678
|
return _superRefine$1(fn, params);
|
|
6462
6679
|
}
|
|
6680
|
+
// stringbool
|
|
6681
|
+
const stringbool = (...args) => _stringbool({
|
|
6682
|
+
Codec: ZodCodec,
|
|
6683
|
+
Boolean: ZodBoolean$1,
|
|
6684
|
+
String: ZodString$1,
|
|
6685
|
+
}, ...args);
|
|
6463
6686
|
|
|
6464
6687
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
6465
6688
|
|
|
@@ -23900,7 +24123,7 @@ const allowsEval = cached(() => {
|
|
|
23900
24123
|
return false;
|
|
23901
24124
|
}
|
|
23902
24125
|
});
|
|
23903
|
-
function isPlainObject(o) {
|
|
24126
|
+
function isPlainObject$1(o) {
|
|
23904
24127
|
if (isObject(o) === false)
|
|
23905
24128
|
return false;
|
|
23906
24129
|
// modified constructor
|
|
@@ -23920,7 +24143,7 @@ function isPlainObject(o) {
|
|
|
23920
24143
|
return true;
|
|
23921
24144
|
}
|
|
23922
24145
|
function shallowClone(o) {
|
|
23923
|
-
if (isPlainObject(o))
|
|
24146
|
+
if (isPlainObject$1(o))
|
|
23924
24147
|
return { ...o };
|
|
23925
24148
|
if (Array.isArray(o))
|
|
23926
24149
|
return [...o];
|
|
@@ -24016,7 +24239,7 @@ function omit(schema, mask) {
|
|
|
24016
24239
|
return clone(schema, def);
|
|
24017
24240
|
}
|
|
24018
24241
|
function extend(schema, shape) {
|
|
24019
|
-
if (!isPlainObject(shape)) {
|
|
24242
|
+
if (!isPlainObject$1(shape)) {
|
|
24020
24243
|
throw new Error("Invalid input to extend: expected a plain object");
|
|
24021
24244
|
}
|
|
24022
24245
|
const checks = schema._zod.def.checks;
|
|
@@ -24041,7 +24264,7 @@ function extend(schema, shape) {
|
|
|
24041
24264
|
return clone(schema, def);
|
|
24042
24265
|
}
|
|
24043
24266
|
function safeExtend(schema, shape) {
|
|
24044
|
-
if (!isPlainObject(shape)) {
|
|
24267
|
+
if (!isPlainObject$1(shape)) {
|
|
24045
24268
|
throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
24046
24269
|
}
|
|
24047
24270
|
const def = mergeDefs(schema._zod.def, {
|
|
@@ -25782,7 +26005,7 @@ function mergeValues(a, b) {
|
|
|
25782
26005
|
if (a instanceof Date && b instanceof Date && +a === +b) {
|
|
25783
26006
|
return { valid: true, data: a };
|
|
25784
26007
|
}
|
|
25785
|
-
if (isPlainObject(a) && isPlainObject(b)) {
|
|
26008
|
+
if (isPlainObject$1(a) && isPlainObject$1(b)) {
|
|
25786
26009
|
const bKeys = Object.keys(b);
|
|
25787
26010
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
25788
26011
|
const newObj = { ...a, ...b };
|
|
@@ -25866,7 +26089,7 @@ const $ZodRecord = /*@__PURE__*/ $constructor("$ZodRecord", (inst, def) => {
|
|
|
25866
26089
|
$ZodType.init(inst, def);
|
|
25867
26090
|
inst._zod.parse = (payload, ctx) => {
|
|
25868
26091
|
const input = payload.value;
|
|
25869
|
-
if (!isPlainObject(input)) {
|
|
26092
|
+
if (!isPlainObject$1(input)) {
|
|
25870
26093
|
payload.issues.push({
|
|
25871
26094
|
expected: "record",
|
|
25872
26095
|
code: "invalid_type",
|
|
@@ -28439,270 +28662,1735 @@ object({
|
|
|
28439
28662
|
});
|
|
28440
28663
|
|
|
28441
28664
|
/**
|
|
28442
|
-
* @module
|
|
28443
|
-
*
|
|
28444
|
-
* Consumed by both the service and plugin packages.
|
|
28665
|
+
* @module schemas/sources
|
|
28666
|
+
* Zod schemas for configured `sources` (named callback endpoints for non-file documents), source references and the callback contract. No I/O.
|
|
28445
28667
|
*/
|
|
28446
|
-
/**
|
|
28447
|
-
|
|
28448
|
-
|
|
28449
|
-
|
|
28450
|
-
|
|
28451
|
-
|
|
28452
|
-
|
|
28453
|
-
|
|
28454
|
-
|
|
28455
|
-
(
|
|
28456
|
-
|
|
28668
|
+
/**
|
|
28669
|
+
* Body of a source name pattern (shared by the name and reference
|
|
28670
|
+
* patterns): a lowercase letter, then 1-31 of `[a-z0-9-]`.
|
|
28671
|
+
*/
|
|
28672
|
+
const SOURCE_NAME_BODY = '[a-z][a-z0-9-]{1,31}';
|
|
28673
|
+
/** Pattern every source name must match (≥ 2 chars, so never a drive letter). */
|
|
28674
|
+
const SOURCE_NAME_PATTERN = new RegExp(`^${SOURCE_NAME_BODY}$`);
|
|
28675
|
+
/**
|
|
28676
|
+
* Pattern of a source reference: `<name>://<id>`, where `<id>` is 1–512
|
|
28677
|
+
* non-whitespace characters (`/`-separated segments allowed).
|
|
28678
|
+
*/
|
|
28679
|
+
const SOURCE_REFERENCE_PATTERN = new RegExp(`^${SOURCE_NAME_BODY}://\\S{1,512}$`);
|
|
28680
|
+
/** Placeholder in a source `url` template that is replaced with the document id. */
|
|
28681
|
+
const SOURCE_ID_PLACEHOLDER = '{id}';
|
|
28682
|
+
/** A source name, e.g. `app`. */
|
|
28683
|
+
const sourceNameSchema = string$2()
|
|
28684
|
+
.regex(SOURCE_NAME_PATTERN, `Source name must match ${SOURCE_NAME_PATTERN.source}`)
|
|
28685
|
+
.describe('Source name: lowercase letter, then 1-31 of [a-z0-9-].');
|
|
28686
|
+
/** A source reference, e.g. `app://chat/thread/123`. */
|
|
28687
|
+
const sourceReferenceSchema = string$2()
|
|
28688
|
+
.regex(SOURCE_REFERENCE_PATTERN, `Source reference must match <name>://<id> (name ${SOURCE_NAME_PATTERN.source}, id 1-512 non-whitespace characters)`)
|
|
28689
|
+
.describe('Source document reference: <name>://<id>.');
|
|
28690
|
+
/** Configuration of a single named source. */
|
|
28691
|
+
const sourceConfigSchema = object$1({
|
|
28692
|
+
/** Callback URL template; must contain `{id}`. */
|
|
28693
|
+
url: string$2()
|
|
28694
|
+
.min(1)
|
|
28695
|
+
.refine((url) => url.includes(SOURCE_ID_PLACEHOLDER), {
|
|
28696
|
+
message: `url must contain the ${SOURCE_ID_PLACEHOLDER} placeholder`,
|
|
28697
|
+
})
|
|
28698
|
+
.describe('Callback URL template. {id} is replaced with the document id (each /-segment URL-encoded, slashes kept). Supports ${ENV_VAR} substitution.'),
|
|
28699
|
+
/** Request headers sent with every callback (e.g. Authorization). */
|
|
28700
|
+
headers: record$1(string$2(), string$2())
|
|
28701
|
+
.optional()
|
|
28702
|
+
.describe('Headers sent with every callback request (supports ${ENV_VAR} substitution).'),
|
|
28703
|
+
/** Callback timeout in milliseconds. */
|
|
28704
|
+
timeoutMs: number$2()
|
|
28705
|
+
.int()
|
|
28706
|
+
.positive()
|
|
28707
|
+
.default(10000)
|
|
28708
|
+
.describe('Callback timeout in milliseconds. Default: 10000.'),
|
|
28709
|
+
/** Maximum parallel callbacks for this source. */
|
|
28710
|
+
concurrency: number$2()
|
|
28711
|
+
.int()
|
|
28712
|
+
.positive()
|
|
28713
|
+
.default(8)
|
|
28714
|
+
.describe('Maximum parallel callbacks for this source (e.g. during reindex). Default: 8.'),
|
|
28715
|
+
});
|
|
28716
|
+
/** The `sources` config section: source name → source configuration. */
|
|
28717
|
+
const sourcesConfigSchema = record$1(sourceNameSchema, sourceConfigSchema)
|
|
28718
|
+
.describe('Named sources for non-file documents, retrieved by callback. Keys are source names used in <name>://<id> references. Read at startup only.');
|
|
28719
|
+
/**
|
|
28720
|
+
* A source document as returned by a source callback: a `GET` to the
|
|
28721
|
+
* configured URL answered with `200` and an `application/json` body. The body
|
|
28722
|
+
* is the record itself, as the owner stores it, with no envelope: any JSON
|
|
28723
|
+
* object. A `404` means the document no longer exists (see
|
|
28724
|
+
* {@link SOURCE_DOCUMENT_GONE_STATUS}); any other status is a transient failure.
|
|
28725
|
+
*/
|
|
28726
|
+
record$1(string$2(), unknown$1())
|
|
28727
|
+
.describe('A source document: any JSON object returned by the callback.');
|
|
28457
28728
|
|
|
28458
28729
|
/**
|
|
28459
|
-
*
|
|
28460
|
-
*
|
|
28461
|
-
|
|
28462
|
-
|
|
28730
|
+
* @module api/common
|
|
28731
|
+
* Shared Zod building blocks for API request/response schemas: Qdrant filters, error bodies and path-or-source document targets. No I/O.
|
|
28732
|
+
*/
|
|
28733
|
+
/**
|
|
28734
|
+
* A native Qdrant filter object schema (passed through to Qdrant
|
|
28735
|
+
* unchanged), optionally with the message reported for a missing or
|
|
28736
|
+
* non-object value.
|
|
28463
28737
|
*
|
|
28738
|
+
* @param error - Message for a missing or non-object filter.
|
|
28739
|
+
* @returns The filter schema.
|
|
28464
28740
|
*/
|
|
28741
|
+
const qdrantFilter = (error) => record$1(string$2(), unknown$1(), error === undefined ? undefined : { error })
|
|
28742
|
+
.describe('Native Qdrant filter object.');
|
|
28743
|
+
/** A native Qdrant filter object (passed through to Qdrant unchanged). */
|
|
28744
|
+
const qdrantFilterSchema = qdrantFilter();
|
|
28745
|
+
/** Message for a missing or non-object required `filter` (scan, points delete). */
|
|
28746
|
+
const FILTER_REQUIRED_MESSAGE = 'Missing required field: filter (object)';
|
|
28747
|
+
/** Boolean clauses of a Qdrant filter whose value is a condition list. */
|
|
28748
|
+
const FILTER_CLAUSES = ['must', 'should', 'must_not'];
|
|
28749
|
+
/** Every key a Qdrant filter object may carry (clauses plus `min_should`). */
|
|
28750
|
+
const FILTER_KEYS = new Set([...FILTER_CLAUSES, 'min_should']);
|
|
28465
28751
|
/**
|
|
28466
|
-
*
|
|
28752
|
+
* Whether a value is a plain object (not `null`, not an array). The one
|
|
28753
|
+
* definition shared by core and the service.
|
|
28467
28754
|
*
|
|
28468
|
-
*
|
|
28469
|
-
*
|
|
28470
|
-
* descriptors rather than hard-coding paths and descriptions.
|
|
28755
|
+
* @param value - Any value.
|
|
28756
|
+
* @returns `true` for a non-null, non-array object.
|
|
28471
28757
|
*/
|
|
28472
|
-
const
|
|
28473
|
-
|
|
28474
|
-
|
|
28475
|
-
|
|
28476
|
-
|
|
28477
|
-
|
|
28478
|
-
|
|
28479
|
-
|
|
28480
|
-
|
|
28481
|
-
|
|
28482
|
-
|
|
28483
|
-
|
|
28484
|
-
|
|
28485
|
-
|
|
28486
|
-
|
|
28487
|
-
|
|
28488
|
-
|
|
28489
|
-
|
|
28490
|
-
|
|
28491
|
-
|
|
28492
|
-
|
|
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.',
|
|
28580
|
-
},
|
|
28581
|
-
{
|
|
28582
|
-
name: 'pointsDelete',
|
|
28583
|
-
method: 'POST',
|
|
28584
|
-
path: '/points/delete',
|
|
28585
|
-
description: 'Delete points from the vector store by filter.',
|
|
28586
|
-
},
|
|
28587
|
-
{
|
|
28588
|
-
name: 'rulesReapply',
|
|
28589
|
-
method: 'POST',
|
|
28590
|
-
path: '/rules/reapply',
|
|
28591
|
-
description: 'Re-apply inference rules to existing vector points.',
|
|
28592
|
-
},
|
|
28593
|
-
{
|
|
28594
|
-
name: 'vcsStatus',
|
|
28595
|
-
method: 'GET',
|
|
28596
|
-
path: '/vcs/status',
|
|
28597
|
-
description: 'VCS state for all roots: enabled state, tracked files, last commit, remote status, commit circuit breaker state.',
|
|
28598
|
-
},
|
|
28599
|
-
{
|
|
28600
|
-
name: 'vcsHistory',
|
|
28601
|
-
method: 'GET',
|
|
28602
|
-
path: '/vcs/history',
|
|
28603
|
-
description: 'Query change history by path or glob with optional date range.',
|
|
28604
|
-
},
|
|
28605
|
-
{
|
|
28606
|
-
name: 'vcsShow',
|
|
28607
|
-
method: 'GET',
|
|
28608
|
-
path: '/vcs/show',
|
|
28609
|
-
description: 'Retrieve file content at a specific version.',
|
|
28610
|
-
},
|
|
28611
|
-
{
|
|
28612
|
-
name: 'vcsDiff',
|
|
28613
|
-
method: 'GET',
|
|
28614
|
-
path: '/vcs/diff',
|
|
28615
|
-
description: 'Show what changed between two versions, or between a version and current.',
|
|
28616
|
-
},
|
|
28617
|
-
{
|
|
28618
|
-
name: 'vcsCheckExclusion',
|
|
28619
|
-
method: 'GET',
|
|
28620
|
-
path: '/vcs/check-exclusion',
|
|
28621
|
-
description: 'Check whether a path is excluded from version tracking and why.',
|
|
28622
|
-
},
|
|
28623
|
-
{
|
|
28624
|
-
name: 'vcsRevert',
|
|
28625
|
-
method: 'POST',
|
|
28626
|
-
path: '/vcs/revert',
|
|
28627
|
-
description: 'Restore files from a historical commit (forward-only reversion).',
|
|
28628
|
-
},
|
|
28629
|
-
{
|
|
28630
|
-
name: 'vcsExclude',
|
|
28631
|
-
method: 'POST',
|
|
28632
|
-
path: '/vcs/exclude',
|
|
28633
|
-
description: 'Manage .gitignore entries for version tracking exclusion.',
|
|
28634
|
-
},
|
|
28635
|
-
];
|
|
28758
|
+
const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28759
|
+
/**
|
|
28760
|
+
* Whether a condition restricts anything. `{}` (an empty nested filter)
|
|
28761
|
+
* does not; a nested filter (only clause keys) restricts when it has a
|
|
28762
|
+
* condition itself; anything else (field, `is_empty`, `has_id`, `nested`, …,
|
|
28763
|
+
* or a malformed value Qdrant will reject) counts as a condition.
|
|
28764
|
+
*/
|
|
28765
|
+
function isEffectiveCondition(condition) {
|
|
28766
|
+
if (!isPlainObject(condition))
|
|
28767
|
+
return true;
|
|
28768
|
+
const keys = Object.keys(condition);
|
|
28769
|
+
if (keys.length === 0)
|
|
28770
|
+
return false;
|
|
28771
|
+
return keys.every((key) => FILTER_KEYS.has(key))
|
|
28772
|
+
? hasFilterCondition(condition)
|
|
28773
|
+
: true;
|
|
28774
|
+
}
|
|
28775
|
+
/** Whether a clause value (a condition or a list of them) has a condition. */
|
|
28776
|
+
const clauseHasCondition = (value) => Array.isArray(value)
|
|
28777
|
+
? value.some(isEffectiveCondition)
|
|
28778
|
+
: isPlainObject(value) && isEffectiveCondition(value);
|
|
28636
28779
|
/**
|
|
28637
|
-
*
|
|
28780
|
+
* Whether a Qdrant filter contains at least one condition. `{}`,
|
|
28781
|
+
* `{ must: [] }`, filters whose clauses are all empty (recursively) and
|
|
28782
|
+
* filters with only unknown keys are condition-free: they would match every
|
|
28783
|
+
* point.
|
|
28638
28784
|
*
|
|
28639
|
-
* @param
|
|
28640
|
-
* @returns
|
|
28785
|
+
* @param filter - A Qdrant filter object.
|
|
28786
|
+
* @returns True when the filter restricts the points it matches.
|
|
28641
28787
|
*/
|
|
28642
|
-
function
|
|
28643
|
-
|
|
28644
|
-
|
|
28645
|
-
|
|
28646
|
-
|
|
28788
|
+
function hasFilterCondition(filter) {
|
|
28789
|
+
if (FILTER_CLAUSES.some((clause) => clauseHasCondition(filter[clause]))) {
|
|
28790
|
+
return true;
|
|
28791
|
+
}
|
|
28792
|
+
const minShould = filter.min_should;
|
|
28793
|
+
return isPlainObject(minShould) && clauseHasCondition(minShould.conditions);
|
|
28647
28794
|
}
|
|
28648
|
-
|
|
28649
28795
|
/**
|
|
28650
|
-
*
|
|
28651
|
-
*
|
|
28796
|
+
* A Qdrant filter that must contain at least one condition (see
|
|
28797
|
+
* {@link hasFilterCondition}); used where a match-everything filter would be
|
|
28798
|
+
* destructive (`POST /documents/delete`).
|
|
28652
28799
|
*/
|
|
28800
|
+
const nonEmptyQdrantFilterSchema = qdrantFilterSchema.refine(hasFilterCondition, { message: 'filter must contain at least one condition' });
|
|
28653
28801
|
/**
|
|
28654
|
-
*
|
|
28802
|
+
* Error body shape common to every watcher version: `error` plus optional
|
|
28803
|
+
* `message` and `details`, no `code`. Responses from 0.20.0+ also carry a
|
|
28804
|
+
* machine-readable `code`; `apiErrorBodySchema` (module `api/errors`)
|
|
28805
|
+
* extends this schema with it.
|
|
28655
28806
|
*/
|
|
28656
|
-
const
|
|
28657
|
-
/**
|
|
28658
|
-
|
|
28659
|
-
|
|
28660
|
-
|
|
28661
|
-
/**
|
|
28662
|
-
|
|
28663
|
-
.default('anthropic')
|
|
28664
|
-
.describe('AI provider for commit message generation. Default: "anthropic".'),
|
|
28665
|
-
/** AI model for commit message generation. */
|
|
28666
|
-
model: string$2()
|
|
28667
|
-
.default('claude-haiku-4-0')
|
|
28668
|
-
.describe('AI model for commit message generation. Default: "claude-haiku-4-0".'),
|
|
28669
|
-
/** API key for commit message provider. Supports env var substitution. */
|
|
28670
|
-
apiKey: string$2()
|
|
28671
|
-
.optional()
|
|
28672
|
-
.describe('API key for commit message provider. Supports env var substitution (e.g., "${ANTHROPIC_API_KEY}").'),
|
|
28807
|
+
const apiErrorResponseSchema = object$1({
|
|
28808
|
+
/** Short error description. */
|
|
28809
|
+
error: string$2(),
|
|
28810
|
+
/** Optional longer explanation. */
|
|
28811
|
+
message: string$2().optional(),
|
|
28812
|
+
/** Optional structured details (e.g. Zod issues for a 400). */
|
|
28813
|
+
details: unknown$1().optional(),
|
|
28673
28814
|
});
|
|
28674
28815
|
/**
|
|
28675
|
-
*
|
|
28816
|
+
* Error detail attached to a per-document result or rejection (`code` is a
|
|
28817
|
+
* machine-readable error code, e.g. an issue type).
|
|
28676
28818
|
*/
|
|
28677
|
-
const
|
|
28678
|
-
/**
|
|
28679
|
-
|
|
28680
|
-
|
|
28681
|
-
|
|
28682
|
-
|
|
28683
|
-
|
|
28684
|
-
|
|
28685
|
-
|
|
28686
|
-
|
|
28687
|
-
|
|
28688
|
-
|
|
28689
|
-
|
|
28690
|
-
|
|
28691
|
-
|
|
28692
|
-
|
|
28693
|
-
|
|
28694
|
-
|
|
28819
|
+
const documentErrorSchema = object$1({
|
|
28820
|
+
/** Machine-readable error code. */
|
|
28821
|
+
code: string$2(),
|
|
28822
|
+
/** Human-readable error message. */
|
|
28823
|
+
message: string$2(),
|
|
28824
|
+
/** Optional structured details (e.g. AJV errors). */
|
|
28825
|
+
details: unknown$1().optional(),
|
|
28826
|
+
});
|
|
28827
|
+
/** A file path target (non-empty). */
|
|
28828
|
+
const filePathSchema = string$2()
|
|
28829
|
+
.min(1, 'path must be a non-empty string')
|
|
28830
|
+
.describe('Absolute or watched file path.');
|
|
28831
|
+
/** Fields addressing a single document by file path or source reference. */
|
|
28832
|
+
const documentTargetShape = {
|
|
28833
|
+
/** File path of a file document. */
|
|
28834
|
+
path: filePathSchema.optional(),
|
|
28835
|
+
/** Source reference of a source document (`<name>://<id>`). */
|
|
28836
|
+
source: sourceReferenceSchema.optional(),
|
|
28837
|
+
};
|
|
28695
28838
|
/**
|
|
28696
|
-
*
|
|
28839
|
+
* Refinement requiring exactly one of `path` or `source`.
|
|
28840
|
+
*
|
|
28841
|
+
* @param val - The object being validated.
|
|
28842
|
+
* @param ctx - Zod refinement context.
|
|
28697
28843
|
*/
|
|
28698
|
-
|
|
28699
|
-
|
|
28700
|
-
|
|
28701
|
-
|
|
28702
|
-
|
|
28703
|
-
|
|
28704
|
-
|
|
28705
|
-
|
|
28844
|
+
function requireExactlyOneTarget(val, ctx) {
|
|
28845
|
+
if ((val.path === undefined) === (val.source === undefined)) {
|
|
28846
|
+
ctx.addIssue({
|
|
28847
|
+
code: 'custom',
|
|
28848
|
+
path: val.path === undefined ? ['path'] : [],
|
|
28849
|
+
message: 'Exactly one of path or source is required',
|
|
28850
|
+
});
|
|
28851
|
+
}
|
|
28852
|
+
}
|
|
28853
|
+
|
|
28854
|
+
/**
|
|
28855
|
+
* @module api/admin
|
|
28856
|
+
* 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.
|
|
28857
|
+
*/
|
|
28858
|
+
/** POST /points/delete request body. */
|
|
28859
|
+
const pointsDeleteRequestSchema = object$1({
|
|
28860
|
+
/** Native Qdrant filter selecting the points to delete. */
|
|
28861
|
+
filter: qdrantFilter(FILTER_REQUIRED_MESSAGE),
|
|
28862
|
+
});
|
|
28863
|
+
/** A virtual-rule source name: a non-empty string. */
|
|
28864
|
+
const ruleSourceSchema = string$2({ error: 'Missing required field: source' })
|
|
28865
|
+
.min(1, { error: 'Missing required field: source' });
|
|
28866
|
+
const RULE_NAME_REQUIRED = 'Every rule must be an object with a name';
|
|
28867
|
+
/** POST /rules/register request body. */
|
|
28868
|
+
const rulesRegisterRequestSchema = object$1({
|
|
28869
|
+
/** Registering source (e.g. a plugin name). */
|
|
28870
|
+
source: ruleSourceSchema,
|
|
28871
|
+
/** Inference rules; each needs a non-empty `name` (the rest is validated on registration). */
|
|
28872
|
+
rules: array$1(looseObject({
|
|
28873
|
+
name: string$2({ error: RULE_NAME_REQUIRED })
|
|
28874
|
+
.min(1, { error: RULE_NAME_REQUIRED }),
|
|
28875
|
+
}, { error: RULE_NAME_REQUIRED }), { error: 'Missing required field: rules (array)' }),
|
|
28876
|
+
});
|
|
28877
|
+
/** DELETE /rules/unregister request body. */
|
|
28878
|
+
const rulesUnregisterRequestSchema = object$1({
|
|
28879
|
+
/** Source whose rules are removed. */
|
|
28880
|
+
source: ruleSourceSchema,
|
|
28881
|
+
});
|
|
28882
|
+
/** POST /rules/reapply request body. */
|
|
28883
|
+
const rulesReapplyRequestSchema = object$1({
|
|
28884
|
+
/** Globs selecting the indexed files to re-apply rules to. */
|
|
28885
|
+
globs: array$1(string$2().min(1), {
|
|
28886
|
+
error: 'Missing required field: globs (non-empty string array)',
|
|
28887
|
+
})
|
|
28888
|
+
.min(1, {
|
|
28889
|
+
error: 'Missing required field: globs (non-empty string array)',
|
|
28890
|
+
}),
|
|
28891
|
+
});
|
|
28892
|
+
/** POST /points/delete response body: how many points were deleted. */
|
|
28893
|
+
const pointsDeleteResponseSchema = object$1({
|
|
28894
|
+
deleted: number$2().int().nonnegative(),
|
|
28895
|
+
});
|
|
28896
|
+
/** POST /rules/register response body. */
|
|
28897
|
+
const rulesRegisterResponseSchema = object$1({
|
|
28898
|
+
source: string$2(),
|
|
28899
|
+
registered: number$2().int().nonnegative(),
|
|
28900
|
+
totalVirtualRules: number$2().int().nonnegative(),
|
|
28901
|
+
});
|
|
28902
|
+
/** Response body of both rule unregister routes. */
|
|
28903
|
+
const rulesUnregisterResponseSchema = object$1({
|
|
28904
|
+
source: string$2(),
|
|
28905
|
+
removed: boolean$2(),
|
|
28906
|
+
});
|
|
28907
|
+
/** Path parameters of DELETE /rules/unregister/:source. */
|
|
28908
|
+
const rulesUnregisterParamsSchema = object$1({
|
|
28909
|
+
source: ruleSourceSchema,
|
|
28910
|
+
});
|
|
28911
|
+
/** POST /rules/reapply response body. */
|
|
28912
|
+
const rulesReapplyResponseSchema = object$1({
|
|
28913
|
+
matched: number$2().int().nonnegative(),
|
|
28914
|
+
updated: number$2().int().nonnegative(),
|
|
28915
|
+
});
|
|
28916
|
+
/** POST /rebuild-metadata request body: takes no fields. */
|
|
28917
|
+
const rebuildMetadataRequestSchema = object$1({});
|
|
28918
|
+
/** POST /rebuild-metadata response body. */
|
|
28919
|
+
const rebuildMetadataResponseSchema = object$1({ ok: literal(true) });
|
|
28920
|
+
|
|
28921
|
+
/**
|
|
28922
|
+
* @module api/changeSource
|
|
28923
|
+
* Zod schema and type of `GET /status` `health.changeSource` (D133, D157): the change source and reconciliation counters. No I/O.
|
|
28924
|
+
*/
|
|
28925
|
+
/**
|
|
28926
|
+
* Why a process exited for a service-manager restart (D196): a watch config
|
|
28927
|
+
* change that needs a watcher rebuild, an error on a live parcel
|
|
28928
|
+
* subscription, or a watched root gone. Windows parcel only.
|
|
28929
|
+
*/
|
|
28930
|
+
const restartCauseSchema = _enum$1(['watch-config-changed', 'subscription-error', 'root-gone'])
|
|
28931
|
+
.describe('Why the previous process exited for a service-manager restart (D196).');
|
|
28932
|
+
/**
|
|
28933
|
+
* Change-source health in `GET /status` `health.changeSource` (D133, D157):
|
|
28934
|
+
* counts and timestamps only, never paths (D89).
|
|
28935
|
+
*/
|
|
28936
|
+
const changeSourceHealthSchema = object$1({
|
|
28937
|
+
/** The running change source. */
|
|
28938
|
+
source: _enum$1(['chokidar', 'parcel'])
|
|
28939
|
+
.describe('The running change source.'),
|
|
28940
|
+
/** Subscriptions re-created after an error since start (D131). */
|
|
28941
|
+
resubscribes: number$2()
|
|
28942
|
+
.describe('Subscriptions re-created after an error since start (D131); scheduled renewals are not counted. Always 0 under chokidar.'),
|
|
28943
|
+
/** Roots currently unavailable (D109). */
|
|
28944
|
+
unavailableRoots: number$2()
|
|
28945
|
+
.describe('Roots currently marked unavailable (D109).'),
|
|
28946
|
+
/** Synthetic events reconciliation has emitted since start (D90). */
|
|
28947
|
+
reconcileEvents: number$2()
|
|
28948
|
+
.describe('Synthetic events reconciliation has emitted since start (D90): a rising value means the source is missing events.'),
|
|
28949
|
+
/** Reconciles that errored and emitted nothing since start (D90). */
|
|
28950
|
+
reconcileErrors: number$2()
|
|
28951
|
+
.describe('Reconciles that errored (walk failure, unreachable root, empty root walk against a non-empty inventory) and emitted nothing, since start (D90).'),
|
|
28952
|
+
/** End of the most recent reconcile. */
|
|
28953
|
+
lastReconcileAt: string$2()
|
|
28954
|
+
.nullable()
|
|
28955
|
+
.describe('End of the most recent reconcile (ISO), null before the first.'),
|
|
28956
|
+
/** Duration of the most recent reconcile (walk and compare). */
|
|
28957
|
+
lastReconcileDurationMs: number$2()
|
|
28958
|
+
.nullable()
|
|
28959
|
+
.describe('Duration of the most recent reconcile, walk and compare (ms), null before the first.'),
|
|
28960
|
+
/** Paths in the stability ticker's pending set (D128). */
|
|
28961
|
+
pendingStability: number$2()
|
|
28962
|
+
.describe("Paths in the stability ticker's pending set (D128); a backlog that keeps growing means the ticker is falling behind."),
|
|
28963
|
+
/** Duration of the most recent subscription renewal (D129). */
|
|
28964
|
+
lastRenewDurationMs: number$2()
|
|
28965
|
+
.nullable()
|
|
28966
|
+
.describe('Duration of the most recent subscription renewal (ms, D129); null until the first, and always null under chokidar and on Windows.'),
|
|
28967
|
+
/** Renewals that forced a source rebuild since start (D165). */
|
|
28968
|
+
renewFailures: number$2()
|
|
28969
|
+
.describe('Renewals whose unsubscribe rejected and forced a source rebuild, plus rejections during a rebuild, since start (D165, D176). Always 0 under chokidar.'),
|
|
28970
|
+
/** Why the previous process exited for a restart (D196). */
|
|
28971
|
+
lastRestartCause: restartCauseSchema
|
|
28972
|
+
.nullable()
|
|
28973
|
+
.describe('Why the previous process exited for a service-manager restart (D196: on Windows parcel never unsubscribes, so a watcher rebuild or a subscription recovery restarts the process); null when this process did not start after such an exit.'),
|
|
28974
|
+
/** When the previous process asked for that restart. */
|
|
28975
|
+
lastRestartAt: string$2()
|
|
28976
|
+
.nullable()
|
|
28977
|
+
.describe('When the previous process asked for that restart (ISO); null with lastRestartCause.'),
|
|
28978
|
+
})
|
|
28979
|
+
.describe('Change-source health (D133, D157, D196).');
|
|
28980
|
+
|
|
28981
|
+
/**
|
|
28982
|
+
* @module api/configRoutes
|
|
28983
|
+
* Request and response schemas for the config inspection routes: `GET /config/schema`, `POST /config/match` and `POST /config/validate`. Envelopes are closed objects; user content (the JSON Schema document, test-path metadata, a candidate config) is an open record (spec D51). No I/O.
|
|
28984
|
+
*/
|
|
28985
|
+
/** A config location and what is wrong (or notable) there. */
|
|
28986
|
+
const configIssueSchema = object$1({
|
|
28987
|
+
path: string$2(),
|
|
28988
|
+
message: string$2(),
|
|
28989
|
+
});
|
|
28990
|
+
/** Response body of `GET /config/schema`: the merged config JSON Schema document. */
|
|
28991
|
+
const getConfigSchemaResponseSchema = record$1(string$2(), unknown$1())
|
|
28992
|
+
.describe('JSON Schema (draft 2020-12) for the watcher config.');
|
|
28993
|
+
/** Request body of `POST /config/match`. */
|
|
28994
|
+
const matchConfigRequestSchema = object$1({
|
|
28995
|
+
paths: array$1(string$2(), { error: 'Request body must include "paths" array' })
|
|
28996
|
+
.describe('File paths to test against the watch scope and rules.'),
|
|
28997
|
+
});
|
|
28998
|
+
/** Response body of `POST /config/match`: one entry per requested path, in order. */
|
|
28999
|
+
const matchConfigResponseSchema = object$1({
|
|
29000
|
+
matches: array$1(object$1({
|
|
29001
|
+
rules: array$1(string$2()),
|
|
29002
|
+
watched: boolean$2(),
|
|
29003
|
+
})),
|
|
29004
|
+
});
|
|
29005
|
+
/** Request body of `POST /config/validate`. */
|
|
29006
|
+
const validateConfigRequestSchema = object$1({
|
|
29007
|
+
config: record$1(string$2(), unknown$1())
|
|
29008
|
+
.optional()
|
|
29009
|
+
.describe('Partial config merged over the current one; omit to check it.'),
|
|
29010
|
+
testPaths: array$1(string$2())
|
|
29011
|
+
.optional()
|
|
29012
|
+
.describe('Paths to run through the candidate config.'),
|
|
29013
|
+
});
|
|
29014
|
+
/** Response body of `POST /config/validate`. */
|
|
29015
|
+
const validateConfigResponseSchema = object$1({
|
|
29016
|
+
valid: boolean$2(),
|
|
29017
|
+
errors: array$1(configIssueSchema).optional(),
|
|
29018
|
+
warnings: array$1(configIssueSchema).optional(),
|
|
29019
|
+
testResults: array$1(object$1({
|
|
29020
|
+
path: string$2(),
|
|
29021
|
+
matchedRules: array$1(string$2()),
|
|
29022
|
+
metadata: record$1(string$2(), unknown$1()),
|
|
29023
|
+
error: string$2().optional(),
|
|
29024
|
+
}))
|
|
29025
|
+
.optional(),
|
|
29026
|
+
});
|
|
29027
|
+
|
|
29028
|
+
/**
|
|
29029
|
+
* @module api/documentOps
|
|
29030
|
+
* Zod request/response schemas for single-document operations POST /render and POST /metadata (path or source target). No I/O.
|
|
29031
|
+
*/
|
|
29032
|
+
/** Request body for `POST /render`: exactly one of `path` or `source`. */
|
|
29033
|
+
const renderRequestSchema = object$1(documentTargetShape)
|
|
29034
|
+
.superRefine(requireExactlyOneTarget);
|
|
29035
|
+
/** Response body for `POST /render`. */
|
|
29036
|
+
const renderResponseSchema = object$1({
|
|
29037
|
+
/** Output format (file extension without dot). */
|
|
29038
|
+
renderAs: string$2(),
|
|
29039
|
+
/** Rendered (or passthrough) content. */
|
|
29040
|
+
content: string$2(),
|
|
29041
|
+
/** Names of the rules that matched. */
|
|
29042
|
+
rules: array$1(string$2()),
|
|
29043
|
+
/** Resulting metadata. */
|
|
29044
|
+
metadata: record$1(string$2(), unknown$1()),
|
|
29045
|
+
});
|
|
29046
|
+
/** Request body for `POST /metadata`: exactly one of `path` or `source`, plus metadata. */
|
|
29047
|
+
const metadataRequestSchema = object$1({
|
|
29048
|
+
...documentTargetShape,
|
|
29049
|
+
/** Enrichment metadata to merge into the document's points. */
|
|
29050
|
+
metadata: record$1(string$2(), unknown$1(), {
|
|
29051
|
+
error: 'Missing required field: metadata (object)',
|
|
29052
|
+
}),
|
|
29053
|
+
})
|
|
29054
|
+
.superRefine(requireExactlyOneTarget);
|
|
29055
|
+
/** Response body for `POST /metadata`. */
|
|
29056
|
+
const metadataResponseSchema = object$1({
|
|
29057
|
+
/** Always true on success. */
|
|
29058
|
+
ok: literal(true),
|
|
29059
|
+
/** Rules whose schemas validated the metadata. */
|
|
29060
|
+
matched_rules: array$1(string$2()),
|
|
29061
|
+
});
|
|
29062
|
+
|
|
29063
|
+
/**
|
|
29064
|
+
* @module api/indexCheck
|
|
29065
|
+
* Zod schema and type of `GET /status` `health.indexCheck` (D197): the state and counts of the index check that runs once after the start walk. No I/O.
|
|
29066
|
+
*/
|
|
29067
|
+
/**
|
|
29068
|
+
* State of the index check at start (D197): `pending` until the start walk
|
|
29069
|
+
* completes, `running`, then `done` or `failed` (the index could not be
|
|
29070
|
+
* listed, or the watcher stopped or was rebuilt first; nothing more is
|
|
29071
|
+
* deleted); `off` when `watch.indexCheckOnStart` is `false`.
|
|
29072
|
+
*/
|
|
29073
|
+
const indexCheckStateSchema = _enum$1(['pending', 'running', 'done', 'failed', 'off'])
|
|
29074
|
+
.describe('Index check state: pending (the start walk is still running), running, done, failed (the index could not be listed, or the watcher stopped or was rebuilt first; nothing more is deleted) or off (watch.indexCheckOnStart is false).');
|
|
29075
|
+
/**
|
|
29076
|
+
* Index-check health in `GET /status` `health.indexCheck` (D197): counts
|
|
29077
|
+
* and timestamps only, never paths (D89).
|
|
29078
|
+
*/
|
|
29079
|
+
const indexCheckHealthSchema = object$1({
|
|
29080
|
+
/** Where the check is. */
|
|
29081
|
+
state: indexCheckStateSchema,
|
|
29082
|
+
/** When it ended. */
|
|
29083
|
+
at: string$2()
|
|
29084
|
+
.nullable()
|
|
29085
|
+
.describe('When the check ended (ISO), null until it is done or failed.'),
|
|
29086
|
+
/** How long it took. */
|
|
29087
|
+
durationMs: number$2()
|
|
29088
|
+
.nullable()
|
|
29089
|
+
.describe('How long the check took, listing and deletes (ms), null until it is done or failed.'),
|
|
29090
|
+
/** File documents compared with the inventory. */
|
|
29091
|
+
checked: number$2()
|
|
29092
|
+
.describe('Indexed file documents compared with the inventory, under the roots that were checked.'),
|
|
29093
|
+
/** Documents deleted. */
|
|
29094
|
+
removed: number$2()
|
|
29095
|
+
.describe('Indexed file documents deleted because their file is gone (deleted or moved away while the service was down).'),
|
|
29096
|
+
/** Roots not checked. */
|
|
29097
|
+
skippedRoots: number$2()
|
|
29098
|
+
.describe('Watch roots not checked: unavailable, gone, unwatched, or whose start walk failed.'),
|
|
29099
|
+
/** Roots over the cap. */
|
|
29100
|
+
capped: number$2()
|
|
29101
|
+
.describe('Watch roots where more than 10% of the indexed documents (once more than 20 are missing) or more than 1,000 were missing from the inventory: nothing was deleted there; run reindex prune deliberately.'),
|
|
29102
|
+
})
|
|
29103
|
+
.describe('Index check at start (D197).');
|
|
29104
|
+
|
|
29105
|
+
/**
|
|
29106
|
+
* @module api/monitoring
|
|
29107
|
+
* Zod response schemas for the read-only monitoring endpoints GET /status and GET /issues, including the issue record. No I/O.
|
|
29108
|
+
*/
|
|
29109
|
+
/** Response body for `GET /status` (standard Jeeves component status). */
|
|
29110
|
+
object$1({
|
|
29111
|
+
/** Component name (`watcher`). */
|
|
29112
|
+
name: string$2(),
|
|
29113
|
+
/** Service version. */
|
|
29114
|
+
version: string$2(),
|
|
29115
|
+
/** Whole seconds since the API server (its status handler) was created. */
|
|
29116
|
+
uptime: number$2(),
|
|
29117
|
+
/** Overall status. */
|
|
29118
|
+
status: _enum$1(['healthy', 'degraded', 'unhealthy']),
|
|
29119
|
+
/** Component health: collection, reindex and initial-scan state. */
|
|
29120
|
+
health: record$1(string$2(), unknown$1()),
|
|
29121
|
+
});
|
|
29122
|
+
/** Reindex state in `GET /status` `health.reindex`. */
|
|
29123
|
+
const reindexStatusSchema = object$1({
|
|
29124
|
+
/** Whether a reindex is in progress. */
|
|
29125
|
+
active: boolean$2(),
|
|
29126
|
+
/** Active reindex scope (while active). */
|
|
29127
|
+
scope: string$2().optional(),
|
|
29128
|
+
/** ISO 8601 start time (while active). */
|
|
29129
|
+
startedAt: string$2().optional(),
|
|
29130
|
+
/** Files processed so far (while active). */
|
|
29131
|
+
filesProcessed: number$2().optional(),
|
|
29132
|
+
/** Files to process (while active). */
|
|
29133
|
+
totalFiles: number$2().optional(),
|
|
29134
|
+
});
|
|
29135
|
+
/** Initial filesystem scan state in `GET /status` `health.initialScan`. */
|
|
29136
|
+
const initialScanStatusSchema = object$1({
|
|
29137
|
+
/** Whether the initial scan is in progress. */
|
|
29138
|
+
active: boolean$2(),
|
|
29139
|
+
/** Files matched by the watch globs. */
|
|
29140
|
+
filesMatched: number$2().optional(),
|
|
29141
|
+
/** Files enqueued so far. */
|
|
29142
|
+
filesEnqueued: number$2().optional(),
|
|
29143
|
+
/** ISO 8601 start time. */
|
|
29144
|
+
startedAt: string$2().optional(),
|
|
29145
|
+
/** ISO 8601 completion time. */
|
|
29146
|
+
completedAt: string$2().optional(),
|
|
29147
|
+
/** Scan duration in milliseconds. */
|
|
29148
|
+
durationMs: number$2().optional(),
|
|
29149
|
+
});
|
|
29150
|
+
/** VCS state in `GET /status` `health.vcs` (D86): roots, backlog, tripped breakers. */
|
|
29151
|
+
const vcsHealthSchema = object$1({
|
|
29152
|
+
/** VCS-enabled roots. */
|
|
29153
|
+
roots: number$2(),
|
|
29154
|
+
/** Files queued or in flight, not yet committed, across roots. */
|
|
29155
|
+
pending: number$2(),
|
|
29156
|
+
/** Roots whose commit circuit breaker is tripped. */
|
|
29157
|
+
trippedRoots: number$2(),
|
|
29158
|
+
/** ISO 8601 time the longest-tripped breaker tripped, or null. */
|
|
29159
|
+
oldestTrippedAt: string$2().nullable(),
|
|
29160
|
+
});
|
|
29161
|
+
/**
|
|
29162
|
+
* State of one system payload index (D74, D154, D155): `building` until its
|
|
29163
|
+
* background creation returns, then `ready`; `conflict` when the field
|
|
29164
|
+
* already has another index type (left alone); `failed` when creation
|
|
29165
|
+
* failed (retried at the next start).
|
|
29166
|
+
*/
|
|
29167
|
+
const systemIndexStateSchema = _enum$1(['building', 'ready', 'conflict', 'failed'])
|
|
29168
|
+
.describe('System payload index state: building (background creation running), ready, conflict (the field has another index type; left alone) or failed (creation failed; retried at the next start).');
|
|
29169
|
+
/** System payload index states by field, in `health.collection.systemIndexes`. */
|
|
29170
|
+
const systemIndexesSchema = record$1(string$2(), systemIndexStateSchema)
|
|
29171
|
+
.describe('System payload indexes the service creates whatever payloadIndexes says (chunk_index integer, file_path and matched_rules keyword), by field.');
|
|
29172
|
+
/**
|
|
29173
|
+
* Embedding health in `GET /status` `health.embedding` (D88, D89): state and
|
|
29174
|
+
* timestamps with error codes only, never provider text or paths.
|
|
29175
|
+
*/
|
|
29176
|
+
const embeddingHealthSchema = object$1({
|
|
29177
|
+
/** `ok`, or `failing` (3+ failures and 60 s without a success). */
|
|
29178
|
+
state: _enum$1(['ok', 'failing'])
|
|
29179
|
+
.describe('ok, or failing: at least 3 embedding failures since the last success and 60 s without one. The first success returns it to ok.'),
|
|
29180
|
+
/** Last successful call. */
|
|
29181
|
+
lastSuccessAt: string$2()
|
|
29182
|
+
.nullable()
|
|
29183
|
+
.describe('Last successful embedding call (ISO), null before the first.'),
|
|
29184
|
+
/** Most recent error. */
|
|
29185
|
+
lastError: object$1({
|
|
29186
|
+
at: string$2().describe('When (ISO).'),
|
|
29187
|
+
code: string$2()
|
|
29188
|
+
.describe('rate_limited, timeout, http_<status>, network or error (no provider text).'),
|
|
29189
|
+
})
|
|
29190
|
+
.nullable()
|
|
29191
|
+
.describe('Most recent embedding error, null if none yet.'),
|
|
29192
|
+
/** When the state turned failing. */
|
|
29193
|
+
failingSince: string$2()
|
|
29194
|
+
.nullable()
|
|
29195
|
+
.describe('When the state turned failing (ISO), null while ok.'),
|
|
29196
|
+
/** End of the stored provider back-off. */
|
|
29197
|
+
backoffUntil: string$2()
|
|
29198
|
+
.nullable()
|
|
29199
|
+
.describe('End of the stored provider back-off (ISO, at most 5 minutes ahead), null if none.'),
|
|
29200
|
+
})
|
|
29201
|
+
.describe('Embedding provider health (D88, D89).');
|
|
29202
|
+
/**
|
|
29203
|
+
* `GET /status` `health`: collection, reindex and initial-scan state, or
|
|
29204
|
+
* `{ error }` when the vector store is unreachable (status `degraded`).
|
|
29205
|
+
* Loose and optional throughout, so new health keys pass through.
|
|
29206
|
+
*/
|
|
29207
|
+
const watcherHealthSchema = looseObject({
|
|
29208
|
+
/** Vector store collection. */
|
|
29209
|
+
collection: object$1({
|
|
29210
|
+
/** Collection name. */
|
|
29211
|
+
name: string$2(),
|
|
29212
|
+
/** Indexed points. */
|
|
29213
|
+
pointCount: number$2(),
|
|
29214
|
+
/** Vector dimensions. */
|
|
29215
|
+
dimensions: number$2(),
|
|
29216
|
+
/** System payload index states (D74, D154, D155). */
|
|
29217
|
+
systemIndexes: systemIndexesSchema.optional(),
|
|
29218
|
+
})
|
|
29219
|
+
.optional(),
|
|
29220
|
+
/** Reindex state. */
|
|
29221
|
+
reindex: reindexStatusSchema.optional(),
|
|
29222
|
+
/** Initial scan state (once the scan tracker exists). */
|
|
29223
|
+
initialScan: initialScanStatusSchema.optional(),
|
|
29224
|
+
/** VCS state (when VCS is enabled on any root). */
|
|
29225
|
+
vcs: vcsHealthSchema.optional(),
|
|
29226
|
+
/** Embedding health (D88, D89). */
|
|
29227
|
+
embedding: embeddingHealthSchema.optional(),
|
|
29228
|
+
/** Change-source health (D133, D157). */
|
|
29229
|
+
changeSource: changeSourceHealthSchema.optional(),
|
|
29230
|
+
/** Index check at start (D197). */
|
|
29231
|
+
indexCheck: indexCheckHealthSchema.optional(),
|
|
29232
|
+
/** Why health could not be read (degraded). */
|
|
29233
|
+
error: string$2().optional(),
|
|
29234
|
+
});
|
|
29235
|
+
/**
|
|
29236
|
+
* Canonical issue codes, in one list (spec Decision 16.4).
|
|
29237
|
+
*
|
|
29238
|
+
* `type_collision` and `interpolation_error` predate 0.20.0; the rest cover
|
|
29239
|
+
* processing failures (#255) and source documents; `too_large` marks a
|
|
29240
|
+
* document over a `processing` ceiling (D44).
|
|
29241
|
+
*/
|
|
29242
|
+
const ISSUE_TYPES = [
|
|
29243
|
+
'type_collision',
|
|
29244
|
+
'interpolation_error',
|
|
29245
|
+
'extraction_error',
|
|
29246
|
+
'embedding_error',
|
|
29247
|
+
'upsert_error',
|
|
29248
|
+
'match_failed',
|
|
29249
|
+
'rule_missing',
|
|
29250
|
+
'no_content',
|
|
29251
|
+
'source_unavailable',
|
|
29252
|
+
'unsupported_content_type',
|
|
29253
|
+
'too_large',
|
|
29254
|
+
];
|
|
29255
|
+
/** Schema for the canonical issue code carried by every issue record. */
|
|
29256
|
+
const issueTypeSchema = _enum$1(ISSUE_TYPES)
|
|
29257
|
+
.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), too_large (over a processing ceiling; its points were deleted).');
|
|
29258
|
+
/** Schema for a single issue record tracking a processing failure. */
|
|
29259
|
+
const issueRecordSchema = object$1({
|
|
29260
|
+
/** Canonical issue code. */
|
|
29261
|
+
type: issueTypeSchema,
|
|
29262
|
+
/** Operation that failed (e.g. `Failed to process file`). */
|
|
29263
|
+
operation: string$2().optional(),
|
|
29264
|
+
/** Property name where the issue occurred. */
|
|
29265
|
+
property: string$2().optional(),
|
|
29266
|
+
/** Name(s) of the rule(s) involved in the issue. */
|
|
29267
|
+
rules: array$1(string$2()).optional(),
|
|
29268
|
+
/**
|
|
29269
|
+
* Rule name for single-rule issues; for a source document, its designated
|
|
29270
|
+
* rule (so an `issues` reindex can retry a document that has no points).
|
|
29271
|
+
*/
|
|
29272
|
+
rule: string$2().optional(),
|
|
29273
|
+
/** Declared types for type collision issues. */
|
|
29274
|
+
types: array$1(string$2()).optional(),
|
|
29275
|
+
/** Error message describing the failure. */
|
|
29276
|
+
message: string$2(),
|
|
29277
|
+
/** Optional structured details (e.g. the AJV errors of `match_failed`); JSON. */
|
|
29278
|
+
details: unknown$1().optional(),
|
|
29279
|
+
/** Unix timestamp (seconds) or ISO string of the last occurrence. */
|
|
29280
|
+
timestamp: union$1([number$2(), string$2()]),
|
|
29281
|
+
});
|
|
29282
|
+
/** Response body for `GET /issues`: issue records keyed by document key. */
|
|
29283
|
+
const issuesResponseSchema = object$1({
|
|
29284
|
+
/** Number of documents with issues. */
|
|
29285
|
+
count: number$2().int().nonnegative(),
|
|
29286
|
+
/** Issue records keyed by document key (file path or source reference). */
|
|
29287
|
+
issues: record$1(string$2(), array$1(issueRecordSchema)),
|
|
29288
|
+
});
|
|
29289
|
+
|
|
29290
|
+
/**
|
|
29291
|
+
* @module api/documents
|
|
29292
|
+
* Zod request/response schemas for source-document notification (POST /documents, 202 or ?wait=true) and deletion (POST /documents/delete). No I/O.
|
|
29293
|
+
*/
|
|
29294
|
+
/** Maximum number of documents in one `POST /documents` request. */
|
|
29295
|
+
const MAX_DOCUMENTS_PER_REQUEST = 100;
|
|
29296
|
+
/** Maximum number of references in one `POST /documents/delete` request. */
|
|
29297
|
+
const MAX_DELETE_SOURCES_PER_REQUEST = 1000;
|
|
29298
|
+
/**
|
|
29299
|
+
* The `documents` array of a notify request: 1 to
|
|
29300
|
+
* {@link MAX_DOCUMENTS_PER_REQUEST} items of the given schema.
|
|
29301
|
+
*
|
|
29302
|
+
* @param item - Schema of one document.
|
|
29303
|
+
* @returns The bounded array schema.
|
|
29304
|
+
*/
|
|
29305
|
+
const documentsArray = (item) => array$1(item)
|
|
29306
|
+
.min(1, 'documents must contain at least one entry')
|
|
29307
|
+
.max(MAX_DOCUMENTS_PER_REQUEST, `documents must contain at most ${String(MAX_DOCUMENTS_PER_REQUEST)} entries`);
|
|
29308
|
+
/**
|
|
29309
|
+
* A document notification: the reference of a changed source document and
|
|
29310
|
+
* the rule that indexes it. Notify only: content is always retrieved through
|
|
29311
|
+
* the source callback, so unknown keys (e.g. inline content) are rejected.
|
|
29312
|
+
*/
|
|
29313
|
+
const documentNotificationSchema = strictObject({
|
|
29314
|
+
/** Source reference (`<name>://<id>`). */
|
|
29315
|
+
source: sourceReferenceSchema,
|
|
29316
|
+
/** Name of the inference rule that indexes the document. */
|
|
29317
|
+
rule: string$2().min(1, 'rule must be a non-empty string'),
|
|
29318
|
+
});
|
|
29319
|
+
/** Request body for `POST /documents`. */
|
|
29320
|
+
object$1({
|
|
29321
|
+
/** Documents to (re)index: 1 to {@link MAX_DOCUMENTS_PER_REQUEST}. */
|
|
29322
|
+
documents: documentsArray(documentNotificationSchema),
|
|
29323
|
+
});
|
|
29324
|
+
/**
|
|
29325
|
+
* Envelope the service parses `POST /documents` with: the shape of
|
|
29326
|
+
* {@link documentsRequestSchema}, but `source` is any non-empty string, so a
|
|
29327
|
+
* malformed reference rejects only its own document (`invalid_reference` in
|
|
29328
|
+
* `rejected`) instead of the whole request. Shape errors (count, missing or
|
|
29329
|
+
* empty `rule`, unknown keys) still fail the request with `400`.
|
|
29330
|
+
*/
|
|
29331
|
+
const documentsEnvelopeSchema = object$1({
|
|
29332
|
+
/** Documents to (re)index: 1 to {@link MAX_DOCUMENTS_PER_REQUEST}. */
|
|
29333
|
+
documents: documentsArray(documentNotificationSchema.extend({
|
|
29334
|
+
/** Reference as submitted; validated per document. */
|
|
29335
|
+
source: string$2().min(1, 'source must be a non-empty string'),
|
|
29336
|
+
})),
|
|
29337
|
+
});
|
|
29338
|
+
/**
|
|
29339
|
+
* Codes of notifications rejected by synchronous validation:
|
|
29340
|
+
* `invalid_reference` (not `<name>://<id>`), `unknown_source` (name not in
|
|
29341
|
+
* config `sources`), `invalid_id` (empty, `.` or `..` id segment),
|
|
29342
|
+
* `rule_missing` (no rule of that name in the merged rule set) and
|
|
29343
|
+
* `queue_stopped` (the service is shutting down).
|
|
29344
|
+
*/
|
|
29345
|
+
const documentRejectionCodeSchema = _enum$1([
|
|
29346
|
+
'invalid_reference',
|
|
29347
|
+
'unknown_source',
|
|
29348
|
+
'invalid_id',
|
|
29349
|
+
'rule_missing',
|
|
29350
|
+
'queue_stopped',
|
|
29351
|
+
]);
|
|
29352
|
+
/**
|
|
29353
|
+
* Query string for `POST /documents`. `wait` accepts the strings
|
|
29354
|
+
* `true`/`false`/`1`/`0` (a query string never carries a JSON boolean).
|
|
29355
|
+
*/
|
|
29356
|
+
const documentsQuerySchema = object$1({
|
|
29357
|
+
/** Process synchronously and return per-document results. */
|
|
29358
|
+
wait: stringbool({ truthy: ['true', '1'], falsy: ['false', '0'] })
|
|
29359
|
+
.optional()
|
|
29360
|
+
.describe('Process synchronously and answer 200 with per-document results: true or 1 (false or 0, the default, answers 202).'),
|
|
29361
|
+
});
|
|
29362
|
+
/** Why a notification was rejected: a typed code plus a message. */
|
|
29363
|
+
const documentRejectionErrorSchema = documentErrorSchema
|
|
29364
|
+
.omit({ details: true })
|
|
29365
|
+
.extend({
|
|
29366
|
+
/** Rejection code. */
|
|
29367
|
+
code: documentRejectionCodeSchema,
|
|
29368
|
+
});
|
|
29369
|
+
/** A notification rejected by synchronous validation. */
|
|
29370
|
+
const documentRejectionSchema = object$1({
|
|
29371
|
+
/** The reference as submitted. */
|
|
29372
|
+
source: string$2(),
|
|
29373
|
+
/** Why it was rejected. */
|
|
29374
|
+
error: documentRejectionErrorSchema,
|
|
29375
|
+
});
|
|
29376
|
+
/** `202` response body for `POST /documents` (asynchronous, the default). */
|
|
29377
|
+
const documentsAcceptedResponseSchema = object$1({
|
|
29378
|
+
/** References enqueued for processing. */
|
|
29379
|
+
accepted: array$1(string$2()),
|
|
29380
|
+
/** Notifications rejected by synchronous validation. */
|
|
29381
|
+
rejected: array$1(documentRejectionSchema),
|
|
29382
|
+
});
|
|
29383
|
+
/** Outcome of processing one document synchronously. */
|
|
29384
|
+
const documentResultStatusSchema = _enum$1([
|
|
29385
|
+
'indexed',
|
|
29386
|
+
'unchanged',
|
|
29387
|
+
'deleted',
|
|
29388
|
+
'failed',
|
|
29389
|
+
]);
|
|
29390
|
+
/**
|
|
29391
|
+
* Code of a failed per-document result: the issue type of a processing
|
|
29392
|
+
* failure, or (with `?wait=true`) the rejection code of a notification that
|
|
29393
|
+
* failed synchronous validation.
|
|
29394
|
+
*/
|
|
29395
|
+
const documentFailureCodeSchema = union$1([
|
|
29396
|
+
issueTypeSchema,
|
|
29397
|
+
documentRejectionCodeSchema,
|
|
29398
|
+
]);
|
|
29399
|
+
/** Failure detail of a per-document result: typed code, message, optional details. */
|
|
29400
|
+
const documentFailureSchema = documentErrorSchema.extend({
|
|
29401
|
+
/** Issue type or rejection code. */
|
|
29402
|
+
code: documentFailureCodeSchema,
|
|
29403
|
+
});
|
|
29404
|
+
/** Per-document result of `POST /documents?wait=true`. */
|
|
29405
|
+
const documentResultSchema = object$1({
|
|
29406
|
+
/** The reference processed. */
|
|
29407
|
+
source: string$2(),
|
|
29408
|
+
/** Outcome. */
|
|
29409
|
+
status: documentResultStatusSchema,
|
|
29410
|
+
/** Number of chunks written (when indexed). */
|
|
29411
|
+
chunks: number$2().int().nonnegative().optional(),
|
|
29412
|
+
/** Rules that matched (when indexed or unchanged). */
|
|
29413
|
+
matched_rules: array$1(string$2()).optional(),
|
|
29414
|
+
/** Failure detail (when failed). */
|
|
29415
|
+
error: documentFailureSchema.optional(),
|
|
29416
|
+
});
|
|
29417
|
+
/** `200` response body for `POST /documents?wait=true`. */
|
|
29418
|
+
const documentsResultsResponseSchema = object$1({
|
|
29419
|
+
/** One result per submitted document. */
|
|
29420
|
+
results: array$1(documentResultSchema),
|
|
29421
|
+
});
|
|
29422
|
+
/**
|
|
29423
|
+
* Request body for `POST /documents/delete`: either explicit references or a
|
|
29424
|
+
* Qdrant filter with at least one condition (a condition-free filter such as
|
|
29425
|
+
* `{}` or `{ must: [] }` is rejected). The server constrains the filter to
|
|
29426
|
+
* points carrying `source` and no `file_path`.
|
|
29427
|
+
*
|
|
29428
|
+
* One strict object with both selectors optional and a refinement requiring
|
|
29429
|
+
* exactly one, because a catalog body must be a single Zod object (spec
|
|
29430
|
+
* Design 3). It accepts exactly the requests 0.20.0's two-shape union did.
|
|
29431
|
+
*/
|
|
29432
|
+
const documentsDeleteRequestSchema = strictObject({
|
|
29433
|
+
/** References of the documents to delete: 1 to {@link MAX_DELETE_SOURCES_PER_REQUEST}. */
|
|
29434
|
+
sources: array$1(sourceReferenceSchema)
|
|
29435
|
+
.min(1, 'sources must contain at least one reference')
|
|
29436
|
+
.max(MAX_DELETE_SOURCES_PER_REQUEST, `sources must contain at most ${String(MAX_DELETE_SOURCES_PER_REQUEST)} references`)
|
|
29437
|
+
.optional(),
|
|
29438
|
+
/** Qdrant filter selecting the documents to delete (at least one condition). */
|
|
29439
|
+
filter: nonEmptyQdrantFilterSchema.optional(),
|
|
29440
|
+
})
|
|
29441
|
+
.superRefine((val, ctx) => {
|
|
29442
|
+
if ((val.sources === undefined) === (val.filter === undefined)) {
|
|
29443
|
+
ctx.addIssue({
|
|
29444
|
+
code: 'custom',
|
|
29445
|
+
path: [],
|
|
29446
|
+
message: 'Exactly one of sources or filter is required',
|
|
29447
|
+
});
|
|
29448
|
+
}
|
|
29449
|
+
});
|
|
29450
|
+
/** Response body for `POST /documents/delete`. */
|
|
29451
|
+
const documentsDeleteResponseSchema = object$1({
|
|
29452
|
+
/** Number of documents (not points) removed. */
|
|
29453
|
+
deleted: number$2().int().nonnegative(),
|
|
29454
|
+
});
|
|
29455
|
+
|
|
29456
|
+
/**
|
|
29457
|
+
* @module api/errors
|
|
29458
|
+
* Canonical machine-readable API error codes (`apiErrorCodeSchema`) and the error body every non-2xx watcher response carries (`apiErrorBodySchema`). No I/O.
|
|
29459
|
+
*/
|
|
29460
|
+
/**
|
|
29461
|
+
* Every machine-readable code a non-2xx response can carry, in one list.
|
|
29462
|
+
* Codes are stable: new codes may be added, existing ones never change
|
|
29463
|
+
* meaning. The usual HTTP status is in {@link API_ERROR_STATUS}.
|
|
29464
|
+
*
|
|
29465
|
+
* - `invalid_request` (400): the body, query or parameters failed validation
|
|
29466
|
+
* (`details` holds the Zod issues when available), or the framework rejected
|
|
29467
|
+
* the request (malformed JSON, unsupported media type, body too large).
|
|
29468
|
+
* - `unauthorized` (401): `api.apiKey` is set and the bearer token is missing
|
|
29469
|
+
* or wrong.
|
|
29470
|
+
* - `path_not_watched` (403): the file path is outside the watched scope.
|
|
29471
|
+
* - `not_found` (404): no such route, file, VCS root, commit content or
|
|
29472
|
+
* exclude pattern.
|
|
29473
|
+
* - `source_not_indexed` (404): no stored point for the source reference, so
|
|
29474
|
+
* no designated rule is known.
|
|
29475
|
+
* - `source_not_found` (404): the source callback answered `404`.
|
|
29476
|
+
* - `unsupported_content_type` (422): the source callback returned a non-JSON
|
|
29477
|
+
* body.
|
|
29478
|
+
* - `pipeline_error` (422): rendering or extraction failed; for a source
|
|
29479
|
+
* document `issueType` names the failing stage.
|
|
29480
|
+
* - `confirm_required` (409): `POST /vcs/exclude` would untrack more than
|
|
29481
|
+
* 100 files; nothing was written, and `untracked` holds the count. Repeat
|
|
29482
|
+
* with `confirm: true`.
|
|
29483
|
+
* - `source_unavailable` (502): the source callback failed.
|
|
29484
|
+
* - `not_available` (503): the feature is not available on this instance
|
|
29485
|
+
* (no source support, no filesystem watcher), or `POST /vcs/exclude`
|
|
29486
|
+
* waited 30 s for the root's git lock (`Retry-After: 30`). A prune reindex
|
|
29487
|
+
* (`POST /reindex`) without a vector store keeps its 0.19 status `400`.
|
|
29488
|
+
* - `scan_in_progress` (503): the initial filesystem scan is still running;
|
|
29489
|
+
* retry later.
|
|
29490
|
+
* - `embedding_unavailable` (503): the embedding provider's retry budget ran
|
|
29491
|
+
* out; honour `Retry-After`.
|
|
29492
|
+
* - `internal_error` (500): an unexpected server-side failure.
|
|
29493
|
+
*/
|
|
29494
|
+
const API_ERROR_CODES = [
|
|
29495
|
+
'invalid_request',
|
|
29496
|
+
'unauthorized',
|
|
29497
|
+
'path_not_watched',
|
|
29498
|
+
'not_found',
|
|
29499
|
+
'source_not_indexed',
|
|
29500
|
+
'source_not_found',
|
|
29501
|
+
'unsupported_content_type',
|
|
29502
|
+
'pipeline_error',
|
|
29503
|
+
'confirm_required',
|
|
29504
|
+
'source_unavailable',
|
|
29505
|
+
'not_available',
|
|
29506
|
+
'scan_in_progress',
|
|
29507
|
+
'embedding_unavailable',
|
|
29508
|
+
'internal_error',
|
|
29509
|
+
];
|
|
29510
|
+
/** Schema for the machine-readable code of an API error response. */
|
|
29511
|
+
const apiErrorCodeSchema = _enum$1(API_ERROR_CODES)
|
|
29512
|
+
.describe('Machine-readable API error code.');
|
|
29513
|
+
/**
|
|
29514
|
+
* Body of every non-2xx watcher response (0.20.0+): the version-independent
|
|
29515
|
+
* `apiErrorResponseSchema` (`error`, `message`, `details`) extended with
|
|
29516
|
+
* `code` and `issueType`. `error` is the human-readable text earlier
|
|
29517
|
+
* versions sent, kept unchanged for backward compatibility; branch on
|
|
29518
|
+
* `code`, never on `error`. Endpoints may add further fields (e.g. `issues`
|
|
29519
|
+
* on `POST /config/apply`).
|
|
29520
|
+
*/
|
|
29521
|
+
apiErrorResponseSchema.extend({
|
|
29522
|
+
/** Machine-readable error code. */
|
|
29523
|
+
code: apiErrorCodeSchema,
|
|
29524
|
+
/** Issue type of the pipeline failure behind a source render error. */
|
|
29525
|
+
issueType: issueTypeSchema.optional(),
|
|
29526
|
+
});
|
|
29527
|
+
|
|
29528
|
+
/**
|
|
29529
|
+
* @module api/facets
|
|
29530
|
+
* Zod schemas for the facet endpoints: `GET /search/facets` (definitions, optionally scoped by `rules`) and `POST /search/facets/counts` (per-field value counts or ranges for a filter; spec Design 12). No I/O.
|
|
29531
|
+
*/
|
|
29532
|
+
/** Most fields one `POST /search/facets/counts` request may name. */
|
|
29533
|
+
const COUNT_FACETS_MAX_FIELDS = 20;
|
|
29534
|
+
/** Values per counted field when `limit` is omitted. */
|
|
29535
|
+
const COUNT_FACETS_DEFAULT_LIMIT = 100;
|
|
29536
|
+
/** Largest `limit` of `POST /search/facets/counts`. */
|
|
29537
|
+
const COUNT_FACETS_MAX_LIMIT = 1000;
|
|
29538
|
+
/** Query for `GET /search/facets`. */
|
|
29539
|
+
const listFacetsQuerySchema = object$1({
|
|
29540
|
+
/** Comma-separated rule names that scope the list. */
|
|
29541
|
+
rules: string$2()
|
|
29542
|
+
.optional()
|
|
29543
|
+
.describe('Comma-separated rule names (configured or registered at runtime): only fields those rules declare, with values recorded for their documents. Unknown names answer 400. Without it, every configured rule.'),
|
|
29544
|
+
});
|
|
29545
|
+
/** Response body for `GET /search/facets`: the facetable fields and their values. */
|
|
29546
|
+
const listFacetsResponseSchema = object$1({
|
|
29547
|
+
facets: array$1(object$1({
|
|
29548
|
+
field: string$2(),
|
|
29549
|
+
type: string$2(),
|
|
29550
|
+
uiHint: string$2(),
|
|
29551
|
+
values: array$1(unknown$1()),
|
|
29552
|
+
rules: array$1(string$2()),
|
|
29553
|
+
})),
|
|
29554
|
+
});
|
|
29555
|
+
const FIELDS_MESSAGE = `fields must name 1 to ${String(COUNT_FACETS_MAX_FIELDS)} fields`;
|
|
29556
|
+
const LIMIT_MESSAGE = `limit must be between 1 and ${String(COUNT_FACETS_MAX_LIMIT)}`;
|
|
29557
|
+
/**
|
|
29558
|
+
* Request body for `POST /search/facets/counts`. Unknown keys are rejected,
|
|
29559
|
+
* so a misspelt option fails with `400` instead of being ignored.
|
|
29560
|
+
*/
|
|
29561
|
+
const countFacetsRequestSchema = strictObject({
|
|
29562
|
+
/** Qdrant filter scoping the counts (e.g. a tenant condition). */
|
|
29563
|
+
filter: qdrantFilterSchema
|
|
29564
|
+
.optional()
|
|
29565
|
+
.describe('Qdrant filter scoping the counts, as for search (e.g. a tenant condition).'),
|
|
29566
|
+
/** Rule names: only documents of at least one of them are counted. */
|
|
29567
|
+
rules: array$1(string$2().min(1, 'rule names must be non-empty'))
|
|
29568
|
+
.min(1, 'rules must name at least one rule')
|
|
29569
|
+
.optional()
|
|
29570
|
+
.describe('Rule names (configured or registered at runtime): only documents that matched at least one of them are counted, each once. Unknown names answer 400.'),
|
|
29571
|
+
/** Fields to count (each needs a payload index). */
|
|
29572
|
+
fields: array$1(string$2().min(1, 'field names must be non-empty'), {
|
|
29573
|
+
error: FIELDS_MESSAGE,
|
|
29574
|
+
})
|
|
29575
|
+
.min(1, FIELDS_MESSAGE)
|
|
29576
|
+
.max(COUNT_FACETS_MAX_FIELDS, FIELDS_MESSAGE)
|
|
29577
|
+
.describe(`Payload fields to count (1 to ${String(COUNT_FACETS_MAX_FIELDS)}). Each needs a payload index: keyword, integer or bool for value counts; integer, float or datetime for a range.`),
|
|
29578
|
+
/** Most values returned per counted field. */
|
|
29579
|
+
limit: number$2({ error: LIMIT_MESSAGE })
|
|
29580
|
+
.int('limit must be an integer')
|
|
29581
|
+
.min(1, LIMIT_MESSAGE)
|
|
29582
|
+
.max(COUNT_FACETS_MAX_LIMIT, LIMIT_MESSAGE)
|
|
29583
|
+
.optional()
|
|
29584
|
+
.describe(`Most values per counted field, highest counts first (default ${String(COUNT_FACETS_DEFAULT_LIMIT)}, max ${String(COUNT_FACETS_MAX_LIMIT)}).`),
|
|
29585
|
+
});
|
|
29586
|
+
/** One counted value: how many documents hold it. */
|
|
29587
|
+
const facetValueCountSchema = object$1({
|
|
29588
|
+
/** The payload value. */
|
|
29589
|
+
value: union$1([string$2(), number$2(), boolean$2()]),
|
|
29590
|
+
/** Documents (not chunks) holding the value. */
|
|
29591
|
+
count: number$2().int().nonnegative(),
|
|
29592
|
+
});
|
|
29593
|
+
/** A bound of a range: a number, a date string, or null when nothing matched. */
|
|
29594
|
+
const rangeBoundSchema = union$1([number$2(), string$2(), _null$3()]);
|
|
29595
|
+
/** Range of a numeric or date field over the matching documents. */
|
|
29596
|
+
const facetRangeSchema = object$1({
|
|
29597
|
+
/** Smallest value, or null when no matching document holds the field. */
|
|
29598
|
+
min: rangeBoundSchema,
|
|
29599
|
+
/** Largest value, or null when no matching document holds the field. */
|
|
29600
|
+
max: rangeBoundSchema,
|
|
29601
|
+
});
|
|
29602
|
+
/** Response body for `POST /search/facets/counts`. */
|
|
29603
|
+
const countFacetsResponseSchema = object$1({
|
|
29604
|
+
/** Per field: value counts (highest first), or a range. */
|
|
29605
|
+
counts: record$1(string$2(), union$1([array$1(facetValueCountSchema), facetRangeSchema]))
|
|
29606
|
+
.describe('Per requested field: value counts (documents, highest first), or { min, max } for a numeric or date field.'),
|
|
29607
|
+
});
|
|
29608
|
+
|
|
29609
|
+
/**
|
|
29610
|
+
* @module api/query
|
|
29611
|
+
* Zod request/response schemas for the read endpoints POST /search, POST /scan and POST /walk. No I/O.
|
|
29612
|
+
*/
|
|
29613
|
+
/** Results `POST /search` returns when `limit` is omitted. */
|
|
29614
|
+
const SEARCH_DEFAULT_LIMIT = 10;
|
|
29615
|
+
/** Page size of `POST /scan` when `limit` is omitted. */
|
|
29616
|
+
const SCAN_DEFAULT_LIMIT = 100;
|
|
29617
|
+
/** Largest `POST /scan` page size. */
|
|
29618
|
+
const SCAN_MAX_LIMIT = 1000;
|
|
29619
|
+
/**
|
|
29620
|
+
* Request body for `POST /search`. Unknown keys are rejected, so an
|
|
29621
|
+
* unsupported option (e.g. `collapse`) fails with `400` instead of being
|
|
29622
|
+
* silently ignored.
|
|
29623
|
+
*/
|
|
29624
|
+
const searchRequestSchema = strictObject({
|
|
29625
|
+
/** Natural-language query text to embed. */
|
|
29626
|
+
query: string$2()
|
|
29627
|
+
.min(1, 'query must be a non-empty string')
|
|
29628
|
+
.describe('Search query text.'),
|
|
29629
|
+
/** Maximum results to return. Default: {@link SEARCH_DEFAULT_LIMIT}. */
|
|
29630
|
+
limit: number$2()
|
|
29631
|
+
.int()
|
|
29632
|
+
.positive()
|
|
29633
|
+
.optional()
|
|
29634
|
+
.describe(`Max results (default ${String(SEARCH_DEFAULT_LIMIT)}).`),
|
|
29635
|
+
/** Number of results to skip (pagination). */
|
|
29636
|
+
offset: number$2()
|
|
29637
|
+
.int()
|
|
29638
|
+
.nonnegative()
|
|
29639
|
+
.optional()
|
|
29640
|
+
.describe('Number of results to skip for pagination.'),
|
|
29641
|
+
/** Optional Qdrant filter. */
|
|
29642
|
+
filter: qdrantFilterSchema.optional().describe('Qdrant filter object.'),
|
|
29643
|
+
});
|
|
29644
|
+
/** A single search hit. */
|
|
29645
|
+
const searchResultSchema = object$1({
|
|
29646
|
+
/** Point ID. */
|
|
29647
|
+
id: string$2(),
|
|
29648
|
+
/** Similarity (or fused) score. */
|
|
29649
|
+
score: number$2(),
|
|
29650
|
+
/** Point payload (system fields plus rule metadata). */
|
|
29651
|
+
payload: record$1(string$2(), unknown$1()),
|
|
29652
|
+
});
|
|
29653
|
+
/** Response body for `POST /search`: hits ordered by score. */
|
|
29654
|
+
const searchResponseSchema = array$1(searchResultSchema);
|
|
29655
|
+
const SCAN_LIMIT_MESSAGE = `limit must be between 1 and ${String(SCAN_MAX_LIMIT)}`;
|
|
29656
|
+
/** Request body for `POST /scan`. */
|
|
29657
|
+
const scanRequestSchema = object$1({
|
|
29658
|
+
/** Qdrant filter (required). */
|
|
29659
|
+
filter: qdrantFilter(FILTER_REQUIRED_MESSAGE).describe('Qdrant filter object (required).'),
|
|
29660
|
+
/** Page size (1 to {@link SCAN_MAX_LIMIT}). Default: {@link SCAN_DEFAULT_LIMIT}. */
|
|
29661
|
+
limit: number$2({ error: SCAN_LIMIT_MESSAGE })
|
|
29662
|
+
.int('limit must be an integer')
|
|
29663
|
+
.min(1, SCAN_LIMIT_MESSAGE)
|
|
29664
|
+
.max(SCAN_MAX_LIMIT, SCAN_LIMIT_MESSAGE)
|
|
29665
|
+
.optional()
|
|
29666
|
+
.describe(`Page size (default ${String(SCAN_DEFAULT_LIMIT)}, max ${String(SCAN_MAX_LIMIT)}).`),
|
|
29667
|
+
/** Opaque cursor from a previous page. */
|
|
29668
|
+
cursor: union$1([string$2(), number$2()])
|
|
29669
|
+
.optional()
|
|
29670
|
+
.describe('Opaque cursor from previous response for pagination.'),
|
|
29671
|
+
/** Payload fields to return (projection). */
|
|
29672
|
+
fields: array$1(string$2())
|
|
29673
|
+
.optional()
|
|
29674
|
+
.describe('Payload fields to return (projection).'),
|
|
29675
|
+
/** Return only `{ count }`. */
|
|
29676
|
+
countOnly: boolean$2()
|
|
29677
|
+
.optional()
|
|
29678
|
+
.describe('If true, return { count } instead of points.'),
|
|
29679
|
+
});
|
|
29680
|
+
/** A scrolled point (one entry of the `POST /scan` `points` page). */
|
|
29681
|
+
const scanPointSchema = object$1({
|
|
29682
|
+
/** Point ID. */
|
|
29683
|
+
id: string$2(),
|
|
29684
|
+
/** Point payload (projected when `fields` is given). */
|
|
29685
|
+
payload: record$1(string$2(), unknown$1()),
|
|
29686
|
+
});
|
|
29687
|
+
/** Response body for `POST /scan`: a page of points, or a count when `countOnly`. */
|
|
29688
|
+
const scanResponseSchema = union$1([
|
|
29689
|
+
object$1({
|
|
29690
|
+
/** Matched points. */
|
|
29691
|
+
points: array$1(scanPointSchema),
|
|
29692
|
+
/** Cursor for the next page, or null when exhausted. */
|
|
29693
|
+
cursor: union$1([string$2(), number$2(), _null$3()]),
|
|
29694
|
+
}),
|
|
29695
|
+
object$1({
|
|
29696
|
+
/** Number of matching points. */
|
|
29697
|
+
count: number$2().int().nonnegative(),
|
|
29698
|
+
}),
|
|
29699
|
+
]);
|
|
29700
|
+
/** Request body for `POST /walk`. */
|
|
29701
|
+
const walkRequestSchema = object$1({
|
|
29702
|
+
globs: array$1(string$2())
|
|
29703
|
+
.min(1)
|
|
29704
|
+
.describe('Globs matched against the watched files.'),
|
|
29705
|
+
});
|
|
29706
|
+
/** Response body for `POST /walk`. */
|
|
29707
|
+
const walkResponseSchema = object$1({
|
|
29708
|
+
paths: array$1(string$2()),
|
|
29709
|
+
matchedCount: number$2().int().nonnegative(),
|
|
29710
|
+
scannedRoots: array$1(string$2()),
|
|
29711
|
+
});
|
|
29712
|
+
|
|
29713
|
+
/**
|
|
29714
|
+
* @module api/reindex
|
|
29715
|
+
* Zod request/response schemas for POST /reindex (scopes issues, full, rules, path, prune; path scope targets file paths or source references). No I/O.
|
|
29716
|
+
*/
|
|
29717
|
+
/** Reindex scopes, in documentation order. */
|
|
29718
|
+
const reindexScopeSchema = _enum$1(['issues', 'full', 'rules', 'path', 'prune'], {
|
|
29719
|
+
error: 'Scope must be one of: issues, full, rules, path, prune',
|
|
29720
|
+
})
|
|
29721
|
+
.describe('Reindex scope.');
|
|
29722
|
+
/**
|
|
29723
|
+
* Scopes `configWatch.reindex` may trigger on a config change: `issues`,
|
|
29724
|
+
* `full` and `rules` (`path` needs targets; auto-pruning is never
|
|
29725
|
+
* allowed).
|
|
29726
|
+
*/
|
|
29727
|
+
const configWatchReindexScopeSchema = reindexScopeSchema.extract(['issues', 'full', 'rules'], { error: 'configWatch.reindex must be one of: issues, full, rules' });
|
|
29728
|
+
/** One or more file paths (a non-empty array or a single path). */
|
|
29729
|
+
const pathTargetsSchema = union$1([
|
|
29730
|
+
filePathSchema,
|
|
29731
|
+
array$1(filePathSchema).min(1, 'path must not be an empty array'),
|
|
29732
|
+
]);
|
|
29733
|
+
/** Maximum number of source references in one `POST /reindex` request. */
|
|
29734
|
+
const MAX_REINDEX_SOURCES_PER_REQUEST = 1000;
|
|
29735
|
+
/**
|
|
29736
|
+
* One or more source references (a single reference, or an array of 1 to
|
|
29737
|
+
* {@link MAX_REINDEX_SOURCES_PER_REQUEST}).
|
|
29738
|
+
*/
|
|
29739
|
+
const sourceTargetsSchema = union$1([
|
|
29740
|
+
sourceReferenceSchema,
|
|
29741
|
+
array$1(sourceReferenceSchema)
|
|
29742
|
+
.min(1, 'source must not be an empty array')
|
|
29743
|
+
.max(MAX_REINDEX_SOURCES_PER_REQUEST, `source must contain at most ${String(MAX_REINDEX_SOURCES_PER_REQUEST)} references`),
|
|
29744
|
+
]);
|
|
29745
|
+
/**
|
|
29746
|
+
* Request body for `POST /reindex`.
|
|
29747
|
+
*
|
|
29748
|
+
* `scope` defaults to `issues`. Scope `path` requires exactly one of `path`
|
|
29749
|
+
* or `source`; `rules` optionally narrows to `path`; `source` is only valid
|
|
29750
|
+
* with scope `path`.
|
|
29751
|
+
*/
|
|
29752
|
+
const reindexRequestSchema = object$1({
|
|
29753
|
+
/** Reindex scope. Default: `issues`. */
|
|
29754
|
+
scope: reindexScopeSchema
|
|
29755
|
+
.optional()
|
|
29756
|
+
.describe('Reindex scope: "issues" re-processes documents that failed; "rules" re-applies inference rules without re-embedding; "full" re-embeds everything; "path" reindexes the given files or directories (`path`) or source references (`source`); "prune" deletes points for files no longer in watch scope.'),
|
|
29757
|
+
/** File path(s) or globs for scopes `path` and `rules`. */
|
|
29758
|
+
path: pathTargetsSchema.optional(),
|
|
29759
|
+
/** Source reference(s) for scope `path`. */
|
|
29760
|
+
source: sourceTargetsSchema.optional(),
|
|
29761
|
+
/** Compute and return the plan without executing. */
|
|
29762
|
+
dryRun: boolean$2()
|
|
29763
|
+
.optional()
|
|
29764
|
+
.describe('Return the blast-area plan (counts by root) without running the reindex.'),
|
|
29765
|
+
})
|
|
29766
|
+
.superRefine((val, ctx) => {
|
|
29767
|
+
if (val.scope === 'path') {
|
|
29768
|
+
if ((val.path === undefined) === (val.source === undefined)) {
|
|
29769
|
+
ctx.addIssue({
|
|
29770
|
+
code: 'custom',
|
|
29771
|
+
path: val.path === undefined ? ['path'] : [],
|
|
29772
|
+
message: 'Exactly one of path or source is required when scope is "path"',
|
|
29773
|
+
});
|
|
29774
|
+
}
|
|
29775
|
+
}
|
|
29776
|
+
else if (val.source !== undefined) {
|
|
29777
|
+
ctx.addIssue({
|
|
29778
|
+
code: 'custom',
|
|
29779
|
+
path: ['source'],
|
|
29780
|
+
message: 'source is only valid when scope is "path"',
|
|
29781
|
+
});
|
|
29782
|
+
}
|
|
29783
|
+
});
|
|
29784
|
+
/** Blast-radius plan for a reindex operation. */
|
|
29785
|
+
const reindexPlanSchema = object$1({
|
|
29786
|
+
/** Total points (prune) or files and source documents (other scopes) examined. */
|
|
29787
|
+
total: number$2().int().nonnegative(),
|
|
29788
|
+
/** Number of items to process. */
|
|
29789
|
+
toProcess: number$2().int().nonnegative(),
|
|
29790
|
+
/** Number of points to delete (prune only). */
|
|
29791
|
+
toDelete: number$2().int().nonnegative(),
|
|
29792
|
+
/** Counts grouped by watch root (files) or source name (source documents). */
|
|
29793
|
+
byRoot: record$1(string$2(), number$2()),
|
|
29794
|
+
/** True when results are partial. */
|
|
29795
|
+
incomplete: boolean$2().optional(),
|
|
29796
|
+
});
|
|
29797
|
+
/** Response body for `POST /reindex`: started (async) or dry-run plan. */
|
|
29798
|
+
const reindexResponseSchema = object$1({
|
|
29799
|
+
/** `started` (executing asynchronously) or `dry_run` (plan only). */
|
|
29800
|
+
status: _enum$1(['started', 'dry_run']),
|
|
29801
|
+
/** The scope requested. */
|
|
29802
|
+
scope: reindexScopeSchema,
|
|
29803
|
+
/** Plan (omitted for a live prune). */
|
|
29804
|
+
plan: reindexPlanSchema.optional(),
|
|
29805
|
+
});
|
|
29806
|
+
|
|
29807
|
+
/**
|
|
29808
|
+
* @module api/vcs
|
|
29809
|
+
* Zod request schemas for the version-history endpoints in the endpoint catalog (`GET /vcs/show`). No I/O.
|
|
29810
|
+
*/
|
|
29811
|
+
/**
|
|
29812
|
+
* A required, non-empty string whose missing or empty value is reported as
|
|
29813
|
+
* `error` (a schema-level `error` also words the schema's own checks, here
|
|
29814
|
+
* `min(1)`).
|
|
29815
|
+
*
|
|
29816
|
+
* @param error - Message for a missing, non-string or empty value.
|
|
29817
|
+
* @param description - Field description (OpenAPI and tool text).
|
|
29818
|
+
* @returns The field schema.
|
|
29819
|
+
*/
|
|
29820
|
+
function requiredString(error, description) {
|
|
29821
|
+
return string$2({ error }).min(1).describe(description);
|
|
29822
|
+
}
|
|
29823
|
+
/**
|
|
29824
|
+
* A required, non-empty query parameter, reported as
|
|
29825
|
+
* `Missing required query parameter: <name>`.
|
|
29826
|
+
*
|
|
29827
|
+
* @param name - Parameter name.
|
|
29828
|
+
* @param description - Parameter description (OpenAPI and tool text).
|
|
29829
|
+
* @returns The parameter schema.
|
|
29830
|
+
*/
|
|
29831
|
+
function requiredQueryParam(name, description) {
|
|
29832
|
+
return requiredString(`Missing required query parameter: ${name}`, description);
|
|
29833
|
+
}
|
|
29834
|
+
/** Query string of `GET /vcs/show`. */
|
|
29835
|
+
const vcsShowQuerySchema = object$1({
|
|
29836
|
+
/** Absolute path of the file inside a VCS-enabled watch root. */
|
|
29837
|
+
path: requiredQueryParam('path', 'File path to retrieve.'),
|
|
29838
|
+
/** Version identifier (a commit). */
|
|
29839
|
+
commit: requiredQueryParam('commit', 'Version identifier.'),
|
|
29840
|
+
});
|
|
29841
|
+
/** Response body of `GET /vcs/status`. */
|
|
29842
|
+
const vcsStatusResponseSchema = object$1({
|
|
29843
|
+
enabled: boolean$2(),
|
|
29844
|
+
roots: array$1(object$1({
|
|
29845
|
+
path: string$2(),
|
|
29846
|
+
tracked: number$2().int().nonnegative(),
|
|
29847
|
+
lastCommit: object$1({
|
|
29848
|
+
hash: string$2(),
|
|
29849
|
+
message: string$2(),
|
|
29850
|
+
timestamp: string$2(),
|
|
29851
|
+
})
|
|
29852
|
+
.nullable(),
|
|
29853
|
+
remoteUrl: string$2().nullable(),
|
|
29854
|
+
lastPush: string$2().nullable(),
|
|
29855
|
+
pushErrors: array$1(object$1({
|
|
29856
|
+
/** ISO 8601 time of the failed push. */
|
|
29857
|
+
timestamp: string$2(),
|
|
29858
|
+
/** Why the push failed (credentials redacted). */
|
|
29859
|
+
message: string$2(),
|
|
29860
|
+
})),
|
|
29861
|
+
/** The root's commit circuit breaker (null without a manager). */
|
|
29862
|
+
breaker: object$1({
|
|
29863
|
+
/** Consecutive commit failures since the last success. */
|
|
29864
|
+
consecutiveFailures: number$2().int().nonnegative(),
|
|
29865
|
+
/** Whether the breaker is tripped. */
|
|
29866
|
+
tripped: boolean$2(),
|
|
29867
|
+
/** ISO 8601 time of the current trip, or null. */
|
|
29868
|
+
trippedAt: string$2().nullable(),
|
|
29869
|
+
/** Message of the most recent failure, or null. */
|
|
29870
|
+
lastError: string$2().nullable(),
|
|
29871
|
+
/** Files queued or in flight, not yet committed. */
|
|
29872
|
+
pendingCount: number$2().int().nonnegative(),
|
|
29873
|
+
})
|
|
29874
|
+
.nullable(),
|
|
29875
|
+
})),
|
|
29876
|
+
});
|
|
29877
|
+
/** Query string of `GET /vcs/history`. */
|
|
29878
|
+
const vcsHistoryQuerySchema = object$1({
|
|
29879
|
+
glob: requiredQueryParam('glob', 'Glob inside a VCS-enabled watch root.'),
|
|
29880
|
+
since: string$2()
|
|
29881
|
+
.optional()
|
|
29882
|
+
.describe('Only commits after this date (any date git log accepts).'),
|
|
29883
|
+
until: string$2()
|
|
29884
|
+
.optional()
|
|
29885
|
+
.describe('Only commits before this date (any date git log accepts).'),
|
|
29886
|
+
limit: string$2().optional().describe('Most commits to return (default 20).'),
|
|
29887
|
+
});
|
|
29888
|
+
/** Response body of `GET /vcs/history`: commits touching the glob, newest first. */
|
|
29889
|
+
const vcsHistoryResponseSchema = array$1(object$1({
|
|
29890
|
+
commit: string$2(),
|
|
29891
|
+
message: string$2(),
|
|
29892
|
+
timestamp: string$2(),
|
|
29893
|
+
files: array$1(string$2()),
|
|
29894
|
+
}));
|
|
29895
|
+
/** Query string of `GET /vcs/diff`. */
|
|
29896
|
+
const vcsDiffQuerySchema = object$1({
|
|
29897
|
+
glob: requiredQueryParam('glob', 'Glob inside a VCS-enabled watch root.'),
|
|
29898
|
+
commit: requiredQueryParam('commit', 'Version to diff from.'),
|
|
29899
|
+
commitEnd: string$2()
|
|
29900
|
+
.optional()
|
|
29901
|
+
.describe('Version to diff to (default: the working tree).'),
|
|
29902
|
+
});
|
|
29903
|
+
/** Response body of `GET /vcs/diff`: a unified diff, `text/plain`. */
|
|
29904
|
+
const vcsDiffResponseSchema = string$2();
|
|
29905
|
+
/** Query string of `GET /vcs/check-exclusion`. */
|
|
29906
|
+
const vcsCheckExclusionQuerySchema = object$1({
|
|
29907
|
+
path: requiredQueryParam('path', 'Path inside a VCS-enabled watch root.'),
|
|
29908
|
+
});
|
|
29909
|
+
/**
|
|
29910
|
+
* Response body of `GET /vcs/check-exclusion` (D82): whether git ignores the
|
|
29911
|
+
* path and, when it does, the matching pattern, the file holding it
|
|
29912
|
+
* (`.git/info/exclude` for a VCS-only exclusion) and its line number.
|
|
29913
|
+
*/
|
|
29914
|
+
const vcsCheckExclusionResponseSchema = object$1({
|
|
29915
|
+
/** Whether git ignores the path (a tracked path never is). */
|
|
29916
|
+
excluded: boolean$2(),
|
|
29917
|
+
/** The matching pattern. */
|
|
29918
|
+
rule: string$2().optional(),
|
|
29919
|
+
/** The file holding the pattern, as git names it. */
|
|
29920
|
+
source: string$2().optional(),
|
|
29921
|
+
/** The pattern's 1-based line number in `source`. */
|
|
29922
|
+
line: number$2().int().positive().optional(),
|
|
29923
|
+
});
|
|
29924
|
+
/** Request body of `POST /vcs/revert`. */
|
|
29925
|
+
const vcsRevertRequestSchema = object$1({
|
|
29926
|
+
glob: requiredString('Missing required body parameters: glob, commit', 'Glob of files to restore.'),
|
|
29927
|
+
commit: requiredString('Missing required body parameters: glob, commit', 'Version to restore from.'),
|
|
29928
|
+
existingOnly: boolean$2()
|
|
29929
|
+
.optional()
|
|
29930
|
+
.describe('Restore only files that exist in the working tree.'),
|
|
29931
|
+
});
|
|
29932
|
+
/** Response body of `POST /vcs/revert`. */
|
|
29933
|
+
const vcsRevertResponseSchema = object$1({
|
|
29934
|
+
restored: number$2().int().nonnegative(),
|
|
29935
|
+
files: array$1(string$2()),
|
|
29936
|
+
});
|
|
29937
|
+
/** Most files `POST /vcs/exclude` untracks without `confirm: true` (D103). */
|
|
29938
|
+
const VCS_EXCLUDE_CONFIRM_THRESHOLD = 100;
|
|
29939
|
+
/** Request body of `POST /vcs/exclude` (Design 5, D81, D103). */
|
|
29940
|
+
const vcsExcludeRequestSchema = object$1({
|
|
29941
|
+
glob: requiredString('Missing required body parameter: glob', "One gitignore pattern, absolute (under a VCS root) or relative to `root`. It is written root-anchored (`/` + the root-relative pattern) to the root's `.git/info/exclude`. Brackets are a character class: escape them to name a file literally (`Report \\[draft\\].docx`). A newline, a leading `!` or `#`, braces, or a pattern matching the whole root is refused."),
|
|
29942
|
+
remove: boolean$2()
|
|
29943
|
+
.optional()
|
|
29944
|
+
.describe('Remove the pattern instead of adding it (from `.git/info/exclude`, else from the `.gitignore` a 0.20.0 exclude wrote). Files are not re-added; their next change is versioned again.'),
|
|
29945
|
+
root: string$2()
|
|
29946
|
+
.optional()
|
|
29947
|
+
.describe('VCS root to write under (default: the root containing the glob); a glob outside it is refused.'),
|
|
29948
|
+
confirm: boolean$2()
|
|
29949
|
+
.optional()
|
|
29950
|
+
.describe(`Required (true) when adding the pattern would untrack more than ${String(VCS_EXCLUDE_CONFIRM_THRESHOLD)} files.`),
|
|
29951
|
+
});
|
|
29952
|
+
/** Response body of `POST /vcs/exclude`. */
|
|
29953
|
+
const vcsExcludeResponseSchema = object$1({
|
|
29954
|
+
ok: literal(true),
|
|
29955
|
+
/** The file written: the root's `.git/info/exclude`, or the `.gitignore` a removed 0.20.0 pattern was in. */
|
|
29956
|
+
file: string$2(),
|
|
29957
|
+
action: _enum$1(['added', 'removed']),
|
|
29958
|
+
/** Paths removed from the index (in HEAD or staged); 0 for `removed`. */
|
|
29959
|
+
untracked: number$2().int().nonnegative(),
|
|
29960
|
+
});
|
|
29961
|
+
|
|
29962
|
+
/**
|
|
29963
|
+
* @module catalog/routes
|
|
29964
|
+
* Shared parts of the watcher catalog entries: the bearer auth every entry but `status` takes, and the `route`/`vcsRoute` helpers giving an entry its name, method, path, description and auth. No I/O.
|
|
29965
|
+
*/
|
|
29966
|
+
/** Bearer-key auth (`api.apiKey`) required by every entry except `status`. */
|
|
29967
|
+
const bearer = { schemes: [{ in: 'bearer' }] };
|
|
29968
|
+
/**
|
|
29969
|
+
* Name, method, path, description and auth of a bearer-protected entry.
|
|
29970
|
+
*
|
|
29971
|
+
* @param name - Entry name: verb-first (D77), the operation's one name.
|
|
29972
|
+
* @param method - HTTP method.
|
|
29973
|
+
* @param path - Route path.
|
|
29974
|
+
* @param description - What the operation does.
|
|
29975
|
+
*/
|
|
29976
|
+
function route(name, method, path, description) {
|
|
29977
|
+
return { name, method, path, description, auth: bearer };
|
|
29978
|
+
}
|
|
29979
|
+
/**
|
|
29980
|
+
* Description suffix of every entry the service serves only while version
|
|
29981
|
+
* control is on (spec Design 3): with VCS off, the route answers the
|
|
29982
|
+
* not-found 404 an unregistered route sends.
|
|
29983
|
+
*/
|
|
29984
|
+
const VCS_DISABLED_NOTE = 'Answers 404 `not_found` when version control is disabled (`vcs.enabled: false`).';
|
|
29985
|
+
/**
|
|
29986
|
+
* {@link route} for a version-control entry: its description ends with
|
|
29987
|
+
* {@link VCS_DISABLED_NOTE}.
|
|
29988
|
+
*
|
|
29989
|
+
* @param name - Entry name.
|
|
29990
|
+
* @param method - HTTP method.
|
|
29991
|
+
* @param path - Route path.
|
|
29992
|
+
* @param description - What the operation does.
|
|
29993
|
+
*/
|
|
29994
|
+
function vcsRoute(name, method, path, description) {
|
|
29995
|
+
return route(name, method, path, `${description} ${VCS_DISABLED_NOTE}`);
|
|
29996
|
+
}
|
|
29997
|
+
|
|
29998
|
+
/**
|
|
29999
|
+
* @module catalog/destructiveEntries
|
|
30000
|
+
* The watcher catalog's `destructive` entries, in catalog order (spec Design 3 kinds). No I/O.
|
|
30001
|
+
*/
|
|
30002
|
+
/** The `destructive` entries. */
|
|
30003
|
+
const DESTRUCTIVE_ENTRIES = [
|
|
30004
|
+
{
|
|
30005
|
+
...route('deleteDocuments', 'POST', '/documents/delete', 'Delete source documents by reference or by Qdrant filter (at least one condition; constrained to points carrying source and no file_path). The route waits at most 10 s for each document lock; if one stays busy it answers 503 `not_available` with `Retry-After: 10`, and documents already deleted stay deleted (retry to finish).'),
|
|
30006
|
+
body: documentsDeleteRequestSchema,
|
|
30007
|
+
response: documentsDeleteResponseSchema,
|
|
30008
|
+
kind: 'destructive',
|
|
30009
|
+
},
|
|
30010
|
+
{
|
|
30011
|
+
...route('deletePoints', 'POST', '/points/delete', 'Delete points from the vector store by filter.'),
|
|
30012
|
+
body: pointsDeleteRequestSchema,
|
|
30013
|
+
response: pointsDeleteResponseSchema,
|
|
30014
|
+
kind: 'destructive',
|
|
30015
|
+
},
|
|
30016
|
+
{
|
|
30017
|
+
...route('unregisterRules', 'DELETE', '/rules/unregister', 'Unregister external inference rules.'),
|
|
30018
|
+
body: rulesUnregisterRequestSchema,
|
|
30019
|
+
response: rulesUnregisterResponseSchema,
|
|
30020
|
+
kind: 'destructive',
|
|
30021
|
+
},
|
|
30022
|
+
{
|
|
30023
|
+
...route('unregisterRulesBySource', 'DELETE', '/rules/unregister/:source', 'Unregister all external inference rules from a source.'),
|
|
30024
|
+
params: rulesUnregisterParamsSchema,
|
|
30025
|
+
response: rulesUnregisterResponseSchema,
|
|
30026
|
+
kind: 'destructive',
|
|
30027
|
+
},
|
|
30028
|
+
{
|
|
30029
|
+
...vcsRoute('revertVcs', 'POST', '/vcs/revert', 'Restore files from a historical commit (forward-only reversion).'),
|
|
30030
|
+
body: vcsRevertRequestSchema,
|
|
30031
|
+
response: vcsRevertResponseSchema,
|
|
30032
|
+
kind: 'destructive',
|
|
30033
|
+
},
|
|
30034
|
+
{
|
|
30035
|
+
// D103: it untracks files, ending their history.
|
|
30036
|
+
...vcsRoute('excludeVcsPath', 'POST', '/vcs/exclude', 'Exclude a path from version history (VCS-only: it stays in search) or remove the exclusion. The glob is a gitignore pattern, so brackets are a character class: escape them to name a file literally (`Report \\[draft\\].docx`). Adding a pattern also untracks the tracked files it matches, in one commit (history stops for them; the files stay on disk and in search); `remove` does not restore that history. More than ' +
|
|
30037
|
+
String(VCS_EXCLUDE_CONFIRM_THRESHOLD) +
|
|
30038
|
+
" files to untrack need `confirm: true` (else 409 `confirm_required` with `untracked`, nothing written). The route waits at most 30 s for the root's git lock, then answers 503 `not_available` with `Retry-After: 30`, nothing written. A glob outside an explicit `root` is 400; a glob under no VCS root is 404."),
|
|
30039
|
+
body: vcsExcludeRequestSchema,
|
|
30040
|
+
response: vcsExcludeResponseSchema,
|
|
30041
|
+
kind: 'destructive',
|
|
30042
|
+
},
|
|
30043
|
+
];
|
|
30044
|
+
|
|
30045
|
+
/**
|
|
30046
|
+
* @module catalog/readEntries
|
|
30047
|
+
* The watcher catalog's `read` entries, in catalog order (spec Design 3 kinds). No I/O.
|
|
30048
|
+
*/
|
|
30049
|
+
/** The `read` entries. */
|
|
30050
|
+
const READ_ENTRIES = [
|
|
30051
|
+
{
|
|
30052
|
+
...route('getConfigSchema', 'GET', '/config/schema', 'Get the JSON Schema for the configuration file.'),
|
|
30053
|
+
response: getConfigSchemaResponseSchema,
|
|
30054
|
+
kind: 'read',
|
|
30055
|
+
},
|
|
30056
|
+
{
|
|
30057
|
+
...route('matchConfig', 'POST', '/config/match', 'Test file paths against config inference rules.'),
|
|
30058
|
+
body: matchConfigRequestSchema,
|
|
30059
|
+
response: matchConfigResponseSchema,
|
|
30060
|
+
kind: 'read',
|
|
30061
|
+
},
|
|
30062
|
+
{
|
|
30063
|
+
...route('validateConfig', 'POST', '/config/validate', 'Validate a candidate config. Optionally test file paths against the config.'),
|
|
30064
|
+
body: validateConfigRequestSchema,
|
|
30065
|
+
response: validateConfigResponseSchema,
|
|
30066
|
+
kind: 'read',
|
|
30067
|
+
},
|
|
30068
|
+
{
|
|
30069
|
+
...route('search', 'POST', '/search', 'Semantic search over indexed documents. Supports Qdrant filters.'),
|
|
30070
|
+
body: searchRequestSchema,
|
|
30071
|
+
response: searchResponseSchema,
|
|
30072
|
+
kind: 'read',
|
|
30073
|
+
},
|
|
30074
|
+
{
|
|
30075
|
+
...route('scan', 'POST', '/scan', 'Filter-only point query without vector search. Returns metadata for points matching a Qdrant filter.'),
|
|
30076
|
+
body: scanRequestSchema,
|
|
30077
|
+
response: scanResponseSchema,
|
|
30078
|
+
kind: 'read',
|
|
30079
|
+
},
|
|
30080
|
+
{
|
|
30081
|
+
...route('listFacets', 'GET', '/search/facets', 'List available search facets and their values: every field a rule schema gives a uiHint or enum, with its declared or recorded values. With `rules`, only the fields and values of those rules.'),
|
|
30082
|
+
query: listFacetsQuerySchema,
|
|
30083
|
+
response: listFacetsResponseSchema,
|
|
30084
|
+
kind: 'read',
|
|
30085
|
+
},
|
|
30086
|
+
{
|
|
30087
|
+
...route('countFacets', 'POST', '/search/facets/counts', 'Count the values of payload fields over the documents matching a filter (each document once, not each chunk), or give { min, max } for a numeric or date field. Counts are approximate (Qdrant facet estimates); ranges are exact. `rules` keeps only documents of those rules. Each field needs a payload index (else 400). Fields are counted 4 at a time within a 10 s budget; past it the route answers 503 `not_available` naming the fields not counted. It also answers 503 until the `chunk_index` system index is ready, and with `rules` until `matched_rules` is.'),
|
|
30088
|
+
body: countFacetsRequestSchema,
|
|
30089
|
+
response: countFacetsResponseSchema,
|
|
30090
|
+
kind: 'read',
|
|
30091
|
+
},
|
|
30092
|
+
{
|
|
30093
|
+
...route('render', 'POST', '/render', 'Render a document (file path, or source reference retrieved through its callback) through the template pipeline.'),
|
|
30094
|
+
body: renderRequestSchema,
|
|
30095
|
+
response: renderResponseSchema,
|
|
30096
|
+
kind: 'read',
|
|
30097
|
+
},
|
|
30098
|
+
{
|
|
30099
|
+
...route('walk', 'POST', '/walk', 'Walk watched filesystem paths with glob intersection. Returns matching file paths.'),
|
|
30100
|
+
body: walkRequestSchema,
|
|
30101
|
+
response: walkResponseSchema,
|
|
30102
|
+
kind: 'read',
|
|
30103
|
+
},
|
|
30104
|
+
{
|
|
30105
|
+
...route('listIssues', 'GET', '/issues', 'Get runtime processing failures. Shows the documents (file path or source reference) that failed processing and why.'),
|
|
30106
|
+
response: issuesResponseSchema,
|
|
30107
|
+
kind: 'read',
|
|
30108
|
+
},
|
|
30109
|
+
{
|
|
30110
|
+
...vcsRoute('getVcsStatus', 'GET', '/vcs/status', 'VCS state for all roots: enabled state, tracked files, last commit, remote status, commit circuit breaker state.'),
|
|
30111
|
+
response: vcsStatusResponseSchema,
|
|
30112
|
+
kind: 'read',
|
|
30113
|
+
},
|
|
30114
|
+
{
|
|
30115
|
+
...vcsRoute('listVcsHistory', 'GET', '/vcs/history', 'Query change history by path or glob with optional date range.'),
|
|
30116
|
+
query: vcsHistoryQuerySchema,
|
|
30117
|
+
response: vcsHistoryResponseSchema,
|
|
30118
|
+
kind: 'read',
|
|
30119
|
+
},
|
|
30120
|
+
{
|
|
30121
|
+
...vcsRoute('showVcsFile', 'GET', '/vcs/show', 'Retrieve file content at a specific version. The reply is the raw file content; its content-type follows the file extension.'),
|
|
30122
|
+
query: vcsShowQuerySchema,
|
|
30123
|
+
response: string$2(),
|
|
30124
|
+
contentType: 'text/plain',
|
|
30125
|
+
kind: 'read',
|
|
30126
|
+
},
|
|
30127
|
+
{
|
|
30128
|
+
...vcsRoute('diffVcs', 'GET', '/vcs/diff', 'Show what changed between two versions, or between a version and current. The reply is a unified diff.'),
|
|
30129
|
+
query: vcsDiffQuerySchema,
|
|
30130
|
+
response: vcsDiffResponseSchema,
|
|
30131
|
+
contentType: 'text/plain',
|
|
30132
|
+
kind: 'read',
|
|
30133
|
+
},
|
|
30134
|
+
{
|
|
30135
|
+
...vcsRoute('checkVcsExclusion', 'GET', '/vcs/check-exclusion', 'Check whether a path is excluded from version tracking and why.'),
|
|
30136
|
+
query: vcsCheckExclusionQuerySchema,
|
|
30137
|
+
response: vcsCheckExclusionResponseSchema,
|
|
30138
|
+
kind: 'read',
|
|
30139
|
+
},
|
|
30140
|
+
];
|
|
30141
|
+
|
|
30142
|
+
/**
|
|
30143
|
+
* @module catalog/writeEntries
|
|
30144
|
+
* The watcher catalog's `write` entries, in catalog order (spec Design 3 kinds). No I/O.
|
|
30145
|
+
*/
|
|
30146
|
+
/** The `write` entries. */
|
|
30147
|
+
const WRITE_ENTRIES = [
|
|
30148
|
+
{
|
|
30149
|
+
...route('setMetadata', 'POST', '/metadata', 'Set or update metadata on a document by file path or source reference. While the document is being processed the route waits at most 10 s for its lock, then answers 503 `not_available` with `Retry-After: 10`, nothing changed (retry).'),
|
|
30150
|
+
body: metadataRequestSchema,
|
|
30151
|
+
response: metadataResponseSchema,
|
|
30152
|
+
kind: 'write',
|
|
30153
|
+
},
|
|
30154
|
+
{
|
|
30155
|
+
...route('reindex', 'POST', '/reindex', 'Trigger a scoped reindex of watched files (scope path also accepts source references).'),
|
|
30156
|
+
body: reindexRequestSchema,
|
|
30157
|
+
response: reindexResponseSchema,
|
|
30158
|
+
kind: 'write',
|
|
30159
|
+
},
|
|
30160
|
+
{
|
|
30161
|
+
...route('notifyDocuments', 'POST', '/documents', 'Notify changed source documents ({ source, rule }); queued and coalesced (202), or processed synchronously with ?wait=true. Answers 202 with accepted and rejected references, or with `wait=true` 200 with per-document results.'),
|
|
30162
|
+
query: documentsQuerySchema,
|
|
30163
|
+
body: documentsEnvelopeSchema,
|
|
30164
|
+
response: union$1([
|
|
30165
|
+
documentsAcceptedResponseSchema,
|
|
30166
|
+
documentsResultsResponseSchema,
|
|
30167
|
+
]),
|
|
30168
|
+
kind: 'write',
|
|
30169
|
+
},
|
|
30170
|
+
{
|
|
30171
|
+
...route('registerRules', 'POST', '/rules/register', 'Register external inference rules.'),
|
|
30172
|
+
body: rulesRegisterRequestSchema,
|
|
30173
|
+
response: rulesRegisterResponseSchema,
|
|
30174
|
+
kind: 'write',
|
|
30175
|
+
},
|
|
30176
|
+
{
|
|
30177
|
+
...route('reapplyRules', 'POST', '/rules/reapply', 'Re-apply inference rules to existing vector points. The route waits at most 10 s for each document lock; if one stays busy it answers 503 `not_available` with `Retry-After: 10`, and documents already updated keep their update (retry to finish).'),
|
|
30178
|
+
body: rulesReapplyRequestSchema,
|
|
30179
|
+
response: rulesReapplyResponseSchema,
|
|
30180
|
+
kind: 'write',
|
|
30181
|
+
},
|
|
30182
|
+
{
|
|
30183
|
+
...route('rebuildMetadata', 'POST', '/rebuild-metadata', 'Rebuild the enrichment store from vector point payloads (file points only).'),
|
|
30184
|
+
body: rebuildMetadataRequestSchema,
|
|
30185
|
+
response: rebuildMetadataResponseSchema,
|
|
30186
|
+
kind: 'write',
|
|
30187
|
+
},
|
|
30188
|
+
];
|
|
30189
|
+
|
|
30190
|
+
/**
|
|
30191
|
+
* @module catalog
|
|
30192
|
+
* The watcher's endpoint catalog (`WATCHER_CATALOG`): one Zod-described entry per route, from which the service's routes and validation, the OpenAPI document, the SDK client, the CLI and the plugin's tools are derived. No I/O.
|
|
30193
|
+
*
|
|
30194
|
+
* @remarks
|
|
30195
|
+
* Holds every route (30 entries; spec Design 3, `countFacets` from Design
|
|
30196
|
+
* 12), each with its `kind`, listed by kind in `catalog/`. Entry
|
|
30197
|
+
* names are verb-first (D77, Design 14), and each is its operation's one
|
|
30198
|
+
* name: catalog entry, SDK method, OpenAPI `operationId` and, through
|
|
30199
|
+
* `toToolName`, plugin tool name. The catalog is the only route list
|
|
30200
|
+
* (D41).
|
|
30201
|
+
*/
|
|
30202
|
+
/** The watcher endpoint catalog. */
|
|
30203
|
+
const WATCHER_CATALOG = defineCatalog([
|
|
30204
|
+
...standardEntries('watcher', {
|
|
30205
|
+
auth: { config: bearer },
|
|
30206
|
+
healthSchema: watcherHealthSchema,
|
|
30207
|
+
}),
|
|
30208
|
+
...READ_ENTRIES,
|
|
30209
|
+
...WRITE_ENTRIES,
|
|
30210
|
+
...DESTRUCTIVE_ENTRIES,
|
|
30211
|
+
]);
|
|
30212
|
+
|
|
30213
|
+
/**
|
|
30214
|
+
* @module constants
|
|
30215
|
+
* Shared identity constants for the jeeves-watcher ecosystem.
|
|
30216
|
+
* Consumed by both the service and plugin packages.
|
|
30217
|
+
*/
|
|
30218
|
+
/** Component name shared across service and plugin descriptors. */
|
|
30219
|
+
const COMPONENT_NAME = 'watcher';
|
|
30220
|
+
/** npm package name for the service. */
|
|
30221
|
+
const SERVICE_PACKAGE = '@karmaniverous/jeeves-watcher';
|
|
30222
|
+
/** npm package name for the plugin. */
|
|
30223
|
+
const PLUGIN_PACKAGE = '@karmaniverous/jeeves-watcher-openclaw';
|
|
30224
|
+
/** Default watcher API port. */
|
|
30225
|
+
const DEFAULT_PORT = DEFAULT_PORTS.watcher;
|
|
30226
|
+
/** Default API values. */
|
|
30227
|
+
({
|
|
30228
|
+
port: DEFAULT_PORTS.watcher});
|
|
30229
|
+
/** Default reindex values. */
|
|
30230
|
+
const REINDEX_DEFAULTS = {
|
|
30231
|
+
/** Files (and source documents, and stored-rule lookups) in flight at once. */
|
|
30232
|
+
concurrency: 50,
|
|
30233
|
+
};
|
|
30234
|
+
/**
|
|
30235
|
+
* Default processing ceilings (D44, D64): from the 2026-10-31 corpus survey,
|
|
30236
|
+
* which only the single largest document exceeded.
|
|
30237
|
+
*/
|
|
30238
|
+
const PROCESSING_DEFAULTS = {
|
|
30239
|
+
/** 64 MiB, checked with `stat` before extraction. */
|
|
30240
|
+
maxFileBytes: '64m',
|
|
30241
|
+
maxChunksPerFile: 5000,
|
|
30242
|
+
/** 8 MiB of extracted text (UTF-8 bytes). */
|
|
30243
|
+
maxTextBytes: '8m',
|
|
30244
|
+
};
|
|
30245
|
+
|
|
30246
|
+
/**
|
|
30247
|
+
* @module httpContract
|
|
30248
|
+
* The HTTP client constant the CLI and the plugin share: the environment variable holding the watcher API bearer key. Request building and error mapping live in jeeves's catalog client and the SDK's `WatcherError` (spec #281 D36, D53). Pure.
|
|
30249
|
+
*/
|
|
30250
|
+
/** Environment variable holding the watcher API bearer key for clients (CLI, plugin). */
|
|
30251
|
+
const API_KEY_ENV_VAR = 'JEEVES_WATCHER_API_KEY';
|
|
30252
|
+
|
|
30253
|
+
/**
|
|
30254
|
+
* @module promptRules
|
|
30255
|
+
* Always-in-context watcher rules (descriptor `promptRules`), injected by the plugin through `before_prompt_build`. Static text; no side effects.
|
|
30256
|
+
*
|
|
30257
|
+
* @remarks
|
|
30258
|
+
* Live numbers (point counts, configured score thresholds, VCS roots) are
|
|
30259
|
+
* served by `watcher_status` and `watcher_config`, and detail lives in the
|
|
30260
|
+
* skill ({@link WATCHER_SKILL}).
|
|
30261
|
+
*/
|
|
30262
|
+
/** Static watcher rules the agent must see on every turn. */
|
|
30263
|
+
const WATCHER_PROMPT_RULES = [
|
|
30264
|
+
'## Watcher',
|
|
30265
|
+
'This environment includes a semantic search index over the document archive (`watcher_search`).',
|
|
30266
|
+
'- **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.',
|
|
30267
|
+
'- **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.',
|
|
30268
|
+
'- **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.',
|
|
30269
|
+
'- **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.',
|
|
30270
|
+
'- **Inventory on demand (`watcher_config`):** inference rules `$.inferenceRules[*]`, watched paths `$.watch.paths[*]`, ignored paths `$.watch.ignored[*]`. Version history: `watcher_list_vcs_history`, `watcher_diff_vcs`, `watcher_show_vcs_file`. See the jeeves-watcher skill for detail.',
|
|
30271
|
+
].join('\n');
|
|
30272
|
+
|
|
30273
|
+
/**
|
|
30274
|
+
* @module reservedFields
|
|
30275
|
+
* 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.
|
|
30276
|
+
*/
|
|
30277
|
+
/**
|
|
30278
|
+
* Payload fields written by the watcher itself, by role. The single
|
|
30279
|
+
* definition of these names: the service's pipeline constants and
|
|
30280
|
+
* {@link RESERVED_PAYLOAD_FIELDS} derive from it.
|
|
30281
|
+
*/
|
|
30282
|
+
const PAYLOAD_FIELD = {
|
|
30283
|
+
/** Normalized file path (file points only). */
|
|
30284
|
+
filePath: 'file_path',
|
|
30285
|
+
/** Source reference `<name>://<id>` (source points only). */
|
|
30286
|
+
source: 'source',
|
|
30287
|
+
/** Designated rule of a source document (source points only). */
|
|
30288
|
+
sourceRule: 'source_rule',
|
|
30289
|
+
/** Zero-based chunk index. */
|
|
30290
|
+
chunkIndex: 'chunk_index',
|
|
30291
|
+
/** The chunk text. */
|
|
30292
|
+
chunkText: 'chunk_text',
|
|
30293
|
+
/** Total number of chunks of the document. */
|
|
30294
|
+
totalChunks: 'total_chunks',
|
|
30295
|
+
/** SHA-256 hash of the embedded text. */
|
|
30296
|
+
contentHash: 'content_hash',
|
|
30297
|
+
/** Names of the rules that matched. */
|
|
30298
|
+
matchedRules: 'matched_rules',
|
|
30299
|
+
/** 1-indexed line where the chunk starts. */
|
|
30300
|
+
lineStart: 'line_start',
|
|
30301
|
+
/** 1-indexed line where the chunk ends. */
|
|
30302
|
+
lineEnd: 'line_end',
|
|
30303
|
+
/** Creation timestamp (unix seconds). */
|
|
30304
|
+
createdAt: 'created_at',
|
|
30305
|
+
/** Modification timestamp (unix seconds). */
|
|
30306
|
+
modifiedAt: 'modified_at',
|
|
30307
|
+
};
|
|
30308
|
+
/**
|
|
30309
|
+
* Payload fields written by the watcher itself. System fields overwrite rule
|
|
30310
|
+
* metadata on upsert, so a rule schema property with one of these names is
|
|
30311
|
+
* silently shadowed; config validation warns about it.
|
|
30312
|
+
*/
|
|
30313
|
+
const RESERVED_PAYLOAD_FIELDS = Object.values(PAYLOAD_FIELD);
|
|
30314
|
+
/**
|
|
30315
|
+
* Fields the watcher always keeps a keyword payload index on. They cannot be
|
|
30316
|
+
* configured in `vectorStore.payloadIndexes`.
|
|
30317
|
+
*/
|
|
30318
|
+
const SYSTEM_PAYLOAD_INDEX_FIELDS = [
|
|
30319
|
+
PAYLOAD_FIELD.source,
|
|
30320
|
+
PAYLOAD_FIELD.sourceRule,
|
|
30321
|
+
];
|
|
30322
|
+
new Set(RESERVED_PAYLOAD_FIELDS);
|
|
30323
|
+
|
|
30324
|
+
/**
|
|
30325
|
+
* @module schemas/byteSize
|
|
30326
|
+
* Byte-size settings (Design 10): a number of bytes, or a string with a `k`, `m` or `g` suffix read as binary units (`10m` = 10,485,760 bytes). One schema and one parser for every byte-size key.
|
|
30327
|
+
*/
|
|
30328
|
+
/** A byte-size string: digits (optionally with a decimal part) and an optional k, m or g suffix. */
|
|
30329
|
+
const BYTE_SIZE_PATTERN = /^\s*(\d+(?:\.\d+)?)\s*([kmg]?)\s*$/i;
|
|
30330
|
+
/** A byte-size setting: bytes, or a string such as `10m` (binary units). */
|
|
30331
|
+
const byteSizeSchema = union$1([
|
|
30332
|
+
number$2().int().min(0),
|
|
30333
|
+
string$2()
|
|
30334
|
+
.regex(BYTE_SIZE_PATTERN, 'Expected a byte size: a number, or a number with a k, m or g suffix (binary units)'),
|
|
30335
|
+
]);
|
|
30336
|
+
|
|
30337
|
+
/**
|
|
30338
|
+
* @module schemas/vcs
|
|
30339
|
+
* VCS (version control system) configuration schema for git-backed content versioning.
|
|
30340
|
+
*/
|
|
30341
|
+
/**
|
|
30342
|
+
* Commit message generation configuration.
|
|
30343
|
+
*/
|
|
30344
|
+
const vcsCommitMessageConfigSchema = object$1({
|
|
30345
|
+
/** Whether AI-generated commit messages are enabled. */
|
|
30346
|
+
enabled: boolean$2()
|
|
30347
|
+
.default(true)
|
|
30348
|
+
.describe('Enable AI-generated commit messages. Default: true.'),
|
|
30349
|
+
/** AI provider for commit message generation. */
|
|
30350
|
+
provider: string$2()
|
|
30351
|
+
.default('anthropic')
|
|
30352
|
+
.describe('AI provider for commit message generation. Default: "anthropic".'),
|
|
30353
|
+
/** AI model for commit message generation. */
|
|
30354
|
+
model: string$2()
|
|
30355
|
+
.default('claude-haiku-4-0')
|
|
30356
|
+
.describe('AI model for commit message generation. Default: "claude-haiku-4-0".'),
|
|
30357
|
+
/** API key for commit message provider. Supports env var substitution. */
|
|
30358
|
+
apiKey: string$2()
|
|
30359
|
+
.optional()
|
|
30360
|
+
.describe('API key for commit message provider. Supports env var substitution (e.g., "${ANTHROPIC_API_KEY}").'),
|
|
30361
|
+
});
|
|
30362
|
+
/**
|
|
30363
|
+
* VCS retention policy configuration.
|
|
30364
|
+
*/
|
|
30365
|
+
const vcsRetentionConfigSchema = object$1({
|
|
30366
|
+
/** Maximum age in days for retained versions. */
|
|
30367
|
+
maxAgeDays: number$2()
|
|
30368
|
+
.int()
|
|
30369
|
+
.min(1)
|
|
30370
|
+
.default(30)
|
|
30371
|
+
.describe('Maximum age in days for retained versions. Default: 30.'),
|
|
30372
|
+
/** Maximum number of versions to retain per file. */
|
|
30373
|
+
maxVersions: number$2()
|
|
30374
|
+
.int()
|
|
30375
|
+
.min(1)
|
|
30376
|
+
.default(100)
|
|
30377
|
+
.describe('Maximum number of versions to retain per file. Default: 100.'),
|
|
30378
|
+
/** Cron expression for squash schedule. */
|
|
30379
|
+
squashCron: string$2()
|
|
30380
|
+
.default('0 0 * * *')
|
|
30381
|
+
.describe('Cron expression for squash schedule. Default: "0 0 * * *" (daily at midnight).'),
|
|
30382
|
+
});
|
|
30383
|
+
/**
|
|
30384
|
+
* Git commit author identity configuration.
|
|
30385
|
+
*/
|
|
30386
|
+
const vcsAuthorConfigSchema = object$1({
|
|
30387
|
+
/** Author name for git commits. */
|
|
30388
|
+
name: string$2()
|
|
30389
|
+
.min(1)
|
|
30390
|
+
.default('jeeves-watcher')
|
|
30391
|
+
.describe('Author name for git commits. Default: "jeeves-watcher".'),
|
|
30392
|
+
/** Author email for git commits. */
|
|
30393
|
+
email: string$2()
|
|
28706
30394
|
.min(1)
|
|
28707
30395
|
.default('watcher@localhost')
|
|
28708
30396
|
.describe('Author email for git commits. Default: "watcher@localhost".'),
|
|
@@ -28766,11 +30454,19 @@ const vcsConfigSchema = object$1({
|
|
|
28766
30454
|
.min(1000)
|
|
28767
30455
|
.default(300000)
|
|
28768
30456
|
.describe('Circuit breaker cooldown in ms before a half-open recovery attempt. Default: 300000 (5 minutes).'),
|
|
28769
|
-
/** Git branch name for VCS operations.
|
|
30457
|
+
/** Git branch name for VCS operations. Squash and the startup branch check use this instead of dynamic detection. */
|
|
28770
30458
|
branch: string$2()
|
|
28771
30459
|
.min(1)
|
|
28772
30460
|
.default('master')
|
|
28773
30461
|
.describe('Git branch name for VCS operations. Default: "master".'),
|
|
30462
|
+
/**
|
|
30463
|
+
* Size cap for versioned files (D80). A new file over it is not staged;
|
|
30464
|
+
* a tracked file that grows past it is untracked in its own commit.
|
|
30465
|
+
* `0` disables the cap. Read at each staging (no restart needed).
|
|
30466
|
+
*/
|
|
30467
|
+
maxFileBytes: byteSizeSchema
|
|
30468
|
+
.optional()
|
|
30469
|
+
.describe("Size cap for versioned files: bytes, or a string with a k, m or g suffix (binary units). A new file over it is not staged; a tracked file that grows past it is untracked (its history ends at its last committed version). 0 disables the cap. Default: 100000000 (100 MB, under GitHub's 100 MiB file limit)."),
|
|
28774
30470
|
});
|
|
28775
30471
|
/**
|
|
28776
30472
|
* Per-root VCS overrides extending the root-level VCS config.
|
|
@@ -28823,6 +30519,10 @@ const watchConfigSchema = object$1({
|
|
|
28823
30519
|
usePolling: boolean$2()
|
|
28824
30520
|
.optional()
|
|
28825
30521
|
.describe('Use polling instead of native file system events (for network drives).'),
|
|
30522
|
+
/** The local change source (D125); `usePolling: true` forces chokidar. */
|
|
30523
|
+
changeSource: _enum$1(['parcel', 'chokidar'])
|
|
30524
|
+
.optional()
|
|
30525
|
+
.describe('Local change source: "parcel" (@parcel/watcher, one native subscription per watch root; the default) or "chokidar" (the manual switch). usePolling: true forces chokidar. A reload that changes it rebuilds the watcher; if @parcel/watcher cannot load, the service refuses to start (or a reload refuses the rebuild) naming this switch.'),
|
|
28826
30526
|
/** Debounce delay in milliseconds for file change events. */
|
|
28827
30527
|
debounceMs: number$2()
|
|
28828
30528
|
.optional()
|
|
@@ -28831,6 +30531,16 @@ const watchConfigSchema = object$1({
|
|
|
28831
30531
|
stabilityThresholdMs: number$2()
|
|
28832
30532
|
.optional()
|
|
28833
30533
|
.describe('Time in milliseconds a file must remain unchanged before processing.'),
|
|
30534
|
+
/** Periodic reconciliation interval (D111); 0 disables it. */
|
|
30535
|
+
reconcileIntervalMs: number$2()
|
|
30536
|
+
.int()
|
|
30537
|
+
.min(0)
|
|
30538
|
+
.optional()
|
|
30539
|
+
.describe('Interval in milliseconds between periodic reconciliations of each watch root against the inventory (default 900000, 15 minutes); 0 disables them. A reload changes it without rebuilding the watcher.'),
|
|
30540
|
+
/** Index check at start (D197). */
|
|
30541
|
+
indexCheckOnStart: boolean$2()
|
|
30542
|
+
.optional()
|
|
30543
|
+
.describe('After the start walk, delete the indexed file documents under each available watch root whose files are gone (deleted or moved away while the service was down). Default: true. A root where more than 10% of its indexed documents (once more than 20 are missing) or more than 1,000 are missing is left alone (run reindex prune deliberately). Read at start.'),
|
|
28834
30544
|
/** Whether to respect .gitignore files when processing. */
|
|
28835
30545
|
respectGitignore: boolean$2()
|
|
28836
30546
|
.optional()
|
|
@@ -28864,13 +30574,7 @@ const configWatchConfigSchema = object$1({
|
|
|
28864
30574
|
.optional()
|
|
28865
30575
|
.describe('Debounce delay in milliseconds for config file change detection.'),
|
|
28866
30576
|
/** Reindex scope triggered on config change. */
|
|
28867
|
-
reindex:
|
|
28868
|
-
literal('issues')
|
|
28869
|
-
.describe('Re-process only files with recorded issues.'),
|
|
28870
|
-
literal('full').describe('Full reindex of all watched files.'),
|
|
28871
|
-
literal('rules')
|
|
28872
|
-
.describe('Re-apply inference rules to existing points without re-embedding.'),
|
|
28873
|
-
])
|
|
30577
|
+
reindex: configWatchReindexScopeSchema
|
|
28874
30578
|
.optional()
|
|
28875
30579
|
.describe('Reindex scope triggered on config change. Default: issues.'),
|
|
28876
30580
|
});
|
|
@@ -28888,6 +30592,30 @@ const apiConfigSchema = object$1({
|
|
|
28888
30592
|
cacheTtlMs: number$2()
|
|
28889
30593
|
.optional()
|
|
28890
30594
|
.describe('TTL in milliseconds for caching read-heavy endpoints (e.g., /status, /config). Default: 30000.'),
|
|
30595
|
+
/** Bearer API key required on every route except GET /status. */
|
|
30596
|
+
apiKey: string$2()
|
|
30597
|
+
.min(1, 'api.apiKey must be a non-empty string (remove it to disable authentication)')
|
|
30598
|
+
.optional()
|
|
30599
|
+
.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.'),
|
|
30600
|
+
});
|
|
30601
|
+
/**
|
|
30602
|
+
* Log file rotation (D171). Every key falls back to its default.
|
|
30603
|
+
*/
|
|
30604
|
+
const logRotationConfigSchema = object$1({
|
|
30605
|
+
/** Rotate when the file reaches this size. */
|
|
30606
|
+
maxSize: byteSizeSchema
|
|
30607
|
+
.optional()
|
|
30608
|
+
.describe('Rotate when the log file reaches this size: bytes, or a string with a k, m or g suffix (binary units). Default: "10m".'),
|
|
30609
|
+
/** Rotate on this schedule too. */
|
|
30610
|
+
frequency: union$1([_enum$1(['daily', 'hourly']), number$2().int().positive()])
|
|
30611
|
+
.optional()
|
|
30612
|
+
.describe('Also rotate "daily", "hourly", or every N milliseconds. Default: "daily".'),
|
|
30613
|
+
/** Rotated files kept in addition to the active one (up to N + 1 files, D184). */
|
|
30614
|
+
maxFiles: number$2()
|
|
30615
|
+
.int()
|
|
30616
|
+
.min(1)
|
|
30617
|
+
.optional()
|
|
30618
|
+
.describe('Number of rotated log files kept in addition to the active one, so up to maxFiles + 1 files remain. Default: 14.'),
|
|
28891
30619
|
});
|
|
28892
30620
|
/**
|
|
28893
30621
|
* Logging configuration.
|
|
@@ -28901,6 +30629,10 @@ const loggingConfigSchema = object$1({
|
|
|
28901
30629
|
file: string$2()
|
|
28902
30630
|
.optional()
|
|
28903
30631
|
.describe('Path to log file (logs to stdout if omitted).'),
|
|
30632
|
+
/** Rotation of the log file (D171). */
|
|
30633
|
+
rotation: union$1([literal(false), logRotationConfigSchema])
|
|
30634
|
+
.optional()
|
|
30635
|
+
.describe('Rotation of logging.file: false to disable, or { maxSize, frequency, maxFiles }. Default when logging.file is set: { maxSize: "10m", frequency: "daily", maxFiles: 14 }.'),
|
|
28904
30636
|
});
|
|
28905
30637
|
|
|
28906
30638
|
/**
|
|
@@ -29035,6 +30767,126 @@ const inferenceRuleSchema = object$1({
|
|
|
29035
30767
|
}
|
|
29036
30768
|
});
|
|
29037
30769
|
|
|
30770
|
+
/**
|
|
30771
|
+
* @module schemas/payloadIndexes
|
|
30772
|
+
* Zod schema for `vectorStore.payloadIndexes`: configured Qdrant payload indexes, including tenant and principal flags. No I/O.
|
|
30773
|
+
*/
|
|
30774
|
+
/** Qdrant payload index types supported by `vectorStore.payloadIndexes`. */
|
|
30775
|
+
const payloadIndexTypeSchema = _enum$1(['keyword', 'integer', 'float', 'bool', 'datetime', 'uuid', 'text'])
|
|
30776
|
+
.describe('Qdrant payload index type.');
|
|
30777
|
+
/** Index types that may carry `isTenant: true` (Qdrant `is_tenant`). */
|
|
30778
|
+
const TENANT_INDEX_TYPES = ['keyword', 'uuid'];
|
|
30779
|
+
/** Index types that may carry `isPrincipal: true` (Qdrant `is_principal`). */
|
|
30780
|
+
const PRINCIPAL_INDEX_TYPES = [
|
|
30781
|
+
'integer',
|
|
30782
|
+
'float',
|
|
30783
|
+
'datetime',
|
|
30784
|
+
];
|
|
30785
|
+
/**
|
|
30786
|
+
* Whether an index type may carry `isTenant: true` (Qdrant `is_tenant`).
|
|
30787
|
+
*
|
|
30788
|
+
* @param type - Payload index type.
|
|
30789
|
+
* @returns `true` for `keyword` and `uuid`.
|
|
30790
|
+
*/
|
|
30791
|
+
function isTenantIndexType(type) {
|
|
30792
|
+
return TENANT_INDEX_TYPES.includes(type);
|
|
30793
|
+
}
|
|
30794
|
+
/**
|
|
30795
|
+
* Whether an index type may carry `isPrincipal: true` (Qdrant
|
|
30796
|
+
* `is_principal`).
|
|
30797
|
+
*
|
|
30798
|
+
* @param type - Payload index type.
|
|
30799
|
+
* @returns `true` for `integer`, `float` and `datetime`.
|
|
30800
|
+
*/
|
|
30801
|
+
function isPrincipalIndexType(type) {
|
|
30802
|
+
return PRINCIPAL_INDEX_TYPES.includes(type);
|
|
30803
|
+
}
|
|
30804
|
+
/**
|
|
30805
|
+
* A single configured payload index.
|
|
30806
|
+
*
|
|
30807
|
+
* `isTenant` is valid only for `keyword`/`uuid`; `isPrincipal` only for
|
|
30808
|
+
* `integer`/`float`/`datetime`. Both constraints are enforced by refinement.
|
|
30809
|
+
*/
|
|
30810
|
+
const payloadIndexSchema = object$1({
|
|
30811
|
+
/** Payload field to index. */
|
|
30812
|
+
field: string$2().min(1).describe('Payload field name to index.'),
|
|
30813
|
+
/** Qdrant index type. */
|
|
30814
|
+
type: payloadIndexTypeSchema,
|
|
30815
|
+
/** Mark the index as a tenant index (keyword/uuid only). */
|
|
30816
|
+
isTenant: boolean$2()
|
|
30817
|
+
.optional()
|
|
30818
|
+
.describe('Qdrant is_tenant: optimise storage for per-tenant filtering (keyword/uuid only).'),
|
|
30819
|
+
/** Mark the index as a principal index (integer/float/datetime only). */
|
|
30820
|
+
isPrincipal: boolean$2()
|
|
30821
|
+
.optional()
|
|
30822
|
+
.describe('Qdrant is_principal: optimise storage for range filtering (integer/float/datetime only).'),
|
|
30823
|
+
})
|
|
30824
|
+
.superRefine((val, ctx) => {
|
|
30825
|
+
if (val.isTenant && !isTenantIndexType(val.type)) {
|
|
30826
|
+
ctx.addIssue({
|
|
30827
|
+
code: 'custom',
|
|
30828
|
+
path: ['isTenant'],
|
|
30829
|
+
message: `isTenant is only valid for ${TENANT_INDEX_TYPES.join('/')} indexes (got "${val.type}")`,
|
|
30830
|
+
});
|
|
30831
|
+
}
|
|
30832
|
+
if (val.isPrincipal && !isPrincipalIndexType(val.type)) {
|
|
30833
|
+
ctx.addIssue({
|
|
30834
|
+
code: 'custom',
|
|
30835
|
+
path: ['isPrincipal'],
|
|
30836
|
+
message: `isPrincipal is only valid for ${PRINCIPAL_INDEX_TYPES.join('/')} indexes (got "${val.type}")`,
|
|
30837
|
+
});
|
|
30838
|
+
}
|
|
30839
|
+
});
|
|
30840
|
+
const systemIndexFields = new Set(SYSTEM_PAYLOAD_INDEX_FIELDS);
|
|
30841
|
+
/**
|
|
30842
|
+
* The `vectorStore.payloadIndexes` list. Each `field` may appear once, and
|
|
30843
|
+
* the system-indexed fields (`source`, `source_rule`) are rejected: the
|
|
30844
|
+
* watcher always keeps a keyword index on them.
|
|
30845
|
+
*/
|
|
30846
|
+
const payloadIndexesSchema = array$1(payloadIndexSchema)
|
|
30847
|
+
.superRefine((indexes, ctx) => {
|
|
30848
|
+
const seen = new Set();
|
|
30849
|
+
indexes.forEach(({ field }, i) => {
|
|
30850
|
+
if (systemIndexFields.has(field)) {
|
|
30851
|
+
ctx.addIssue({
|
|
30852
|
+
code: 'custom',
|
|
30853
|
+
path: [i, 'field'],
|
|
30854
|
+
message: `"${field}" is a system field the watcher always indexes (keyword); remove this entry`,
|
|
30855
|
+
});
|
|
30856
|
+
}
|
|
30857
|
+
else if (seen.has(field)) {
|
|
30858
|
+
ctx.addIssue({
|
|
30859
|
+
code: 'custom',
|
|
30860
|
+
path: [i, 'field'],
|
|
30861
|
+
message: `Duplicate payload index for field "${field}"; each field may be indexed once`,
|
|
30862
|
+
});
|
|
30863
|
+
}
|
|
30864
|
+
seen.add(field);
|
|
30865
|
+
});
|
|
30866
|
+
});
|
|
30867
|
+
|
|
30868
|
+
/**
|
|
30869
|
+
* @module schemas/processing
|
|
30870
|
+
* `processing` config: the per-document ceilings (file size before extraction, extracted text bytes, chunk count) past which a document is `too_large` (D44, D64).
|
|
30871
|
+
*/
|
|
30872
|
+
/** Processing ceilings, read per document (a reload applies to the next one). */
|
|
30873
|
+
const processingConfigSchema = object$1({
|
|
30874
|
+
/** File size cap, checked with `stat` before extraction (files only). */
|
|
30875
|
+
maxFileBytes: byteSizeSchema
|
|
30876
|
+
.optional()
|
|
30877
|
+
.describe(`Largest file processed, checked with stat before extraction (files only; source documents keep the 10 MiB response cap): bytes, or a string with a k, m or g suffix (binary units). A larger file has its points deleted and a too_large issue recorded. Default: '${PROCESSING_DEFAULTS.maxFileBytes}'.`),
|
|
30878
|
+
/** Most chunks one document may split into. */
|
|
30879
|
+
maxChunksPerFile: number$2()
|
|
30880
|
+
.int()
|
|
30881
|
+
.min(1)
|
|
30882
|
+
.optional()
|
|
30883
|
+
.describe(`Most chunks one document (file or source) may split into; more has its points deleted and a too_large issue recorded. Default: ${String(PROCESSING_DEFAULTS.maxChunksPerFile)}.`),
|
|
30884
|
+
/** Extracted text cap (UTF-8 bytes). */
|
|
30885
|
+
maxTextBytes: byteSizeSchema
|
|
30886
|
+
.optional()
|
|
30887
|
+
.describe(`Largest extracted text of one document (file or source), in UTF-8 bytes: bytes, or a string with a k, m or g suffix (binary units). More has its points deleted and a too_large issue recorded. Default: '${PROCESSING_DEFAULTS.maxTextBytes}'.`),
|
|
30888
|
+
});
|
|
30889
|
+
|
|
29038
30890
|
/**
|
|
29039
30891
|
* @module schemas/services
|
|
29040
30892
|
* Service configuration schemas: embedding and vector store.
|
|
@@ -29046,11 +30898,11 @@ const embeddingConfigSchema = object$1({
|
|
|
29046
30898
|
/** The embedding model provider. */
|
|
29047
30899
|
provider: string$2()
|
|
29048
30900
|
.default('gemini')
|
|
29049
|
-
.describe('Embedding provider name
|
|
30901
|
+
.describe('Embedding provider name. Default: "gemini".'),
|
|
29050
30902
|
/** The embedding model name. */
|
|
29051
30903
|
model: string$2()
|
|
29052
30904
|
.default('gemini-embedding-001')
|
|
29053
|
-
.describe('Embedding model identifier
|
|
30905
|
+
.describe('Embedding model identifier for the provider. Default: "gemini-embedding-001".'),
|
|
29054
30906
|
/** Maximum tokens per chunk for splitting. */
|
|
29055
30907
|
chunkSize: number$2()
|
|
29056
30908
|
.optional()
|
|
@@ -29075,6 +30927,10 @@ const embeddingConfigSchema = object$1({
|
|
|
29075
30927
|
concurrency: number$2()
|
|
29076
30928
|
.optional()
|
|
29077
30929
|
.describe('Maximum concurrent embedding requests.'),
|
|
30930
|
+
/** URL POSTed when embedding health changes state (unsigned body). */
|
|
30931
|
+
alertUrl: url()
|
|
30932
|
+
.optional()
|
|
30933
|
+
.describe('URL that receives a POST when embedding health turns failing (embedding_failed) or recovers (embedding_recovered). The body is unsigned and sent without credentials: use a private endpoint. Read at each transition.'),
|
|
29078
30934
|
});
|
|
29079
30935
|
/**
|
|
29080
30936
|
* Vector store configuration for Qdrant.
|
|
@@ -29090,6 +30946,10 @@ const vectorStoreConfigSchema = object$1({
|
|
|
29090
30946
|
apiKey: string$2()
|
|
29091
30947
|
.optional()
|
|
29092
30948
|
.describe('Qdrant API key for authentication (supports ${ENV_VAR} substitution).'),
|
|
30949
|
+
/** Payload indexes created at startup when missing (never dropped). */
|
|
30950
|
+
payloadIndexes: payloadIndexesSchema
|
|
30951
|
+
.optional()
|
|
30952
|
+
.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.'),
|
|
29093
30953
|
});
|
|
29094
30954
|
|
|
29095
30955
|
/**
|
|
@@ -29120,10 +30980,10 @@ object$1({
|
|
|
29120
30980
|
vectorStore: vectorStoreConfigSchema.describe('Qdrant vector store configuration.'),
|
|
29121
30981
|
/** API server configuration. */
|
|
29122
30982
|
api: apiConfigSchema.optional().describe('API server configuration.'),
|
|
29123
|
-
/** Directory for persistent state files (issues.json, values.
|
|
30983
|
+
/** Directory for persistent state files (issues.json, values.v8, enrichments.sqlite). */
|
|
29124
30984
|
stateDir: string$2()
|
|
29125
30985
|
.optional()
|
|
29126
|
-
.describe('Directory for persistent state files (issues.json, values.
|
|
30986
|
+
.describe('Directory for persistent state files (issues.json, values.v8, enrichments.sqlite). Defaults to .jeeves-metadata.'),
|
|
29127
30987
|
/** Rules for inferring metadata from document properties (inline objects or file paths). */
|
|
29128
30988
|
inferenceRules: array$1(union$1([inferenceRuleSchema, string$2()]))
|
|
29129
30989
|
.optional()
|
|
@@ -29179,8 +31039,8 @@ object$1({
|
|
|
29179
31039
|
concurrency: number$2()
|
|
29180
31040
|
.int()
|
|
29181
31041
|
.min(1)
|
|
29182
|
-
.default(
|
|
29183
|
-
.describe(
|
|
31042
|
+
.default(REINDEX_DEFAULTS.concurrency)
|
|
31043
|
+
.describe(`Maximum concurrent file operations during reindex (default ${String(REINDEX_DEFAULTS.concurrency)}).`),
|
|
29184
31044
|
})
|
|
29185
31045
|
.optional()
|
|
29186
31046
|
.describe('Reindex configuration.'),
|
|
@@ -29211,6 +31071,12 @@ object$1({
|
|
|
29211
31071
|
vcs: vcsConfigSchema
|
|
29212
31072
|
.optional()
|
|
29213
31073
|
.describe('VCS configuration for git-backed version control of watched content.'),
|
|
31074
|
+
/** Processing ceilings (D44, D64). */
|
|
31075
|
+
processing: processingConfigSchema
|
|
31076
|
+
.optional()
|
|
31077
|
+
.describe('Processing ceilings: a document over one has its points deleted and a too_large issue recorded. Read per document.'),
|
|
31078
|
+
/** Named sources for non-file documents (callback retrieval). */
|
|
31079
|
+
sources: sourcesConfigSchema.optional(),
|
|
29214
31080
|
/** Logging configuration. */
|
|
29215
31081
|
logging: loggingConfigSchema.optional().describe('Logging configuration.'),
|
|
29216
31082
|
/** Timeout in milliseconds for graceful shutdown. */
|
|
@@ -29227,6 +31093,13 @@ object$1({
|
|
|
29227
31093
|
.describe('Maximum backoff delay in milliseconds for system errors. Default: 60000.'),
|
|
29228
31094
|
});
|
|
29229
31095
|
|
|
31096
|
+
/**
|
|
31097
|
+
* @module skill.generated
|
|
31098
|
+
* The jeeves-watcher skill (descriptor `skill`), compiled from `skills/jeeves-watcher/SKILL.md` by `npm run skill`. Generated; do not edit.
|
|
31099
|
+
*/
|
|
31100
|
+
/** Contents of the jeeves-watcher `SKILL.md`. */
|
|
31101
|
+
const WATCHER_SKILL = '---\nname: jeeves-watcher\ndescription: >\n Semantic search of the indexed archive: recall prior context, find documents, scan, enrich metadata, watcher config, reindex, indexing issues, file history.\n---\n\n# jeeves-watcher — Search, Discovery & Administration\n\n## Service Architecture\n\nThe watcher is an HTTP API running as a background service (typically NSSM on Windows, systemd on Linux).\n\n**Default port:** 1936 (configurable via `api.port` in watcher config) **Non-default port:** If the watcher runs on a different port, the user must set `plugins.entries.jeeves-watcher-openclaw.config.apiUrl` in `openclaw.json`. The plugin cannot auto-discover a non-default port.\n\n**API key:** If the watcher sets `api.apiKey`, every route except `GET /status` and `GET /openapi.json` needs `Authorization: Bearer <key>`. The tools send it from `plugins.entries.jeeves-watcher-openclaw.config.apiKey` (or the `JEEVES_WATCHER_API_KEY` env var). From `watcher_config`, `watcher_config_apply`, `watcher_search`, `watcher_scan` and `watcher_show_vcs_file`, a missing or wrong key comes back as `HTTP 401: {"error":"Unauthorized","code":"unauthorized"}` (see [Error Handling](#error-handling) for the fix). From the other tools it reads `HTTP 401 unauthorized: the watcher API requires an API key. Provide it via plugins.entries.jeeves-watcher-openclaw.config.apiKey or the JEEVES_WATCHER_API_KEY env var.` (or, when a key was sent, `... rejected the API key. Check ... against the service\'s api.apiKey.`). When calling the API directly, add the header yourself. Never echo the key.\n\n**Tool errors:** Tool error text is prefixed `Error: `. Every `watcher_*` tool is built from the watcher\'s endpoint catalog and reports a non-2xx response as `HTTP <status>: <body>`, with the server\'s JSON body verbatim: read its `code` (and `details` or `issues`) there, e.g. `HTTP 400: {"error":"Validation failed","code":"invalid_request",...}` or `HTTP 503: {"error":"Scan in progress","code":"scan_in_progress"}`. Branch on the code, not the message. The tools reject parameters that don\'t match their schema before sending anything. Each call waits up to 120 s; a timeout does not cancel a state change on the service, so check the outcome (e.g. `watcher_check_vcs_exclusion` after `watcher_exclude_vcs_path`) before retrying. An unreachable service reads `Service not reachable at <url>.` with a hint to set `apiUrl`. (`watcher_service` comes from the shared Jeeves core toolset and words its own errors.)\n\n**Health check:** `GET /status` returns `name`, `version`, `uptime`, `status` (`healthy`/`degraded`/`unhealthy`), and a `health` object containing `collection` (`name`, `pointCount`, `dimensions`, `systemIndexes`), `reindex` progress, `initialScan` progress, `embedding` (provider health), and (when version tracking is on) `vcs` (`roots`, `pending`, `trippedRoots`, `oldestTrippedAt`). `status` is `degraded` (still HTTP 200) when embeddings are failing or a version-control root has tripped.\n\n**Mental model:** The `watcher_*` tools are thin HTTP wrappers. Each tool call translates to an HTTP request to the watcher API. When tools are available, use them. When they\'re not (e.g., different session, plugin not loaded), you can hit the API directly. Replace `<PORT>` below with the configured port (default 1936; check `plugins.entries.jeeves-watcher-openclaw.config.apiUrl` in `openclaw.json` if overridden), and add `-H "Authorization: Bearer <key>"` when the service sets `api.apiKey`:\n\n```\n# Health check\ncurl http://127.0.0.1:<PORT>/status\n\n# Search\ncurl -X POST http://127.0.0.1:<PORT>/search \\\n -H "Content-Type: application/json" \\\n -d \'{"query": "search text", "limit": 5}\'\n\n# Query config\ncurl http://127.0.0.1:<PORT>/config\n```\n\n**Key endpoints:**\n\n| Endpoint | Method | Purpose |\n| --- | --- | --- |\n| `/status` | GET | Health check, uptime, collection stats |\n| `/openapi.json` | GET | OpenAPI 3.1 document of the catalog routes (no key needed) |\n| `/search` | POST | Semantic search (main query interface) |\n| `/config` | GET | Public resolved config (allow-listed keys plus `issues`); optional `?path=<jsonpath>` filter |\n| `/config/validate` | POST | Validate candidate config |\n| `/config/apply` | POST | Apply config changes |\n| `/reindex` | POST | Trigger reindex |\n| `/metadata` | POST | Enrich document metadata (file path or source reference) |\n| `/scan` | POST | Filter-only point query (no embeddings) |\n| `/walk` | POST | Watched-file listing with glob intersection |\n| `/issues` | GET | Runtime processing failures (file paths and source references) |\n| `/documents` | POST | Notify changed source documents (`{ source, rule }`) |\n| `/documents/delete` | POST | Delete source documents by reference or filter |\n| `/rules/register` | POST | Register virtual inference rules (auto-triggers rules reindex) |\n| `/rules/unregister` | DELETE | Remove virtual rules by source (`{ source }` body) |\n| `/rules/unregister/:source` | DELETE | Remove virtual rules by source (parameterized) |\n| `/rules/reapply` | POST | Re-apply rules to indexed files matching `globs` |\n| `/points/delete` | POST | Delete points matching a Qdrant filter |\n| `/render` | POST | Render a document (file path or source reference) through its rule |\n| `/search/facets` | GET | Get search facet definitions and values (`?rules=a,b` scopes them to those rules) |\n| `/search/facets/counts` | POST | Count values of indexed fields over a filter, each document once; counts are approximate (`{ fields, filter?, rules?, limit? }`); `{ min, max }` for number/date fields; `503 not_available` while its system indexes build or past its 10 s budget |\n| `/rebuild-metadata` | POST | Rebuild the enrichment store from Qdrant file point payloads |\n| `/config/schema` | GET | Get config JSON schema |\n| `/config/match` | POST | Test file paths against inference rules and watch scope |\n| `/vcs/*` | GET/POST | Version tracking (`status`, `history`, `show`, `diff`, `check-exclusion`, `revert`, `exclude`) |\n\n**If the watcher is unreachable:** Check the service status (`nssm status jeeves-watcher` on Windows), check the configured port in the watcher config file, and check logs for startup errors.\n\n## Theory of Operation\n\nYou have access to a **semantic archive** of your human\'s working world. Documents, messages, tickets, notes, code, and other artifacts are indexed, chunked, embedded, and searchable. This is your long-term recall for anything beyond the current conversation.\n\n**When to reach for the watcher:**\n\n- **Someone asks about something that happened.** A meeting, a decision, a conversation, a ticket, a message. You weren\'t there, but the archive was. Search it.\n- **You need context you don\'t have.** Before asking the human "what\'s the status of X?", search for X. The answer is probably already indexed.\n- **You\'re working on something and need background.** Prior discussions, related records, relevant documents. Search by topic, filter by domain.\n- **You need to verify something.** Don\'t guess from stale memory. Search for the current state.\n- **You want to connect dots across domains.** The same topic might appear in multiple domains. A single semantic search surfaces all of them.\n\n**When NOT to use it:**\n\n- You already have the information in the current conversation\n- The question is about general knowledge, not the human\'s specific context\n- The watcher is unreachable (fall back to filesystem browsing)\n\n**The principle:** Memory-core is your curated highlights. The watcher archive is your perfect recall. Use memory first for speed and signal, but never let its narrow scope be the ceiling of what you can remember.\n\n## ⚠️ Embedding Cost — Hard Behavioral Gate\n\n**The watcher embeds every file it sees.** Embedding is the most expensive operation in the pipeline — each file triggers a Gemini API call. The index may contain hundreds of thousands of points. Any action that causes the watcher to process a large number of files has real, potentially significant cost.\n\n**STOP and ask for a human decision before:**\n\n- **Renaming, moving, or reorganizing watched directories.** The watcher has **move detection** enabled by default (`watch.moveDetection.enabled: true`). When a file is moved, the change source reports an unlink and an add; the `MoveCorrelator` buffers the unlink, matches it with the add via content hash, and processes it as a zero-embedding move (point ID remap, metadata re-inference, enrichment migration). Under the default parcel source, a renamed or moved directory is paired as a whole (at least 90% of its files at the same relative path and size) and each file becomes such a move. **No re-embedding cost for moves.** However, if a rename is blocked (e.g., by a file lock from the watcher or another service), **do NOT work around it by creating new directories and copying/moving files**. That creates duplicate embeddings at the new paths while the old paths still exist. Instead: stop the blocking service, perform the rename, restart.\n- **Changing `watch.paths` to add large directory trees.** Adding a new watch root triggers initial indexing of every matching file under it.\n- **Running `scope: "full"` reindex.** Every watched file is re-extracted (a file whose text to embed changed, or that was never indexed, is re-embedded) and every indexed source document is retrieved through its callback and always re-embedded. Use `scope: "rules"` for inference rule changes (no file re-embedding; only metadata reapplication).\n- **Any bulk file operation** (mass copy, mass move, template-based file generation) under watched paths.\n\n**The right instinct when blocked:** If a filesystem operation fails because a service has a lock, the correct response is to stop the service, do the operation, and restart — NOT to find a creative workaround. Creative workarounds in the presence of a live watcher are how you accidentally trigger re-embedding of tens of thousands of files.\n\n**Cost context:** A single file embedding costs a Gemini API call. 1,000 unnecessary embeddings is a noticeable cost. 100,000 unnecessary embeddings (e.g., copying an entire domain directory to a new path) is a billing event worth flagging.\n\n## Plugin Installation\n\nThe plugin is a standard OpenClaw plugin. On a Jeeves box, `jeeves install watcher --config-root <path>` installs it and writes its config; otherwise use `openclaw plugins install npm:@karmaniverous/jeeves-watcher-openclaw@<version> --pin --accept-capabilities` and set `plugins.entries.jeeves-watcher-openclaw.config` yourself. Restart the gateway to load it.\n\n`configRoot` has no default. Only `watcher_service` with `action: "install"` needs it; other `watcher_service` actions and all HTTP API tools work without it. If `watcher_service install` returns "configRoot not configured", set `configRoot` in the plugin config or the `JEEVES_CONFIG_ROOT` env var.\n\n## Quick Start (Existing Deployment)\n\nIf the watcher service is already running and healthy:\n\n1. **Search** — use `watcher_search` with a natural language query and optional metadata filters\n2. **Read source** — use `read` (standard file read) with `file_path` from search results for full document content. Source documents (points carrying `source`, such as `app://chat/thread/42`, and no `file_path`) are not files: their content comes from the source\'s callback (`POST /render` with `{ "source": "<ref>" }`).\n\n## Bootstrap (First-Time Setup)\n\nWhen the plugin loads and the watcher service is NOT yet set up, drive the entire setup proactively. The user should be able to install the plugin with nothing else in place and the bootstrap process gets them to a working system.\n\n**The agent drives this process.** Don\'t hand the user CLI commands and wait. Check each prerequisite, explain what\'s needed, execute what you can, and prompt the user only for decisions that require human judgment.\n\n### Step 1: Check Node.js\n\nVerify Node.js is installed and version ≥ 24.22.2:\n\n```bash\nnode --version\n```\n\nIf missing or too old, guide the user to install Node.js 24.22.2 or later from https://nodejs.org or via their package manager.\n\n### Step 2: Install Qdrant\n\nCheck if Qdrant is already running:\n\n```bash\ncurl -s http://localhost:6333/healthz\n```\n\nIf not running, install it. **Prefer native installation** (especially on cloud instances where Docker may not be available):\n\n**Linux (recommended for servers):**\n\n```bash\n# Download and install binary\ncurl -L https://github.com/qdrant/qdrant/releases/latest/download/qdrant-x86_64-unknown-linux-musl.tar.gz -o /tmp/qdrant.tar.gz\nsudo tar xzf /tmp/qdrant.tar.gz -C /usr/local/bin/\n\n# Create qdrant user and directories\nsudo useradd -r -s /bin/false qdrant\nsudo mkdir -p /var/lib/qdrant/storage /var/lib/qdrant/snapshots /etc/qdrant\nsudo chown -R qdrant:qdrant /var/lib/qdrant\n\n# Create config\nsudo tee /etc/qdrant/config.yaml > /dev/null <<EOF\nstorage:\n storage_path: /var/lib/qdrant/storage\n snapshots_path: /var/lib/qdrant/snapshots\nservice:\n host: 0.0.0.0\n http_port: 6333\n grpc_port: 6334\nEOF\n\n# Create systemd service\nsudo tee /etc/systemd/system/qdrant.service > /dev/null <<EOF\n[Unit]\nDescription=Qdrant Vector Database\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=/usr/local/bin/qdrant --config-path /etc/qdrant/config.yaml\nWorkingDirectory=/var/lib/qdrant\nRestart=always\nUser=qdrant\n\n[Install]\nWantedBy=multi-user.target\nEOF\nsudo systemctl daemon-reload\nsudo systemctl enable --now qdrant\n```\n\n**Windows:**\n\n```powershell\n# Download from GitHub releases page\n# https://github.com/qdrant/qdrant/releases\n# Extract and run, or register as NSSM service:\nnssm install Qdrant <path-to-qdrant.exe>\nnssm start Qdrant\n```\n\n**Docker (fallback, if available):**\n\n```bash\ndocker run -d -p 6333:6333 -v qdrant_data:/qdrant/storage qdrant/qdrant\n```\n\nAfter installation, verify:\n\n```bash\ncurl -s http://localhost:6333/healthz\n```\n\n### Step 3: Install Watcher Service\n\nInstall the watcher CLI globally:\n\n```bash\nnpm install -g @karmaniverous/jeeves-watcher\n```\n\nVerify:\n\n```bash\njeeves-watcher --version\n```\n\n### Step 4: Set Up Embedding Provider\n\nThe watcher uses Google Gemini for embeddings by default (`gemini-embedding-001`, 3072 dimensions).\n\nCheck for an existing API key:\n\n```bash\necho $GOOGLE_API_KEY # Linux/Mac\necho %GOOGLE_API_KEY% # Windows cmd\n$env:GOOGLE_API_KEY # PowerShell\n```\n\nIf not set, guide the user:\n\n1. Go to https://aistudio.google.com/apikey\n2. Create an API key (free tier supports 1,000 embedding requests/minute)\n3. Set it as a persistent environment variable:\n - **Linux:** Add `export GOOGLE_API_KEY=<key>` to `~/.bashrc` or `~/.profile`\n - **Windows:** `setx GOOGLE_API_KEY "<key>"` (new shell sessions only) or set via System Properties → Environment Variables\n - **macOS:** Add to `~/.zshrc` or use `launchctl setenv`\n\nVerify the key works by testing a Gemini API call:\n\n```bash\ncurl -s "https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key=$GOOGLE_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{"model":"models/gemini-embedding-001","content":{"parts":[{"text":"test"}]}}\'\n```\n\nA successful response contains an `embedding.values` array.\n\n### Step 5: Author Initial Config\n\nAsk the user these questions:\n\n- **What directories should the watcher index?** (e.g., `~/documents`, `~/projects`, a workspace path)\n- **What types of files matter?** (helps determine file extensions for watch globs)\n- **Are there directories to exclude?** (node_modules, .git, build outputs, etc.)\n\nThen generate a starter config file. Example minimal config:\n\n```json\n{\n "description": "Personal knowledge base indexing",\n "api": { "port": 1936 },\n "watch": {\n "paths": ["/home/user/documents/**/*.{md,txt,json,pdf,html,docx}"],\n "ignored": ["**/node_modules/**", "**/.git/**", "**/dist/**"]\n },\n "embedding": {\n "provider": "gemini",\n "model": "gemini-embedding-001",\n "dimensions": 3072,\n "apiKey": "${GOOGLE_API_KEY}",\n "chunkSize": 1000,\n "chunkOverlap": 200,\n "rateLimitPerMinute": 1000,\n "concurrency": 5\n },\n "vectorStore": {\n "url": "http://localhost:6333",\n "collectionName": "jeeves_archive"\n },\n "search": {\n "scoreThresholds": { "strong": 0.75, "relevant": 0.5, "noise": 0.25 },\n "hybrid": { "enabled": true }\n },\n "logging": { "level": "info" },\n "inferenceRules": []\n}\n```\n\nWrite the config to a sensible location (e.g., `~/.config/jeeves-watcher.config.json` on Linux, or alongside the user\'s workspace). Validate it against the config schema (offline; no running service needed) with:\n\n```bash\njeeves-watcher config validate -c <config-path>\n```\n\n### Step 6: Register and Start as a Service\n\n**The watcher should run as a persistent service, not a foreground process.**\n\n**Linux (systemd):**\n\n```bash\nsudo tee /etc/systemd/system/jeeves-watcher.service > /dev/null <<EOF\n[Unit]\nDescription=Jeeves Watcher - Filesystem Indexing Service\nAfter=network.target qdrant.service\n\n[Service]\nType=simple\nExecStart=$(which jeeves-watcher) start -c <config-path>\nWorkingDirectory=%h\nRestart=always\nEnvironment=GOOGLE_API_KEY=<key>\nUser=$USER\n\n[Install]\nWantedBy=multi-user.target\nEOF\nsudo systemctl enable --now jeeves-watcher\n```\n\n**Windows (NSSM):**\n\n```powershell\njeeves-watcher service install -c <config-path>\n# Or manually:\nnssm install jeeves-watcher (Get-Command jeeves-watcher).Source start -c <config-path>\nnssm set jeeves-watcher AppEnvironmentExtra GOOGLE_API_KEY=<key>\nnssm start jeeves-watcher\n```\n\nVerify the service started:\n\n```bash\ncurl -s http://127.0.0.1:1936/status\n```\n\n### Step 7: Verify Health\n\nCall `watcher_status` (or `curl http://127.0.0.1:1936/status`). Confirm:\n\n- Service is running\n- Qdrant collection exists with expected dimensions (3072)\n- Point count is increasing (initial indexing in progress)\n\nIf the point count is 0 after a minute, check `watcher_list_issues` for processing failures.\n\n### Step 8: Orientation\n\nOnce health is confirmed and initial indexing has started:\n\n1. Query `$.[\'description\',\'search\']` for the deployment\'s organizational strategy and score thresholds.\n2. Query `$.inferenceRules[*].[\'name\',\'description\']` for available record types.\n3. Report to the user: how many points indexed so far, which domains are available, estimated time to complete initial indexing (based on file count and embedding rate).\n\n### On Subsequent Sessions\n\nOn sessions after bootstrap is complete:\n\n1. Call `watcher_status` silently.\n2. Run the orientation queries silently.\n3. Only report if something changed (service down, point count dropped significantly, new domains appeared).\n\n**Key principle:** The agent drives discovery. The user shouldn\'t have to explain their archive to you — the archive explains itself through its config.\n\n## Tools\n\nThe plugin registers 18 tools: `watcher_status`, `watcher_config`, `watcher_config_apply`, `watcher_service`, `watcher_search`, `watcher_set_metadata`, `watcher_validate_config`, `watcher_reindex`, `watcher_scan`, `watcher_list_issues`, `watcher_walk`, and the seven version-control tools (see [Version Tracking](#version-tracking)).\n\n### `watcher_search`\n\nSemantic search over indexed documents (`POST /search`).\n\n- `query` (string, required) — natural language search query (non-empty)\n- `limit` (number, optional) — max results (positive integer), default 10\n- `offset` (number, optional) — skip N results for pagination (non-negative integer)\n- `filter` (object, optional) — Qdrant filter for metadata filtering\n\nReturns an array of `{ id, score, payload }` ordered by score (see [Search Result Shape](#search-result-shape)). A filter Qdrant rejects is `400 invalid_request`; an exhausted embedding budget is `503 embedding_unavailable`.\n\n### `watcher_set_metadata`\n\nSet or update metadata on a document (`POST /metadata`).\n\n- `path` (string, required) — file path of the document (absolute or watched path)\n- `metadata` (object, required) — key-value metadata to merge\n\nReturns `{ ok: true, matched_rules }`. If the document is busy (being indexed) for 10 s, the tool reports `503 not_available` with `Retry-After: 10` and changes nothing; retry after 10 s. The tool addresses files only; to enrich a source document, call `POST /metadata` directly with `{ "source": "<name>://<id>", "metadata": { ... } }` (exactly one of `path` or `source`).\n\n### `watcher_status`\n\nService health check (`GET /status`, never needs the API key). Returns `name`, `version`, `uptime`, `status`, and a `health` object: `collection` (`name`, `pointCount`, `dimensions`, and `systemIndexes`: the state of each index the service creates itself, e.g. `{ "chunk_index": "ready", "file_path": "ready", "matched_rules": "building" }`; `building`, `ready`, `conflict` or `failed`), `embedding` (`state` `ok` or `failing`, `lastSuccessAt`, `lastError` `{ at, code }`, `failingSince`, `backoffUntil`; codes only, never provider text), `reindex` (`active`; while active also `scope`, `startedAt`, `filesProcessed`, `totalFiles`), `initialScan`, and `vcs` when version tracking is on: `roots`, `pending` (changes not yet committed), `trippedRoots` (roots whose commits keep failing) and `oldestTrippedAt`.\n\nAfter a service restart, the `health.initialScan` field shows scan progress:\n\n- `active: true` — filesystem walk in progress; `filesMatched` and `filesEnqueued` grow until the initial walk (or chokidar\'s scan) completes\n- `active: false` with `completedAt`/`durationMs` — scan finished\n\nUse this to determine if the service is still initializing after a restart.\n\n### `watcher_config`\n\nQuery the effective runtime config via JSONPath (`GET /config`). Returns the full resolved merged document when no path is provided.\n\n- `path` (string, optional) — JSONPath expression\n\nThe document holds only the allow-listed keys `description`, `watch`, `configWatch`, `search`, `schemas`, `inferenceRules`, `mapHelpers`, `templateHelpers`, `maps`, `templates`, plus `issues` (the current issues). `api`, `sources`, `embedding`, `vectorStore` (and their keys) are never included. Each rule carries `source` (`config`, or the registering source of a virtual rule) and `values` (the runtime values index). File references (`map`, `maps`, `templates`) and named schema references are always resolved. An invalid JSONPath is `400 invalid_request`.\n\n### `watcher_validate_config`\n\nValidate config and optionally test file paths (`POST /config/validate`).\n\n- `config` (object, optional) — candidate config (partial or full). Omit to validate current config.\n- `testPaths` (string[], optional) — file paths to test against the config\n\nPartial configs are merged into the running config shallowly: each top-level key replaces the running value, except `inferenceRules`, which merge by rule name. This differs from `watcher_config_apply` (deep merge), so pass a whole top-level object (e.g. all of `watch`) when validating a change inside it. Returns `{ valid: false, errors: [{ path, message }] }`, or `{ valid: true, warnings?, testResults? }`, where each test result is `{ path, matchedRules, metadata, error? }` (`error: "File not found"` when the path cannot be read). Validation problems come back in a `200` body, so this is the place to see them in full.\n\n### `watcher_config_apply`\n\nApply config changes (`POST /config/apply`, sent as `{ patch: config }`).\n\n- `config` (object, required) — full or partial config to apply\n- `replace` (boolean, optional) — replace the whole config with `config` instead of merging\n\nPartial configs are deep-merged into the config file the service runs from (objects recursively, arrays replaced, `inferenceRules` by rule name), so `{}` or a single key is valid. The merged result is validated and written to disk, then the `configWatch.reindex` scope (default `issues`) runs; that reindex covers files only. With `configWatch.enabled` (default `true`) the service picks up the written file without a restart, including new/removed watch paths (the filesystem watcher is rebuilt), inference rule changes, move detection settings, and gitignore filter updates.\n\nReturns `{ applied: true, config }`. `config` is redacted through the same allow-list as `GET /config`: `api`, `sources` and provider keys are never echoed. When the file was written but the post-write apply step failed, the body also carries `warning` (a string describing the failure). An invalid merged config is a tool error reading `HTTP 400: {"error":"Config validation failed","code":"invalid_request","issues":[...]}`, so run `watcher_validate_config` first. A config file the service cannot read or write is `HTTP 500` with the generic `internal_error` body, the cause in the service log.\n\n### `watcher_reindex`\n\nTrigger a reindex operation (`POST /reindex`). Non-prune scopes return a `plan` object showing the blast area; the live run reuses the same targets. A live prune (non-dry-run) returns immediately without a plan.\n\n**Parameters:**\n\n- `scope` (string, optional) — Reindex scope. The tool sends `"rules"` when omitted (the HTTP API\'s own default is `"issues"`). One of:\n - `"rules"` — Re-apply inference rules to every watched file (no re-embedding; changed values written, keys the rules no longer project removed). With `path`, only files matching those globs. Without `path`, every indexed source document is also retrieved through its callback and its stored rule re-applied: unchanged content gets a metadata-only update, and a document is re-embedded only if its rendered text changed.\n - `"full"` — Re-extract and re-process every watched file (a file is re-embedded when the text to embed changed or it was never indexed), and retrieve and always re-embed every indexed source document. Clears the runtime values index first.\n - `"issues"` — Re-process the documents in `watcher_list_issues`: failed files, and failed source references (with their stored rule, or the `rule` recorded on the issue when the document was never indexed).\n - `"path"` — Re-process a specific file or all files under a directory. Requires `path`. Each target must be watched and not gitignored (`403 path_not_watched`) and must exist (`404 not_found`). Files only.\n - `"prune"` — Delete Qdrant points for files no longer in watch scope (removed paths, gitignored files, deleted files). No re-embedding. Pure cleanup. Never selects source points (`source` set, no `file_path`).\n- `path` (string | string[], optional) — Target file(s) or directories for `"path"` (required there), or globs narrowing `"rules"`. Ignored by other scopes.\n- `dryRun` (boolean, optional) — When `true`, compute and return the plan without executing.\n\nThe tool has no `source` parameter. To re-process specific source references, call `POST /reindex` directly with `{ "scope": "path", "source": ["<name>://<id>", ...] }` (at most 1000): they are enqueued on the notify queue, are not tracked by `GET /status`, and send no completion callback. A reindex triggered by a config change (`watcher_config_apply`, config-file reload, rule registration) covers files only.\n\n**Response (normal):**\n\n```json\n{\n "status": "started",\n "scope": "rules",\n "plan": {\n "total": 148000,\n "toProcess": 148000,\n "toDelete": 0,\n "byRoot": { "/data": 95000, "/srv/config": 3000 }\n }\n}\n```\n\n**Response (dryRun):**\n\n```json\n{\n "status": "dry_run",\n "scope": "prune",\n "plan": {\n "total": 562000,\n "toProcess": 0,\n "toDelete": 2300,\n "byRoot": { "/srv/app/node_modules": 1800, "/srv/app/.cache": 500 }\n }\n}\n```\n\n**Plan fields:**\n\n- `total` — Points scrolled (prune), or files plus source documents covered (other scopes).\n- `toProcess` — Files plus source documents to process (0 for prune).\n- `toDelete` — Points to delete (prune only, 0 for others).\n- `byRoot` — Files grouped by watch root (for prune: orphaned files), plus one entry per source name for source documents. Shows where the impact concentrates.\n- `incomplete` — `true` when the prune scroll failed part-way and the plan is partial.\n\n**Progress:** a live run (every scope except `path` with `source`) is tracked in `watcher_status` under `health.reindex` (`filesProcessed` / `totalFiles`, counting files plus source documents). A failing file or source document is logged and recorded; the rest of the run continues.\n\n**Guidance:**\n\n- Use `dryRun: true` before any large-blast operation to preview impact.\n- `prune` is safe — it only deletes orphaned file points, never re-embeds. Use after changing watch paths, fixing gitignore, or cleaning up stale data.\n- `prune` is NOT triggered by config-watch auto-reindex (too dangerous for auto-trigger).\n\n### `watcher_scan`\n\nFilter-only point query without vector search (`POST /scan`). Use for structural queries where the question has no semantic dimension.\n\n- `filter` (object, required) — Qdrant filter object. Required to prevent accidental full-collection scans.\n- `limit` (number, optional) — page size (integer 1 to 1000), default 100\n- `cursor` (string or number, optional) — opaque cursor from previous response for pagination\n- `fields` (string[], optional) — payload fields to return (projection)\n- `countOnly` (boolean, optional) — if true, return `{ count }` instead of points\n\n**Response (normal):**\n\n```json\n{\n "points": [\n {\n "id": "uuid",\n "payload": { "file_path": "/data/tickets/PROJ-123.json" }\n }\n ],\n "cursor": "opaque string, number or null"\n}\n```\n\n**Response (countOnly):**\n\n```json\n{ "count": 1234 }\n```\n\n**Key differences from `watcher_search`:**\n\n- No `query` parameter — does NOT use embeddings\n- No `score` field — results are unranked filter matches\n- Cursor-based pagination (not offset-based); `cursor` is `null` on the last page\n- Zero cost per call beyond Qdrant\'s filtered scroll\n\n**Pagination pattern:**\n\n```\nlet cursor = undefined;\ndo {\n const result = await watcher_scan({ filter, limit: 100, cursor });\n // process result.points\n cursor = result.cursor;\n} while (cursor);\n```\n\n### `watcher_service`\n\nManage the watcher background service (install, uninstall, start, stop, restart, check status). No HTTP call.\n\n- `action` (string, required) — one of: `install`, `uninstall`, `start`, `stop`, `restart`, `status`\n\nReturns the service manager\'s response. On Windows uses NSSM, on Linux systemd, on macOS launchd. Only `install` needs `configRoot` (see [Plugin Installation](#plugin-installation)).\n\n### `watcher_list_issues`\n\nGet runtime processing failures (`GET /issues`). Returns `{ count, issues: { [key]: IssueRecord[] } }`: `count` is the number of documents with issues, and each key is a file path or a source reference (`<name>://<id>`). Each `IssueRecord` has `type` (one of the [issue codes](#error-categories)), `message`, `timestamp`, and optionally `operation`, `property`, `rule`, `rules`, `types`, `details`.\n\n### `watcher_walk`\n\nList watched files matching globs (`POST /walk`). Returns matching file paths from the watcher\'s in-memory list of watched files across all configured watch roots.\n\n- `globs` (string[], required) — glob patterns to intersect with watch paths (non-empty; matched case-insensitively, dotfiles included)\n\n**Response:**\n\n```json\n{\n "paths": ["/data/foo/.meta/meta.json", "/data/bar/.meta/meta.json"],\n "matchedCount": 2,\n "scannedRoots": ["/data", "/srv/config"]\n}\n```\n\n**Use cases:**\n\n- Discover files matching a pattern across all watched directories (e.g., `["**/.meta/meta.json"]`)\n- Enumerate files before rule registration to understand scope\n- Find files that aren\'t yet indexed (no Qdrant dependency)\n\n**Key differences from `watcher_scan`:**\n\n- Lists the files the filesystem watcher tracks, not the Qdrant index\n- No embedding or indexing required, but the initial filesystem scan must be complete: until then the tool returns `HTTP 503 scan_in_progress` (check `health.initialScan` in `watcher_status`)\n- Returns file paths only (no metadata, no vectors)\n- Applies `watch.ignored` and gitignore filtering automatically\n\n## Query Planning: Scan vs Search\n\n**Decision rule:** If the query has no semantic/natural-language dimension, use `watcher_scan`. If you need meaning-based similarity, use `watcher_search`.\n\n| Use `watcher_scan` | Use `watcher_search` |\n| --- | --- |\n| "List all files in domain X" | "Find documents about authentication" |\n| "Files modified after timestamp T" | "What discusses rate limiting?" |\n| "Enumerate paths under prefix P" | "Prior conversations about deployment" |\n| "Count files matching a condition" | "Related tickets to this issue" |\n| "Staleness detection / delta computation" | "What happened in last week\'s meetings?" |\n\n**Scan-specific filter examples:**\n\n**Domain enumeration:**\n\n```json\n{ "must": [{ "key": "domains", "match": { "value": "email" } }] }\n```\n\n**Modified after timestamp:**\n\n```json\n{ "must": [{ "key": "modified_at", "range": { "gte": 1772800000 } }] }\n```\n\n**Path prefix matching:**\n\n```json\n{ "must": [{ "key": "file_path", "match": { "text": "/data/tickets" } }] }\n```\n\n**Count files in a domain (no point data transferred):**\n\n```\nwatcher_scan: filter={"must":[{"key":"domains","match":{"value":"github"}}]}, countOnly=true\n```\n\n---\n\n## Qdrant Filter Syntax\n\nFilters use Qdrant\'s native JSON filter format, passed as the `filter` parameter to `watcher_search`.\n\n### Basic Patterns\n\n**Match exact value:**\n\n```json\n{ "must": [{ "key": "domain", "match": { "value": "email" } }] }\n```\n\n**Match text (full-text search within field):**\n\n```json\n{ "must": [{ "key": "chunk_text", "match": { "text": "authentication" } }] }\n```\n\n**Combine conditions (AND):**\n\n```json\n{\n "must": [\n { "key": "domain", "match": { "value": "jira" } },\n { "key": "status", "match": { "value": "In Progress" } }\n ]\n}\n```\n\n**Exclude (NOT):**\n\n```json\n{\n "must_not": [{ "key": "domain", "match": { "value": "repos" } }]\n}\n```\n\n**Any of (OR):**\n\n```json\n{\n "should": [\n { "key": "domain", "match": { "value": "email" } },\n { "key": "domain", "match": { "value": "slack" } }\n ]\n}\n```\n\n**Nested (combine AND + NOT):**\n\n```json\n{\n "must": [{ "key": "domain", "match": { "value": "jira" } }],\n "must_not": [{ "key": "status", "match": { "value": "Done" } }]\n}\n```\n\n### Key Differences\n\n- `match.value` — exact match (case-sensitive, for keyword fields like `domain`, `status`)\n- `match.text` — full-text match (for text fields like `chunk_text`)\n\n## Search Result Shape\n\nEach result from `watcher_search` contains:\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `id` | string | Qdrant point ID |\n| `score` | number | Similarity (or hybrid fused) score; higher = more relevant |\n| `payload.file_path` | string | Normalized file path (file points only) |\n| `payload.source` | string | Source reference `<name>://<id>` (source points only) |\n| `payload.source_rule` | string | Designated rule of a source document (source points only) |\n| `payload.chunk_text` | string | The matched text chunk |\n| `payload.chunk_index` | number | Zero-based chunk position within the document |\n| `payload.total_chunks` | number | Total chunks for this document |\n| `payload.line_start` | number | 1-indexed line where the chunk starts |\n| `payload.line_end` | number | 1-indexed line where the chunk ends |\n| `payload.created_at` | number | Creation timestamp (unix seconds) |\n| `payload.modified_at` | number | Modification timestamp (unix seconds) |\n| `payload.content_hash` | string | SHA-256 hash of the embedded text |\n| `payload.matched_rules` | string[] | Names of inference rules that matched |\n\n`file_path` is not always present: a source point (a document indexed through a source callback) carries a non-empty `source` and no `file_path`. A file point may also carry a `source` written by its own rules; it is still a file point. Additional metadata fields depend on the deployment\'s inference rules (e.g., `domain`, `status`, `author`). Use `watcher_config` to discover available fields.\n\n## Query Planning (Per Search Task)\n\nIdentify relevant rule(s) from the orientation model, then retrieve their schemas:\n\n**Retrieve complete schema for a rule:**\n\n```\nwatcher_config: path="$.inferenceRules[?(@.name==\'jira-issue\')].schema"\n```\n\nReturns the rule\'s schema array with named schema references and file references already resolved: properties, types, `set` expressions, `uiHint`, `enum`, etc.\n\n**For select/multiselect fields without `enum` in schema:**\n\n```\nwatcher_config: path="$.inferenceRules[?(@.name==\'jira-issue\')].values.status"\n```\n\nRetrieves valid filter values from the runtime values index (distinct values accumulated during embedding).\n\n**When search results span multiple rules** (indicated by `matched_rules` on results): query each unique rule\'s schema separately and merge mentally. Most result sets share the same rule combination, so this is typically one or two queries, not one per result.\n\n---\n\n## uiHint → Filter Mapping\n\nUse `uiHint` to determine filter construction strategy. **This table is explicit, not intuited:**\n\n| `uiHint` | Qdrant filter | Notes |\n| --- | --- | --- |\n| `text` | `{ "key": "<field>", "match": { "text": "<value>" } }` | Substring/keyword match |\n| `select` | `{ "key": "<field>", "match": { "value": "<enum_value>" } }` | Exact match; use `enum` values from schema or runtime values index |\n| `multiselect` | `{ "key": "<field>", "match": { "value": "<enum_value>" } }` | Any-element match on array field; use `enum` or runtime values index |\n| `date` | `{ "key": "<field>", "range": { "gte": <unix_ts>, "lt": <unix_ts> } }` | Range filter against integer fields holding Unix timestamps (seconds). Source dates should be normalized in config via `{{toUnix ...}}` in `set` expressions. Either bound optional for open-ended ranges (e.g., "after January" → `gte` only) |\n| `number` | `{ "key": "<field>", "range": { "gte": <n>, "lte": <n> } }` | Either bound optional for open-ended ranges |\n| `check` | `{ "key": "<field>", "match": { "value": true } }` | Boolean match |\n| _(absent)_ | Do not use in filters | Internal bookkeeping field, not intended for search |\n\n**Fallback:** If a `select`/`multiselect` field has neither `enum` in schema nor values in the index, treat it as `text` (substring match instead of exact match).\n\n---\n\n## Qdrant Filter Combinators\n\nCompose individual field conditions into complex queries using three combinators:\n\n| Combinator | Semantics | Use case |\n| --- | --- | --- |\n| `must` | AND — all conditions required | Intersecting constraints (domain + date range + assignee) |\n| `should` | OR — at least one must match | Alternative values, fuzzy criteria ("assigned to X or Y") |\n| `must_not` | Exclusion — any match triggers exclude | Filtering out noise (exclude Done, exclude codebase domain) |\n\n**Combinators nest arbitrarily for complex boolean logic:**\n\n```json\n{\n "must": [\n { "key": "domain", "match": { "value": "jira" } },\n { "key": "created", "range": { "gte": 1735689600 } }\n ],\n "should": [\n { "key": "assignee", "match": { "value": "Jane Doe" } },\n { "key": "assignee", "match": { "value": null } }\n ],\n "must_not": [{ "key": "status", "match": { "value": "Done" } }]\n}\n```\n\nA consuming UI will necessarily compose simple single-field filters. The assistant can compose deeply complex queries combining multiple fields, nested boolean logic, and open-ended ranges to precisely target what it needs.\n\n---\n\n## Search Execution\n\n**Plain semantic search is valid and often sufficient.** Not every query needs metadata filters. When the user\'s question is broad or exploratory, a natural language query with no filter object is the right starting point. Add filters to narrow, not as a default.\n\n**Result limit guidance:**\n\n- Default: 10 results\n- Broad discovery / exploratory: 20–30, apply score threshold cutoff from config\n- Targeted retrieval with tight filters: 5\n- Cross-domain sweep: 15–20, no domain filter, use score to separate signal from noise\n\n---\n\n## Post-Processing Guidance\n\n### Score Interpretation\n\nUse `scoreThresholds` from config (queried during orientation). Values are deployment-specific, constrained to [-1, 1]:\n\n- `strong` — minimum score for a strong match. **Action:** High confidence. Use these results directly.\n- `relevant` — minimum score for relevance. **Action:** Likely useful but verify context before relying on them.\n- `noise` — maximum score below which results are noise. **Action:** Discard. If all results fall below this threshold, broaden your query or try different terms.\n\n### Chunk Grouping\n\nMultiple results with the same `file_path` (or, for source documents, the same `source`) are chunks of one document. Read the full file for complete context.\n\n### Schema Lookup\n\nUse `matched_rules` on results to look up applicable schemas for metadata interpretation:\n\n```\nwatcher_config: path="$.inferenceRules[?(@.name==\'jira-issue\')].schema"\n```\n\n### Full Context\n\nSearch gives you chunks; use `read` with `file_path` for the complete document. A source document has no file: render it with `POST /render` and `{ "source": "<ref>" }`, which retrieves it through its callback.\n\n---\n\n## Path Testing\n\nWhen uncertain whether a file would be indexed, test it against the rules. `watcher_validate_config` with `testPaths` (and no `config`) runs the current rules against each path and returns `{ path, matchedRules, metadata }` per path (`error: "File not found"` when the file cannot be read):\n\n```\nwatcher_validate_config: testPaths=["/data/tickets/PROJ-123.json"]\n```\n\nOr call `POST /config/match` directly with `{ "paths": [...] }`. It returns `{ matches: [{ rules, watched }] }`, one entry per path, without reading the files:\n\n- `rules` lists the matching rule names; an empty array means no inference rules match\n- `watched: false` means the path falls outside watch paths or is excluded by ignore patterns\n\n---\n\n## Config Authoring\n\n### Rule Structure\n\nEach inference rule has:\n\n- `name` (required) — unique identifier\n- `description` (required) — human-readable purpose\n- `match` (required) — JSON Schema matched against file attributes (e.g. a picomatch `glob` on `file.path`)\n- `schema` (optional) — array of named schema references and/or inline schema objects with `set` templates\n- `map` (optional) — JsonMap transform (inline, named map, or `.json` file path)\n- `template` (optional) — Handlebars template (inline, named template, or `.hbs`/`.handlebars` file path)\n- `render` (optional) — declarative frontmatter + Markdown renderer (mutually exclusive with `template`)\n- `renderAs` (optional) — output extension (1-10 lowercase alphanumerics); requires `template` or `render`\n\n### Authoring Notes\n\n- **`maps` entries are bare strings or inline maps.** A string value is a file path, loaded and parsed as a JsonMap at startup. A `{ path, description }` object is kept as-is and never loaded from the file, so it silently does nothing useful. (`mapHelpers` and `templateHelpers` are different: `{ path, description }` objects are their documented form.)\n- **Metadata is derived with `set`, not `attributes`.** Each schema property\'s `set` template produces its value. There is no `attributes` key in the config schema; older configs and examples that use one need rewriting.\n- **`render.frontmatter` accepts picomatch globs and `!` exclusions** alongside explicit keys, e.g. `["meta_id", "*", "!_*", "!chunk_*"]`. Explicit names keep their listed order; keys matched by a glob follow, sorted alphabetically; a key matching an exclusion is dropped.\n- **Virtual rules have a startup window.** Virtual rules are registered at runtime by other services (`POST /rules/register`, e.g. jeeves-meta), so they are absent during the initial file scan after a watcher start. Files indexed in that window lack virtual-rule metadata until the registration\'s automatic rules reindex runs, or until `POST /rules/reapply` covers them. If metadata from a virtual rule is missing right after a restart, check whether its service has registered yet.\n\n### Config Workflow\n\n1. Edit config (or build partial config object)\n2. Validate: `watcher_validate_config` with optional `testPaths` for dry-run preview\n3. Apply: `watcher_config_apply` — validates, writes, triggers the `configWatch.reindex` reindex\n4. Monitor: `watcher_list_issues` for runtime processing failures\n\n### Reindex Concurrency\n\nReindex operations process files and source documents concurrently using `reindex.concurrency` (default 50). This applies to every scope, to issue-source discovery, and to the stored-rule lookups of `POST /reindex` with `source`. The watcher\'s incremental processing (file events and source notifications) uses a separate `embedding.concurrency` setting.\n\nProgress is reported via `watcher_status` (`health.reindex.filesProcessed` / `health.reindex.totalFiles`, counting files plus source documents).\n\n### When to Reindex\n\n- **Rules scope** (`"rules"`): Changed rule matching patterns, set expressions, schema mappings. No file re-embedding needed.\n- **Full scope** (`"full"`): Added watch paths, broad schema restructuring, or source documents whose vectors must be rebuilt. Re-extracts every file (re-embedding the ones whose text to embed changed) and re-embeds every source document.\n- **Issues scope** (`"issues"`): After fixing the root cause of processing failures (permissions, encoding, file format, a source outage). Re-processes only the failed files and source documents.\n- **Path scope** (`"path"`): Edited files in a specific directory, or a single file looks stale, and you want them re-processed without a full reindex. A file whose text to embed is unchanged is not re-embedded.\n- **Prune scope** (`"prune"`): After changing `watch.paths`, adding gitignore rules, or discovering stale/orphaned points (e.g., indexed `node_modules`). Deletes points for out-of-scope or deleted files. Always `dryRun: true` first to preview.\n\n---\n\n## Diagnostics\n\n### Escalation Path\n\n1. `watcher_status` — is the service healthy? Is a reindex running? Is the initial scan still active?\n2. `watcher_list_issues` — which documents are failing and why?\n3. `watcher_config` with `$.issues` — the same issue records via JSONPath\n4. Check logs at the configured log path\n\n### Error Categories\n\nEvery issue record\'s `type` is one of these codes:\n\n- `type_collision` — conflicting metadata types across rules (includes `property`, `rules[]`, `types[]`)\n- `interpolation_error` — template or `set` expression rendering failed (includes `property`, `rule`; `details` holds per-step failures)\n- `extraction_error` — reading, rule application or chunking failed\n- `embedding_error` — the embedding provider failed\n- `upsert_error` — the vector store write failed\n- `match_failed` — a source document\'s designated rule did not match (`details` holds the AJV errors)\n- `rule_missing` — a source document\'s designated rule was not found\n- `no_content` — nothing to embed\n- `source_unavailable` — the source callback failed\n- `unsupported_content_type` — the source callback returned a non-JSON body\n- `too_large` — the document is over a `processing` ceiling (`maxFileBytes`, `maxChunksPerFile` or `maxTextBytes`); it is not embedded and its points are deleted. Raise the ceiling and run `reindex` with scope `issues` to restore it\n\nSource-document issues always carry `rule` (the designated rule of the failed run).\n\n**Issues are self-healing:** resolved on successful re-process. The issues file always represents the current set of unresolved problems: a live todo list.\n\n### Change Source\n\n`watcher_status` reports the change source under `health.changeSource` (`@parcel/watcher` by default, chokidar on request or when polling):\n\n- **Service refuses to start, log names `@parcel/watcher failed to load`:** the platform binary is missing or broken. Reinstall without `--omit=optional`, or set `watch.changeSource: chokidar` (the manual switch; a reload rebuilds the watcher with one full initial scan).\n- **Rising `reconcileEvents`:** the source is missing events and reconciliation is finding them. **Rising `resubscribes`:** subscriptions keep failing (Linux). **Rising `renewFailures`:** a Linux renewal could not unsubscribe; restart the service. **`unavailableRoots` above 0:** a watch root is missing or unreadable (an unmounted share); its files keep their points until it returns. **High `pendingStability`, or a long `lastReconcileDurationMs` / `lastRenewDurationMs`:** a large write burst or a slow disk; a long renewal pauses event handling for every root.\n- **Linux, a file in a moved-in or renamed folder is indexed late:** changes inside folders nested in a newly created or moved-in tree, or in a directory renamed within a root, are missed until the root\'s subscription is renewed at the next periodic reconcile, up to `watch.reconcileIntervalMs` (15 minutes by default) late. Expected; shorten the interval if the latency matters. The files moved in with the tree are indexed at once.\n- **Linux, events lost under a write burst with nothing in the log:** parcel cannot detect an inotify queue overflow. Check `sysctl fs.inotify.max_queued_events` (recommended 1048576) and `fs.inotify.max_user_watches` (the start log warns at 90%); the periodic reconcile finds what was lost (`reconcileEvents` rises).\n- **Windows, files in deep folders indexed only by reconciliation:** expected. Under parcel, creating or changing a file whose path is 260 characters or more produces no change event at either `LongPathsEnabled` setting, so it is indexed at the next periodic reconcile, up to `watch.reconcileIntervalMs` late. Nothing to fix; check that `watch.reconcileIntervalMs` is not 0. (`LongPathsEnabled = 1` is still recommended for other tools; it is a host change for the owner.)\n- **Windows, `lastRestartCause` set:** under parcel the service never unsubscribes on Windows (`@parcel/watcher` 2.6.0 crashes the process there), so a reload that rebuilds the watcher, a subscription error or a vanished watch root exits with code 75 and the service manager restarts it. `watch-config-changed`, `subscription-error` or `root-gone` says why, `lastRestartAt` when. If the service stays down after such an exit, its service manager is not restarting it on exit (NSSM: `AppExit Default Restart`).\n\n### Index Check at Start\n\nAfter the start walk the service deletes the indexed documents of files gone from disk (deleted or moved while it was down). `watcher_status` reports it under `health.indexCheck` (`state`, `at`, `durationMs`, `checked`, `removed`, `skippedRoots`, `capped`):\n\n- **`capped` above 0:** under some watch root more than 10% of the indexed documents (once more than 20 are missing) or more than 1,000 were missing from disk or filters, so nothing was deleted there; the log names the root. Run `watcher_reindex` with scope `prune` and `dryRun: true`, check the plan, then run it.\n- **`state: failed`:** the index could not be listed (Qdrant down), or the service stopped first; it runs again at the next start.\n- **`skippedRoots` above 0:** a root was unavailable, unwatched, gone, or its start walk failed; its deletions wait for the next start or a `prune`.\n- **Files that still exist but are now ignored** keep their points: that is `prune`\'s job. `watch.indexCheckOnStart: false` turns the check off.\n\n---\n\n## Helper Management\n\nHelpers use namespace prefixing: config key becomes prefix. A helper named `slack` exports `slack_extractParticipants`.\n\nEnumerate loaded helpers:\n\n```\n$.mapHelpers — JsonMap helper namespaces with exports\n$.templateHelpers — Handlebars helper namespaces with exports\n```\n\n### Built-in JsonMap Helpers\n\nThe following helpers are available in every JsonMap `lib` context without any helper config:\n\n#### `fetchSiblings(filePath, options?)`\n\nRetrieve extracted text from neighboring files in the same directory with the same extension. Useful for contextual embedding — e.g., injecting surrounding email messages into a thread member\'s embedding for better semantic search.\n\n**Parameters:**\n\n- `filePath` (string) — the current file path (in a rule\'s `map`, `$.input.file.path`)\n- `options` (object, optional; JsonMap `params` are path strings, so a call from a config `map` uses the defaults):\n - `before` (number, default 3) — number of preceding siblings to include\n - `after` (number, default 1) — number of following siblings to include\n - `sort` (`"name"` | `"mtime"`, default `"name"`) — sort order for determining neighbor position\n\n**Returns:** `string[]` — extracted text from sibling files, in sort order. Files that fail extraction are silently skipped. For a source document (a source rule\'s `map`) there is no file reader, so it returns `[]`.\n\n**Example** (in a rule\'s `map`):\n\n```json\n{\n "context": {\n "$": { "method": "$.lib.fetchSiblings", "params": "$.input.file.path" }\n }\n}\n```\n\n---\n\n## Enrichment\n\nUse `watcher_set_metadata` to tag documents after analysis (e.g., `reviewed: true`, project labels).\n\n**Enrichments are durable.** Stored in a SQLite database (`<stateDir>/enrichments.sqlite`, default `stateDir` `.jeeves-metadata`), enrichments survive full reindexes. Enriching a file that is not indexed yet stores the enrichment, which applies once the file is indexed. When the watcher re-processes a file, enrichments are merged with inference rule output using composable semantics: scalar fields overwrite, array fields union+deduplicate.\n\n**Metadata is validated against the file\'s matched rule schemas** (for a source document, its designated rule\'s schema). Only declared property types are checked; unknown properties and `null` values pass. The tool reports a failure as `HTTP 400 invalid_request: Validation failed`; the structured `details` are in the HTTP response body:\n\n```json\n{\n "code": "invalid_request",\n "error": "Validation failed",\n "details": [\n {\n "property": "priority",\n "expected": "string",\n "received": "number",\n "rule": "jira-issue",\n "message": "Property \'priority\' is declared as string in jira-issue schema, received number"\n }\n ]\n}\n```\n\n---\n\n## Version Tracking\n\nThe watcher includes built-in history and undo for your watched files. When enabled, every change to a watched file is automatically versioned. You can view the history of any file or folder, see exactly what changed between versions, retrieve old versions, and undo changes — all through the version-control tools below.\n\n### Tools\n\n| Tool | Purpose |\n| --- | --- |\n| `watcher_get_vcs_status` | Check version tracking health: enabled state, tracked roots, remote sync status |\n| `watcher_list_vcs_history` | View change history for a file or folder, with optional date range and limit |\n| `watcher_show_vcs_file` | Retrieve the content of a file at a specific past version |\n| `watcher_diff_vcs` | Show what changed between two versions, or between a past version and now |\n| `watcher_revert_vcs` | Undo changes by restoring files to a specific past version |\n| `watcher_exclude_vcs_path` | Exclude a path from version history (it stays in search), or remove the exclusion; untracks matching files |\n| `watcher_check_vcs_exclusion` | Check whether git ignores a path and why (rule, file, line) |\n\n### Usage Patterns\n\n**View recent changes to a file:**\n\n```\nwatcher_list_vcs_history: glob="/data/tickets/PROJ-123.json", limit=10\n```\n\n**See what changed between two versions:**\n\n```\nwatcher_diff_vcs: glob="/data/tickets/PROJ-123.json", commit="abc1234"\n```\n\n**Restore a file to a previous version:**\n\n```\nwatcher_revert_vcs: glob="/data/tickets/PROJ-123.json", commit="abc1234"\n```\n\n**View old content without restoring:**\n\n```\nwatcher_show_vcs_file: path="/data/tickets/PROJ-123.json", commit="abc1234"\n```\n\n**Exclude files that don\'t need versioning** (e.g., Jira issues that are read-only syncs):\n\n```\nwatcher_exclude_vcs_path: glob="/data/tickets/**/*.json"\n```\n\n**Check if a file is excluded and why:**\n\n```\nwatcher_check_vcs_exclusion: path="/data/tickets/PROJ-123.json"\n```\n\n### Enabling Version Tracking\n\nVersion tracking is controlled by the `vcs.enabled` config setting. When set to `true`, the watcher automatically versions all watched files. No additional setup is required — just enable it and history starts accumulating.\n\nSquash retention activates by default when VCS is enabled, even without explicit `retention` config. The defaults are: 30 days max age, 100 versions max, and a daily midnight cron schedule (`0 0 * * *`). These defaults prevent unbounded commit history growth.\n\n### Excluding Paths\n\nNot all watched files need version history. For example, if Jira issues are synced read-only, there\'s nothing to "undo." Use `watcher_exclude_vcs_path` to stop tracking specific paths.\n\nThe exclusion is VCS-only: the pattern goes to the root\'s `.git/info/exclude`, so the files stop being versioned but stay indexed and searchable.\n\n- **Destructive.** Adding a pattern untracks the tracked files it matches, in one commit: their history ends at their last committed version (the files stay on disk). `remove: true` deletes the pattern but does not restore history; the files are versioned again from their next change.\n- **One gitignore pattern, root-anchored.** The pattern is written as `/` plus the glob relative to the root, so `/data/tickets/*.json` (root `/data`) matches only directly under `tickets/`; use `**/` for any depth. Pass `root` to give a root-relative glob (`tickets/**/*.json`, without a leading `/`: with `root`, a glob starting with `/` is a filesystem path and must lie under the root).\n- **Brackets are a character class.** `Report [draft].docx` matches `Report d.docx`; escape them to name the file: `Report \\[draft\\].docx`.\n- **Refused (`400`):** a newline, a leading `!` or `#`, braces, a pattern matching the whole root (`*`, `**`, `**/*`, ...), or a glob outside the given `root`.\n- **More than 100 files** to untrack need `confirm: true`; without it the tool reports `409 confirm_required` with the count and changes nothing. Check the count with the user before confirming.\n- **Busy root.** If version control is busy on the root for 30 s the tool reports `503 not_available`; retry after 30 s.\n- `watcher_check_vcs_exclusion` reports `excluded: false` for a tracked file even when a human `.gitignore` matches it (git never reports tracked paths as ignored); exclude it to untrack it.\n\n### Score Interpretation\n\nVCS tools do not use vector search. There are no relevance scores to interpret — results are exact history lookups, diffs, and file retrievals.\n\n---\n\n## Error Handling\n\nIf the watcher is unreachable:\n\n- Inform the user that semantic search is temporarily unavailable\n- Fall back to direct `read` for known file paths\n- Do not retry silently in a loop\n\nIf a tool fails with `HTTP 401` (`unauthorized`):\n\n- The watcher sets `api.apiKey` and the plugin sent no key, or a different one.\n- Ask the user to set `plugins.entries.jeeves-watcher-openclaw.config.apiKey` in `openclaw.json` (or the `JEEVES_WATCHER_API_KEY` environment variable of the gateway) to the service\'s `api.apiKey`. The tools read it on every call, so no restart is needed.\n- Never ask for, print or guess the key. `GET /status` and `GET /openapi.json` never need it.\n\nIf a tool or call fails with `HTTP 503` (`not_available`) and a `Retry-After` header:\n\n- `POST /metadata` (`watcher_set_metadata`), `POST /rules/reapply` and `POST /documents/delete` wait at most 10 s for a document that is being indexed, then answer `503` with `Retry-After: 10`. A single-document request changed nothing; a multi-document request kept the documents it already changed. All three are idempotent: retry the same request after 10 s.\n- `watcher_exclude_vcs_path` waits 30 s for a busy version-control root.\n\nIf a tool fails with `HTTP 500: {"error":"Response validation failed","code":"internal_error"}`, the watcher produced a response that doesn\'t match its own schema: a server bug. Report it with the request you made; retrying won\'t help.\n\nIf tools are unavailable (plugin not loaded in this session):\n\n- The watcher API is still accessible via direct HTTP calls\n- Use `exec` to call the endpoints listed in Service Architecture\n- Default: `http://127.0.0.1:1936`\n\n**CLI Fallbacks:**\n\n- `jeeves-watcher status [-p <port>]` — check if the service is running (`GET /status`)\n- `jeeves-watcher config validate -c <path>` — validate a config file against the schema (offline)\n- Restart via NSSM (Windows) or systemctl (Linux)\n\n---\n\n## References\n\n- [JSONPath Plus documentation](https://www.npmjs.com/package/jsonpath-plus) for JSONPath syntax\n- [Qdrant filtering documentation](https://qdrant.tech/documentation/concepts/filtering/) for advanced query patterns and search response format\n';
|
|
31102
|
+
|
|
29230
31103
|
/**
|
|
29231
31104
|
* @module plugin/constants
|
|
29232
31105
|
* Shared constants for the OpenClaw plugin package.
|
|
@@ -29252,9 +31125,10 @@ const CONFIG_ROOT_UNSET_WARNING = `[${PLUGIN_ID}] configRoot not configured yet
|
|
|
29252
31125
|
* Watcher-specific convenience wrappers over `@karmaniverous/jeeves` core SDK.
|
|
29253
31126
|
*
|
|
29254
31127
|
* @remarks
|
|
29255
|
-
* Only watcher-specific resolution logic
|
|
29256
|
-
* utilities
|
|
29257
|
-
*
|
|
31128
|
+
* Only watcher-specific resolution logic (API URL, API key, config root)
|
|
31129
|
+
* lives here. Core SDK types and utilities should be imported directly from
|
|
31130
|
+
* `@karmaniverous/jeeves`; every watcher API request goes through core's
|
|
31131
|
+
* catalog toolset (`catalogTools.ts`).
|
|
29258
31132
|
*
|
|
29259
31133
|
* @module plugin/helpers
|
|
29260
31134
|
*/
|
|
@@ -29272,6 +31146,23 @@ function getApiUrl(api) {
|
|
|
29272
31146
|
return scoped;
|
|
29273
31147
|
return resolvePluginSetting(api, PLUGIN_ID, 'apiUrl', 'JEEVES_WATCHER_URL', DEFAULT_API_URL);
|
|
29274
31148
|
}
|
|
31149
|
+
/**
|
|
31150
|
+
* Resolve the watcher API bearer key, if configured.
|
|
31151
|
+
*
|
|
31152
|
+
* @remarks
|
|
31153
|
+
* Resolution order: plugin-scoped config (`api.pluginConfig.apiKey`), then
|
|
31154
|
+
* `plugins.entries.<id>.config.apiKey`, then `JEEVES_WATCHER_API_KEY`. Blank
|
|
31155
|
+
* values count as unset. Resolved per request; never logged.
|
|
31156
|
+
*
|
|
31157
|
+
* @returns The key, or `undefined` when unset (requests carry no header).
|
|
31158
|
+
*/
|
|
31159
|
+
function getApiKey(api) {
|
|
31160
|
+
const scoped = api.pluginConfig?.apiKey;
|
|
31161
|
+
if (typeof scoped === 'string' && scoped.trim() !== '')
|
|
31162
|
+
return scoped;
|
|
31163
|
+
const resolved = resolveOptionalPluginSetting(api, PLUGIN_ID, 'apiKey', API_KEY_ENV_VAR);
|
|
31164
|
+
return resolved?.trim() ? resolved : undefined;
|
|
31165
|
+
}
|
|
29275
31166
|
/**
|
|
29276
31167
|
* Resolve the platform config root path, if configured.
|
|
29277
31168
|
*
|
|
@@ -29292,6 +31183,134 @@ function getConfigRoot(api) {
|
|
|
29292
31183
|
return resolved?.trim() ? resolved : undefined;
|
|
29293
31184
|
}
|
|
29294
31185
|
|
|
31186
|
+
/**
|
|
31187
|
+
* @module plugin/catalogTools
|
|
31188
|
+
* Every watcher tool, built from `WATCHER_CATALOG` by core's toolset (D38): the four standard tools (`watcher_status`, `watcher_service`, `watcher_config`, `watcher_config_apply`) and the 14 domain tools, all sending the bearer key where their entry requires it; `registerCatalogTool` registers one of them as an optional tool.
|
|
31189
|
+
*
|
|
31190
|
+
* @remarks
|
|
31191
|
+
* Each tool is named `watcher_` plus its entry's verb-first name in
|
|
31192
|
+
* snake_case (D77, `toToolName`). Parameters come from the entry's request
|
|
31193
|
+
* schemas; descriptions keep the 0.17.0 wording where it is still true
|
|
31194
|
+
* (`WATCHER_TOOL_SELECTION`). `watcher_search` and `watcher_scan` are
|
|
31195
|
+
* `catalogMode: 'direct-only'`, so OpenClaw Tool Search keeps them
|
|
31196
|
+
* model-visible: semantic search must be a direct tool or the model reaches
|
|
31197
|
+
* for grep first. Every call runs on core's 120 s default timeout
|
|
31198
|
+
* (`DEFAULT_FETCH_TIMEOUT_MS`, D137), the status probe on 5 s. Errors are
|
|
31199
|
+
* reported as core's `HTTP <status>: <body>`; API-key guidance lives in the
|
|
31200
|
+
* skill's troubleshooting section (spec #281 D31).
|
|
31201
|
+
*/
|
|
31202
|
+
/** `watcher_exclude_vcs_path` description (D103, D137). */
|
|
31203
|
+
const VCS_EXCLUDE_DESCRIPTION = `Exclude a path from version history (VCS-only: it stays in search), or remove the exclusion. Adding untracks the files the pattern matches, ending their history; more than ${String(VCS_EXCLUDE_CONFIRM_THRESHOLD)} files need confirm: true. A timeout does not cancel the exclusion: check the outcome with watcher_check_vcs_exclusion before retrying.`;
|
|
31204
|
+
/** `watcher_check_vcs_exclusion` description (0.21.0, spec B4). */
|
|
31205
|
+
const VCS_CHECK_DESCRIPTION = 'Check whether git ignores a path and why: the rule, the file holding it and its line';
|
|
31206
|
+
/** The scope `watcher_reindex` sends when the caller omits one: 0.17.0's default (owner ruling, 2026-11-04). */
|
|
31207
|
+
const REINDEX_DEFAULT_SCOPE = 'rules';
|
|
31208
|
+
/** `watcher_reindex` description. */
|
|
31209
|
+
const REINDEX_DESCRIPTION = `Trigger a reindex of the watched files. Without scope, re-applies inference rules (scope "${REINDEX_DEFAULT_SCOPE}"); the HTTP API's own default is "issues".`;
|
|
31210
|
+
/**
|
|
31211
|
+
* The 14 domain tools, in their 0.17.0 registration order, by catalog entry.
|
|
31212
|
+
* Descriptions keep the 0.17.0 text unless it changed with the tool.
|
|
31213
|
+
*/
|
|
31214
|
+
const WATCHER_TOOL_SELECTION = {
|
|
31215
|
+
search: {
|
|
31216
|
+
description: 'Semantic search over indexed documents. Supports Qdrant filters. This is a direct tool: call it directly, never through tool_call.',
|
|
31217
|
+
catalogMode: 'direct-only',
|
|
31218
|
+
},
|
|
31219
|
+
setMetadata: {
|
|
31220
|
+
description: 'Set or update metadata on a document by file path or source reference.',
|
|
31221
|
+
},
|
|
31222
|
+
validateConfig: {
|
|
31223
|
+
description: 'Validate a candidate config (or current config if omitted). Optionally test file paths against the config to preview rule matching and metadata output.',
|
|
31224
|
+
},
|
|
31225
|
+
reindex: { description: REINDEX_DESCRIPTION },
|
|
31226
|
+
scan: {
|
|
31227
|
+
description: 'Filter-only point query without vector search. Returns metadata for points matching a Qdrant filter. Use for structural queries: file enumeration, staleness checks, delta computation. Use watcher_search for semantic/similarity queries. This is a direct tool: call it directly, never through tool_call.',
|
|
31228
|
+
catalogMode: 'direct-only',
|
|
31229
|
+
},
|
|
31230
|
+
listIssues: {
|
|
31231
|
+
description: 'Get runtime processing failures. Shows the documents (file path or source reference) that failed processing and why.',
|
|
31232
|
+
},
|
|
31233
|
+
walk: {
|
|
31234
|
+
description: 'Walk watched filesystem paths with glob intersection. Returns matching file paths from all configured watch roots, applying watch.ignored and gitignore filtering.',
|
|
31235
|
+
},
|
|
31236
|
+
getVcsStatus: {
|
|
31237
|
+
description: 'Get version tracking health: enabled state, tracked roots, remote status, last activity',
|
|
31238
|
+
},
|
|
31239
|
+
listVcsHistory: {
|
|
31240
|
+
description: 'Query change history by path or glob with optional date range',
|
|
31241
|
+
},
|
|
31242
|
+
showVcsFile: { description: 'Retrieve file content at a specific version' },
|
|
31243
|
+
diffVcs: {
|
|
31244
|
+
description: 'Show what changed between two versions, or between a version and current',
|
|
31245
|
+
},
|
|
31246
|
+
revertVcs: {
|
|
31247
|
+
description: 'Undo changes by restoring files to a specific version',
|
|
31248
|
+
},
|
|
31249
|
+
excludeVcsPath: { description: VCS_EXCLUDE_DESCRIPTION },
|
|
31250
|
+
checkVcsExclusion: { description: VCS_CHECK_DESCRIPTION },
|
|
31251
|
+
};
|
|
31252
|
+
/** The standard tools, in their 0.17.0 registration order. */
|
|
31253
|
+
const STANDARD_TOOLS = [
|
|
31254
|
+
'watcher_status',
|
|
31255
|
+
'watcher_service',
|
|
31256
|
+
'watcher_config',
|
|
31257
|
+
'watcher_config_apply',
|
|
31258
|
+
];
|
|
31259
|
+
/** Every tool the plugin registers, in registration order (0.17.0 order). */
|
|
31260
|
+
const WATCHER_TOOL_NAMES = [
|
|
31261
|
+
...STANDARD_TOOLS,
|
|
31262
|
+
...Object.keys(WATCHER_TOOL_SELECTION).map((entry) => toToolName('watcher', entry)),
|
|
31263
|
+
];
|
|
31264
|
+
/**
|
|
31265
|
+
* Core's toolset for the watcher: the four standard tools and the 14 domain
|
|
31266
|
+
* tools. The base URL and the bearer key are resolved from `api` on every
|
|
31267
|
+
* call.
|
|
31268
|
+
*
|
|
31269
|
+
* @param api - Plugin API (config source).
|
|
31270
|
+
* @param component - Plugin-side component descriptor.
|
|
31271
|
+
* @returns The tools, keyed by name.
|
|
31272
|
+
*/
|
|
31273
|
+
function createCatalogTools(api, component) {
|
|
31274
|
+
const auth = { bearer: () => getApiKey(api) };
|
|
31275
|
+
const tools = createPluginToolset(component, {
|
|
31276
|
+
apiUrl: () => getApiUrl(api),
|
|
31277
|
+
catalog: WATCHER_CATALOG,
|
|
31278
|
+
auth,
|
|
31279
|
+
tools: WATCHER_TOOL_SELECTION,
|
|
31280
|
+
});
|
|
31281
|
+
return new Map(tools.map((tool) => [
|
|
31282
|
+
tool.name,
|
|
31283
|
+
tool.name === 'watcher_reindex' ? withDefaultScope(tool) : tool,
|
|
31284
|
+
]));
|
|
31285
|
+
}
|
|
31286
|
+
/**
|
|
31287
|
+
* `watcher_reindex` with 0.17.0's default scope: a call without `scope` sends
|
|
31288
|
+
* {@link REINDEX_DEFAULT_SCOPE}, so agents see no change from 0.17.0.
|
|
31289
|
+
*
|
|
31290
|
+
* @param tool - The catalog-built reindex tool.
|
|
31291
|
+
* @returns The same tool, defaulting `scope`.
|
|
31292
|
+
*/
|
|
31293
|
+
function withDefaultScope(tool) {
|
|
31294
|
+
return {
|
|
31295
|
+
...tool,
|
|
31296
|
+
execute: (id, params) => tool.execute(id, { scope: REINDEX_DEFAULT_SCOPE, ...params }),
|
|
31297
|
+
};
|
|
31298
|
+
}
|
|
31299
|
+
/**
|
|
31300
|
+
* Register one catalog-built tool as an optional tool.
|
|
31301
|
+
*
|
|
31302
|
+
* @param api - Plugin API (tool registration target).
|
|
31303
|
+
* @param catalogTools - Catalog-built tools, by name ({@link createCatalogTools}).
|
|
31304
|
+
* @param name - Tool name.
|
|
31305
|
+
* @throws When no tool of that name was built.
|
|
31306
|
+
*/
|
|
31307
|
+
function registerCatalogTool(api, catalogTools, name) {
|
|
31308
|
+
const tool = catalogTools.get(name);
|
|
31309
|
+
if (!tool)
|
|
31310
|
+
throw new Error(`Catalog tool not built: ${name}`);
|
|
31311
|
+
api.registerTool(tool, { optional: true });
|
|
31312
|
+
}
|
|
31313
|
+
|
|
29295
31314
|
/**
|
|
29296
31315
|
* @module plugin/toolGating
|
|
29297
31316
|
* Which watcher tool invocations read `configRoot`, and so must be gated on it.
|
|
@@ -29301,15 +31320,17 @@ function getConfigRoot(api) {
|
|
|
29301
31320
|
* error only when its implementation actually reads `configRoot`. Tools
|
|
29302
31321
|
* that only call the service HTTP API keep working without it.
|
|
29303
31322
|
*
|
|
29304
|
-
* Audit of the
|
|
29305
|
-
* - `watcher_status`, `watcher_config`, `watcher_config_apply
|
|
29306
|
-
*
|
|
29307
|
-
*
|
|
29308
|
-
*
|
|
31323
|
+
* Audit of the tools the plugin registers:
|
|
31324
|
+
* - `watcher_status`, `watcher_config`, `watcher_config_apply` (core
|
|
31325
|
+
* toolset from `WATCHER_CATALOG`, `catalogTools.ts`): HTTP only
|
|
31326
|
+
* (`{apiUrl}` resolved per call; the bearer key sent where the entry
|
|
31327
|
+
* requires it).
|
|
31328
|
+
* - `watcher_service` (core standard toolset): only `install` reads
|
|
31329
|
+
* `configRoot` (`createServiceManager` → `resolveConfigFilePath` →
|
|
29309
31330
|
* `getComponentConfigDir`); the other actions query or drive the OS
|
|
29310
31331
|
* service manager by service name.
|
|
29311
31332
|
*
|
|
29312
|
-
* The 14 domain tools
|
|
31333
|
+
* The 14 domain tools come from the same core toolset (D38): HTTP only,
|
|
29313
31334
|
* never gated.
|
|
29314
31335
|
*/
|
|
29315
31336
|
/**
|
|
@@ -29407,31 +31428,25 @@ function withGuardedTools(api, ensureCore, readers = CONFIG_ROOT_READERS) {
|
|
|
29407
31428
|
|
|
29408
31429
|
/**
|
|
29409
31430
|
* @module plugin/promptContext
|
|
29410
|
-
*
|
|
31431
|
+
* Injects the descriptor's always-in-context rules (`promptRules`, from watcher-core) via `before_prompt_build`.
|
|
29411
31432
|
*
|
|
29412
31433
|
* @remarks
|
|
29413
31434
|
* Replaces the v0.x TOOLS.md `## Watcher` section that core's
|
|
29414
|
-
* `ComponentWriter` wrote on a timer.
|
|
29415
|
-
*
|
|
29416
|
-
* `watcher_status` and `watcher_config`, and detail lives in the skill.
|
|
31435
|
+
* `ComponentWriter` wrote on a timer. Core registers nothing itself: the
|
|
31436
|
+
* plugin passes the descriptor's `promptRules` to `registerPromptContext`.
|
|
29417
31437
|
*/
|
|
29418
|
-
/** Static watcher rules the agent must see on every turn. */
|
|
29419
|
-
const WATCHER_PROMPT_CONTEXT = [
|
|
29420
|
-
'## Watcher',
|
|
29421
|
-
'This environment includes a semantic search index over the document archive (`watcher_search`).',
|
|
29422
|
-
'- **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.',
|
|
29423
|
-
'- **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.',
|
|
29424
|
-
'- **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.',
|
|
29425
|
-
'- **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.',
|
|
29426
|
-
'- **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.',
|
|
29427
|
-
].join('\n');
|
|
29428
31438
|
/**
|
|
29429
|
-
* Register the
|
|
31439
|
+
* Register the component's prompt rules with OpenClaw's `before_prompt_build` hook.
|
|
29430
31440
|
*
|
|
29431
|
-
* @
|
|
31441
|
+
* @param api - Plugin API.
|
|
31442
|
+
* @param component - Descriptor carrying `promptRules`.
|
|
31443
|
+
* @returns `true` when the hook was registered; `false` when the host has no
|
|
31444
|
+
* `api.on` or the descriptor has no rules.
|
|
29432
31445
|
*/
|
|
29433
|
-
function registerWatcherPromptContext(api) {
|
|
29434
|
-
|
|
31446
|
+
function registerWatcherPromptContext(api, component) {
|
|
31447
|
+
if (!component.promptRules)
|
|
31448
|
+
return false;
|
|
31449
|
+
return registerPromptContext(api, { content: component.promptRules });
|
|
29435
31450
|
}
|
|
29436
31451
|
|
|
29437
31452
|
/**
|
|
@@ -29439,8 +31454,9 @@ function registerWatcherPromptContext(api) {
|
|
|
29439
31454
|
* Jeeves component descriptor for the watcher OpenClaw plugin.
|
|
29440
31455
|
*
|
|
29441
31456
|
* @remarks
|
|
29442
|
-
* Feeds `createPluginToolset()` (`
|
|
29443
|
-
*
|
|
31457
|
+
* Feeds `createPluginToolset()` (with `WATCHER_CATALOG`, `catalogTools.ts`),
|
|
31458
|
+
* which builds the four standard tools and the catalog-built domain tools.
|
|
31459
|
+
* The service descriptor
|
|
29444
31460
|
* (`packages/service/src/descriptor.ts`) is the canonical source for the
|
|
29445
31461
|
* config schema and CLI commands.
|
|
29446
31462
|
*/
|
|
@@ -29462,6 +31478,9 @@ function createWatcherComponent(pluginVersion) {
|
|
|
29462
31478
|
configSchema: jeevesComponentDescriptorSchema.shape.name,
|
|
29463
31479
|
configFileName: 'config.json',
|
|
29464
31480
|
initTemplate: () => ({}),
|
|
31481
|
+
// Injected by the plugin (prompt rules) and written by its build (skill).
|
|
31482
|
+
promptRules: WATCHER_PROMPT_RULES,
|
|
31483
|
+
skill: WATCHER_SKILL,
|
|
29465
31484
|
startCommand: (configPath) => [
|
|
29466
31485
|
'jeeves-watcher',
|
|
29467
31486
|
'start',
|
|
@@ -29475,404 +31494,6 @@ function createWatcherComponent(pluginVersion) {
|
|
|
29475
31494
|
};
|
|
29476
31495
|
}
|
|
29477
31496
|
|
|
29478
|
-
/**
|
|
29479
|
-
* @module plugin/apiTool
|
|
29480
|
-
* Generic HTTP-backed watcher tool registration: config shape, request helpers, and ok/connectionFail wrapping.
|
|
29481
|
-
*/
|
|
29482
|
-
/** Register a single API tool with standard try/catch + ok/connectionFail. */
|
|
29483
|
-
function registerApiTool(api, baseUrl, config) {
|
|
29484
|
-
const tool = {
|
|
29485
|
-
name: config.name,
|
|
29486
|
-
description: config.description,
|
|
29487
|
-
parameters: config.parameters,
|
|
29488
|
-
...(config.catalogMode ? { catalogMode: config.catalogMode } : {}),
|
|
29489
|
-
execute: async (_id, params) => {
|
|
29490
|
-
try {
|
|
29491
|
-
const [endpoint, body] = config.buildRequest(params);
|
|
29492
|
-
const url = `${baseUrl}${endpoint}`;
|
|
29493
|
-
const data = body !== undefined ? await postJson(url, body) : await fetchJson(url);
|
|
29494
|
-
return ok(data);
|
|
29495
|
-
}
|
|
29496
|
-
catch (error) {
|
|
29497
|
-
return connectionFail(error, baseUrl, PLUGIN_ID);
|
|
29498
|
-
}
|
|
29499
|
-
},
|
|
29500
|
-
};
|
|
29501
|
-
api.registerTool(tool, { optional: true });
|
|
29502
|
-
}
|
|
29503
|
-
/** Build a query string from defined params. */
|
|
29504
|
-
function buildQuery(params, keys) {
|
|
29505
|
-
const parts = [];
|
|
29506
|
-
for (const key of keys) {
|
|
29507
|
-
const val = params[key];
|
|
29508
|
-
if (val !== undefined) {
|
|
29509
|
-
const s = typeof val === 'string' ? val : JSON.stringify(val);
|
|
29510
|
-
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(s)}`);
|
|
29511
|
-
}
|
|
29512
|
-
}
|
|
29513
|
-
return parts.length > 0 ? `?${parts.join('&')}` : '';
|
|
29514
|
-
}
|
|
29515
|
-
/** Pick defined keys from params into a body object. */
|
|
29516
|
-
function pickDefined(params, keys) {
|
|
29517
|
-
const body = {};
|
|
29518
|
-
for (const key of keys) {
|
|
29519
|
-
if (params[key] !== undefined)
|
|
29520
|
-
body[key] = params[key];
|
|
29521
|
-
}
|
|
29522
|
-
return body;
|
|
29523
|
-
}
|
|
29524
|
-
|
|
29525
|
-
/**
|
|
29526
|
-
* @module plugin/vcsTools
|
|
29527
|
-
* Version-tracking (watcher_vcs_*) tool configs for the OpenClaw plugin; each maps tool params to a watcher VCS endpoint.
|
|
29528
|
-
*/
|
|
29529
|
-
/** The 7 watcher_vcs_* tool configs. */
|
|
29530
|
-
const vcsToolConfigs = [
|
|
29531
|
-
{
|
|
29532
|
-
name: 'watcher_vcs_status',
|
|
29533
|
-
description: 'Get version tracking health: enabled state, tracked roots, remote status, last activity',
|
|
29534
|
-
parameters: { type: 'object', properties: {} },
|
|
29535
|
-
buildRequest: () => [getEndpoint('vcsStatus').path],
|
|
29536
|
-
},
|
|
29537
|
-
{
|
|
29538
|
-
name: 'watcher_vcs_history',
|
|
29539
|
-
description: 'Query change history by path or glob with optional date range',
|
|
29540
|
-
parameters: {
|
|
29541
|
-
type: 'object',
|
|
29542
|
-
required: ['glob'],
|
|
29543
|
-
properties: {
|
|
29544
|
-
glob: {
|
|
29545
|
-
type: 'string',
|
|
29546
|
-
description: 'Path or glob pattern to query history for.',
|
|
29547
|
-
},
|
|
29548
|
-
since: {
|
|
29549
|
-
type: 'string',
|
|
29550
|
-
description: 'Start date (ISO 8601 or git date string).',
|
|
29551
|
-
},
|
|
29552
|
-
until: {
|
|
29553
|
-
type: 'string',
|
|
29554
|
-
description: 'End date (ISO 8601 or git date string).',
|
|
29555
|
-
},
|
|
29556
|
-
limit: {
|
|
29557
|
-
type: 'number',
|
|
29558
|
-
description: 'Maximum number of history entries to return.',
|
|
29559
|
-
},
|
|
29560
|
-
},
|
|
29561
|
-
},
|
|
29562
|
-
buildRequest: (params) => [
|
|
29563
|
-
`${getEndpoint('vcsHistory').path}${buildQuery(params, ['glob', 'since', 'until', 'limit'])}`,
|
|
29564
|
-
],
|
|
29565
|
-
},
|
|
29566
|
-
{
|
|
29567
|
-
name: 'watcher_vcs_show',
|
|
29568
|
-
description: 'Retrieve file content at a specific version',
|
|
29569
|
-
parameters: {
|
|
29570
|
-
type: 'object',
|
|
29571
|
-
required: ['path', 'commit'],
|
|
29572
|
-
properties: {
|
|
29573
|
-
path: {
|
|
29574
|
-
type: 'string',
|
|
29575
|
-
description: 'File path to retrieve.',
|
|
29576
|
-
},
|
|
29577
|
-
commit: {
|
|
29578
|
-
type: 'string',
|
|
29579
|
-
description: 'Version identifier.',
|
|
29580
|
-
},
|
|
29581
|
-
},
|
|
29582
|
-
},
|
|
29583
|
-
buildRequest: (params) => [
|
|
29584
|
-
`${getEndpoint('vcsShow').path}${buildQuery(params, ['path', 'commit'])}`,
|
|
29585
|
-
],
|
|
29586
|
-
},
|
|
29587
|
-
{
|
|
29588
|
-
name: 'watcher_vcs_diff',
|
|
29589
|
-
description: 'Show what changed between two versions, or between a version and current',
|
|
29590
|
-
parameters: {
|
|
29591
|
-
type: 'object',
|
|
29592
|
-
required: ['glob', 'commit'],
|
|
29593
|
-
properties: {
|
|
29594
|
-
glob: {
|
|
29595
|
-
type: 'string',
|
|
29596
|
-
description: 'Path or glob pattern to diff.',
|
|
29597
|
-
},
|
|
29598
|
-
commit: {
|
|
29599
|
-
type: 'string',
|
|
29600
|
-
description: 'Start version identifier.',
|
|
29601
|
-
},
|
|
29602
|
-
commitEnd: {
|
|
29603
|
-
type: 'string',
|
|
29604
|
-
description: 'End version identifier (defaults to current if omitted).',
|
|
29605
|
-
},
|
|
29606
|
-
},
|
|
29607
|
-
},
|
|
29608
|
-
buildRequest: (params) => [
|
|
29609
|
-
`${getEndpoint('vcsDiff').path}${buildQuery(params, ['glob', 'commit', 'commitEnd'])}`,
|
|
29610
|
-
],
|
|
29611
|
-
},
|
|
29612
|
-
{
|
|
29613
|
-
name: 'watcher_vcs_revert',
|
|
29614
|
-
description: 'Undo changes by restoring files to a specific version',
|
|
29615
|
-
parameters: {
|
|
29616
|
-
type: 'object',
|
|
29617
|
-
required: ['glob', 'commit'],
|
|
29618
|
-
properties: {
|
|
29619
|
-
glob: {
|
|
29620
|
-
type: 'string',
|
|
29621
|
-
description: 'Path or glob pattern to revert.',
|
|
29622
|
-
},
|
|
29623
|
-
commit: {
|
|
29624
|
-
type: 'string',
|
|
29625
|
-
description: 'Version to restore files to.',
|
|
29626
|
-
},
|
|
29627
|
-
existingOnly: {
|
|
29628
|
-
type: 'boolean',
|
|
29629
|
-
description: 'When true, only revert files that currently exist (skip deleted files).',
|
|
29630
|
-
},
|
|
29631
|
-
},
|
|
29632
|
-
},
|
|
29633
|
-
buildRequest: (params) => {
|
|
29634
|
-
const body = pickDefined(params, ['glob', 'commit', 'existingOnly']);
|
|
29635
|
-
return [getEndpoint('vcsRevert').path, body];
|
|
29636
|
-
},
|
|
29637
|
-
},
|
|
29638
|
-
{
|
|
29639
|
-
name: 'watcher_vcs_exclude',
|
|
29640
|
-
description: 'Exclude or re-include paths from version tracking',
|
|
29641
|
-
parameters: {
|
|
29642
|
-
type: 'object',
|
|
29643
|
-
required: ['glob'],
|
|
29644
|
-
properties: {
|
|
29645
|
-
glob: {
|
|
29646
|
-
type: 'string',
|
|
29647
|
-
description: 'Glob pattern to exclude or re-include.',
|
|
29648
|
-
},
|
|
29649
|
-
root: {
|
|
29650
|
-
type: 'string',
|
|
29651
|
-
description: 'Tracked root to target (defaults to auto-detect).',
|
|
29652
|
-
},
|
|
29653
|
-
remove: {
|
|
29654
|
-
type: 'boolean',
|
|
29655
|
-
description: 'When true, remove the exclusion rule (re-include the path).',
|
|
29656
|
-
},
|
|
29657
|
-
},
|
|
29658
|
-
},
|
|
29659
|
-
buildRequest: (params) => {
|
|
29660
|
-
const body = pickDefined(params, ['glob', 'root', 'remove']);
|
|
29661
|
-
return [getEndpoint('vcsExclude').path, body];
|
|
29662
|
-
},
|
|
29663
|
-
},
|
|
29664
|
-
{
|
|
29665
|
-
name: 'watcher_vcs_check',
|
|
29666
|
-
description: 'Check whether a path is excluded from version tracking and why',
|
|
29667
|
-
parameters: {
|
|
29668
|
-
type: 'object',
|
|
29669
|
-
required: ['path'],
|
|
29670
|
-
properties: {
|
|
29671
|
-
path: {
|
|
29672
|
-
type: 'string',
|
|
29673
|
-
description: 'File path to check exclusion status for.',
|
|
29674
|
-
},
|
|
29675
|
-
},
|
|
29676
|
-
},
|
|
29677
|
-
buildRequest: (params) => [
|
|
29678
|
-
`${getEndpoint('vcsCheckExclusion').path}${buildQuery(params, ['path'])}`,
|
|
29679
|
-
],
|
|
29680
|
-
},
|
|
29681
|
-
];
|
|
29682
|
-
|
|
29683
|
-
/**
|
|
29684
|
-
* @module plugin/watcherTools
|
|
29685
|
-
* Domain-specific watcher tool registrations (14 tools) for the OpenClaw plugin.
|
|
29686
|
-
*/
|
|
29687
|
-
/**
|
|
29688
|
-
* Register the 14 domain-specific watcher_* tools with the OpenClaw plugin API.
|
|
29689
|
-
*
|
|
29690
|
-
* @remarks
|
|
29691
|
-
* `watcher_search` and `watcher_scan` are `catalogMode: 'direct-only'` so
|
|
29692
|
-
* OpenClaw Tool Search keeps them model-visible: semantic search must be a
|
|
29693
|
-
* direct tool or the model reaches for grep first. All other tools remain
|
|
29694
|
-
* catalog-eligible.
|
|
29695
|
-
*/
|
|
29696
|
-
function registerWatcherTools(api, baseUrl) {
|
|
29697
|
-
const tools = [
|
|
29698
|
-
{
|
|
29699
|
-
name: 'watcher_search',
|
|
29700
|
-
description: 'Semantic search over indexed documents. Supports Qdrant filters. This is a direct tool: call it directly, never through tool_call.',
|
|
29701
|
-
catalogMode: 'direct-only',
|
|
29702
|
-
parameters: {
|
|
29703
|
-
type: 'object',
|
|
29704
|
-
required: ['query'],
|
|
29705
|
-
properties: {
|
|
29706
|
-
query: { type: 'string', description: 'Search query text.' },
|
|
29707
|
-
limit: { type: 'number', description: 'Max results (default 10).' },
|
|
29708
|
-
offset: {
|
|
29709
|
-
type: 'number',
|
|
29710
|
-
description: 'Number of results to skip for pagination.',
|
|
29711
|
-
},
|
|
29712
|
-
filter: { type: 'object', description: 'Qdrant filter object.' },
|
|
29713
|
-
},
|
|
29714
|
-
},
|
|
29715
|
-
buildRequest: (params) => {
|
|
29716
|
-
const body = pickDefined(params, [
|
|
29717
|
-
'query',
|
|
29718
|
-
'limit',
|
|
29719
|
-
'offset',
|
|
29720
|
-
'filter',
|
|
29721
|
-
]);
|
|
29722
|
-
return [getEndpoint('search').path, body];
|
|
29723
|
-
},
|
|
29724
|
-
},
|
|
29725
|
-
{
|
|
29726
|
-
name: 'watcher_enrich',
|
|
29727
|
-
description: 'Set or update metadata on a document by file path.',
|
|
29728
|
-
parameters: {
|
|
29729
|
-
type: 'object',
|
|
29730
|
-
required: ['path', 'metadata'],
|
|
29731
|
-
properties: {
|
|
29732
|
-
path: {
|
|
29733
|
-
type: 'string',
|
|
29734
|
-
description: 'Relative file path of the document.',
|
|
29735
|
-
},
|
|
29736
|
-
metadata: {
|
|
29737
|
-
type: 'object',
|
|
29738
|
-
description: 'Key-value metadata to set on the document.',
|
|
29739
|
-
},
|
|
29740
|
-
},
|
|
29741
|
-
},
|
|
29742
|
-
buildRequest: (params) => [
|
|
29743
|
-
getEndpoint('metadata').path,
|
|
29744
|
-
{ path: params.path, metadata: params.metadata },
|
|
29745
|
-
],
|
|
29746
|
-
},
|
|
29747
|
-
{
|
|
29748
|
-
name: 'watcher_validate',
|
|
29749
|
-
description: 'Validate a candidate config (or current config if omitted). Optionally test file paths against the config to preview rule matching and metadata output.',
|
|
29750
|
-
parameters: {
|
|
29751
|
-
type: 'object',
|
|
29752
|
-
properties: {
|
|
29753
|
-
config: {
|
|
29754
|
-
type: 'object',
|
|
29755
|
-
description: 'Candidate config (partial or full). Omit to validate current config.',
|
|
29756
|
-
},
|
|
29757
|
-
testPaths: {
|
|
29758
|
-
type: 'array',
|
|
29759
|
-
items: { type: 'string' },
|
|
29760
|
-
description: 'File paths to test against the config for dry-run preview.',
|
|
29761
|
-
},
|
|
29762
|
-
},
|
|
29763
|
-
},
|
|
29764
|
-
buildRequest: (params) => {
|
|
29765
|
-
const body = pickDefined(params, ['config', 'testPaths']);
|
|
29766
|
-
return [getEndpoint('configValidate').path, body];
|
|
29767
|
-
},
|
|
29768
|
-
},
|
|
29769
|
-
{
|
|
29770
|
-
name: 'watcher_reindex',
|
|
29771
|
-
description: 'Trigger a reindex of the watched files.',
|
|
29772
|
-
parameters: {
|
|
29773
|
-
type: 'object',
|
|
29774
|
-
properties: {
|
|
29775
|
-
scope: {
|
|
29776
|
-
type: 'string',
|
|
29777
|
-
enum: ['rules', 'full', 'issues', 'path', 'prune'],
|
|
29778
|
-
description: 'Reindex scope: "rules" (default) re-applies inference rules; "full" re-embeds everything; "issues" re-processes files with errors; "path" reindexes a specific file or directory (requires path parameter); "prune" deletes points for files no longer in watch scope.',
|
|
29779
|
-
},
|
|
29780
|
-
path: {
|
|
29781
|
-
oneOf: [
|
|
29782
|
-
{ type: 'string' },
|
|
29783
|
-
{ type: 'array', items: { type: 'string' } },
|
|
29784
|
-
],
|
|
29785
|
-
description: 'Target file or directory path (required when scope is "path"). Accepts a single path or array of paths.',
|
|
29786
|
-
},
|
|
29787
|
-
dryRun: {
|
|
29788
|
-
type: 'boolean',
|
|
29789
|
-
description: 'When true, compute and return the blast area plan without executing. Returns counts by root showing impact.',
|
|
29790
|
-
},
|
|
29791
|
-
},
|
|
29792
|
-
},
|
|
29793
|
-
buildRequest: (params) => [
|
|
29794
|
-
getEndpoint('reindex').path,
|
|
29795
|
-
{
|
|
29796
|
-
scope: params.scope ?? 'rules',
|
|
29797
|
-
...(params.path ? { path: params.path } : {}),
|
|
29798
|
-
...(params.dryRun ? { dryRun: true } : {}),
|
|
29799
|
-
},
|
|
29800
|
-
],
|
|
29801
|
-
},
|
|
29802
|
-
{
|
|
29803
|
-
name: 'watcher_scan',
|
|
29804
|
-
description: 'Filter-only point query without vector search. Returns metadata for points matching a Qdrant filter. Use for structural queries: file enumeration, staleness checks, delta computation. Use watcher_search for semantic/similarity queries. This is a direct tool: call it directly, never through tool_call.',
|
|
29805
|
-
catalogMode: 'direct-only',
|
|
29806
|
-
parameters: {
|
|
29807
|
-
type: 'object',
|
|
29808
|
-
required: ['filter'],
|
|
29809
|
-
properties: {
|
|
29810
|
-
filter: {
|
|
29811
|
-
type: 'object',
|
|
29812
|
-
description: 'Qdrant filter object (required).',
|
|
29813
|
-
},
|
|
29814
|
-
limit: {
|
|
29815
|
-
type: 'number',
|
|
29816
|
-
description: 'Page size (default 100, max 1000).',
|
|
29817
|
-
},
|
|
29818
|
-
cursor: {
|
|
29819
|
-
type: 'string',
|
|
29820
|
-
description: 'Opaque cursor from previous response for pagination.',
|
|
29821
|
-
},
|
|
29822
|
-
fields: {
|
|
29823
|
-
type: 'array',
|
|
29824
|
-
items: { type: 'string' },
|
|
29825
|
-
description: 'Payload fields to return (projection).',
|
|
29826
|
-
},
|
|
29827
|
-
countOnly: {
|
|
29828
|
-
type: 'boolean',
|
|
29829
|
-
description: 'If true, return { count } instead of points.',
|
|
29830
|
-
},
|
|
29831
|
-
},
|
|
29832
|
-
},
|
|
29833
|
-
buildRequest: (params) => {
|
|
29834
|
-
const body = pickDefined(params, [
|
|
29835
|
-
'filter',
|
|
29836
|
-
'limit',
|
|
29837
|
-
'cursor',
|
|
29838
|
-
'fields',
|
|
29839
|
-
'countOnly',
|
|
29840
|
-
]);
|
|
29841
|
-
return [getEndpoint('scan').path, body];
|
|
29842
|
-
},
|
|
29843
|
-
},
|
|
29844
|
-
{
|
|
29845
|
-
name: 'watcher_issues',
|
|
29846
|
-
description: 'Get runtime embedding failures. Shows files that failed processing and why.',
|
|
29847
|
-
parameters: { type: 'object', properties: {} },
|
|
29848
|
-
buildRequest: () => [getEndpoint('issues').path],
|
|
29849
|
-
},
|
|
29850
|
-
{
|
|
29851
|
-
name: 'watcher_walk',
|
|
29852
|
-
description: 'Walk watched filesystem paths with glob intersection. Returns matching file paths from all configured watch roots, applying watch.ignored and gitignore filtering.',
|
|
29853
|
-
parameters: {
|
|
29854
|
-
type: 'object',
|
|
29855
|
-
required: ['globs'],
|
|
29856
|
-
properties: {
|
|
29857
|
-
globs: {
|
|
29858
|
-
type: 'array',
|
|
29859
|
-
items: { type: 'string' },
|
|
29860
|
-
description: 'Glob patterns to intersect with watch paths (e.g., ["**/.meta/meta.json"]).',
|
|
29861
|
-
},
|
|
29862
|
-
},
|
|
29863
|
-
},
|
|
29864
|
-
buildRequest: (params) => [
|
|
29865
|
-
getEndpoint('walk').path,
|
|
29866
|
-
{ globs: params.globs },
|
|
29867
|
-
],
|
|
29868
|
-
},
|
|
29869
|
-
...vcsToolConfigs,
|
|
29870
|
-
];
|
|
29871
|
-
for (const tool of tools) {
|
|
29872
|
-
registerApiTool(api, baseUrl, tool);
|
|
29873
|
-
}
|
|
29874
|
-
}
|
|
29875
|
-
|
|
29876
31497
|
/**
|
|
29877
31498
|
* @module plugin
|
|
29878
31499
|
* OpenClaw plugin entry point. Registers the jeeves-watcher tools and the
|
|
@@ -29890,16 +31511,16 @@ const PLUGIN_VERSION = getPackageVersion(import.meta.url);
|
|
|
29890
31511
|
function register(api) {
|
|
29891
31512
|
warnIfConfigRootUnset(api);
|
|
29892
31513
|
const toolApi = withGuardedTools(api, createLazyCore(api));
|
|
29893
|
-
|
|
29894
|
-
//
|
|
29895
|
-
//
|
|
29896
|
-
//
|
|
29897
|
-
|
|
29898
|
-
|
|
29899
|
-
|
|
29900
|
-
|
|
29901
|
-
|
|
29902
|
-
registerWatcherPromptContext(api);
|
|
31514
|
+
const component = createWatcherComponent(PLUGIN_VERSION);
|
|
31515
|
+
// Every tool comes from core's toolset over WATCHER_CATALOG (D38): the 4
|
|
31516
|
+
// standard tools and the 14 domain tools, in their 0.17.0 order. Each
|
|
31517
|
+
// resolves the base URL (and, where its entry requires it, the bearer key)
|
|
31518
|
+
// on every call.
|
|
31519
|
+
const catalogTools = createCatalogTools(api, component);
|
|
31520
|
+
for (const name of WATCHER_TOOL_NAMES) {
|
|
31521
|
+
registerCatalogTool(toolApi, catalogTools, name);
|
|
31522
|
+
}
|
|
31523
|
+
registerWatcherPromptContext(api, component);
|
|
29903
31524
|
}
|
|
29904
31525
|
|
|
29905
31526
|
export { register as default };
|