@karmaniverous/jeeves-watcher 0.20.0 → 0.21.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/README.md +16 -12
- package/config.schema.json +412 -210
- package/dist/cli/jeeves-watcher/{index-C9-xTM7k.js → index-CGVLEt3K.js} +18638 -11198
- package/dist/cli/jeeves-watcher/index.js +583 -185
- package/dist/index.d.ts +918 -407
- package/dist/index.js +19482 -11667
- package/package.json +32 -22
|
@@ -2,11 +2,35 @@
|
|
|
2
2
|
import { substituteEnvVars, getPackageVersion, createServiceCli } from '@karmaniverous/jeeves';
|
|
3
3
|
import { resolve, dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { isPlainObject as isPlainObject$1, findReservedPropertyNames, sourceConfigSchema, jeevesWatcherConfigSchema, LOGGING_DEFAULTS, API_DEFAULTS, EMBEDDING_DEFAULTS, CONFIG_WATCH_DEFAULTS, WATCH_DEFAULTS, ROOT_DEFAULTS,
|
|
5
|
+
import { isPlainObject as isPlainObject$1, findReservedPropertyNames, sourceConfigSchema, jeevesWatcherConfigSchema, LOGGING_DEFAULTS, API_DEFAULTS, EMBEDDING_DEFAULTS, CONFIG_WATCH_DEFAULTS, WATCH_DEFAULTS, ROOT_DEFAULTS, WATCHER_CATALOG, API_KEY_ENV_VAR, VCS_EXCLUDE_CONFIRM_THRESHOLD, reindexScopeSchema, WATCHER_SKILL, WATCHER_PROMPT_RULES, INIT_CONFIG_TEMPLATE, DEFAULT_PORT as DEFAULT_PORT$1, PLUGIN_PACKAGE, SERVICE_PACKAGE, COMPONENT_NAME } from '@karmaniverous/jeeves-watcher-core';
|
|
6
6
|
import { packageDirectorySync } from 'package-directory';
|
|
7
7
|
import { readFileSync, existsSync, mkdirSync, renameSync } from 'node:fs';
|
|
8
8
|
import { cosmiconfig } from 'cosmiconfig';
|
|
9
9
|
import { ZodError, z } from 'zod';
|
|
10
|
+
import { createCatalogClient, CatalogClientError } from '@karmaniverous/jeeves/catalog';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @module util/threadpool
|
|
14
|
+
* The libuv threadpool size the service runs with (D170). Pure: edits the given environment object.
|
|
15
|
+
*/
|
|
16
|
+
/** `UV_THREADPOOL_SIZE` the service uses unless the environment sets one. */
|
|
17
|
+
const SERVICE_THREADPOOL_SIZE = '16';
|
|
18
|
+
/**
|
|
19
|
+
* Default `UV_THREADPOOL_SIZE` to 16, keeping a value already set. libuv
|
|
20
|
+
* reads it when the threadpool first starts, so this must run before any
|
|
21
|
+
* threadpool work: the CLI entry imports it first.
|
|
22
|
+
*
|
|
23
|
+
* @param env - The environment to edit (`process.env` in the entry).
|
|
24
|
+
*/
|
|
25
|
+
function ensureThreadpoolSize(env) {
|
|
26
|
+
env.UV_THREADPOOL_SIZE ??= SERVICE_THREADPOOL_SIZE;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @module cli/jeeves-watcher/threadpool
|
|
31
|
+
* Side effect only: sets `UV_THREADPOOL_SIZE` (D170) before anything else in the CLI loads. Imported first by the entry point.
|
|
32
|
+
*/
|
|
33
|
+
ensureThreadpoolSize(process.env);
|
|
10
34
|
|
|
11
35
|
/**
|
|
12
36
|
* @module api/handlers/configMerge
|
|
@@ -666,7 +690,7 @@ async function drainQueues(queues, timeoutMs, logger) {
|
|
|
666
690
|
/**
|
|
667
691
|
* Stop every component in a safe order: close the API server (bounded), stop
|
|
668
692
|
* producers, drain and stop the queues (bounded), flush the values index, and
|
|
669
|
-
* close stores. When a queue step timed out, runs may still be in flight, so
|
|
693
|
+
* close stores. When a queue step or a producer's bounded stop timed out, runs may still be in flight, so
|
|
670
694
|
* stores are left open (the process exits) rather than closed under them.
|
|
671
695
|
*
|
|
672
696
|
* @param components - The components.
|
|
@@ -677,9 +701,13 @@ async function shutdownComponents(components, timeoutMs, logger) {
|
|
|
677
701
|
const { server, producers = [], valuesManager, stores = [] } = components;
|
|
678
702
|
if (server)
|
|
679
703
|
await step('API server close', server.close(), timeoutMs, logger);
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
704
|
+
let producersIdle = true;
|
|
705
|
+
for (const producer of producers) {
|
|
706
|
+
if ((await producer?.stop()) === false)
|
|
707
|
+
producersIdle = false;
|
|
708
|
+
}
|
|
709
|
+
const queuesIdle = await drainQueues(components, timeoutMs, logger);
|
|
710
|
+
const idle = producersIdle && queuesIdle;
|
|
683
711
|
valuesManager?.stopAutoFlush();
|
|
684
712
|
valuesManager?.flush();
|
|
685
713
|
if (idle) {
|
|
@@ -750,7 +778,7 @@ async function startFromConfig(configPath, descriptor) {
|
|
|
750
778
|
});
|
|
751
779
|
// Dynamic import breaks the circular dependency between this module
|
|
752
780
|
// and app/index.ts (which defines JeevesWatcher and re-exports startFromConfig).
|
|
753
|
-
const { JeevesWatcher } = await import('./index-
|
|
781
|
+
const { JeevesWatcher } = await import('./index-CGVLEt3K.js');
|
|
754
782
|
const app = new JeevesWatcher(config, resolvedPath, descriptor);
|
|
755
783
|
installShutdownHandlers(() => app.stop());
|
|
756
784
|
await app.start();
|
|
@@ -758,8 +786,300 @@ async function startFromConfig(configPath, descriptor) {
|
|
|
758
786
|
}
|
|
759
787
|
|
|
760
788
|
/**
|
|
761
|
-
* @module
|
|
762
|
-
*
|
|
789
|
+
* @module WatcherError
|
|
790
|
+
* The single error class thrown by the SDK: client-side validation, timeouts, network failures, non-2xx responses and unparseable responses. No I/O.
|
|
791
|
+
*/
|
|
792
|
+
/**
|
|
793
|
+
* The one error type thrown by every SDK call. The SDK never retries; inspect
|
|
794
|
+
* `status` and `code` to decide whether to.
|
|
795
|
+
*/
|
|
796
|
+
class WatcherError extends Error {
|
|
797
|
+
/** Machine-readable code: a {@link WatcherClientErrorCode} or a server code. */
|
|
798
|
+
code;
|
|
799
|
+
/** HTTP status, when a response was received. */
|
|
800
|
+
status;
|
|
801
|
+
/** Structured details (Zod issues, server `details`, or the raw body). */
|
|
802
|
+
details;
|
|
803
|
+
/**
|
|
804
|
+
* Delay the server asked for before a retry (`Retry-After` on a `503` or
|
|
805
|
+
* `429`), in milliseconds, clamped to 0..3,600,000 (D78). The SDK never
|
|
806
|
+
* waits or retries itself.
|
|
807
|
+
*/
|
|
808
|
+
retryAfterMs;
|
|
809
|
+
/**
|
|
810
|
+
* Create a watcher error.
|
|
811
|
+
*
|
|
812
|
+
* @param init - Code, message and optional status, details, retry delay and cause.
|
|
813
|
+
*/
|
|
814
|
+
constructor(init) {
|
|
815
|
+
super(init.message, init.cause === undefined ? {} : { cause: init.cause });
|
|
816
|
+
this.name = 'WatcherError';
|
|
817
|
+
this.code = init.code;
|
|
818
|
+
this.status = init.status;
|
|
819
|
+
this.details = init.details;
|
|
820
|
+
this.retryAfterMs = init.retryAfterMs;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Type guard for {@link WatcherError}.
|
|
825
|
+
*
|
|
826
|
+
* @param value - Any value (typically a caught error).
|
|
827
|
+
* @returns `true` when `value` is a {@link WatcherError}.
|
|
828
|
+
*/
|
|
829
|
+
function isWatcherError(value) {
|
|
830
|
+
return value instanceof WatcherError;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* @module documents
|
|
835
|
+
* The `documents` method group over the catalog's `notifyDocuments` (POST /documents, 202 or ?wait=true) and `deleteDocuments` (POST /documents/delete) entries (D60). Network I/O via the client methods it wraps.
|
|
836
|
+
*/
|
|
837
|
+
/**
|
|
838
|
+
* Build the `documents` method group.
|
|
839
|
+
*
|
|
840
|
+
* @param notifyDocuments - Client method of the `notifyDocuments` entry.
|
|
841
|
+
* @param deleteDocuments - Client method of the `deleteDocuments` entry.
|
|
842
|
+
* @returns The {@link DocumentsApi}.
|
|
843
|
+
*/
|
|
844
|
+
function createDocumentsApi(notifyDocuments, deleteDocuments) {
|
|
845
|
+
return {
|
|
846
|
+
notify: (async (documents, options) => {
|
|
847
|
+
const wait = options?.wait === true;
|
|
848
|
+
const body = await notifyDocuments(wait ? { documents, wait: 'true' } : { documents });
|
|
849
|
+
if (wait !== 'results' in body) {
|
|
850
|
+
throw new WatcherError({
|
|
851
|
+
code: 'invalid_response',
|
|
852
|
+
message: `notifyDocuments answered without ${wait ? 'per-document results' : 'accepted references'}`,
|
|
853
|
+
details: body,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
return body;
|
|
857
|
+
}),
|
|
858
|
+
delete: (request) => deleteDocuments(request),
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* @module options
|
|
864
|
+
* Client options for createWatcherClient and their validation/normalisation (base URL, API key, timeout, fetch injection). Pure; throws WatcherError invalid_options.
|
|
865
|
+
*/
|
|
866
|
+
/**
|
|
867
|
+
* Default request timeout in milliseconds. Shorter than the server's
|
|
868
|
+
* longest waits (`RootGitLock` 30 s, document lock 10 s): raise `timeoutMs`
|
|
869
|
+
* for state-changing VCS calls and `documents.notify(..., { wait: true })`.
|
|
870
|
+
*/
|
|
871
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
872
|
+
const invalid = (message) => new WatcherError({ code: 'invalid_options', message });
|
|
873
|
+
/**
|
|
874
|
+
* Adapt a {@link FetchLike} to the `fetch` signature jeeves's catalog client
|
|
875
|
+
* takes. The client always passes a URL string and an init object.
|
|
876
|
+
*
|
|
877
|
+
* @param fetchImpl - The caller's fetch.
|
|
878
|
+
* @returns A `typeof fetch` that calls `fetchImpl`.
|
|
879
|
+
*/
|
|
880
|
+
function adaptFetch(fetchImpl) {
|
|
881
|
+
return (input, init) => fetchImpl(typeof input === 'string'
|
|
882
|
+
? input
|
|
883
|
+
: input instanceof URL
|
|
884
|
+
? input.href
|
|
885
|
+
: input.url, init ?? {});
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Validate and normalise client options.
|
|
889
|
+
*
|
|
890
|
+
* @param options - Caller options.
|
|
891
|
+
* @returns Resolved options.
|
|
892
|
+
* @throws {@link WatcherError} `invalid_options` on a bad base URL (not http(s), or carrying a query string or fragment), empty API key, non-positive timeout or missing `fetch`.
|
|
893
|
+
*/
|
|
894
|
+
function resolveClientOptions(options) {
|
|
895
|
+
const { baseUrl, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
896
|
+
if (!URL.canParse(baseUrl) || !/^https?:$/.test(new URL(baseUrl).protocol)) {
|
|
897
|
+
throw invalid(`baseUrl must be an http(s) URL, got "${baseUrl}"`);
|
|
898
|
+
}
|
|
899
|
+
// Entry paths are appended to baseUrl, so a query or fragment would
|
|
900
|
+
// swallow them. Check the raw string: URL drops an empty `?` or `#`.
|
|
901
|
+
if (/[?#]/.test(baseUrl)) {
|
|
902
|
+
throw invalid(`baseUrl must not contain a query string or fragment, got "${baseUrl}"`);
|
|
903
|
+
}
|
|
904
|
+
if (apiKey?.length === 0) {
|
|
905
|
+
throw invalid('apiKey must be a non-empty string when set');
|
|
906
|
+
}
|
|
907
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
908
|
+
throw invalid('timeoutMs must be a positive number');
|
|
909
|
+
}
|
|
910
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
911
|
+
if (fetchImpl === undefined) {
|
|
912
|
+
throw invalid('No fetch implementation available; pass options.fetch');
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
baseUrl: baseUrl.replace(/\/+$/, ''),
|
|
916
|
+
apiKey,
|
|
917
|
+
timeoutMs,
|
|
918
|
+
fetch: adaptFetch(fetchImpl),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* @module toWatcherError
|
|
924
|
+
* The one mapping from jeeves's `CatalogClientError` to {@link WatcherError}: the server body's `code` wins; client-side failures get the SDK's own codes; `retryAfterMs` is clamped. Pure.
|
|
925
|
+
*/
|
|
926
|
+
/** Largest `retryAfterMs` the SDK reports: one hour (D78). */
|
|
927
|
+
const MAX_RETRY_AFTER_MS = 3_600_000;
|
|
928
|
+
/**
|
|
929
|
+
* Clamp a server-requested delay to 0..{@link MAX_RETRY_AFTER_MS}.
|
|
930
|
+
*
|
|
931
|
+
* @param ms - jeeves's `CatalogClientError.retryAfterMs` (uncapped).
|
|
932
|
+
* @returns The clamped delay, or `undefined` when the server sent none.
|
|
933
|
+
*/
|
|
934
|
+
function clampRetryAfter(ms) {
|
|
935
|
+
return ms === undefined
|
|
936
|
+
? undefined
|
|
937
|
+
: Math.min(Math.max(ms, 0), MAX_RETRY_AFTER_MS);
|
|
938
|
+
}
|
|
939
|
+
const stringField = (body, key) => {
|
|
940
|
+
const value = body?.[key];
|
|
941
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
942
|
+
};
|
|
943
|
+
/** Longest message kept from a body without a `code` (a proxy's error page, say); the whole body stays in `details`. */
|
|
944
|
+
const MAX_UNCODED_MESSAGE_LENGTH = 200;
|
|
945
|
+
/**
|
|
946
|
+
* Code, message and details of a non-2xx response body.
|
|
947
|
+
*
|
|
948
|
+
* @param status - HTTP status.
|
|
949
|
+
* @param body - jeeves's parsed error body (`{ error, ... }`).
|
|
950
|
+
* @returns The mapped parts.
|
|
951
|
+
*/
|
|
952
|
+
function serverError(status, body) {
|
|
953
|
+
const error = stringField(body, 'error');
|
|
954
|
+
const extra = stringField(body, 'message');
|
|
955
|
+
const message = error === undefined
|
|
956
|
+
? (extra ?? `HTTP ${String(status)}`)
|
|
957
|
+
: extra !== undefined && extra !== error
|
|
958
|
+
? `${error}: ${extra}`
|
|
959
|
+
: error;
|
|
960
|
+
const code = stringField(body, 'code');
|
|
961
|
+
return {
|
|
962
|
+
code: code ?? `http_${String(status)}`,
|
|
963
|
+
message: code === undefined && message.length > MAX_UNCODED_MESSAGE_LENGTH
|
|
964
|
+
? `${message.slice(0, MAX_UNCODED_MESSAGE_LENGTH)}...`
|
|
965
|
+
: message,
|
|
966
|
+
details: body !== undefined && 'details' in body ? body.details : body,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* The SDK code of a failure that produced no response.
|
|
971
|
+
*
|
|
972
|
+
* @param cause - The underlying error.
|
|
973
|
+
* @returns `timeout`, `invalid_response` (an unparseable 2xx body) or `network_error`.
|
|
974
|
+
*
|
|
975
|
+
* @remarks
|
|
976
|
+
* Assumes jeeves's client wraps a `SyntaxError` only from parsing a 2xx
|
|
977
|
+
* body; a caller's own `fetch` throwing one before any response would also
|
|
978
|
+
* read as `invalid_response`.
|
|
979
|
+
*/
|
|
980
|
+
function noResponseCode(cause) {
|
|
981
|
+
if (cause instanceof Error && cause.name === 'TimeoutError')
|
|
982
|
+
return 'timeout';
|
|
983
|
+
if (cause instanceof SyntaxError)
|
|
984
|
+
return 'invalid_response';
|
|
985
|
+
return 'network_error';
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Convert a failed catalog call to a {@link WatcherError}.
|
|
989
|
+
*
|
|
990
|
+
* @param error - What the catalog client threw.
|
|
991
|
+
* @param operation - Catalog entry name (used in messages).
|
|
992
|
+
* @param timeoutMs - The client's timeout (used in the timeout message).
|
|
993
|
+
* @returns The error to throw.
|
|
994
|
+
*/
|
|
995
|
+
function toWatcherError(error, operation, timeoutMs) {
|
|
996
|
+
if (!(error instanceof CatalogClientError)) {
|
|
997
|
+
return new WatcherError({
|
|
998
|
+
code: 'network_error',
|
|
999
|
+
message: `${operation} request failed: ${String(error)}`,
|
|
1000
|
+
cause: error,
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
const retryAfterMs = clampRetryAfter(error.retryAfterMs);
|
|
1004
|
+
const common = {
|
|
1005
|
+
cause: error,
|
|
1006
|
+
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1007
|
+
};
|
|
1008
|
+
if (error.status >= 300) {
|
|
1009
|
+
return new WatcherError({
|
|
1010
|
+
...serverError(error.status, error.body),
|
|
1011
|
+
status: error.status,
|
|
1012
|
+
...common,
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
if (error.issues !== undefined) {
|
|
1016
|
+
const responded = error.status > 0;
|
|
1017
|
+
return new WatcherError({
|
|
1018
|
+
code: responded ? 'invalid_response' : 'invalid_request',
|
|
1019
|
+
message: responded
|
|
1020
|
+
? `${operation} response did not match the expected shape`
|
|
1021
|
+
: error.message,
|
|
1022
|
+
details: error.issues,
|
|
1023
|
+
...(responded ? { status: error.status } : {}),
|
|
1024
|
+
...common,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
const code = noResponseCode(error.cause);
|
|
1028
|
+
const message = code === 'timeout'
|
|
1029
|
+
? `${operation} timed out after ${String(timeoutMs)} ms`
|
|
1030
|
+
: code === 'invalid_response'
|
|
1031
|
+
? `${operation} returned a response that is not JSON`
|
|
1032
|
+
: `${operation} request failed: ${error.cause instanceof Error ? error.cause.message : String(error.cause)}`;
|
|
1033
|
+
return new WatcherError({ code, message, ...common });
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* @module createWatcherClient
|
|
1038
|
+
* Typed watcher API client built on jeeves's catalog client: one method per `WATCHER_CATALOG` entry, named by the entry, plus the `documents` group; every failure is a {@link WatcherError}. Network I/O via fetch.
|
|
1039
|
+
*/
|
|
1040
|
+
/**
|
|
1041
|
+
* Create a watcher API client.
|
|
1042
|
+
*
|
|
1043
|
+
* @example
|
|
1044
|
+
* ```ts
|
|
1045
|
+
* const watcher = createWatcherClient({
|
|
1046
|
+
* baseUrl: 'http://watcher.internal.example:1936',
|
|
1047
|
+
* apiKey: process.env.WATCHER_API_KEY,
|
|
1048
|
+
* });
|
|
1049
|
+
* await watcher.documents.notify([{ source: 'app://chat/thread/123', rule: 'chat-thread' }]);
|
|
1050
|
+
* ```
|
|
1051
|
+
*
|
|
1052
|
+
* @param options - Base URL, optional API key, timeout (default 10000 ms) and `fetch`.
|
|
1053
|
+
* @returns A {@link WatcherClient}.
|
|
1054
|
+
* @throws {@link WatcherError} `invalid_options` when the options are invalid.
|
|
1055
|
+
*/
|
|
1056
|
+
function createWatcherClient(options) {
|
|
1057
|
+
const resolved = resolveClientOptions(options);
|
|
1058
|
+
const catalogClient = createCatalogClient(WATCHER_CATALOG, {
|
|
1059
|
+
baseUrl: resolved.baseUrl,
|
|
1060
|
+
...(resolved.apiKey === undefined
|
|
1061
|
+
? {}
|
|
1062
|
+
: { auth: { bearer: resolved.apiKey } }),
|
|
1063
|
+
timeoutMs: resolved.timeoutMs,
|
|
1064
|
+
fetch: resolved.fetch,
|
|
1065
|
+
});
|
|
1066
|
+
const methods = Object.fromEntries(WATCHER_CATALOG.entries.map(({ name }) => {
|
|
1067
|
+
const call = catalogClient[name];
|
|
1068
|
+
const method = (input) => call(input).catch((error) => {
|
|
1069
|
+
throw toWatcherError(error, name, resolved.timeoutMs);
|
|
1070
|
+
});
|
|
1071
|
+
return [name, method];
|
|
1072
|
+
}));
|
|
1073
|
+
const { notifyDocuments, deleteDocuments, ...rest } = methods;
|
|
1074
|
+
return {
|
|
1075
|
+
...rest,
|
|
1076
|
+
documents: createDocumentsApi(notifyDocuments, deleteDocuments),
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* @module cli/jeeves-watcher/cliClient
|
|
1082
|
+
* The SDK client every API-calling CLI command uses (D37): `http://<host>:<port>`, the resolved bearer key, and a 60 s timeout (D119). Also the key resolution and the key hint printed after a 401. No I/O until a method is called.
|
|
763
1083
|
*
|
|
764
1084
|
* @remarks
|
|
765
1085
|
* Key resolution ({@link resolveApiKey}): the `--api-key` flag, then the
|
|
@@ -767,15 +1087,14 @@ async function startFromConfig(configPath, descriptor) {
|
|
|
767
1087
|
* API commands do not load the watcher config file, so `api.apiKey` from
|
|
768
1088
|
* config is not consulted.
|
|
769
1089
|
*/
|
|
770
|
-
/**
|
|
1090
|
+
/**
|
|
1091
|
+
* Request timeout of every CLI command, in milliseconds (D119): longer than
|
|
1092
|
+
* any server-side wait (`RootGitLock` 30 s, document lock 10 s), so a busy
|
|
1093
|
+
* server's `503` reaches the user instead of a timeout.
|
|
1094
|
+
*/
|
|
1095
|
+
const CLI_TIMEOUT_MS = 60_000;
|
|
1096
|
+
/** Where the CLI reads its API key from (named in the 401 hint). */
|
|
771
1097
|
const CLI_KEY_SOURCES = `--api-key or the ${API_KEY_ENV_VAR} env var`;
|
|
772
|
-
/** Options for {@link createApiClient}; the Zod schema is the source of truth. */
|
|
773
|
-
const apiClientOptionsSchema = z.object({
|
|
774
|
-
/** API base URL, e.g. `http://127.0.0.1:1936`. */
|
|
775
|
-
baseUrl: z.string().min(1),
|
|
776
|
-
/** Bearer key; omitted = no `Authorization` header. */
|
|
777
|
-
apiKey: z.string().min(1).optional(),
|
|
778
|
-
});
|
|
779
1098
|
/** Treat missing and blank values as unset. */
|
|
780
1099
|
function nonBlank(value) {
|
|
781
1100
|
return value !== undefined && value.trim() !== '' ? value : undefined;
|
|
@@ -791,67 +1110,44 @@ function resolveApiKey(flag, env = process.env) {
|
|
|
791
1110
|
return nonBlank(flag) ?? nonBlank(env[API_KEY_ENV_VAR]);
|
|
792
1111
|
}
|
|
793
1112
|
/**
|
|
794
|
-
*
|
|
795
|
-
* {@link CLI_KEY_SOURCES}). Never contains the key. Pure.
|
|
1113
|
+
* The hint printed after a `401`. Never contains the key. Pure.
|
|
796
1114
|
*
|
|
797
1115
|
* @param keySent - Whether the request carried a bearer key.
|
|
798
1116
|
* @returns Guidance on supplying (or checking) the key.
|
|
799
1117
|
*/
|
|
800
|
-
function
|
|
801
|
-
return
|
|
1118
|
+
function unauthorizedHint(keySent) {
|
|
1119
|
+
return keySent
|
|
1120
|
+
? `The watcher API rejected the API key. Check ${CLI_KEY_SOURCES} against the service's api.apiKey.`
|
|
1121
|
+
: `The watcher API requires an API key. Provide it via ${CLI_KEY_SOURCES}.`;
|
|
802
1122
|
}
|
|
803
1123
|
/**
|
|
804
|
-
*
|
|
1124
|
+
* The SDK client for a command invocation.
|
|
805
1125
|
*
|
|
806
|
-
* @param
|
|
807
|
-
* @
|
|
808
|
-
*
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
throw new Error(cliUnauthorizedMessage(keySent));
|
|
818
|
-
}
|
|
819
|
-
if (!res.ok) {
|
|
820
|
-
const info = httpError(res.status, res.statusText, await res.text());
|
|
821
|
-
throw new Error(formatHttpError(info));
|
|
822
|
-
}
|
|
823
|
-
return res.json();
|
|
824
|
-
}
|
|
825
|
-
/**
|
|
826
|
-
* Create a JSON client for the watcher API.
|
|
827
|
-
*
|
|
828
|
-
* @param options - Base URL and optional bearer key.
|
|
829
|
-
* @returns A client whose requests carry `Authorization: Bearer <apiKey>`
|
|
830
|
-
* when a key is set, and no `Authorization` header otherwise.
|
|
831
|
-
*/
|
|
832
|
-
function createApiClient(options) {
|
|
833
|
-
const { baseUrl, apiKey } = apiClientOptionsSchema.parse(options);
|
|
834
|
-
const keySent = apiKey !== undefined;
|
|
835
|
-
const auth = bearerHeaders(apiKey);
|
|
836
|
-
return {
|
|
837
|
-
get: (path) => send(`${baseUrl}${path}`, keySent ? { headers: auth } : undefined, keySent),
|
|
838
|
-
post: (path, body) => send(`${baseUrl}${path}`, {
|
|
839
|
-
method: 'POST',
|
|
840
|
-
headers: { 'Content-Type': 'application/json', ...auth },
|
|
841
|
-
body: JSON.stringify(body),
|
|
842
|
-
}, keySent),
|
|
843
|
-
};
|
|
1126
|
+
* @param opts - Parsed command options.
|
|
1127
|
+
* @returns A client for `http://<host>:<port>` with the resolved key and
|
|
1128
|
+
* {@link CLI_TIMEOUT_MS}.
|
|
1129
|
+
*/
|
|
1130
|
+
function cliClient(opts) {
|
|
1131
|
+
const apiKey = resolveApiKey(opts.apiKey);
|
|
1132
|
+
return createWatcherClient({
|
|
1133
|
+
baseUrl: `http://${opts.host}:${opts.port}`,
|
|
1134
|
+
...(apiKey === undefined ? {} : { apiKey }),
|
|
1135
|
+
timeoutMs: CLI_TIMEOUT_MS,
|
|
1136
|
+
});
|
|
844
1137
|
}
|
|
845
1138
|
|
|
846
1139
|
/**
|
|
847
1140
|
* @module cli/jeeves-watcher/apiOptions
|
|
848
|
-
* Commander plumbing shared by the API-calling CLI commands: --port/--host/--api-key
|
|
1141
|
+
* Commander plumbing shared by the API-calling CLI commands: --port/--host/--api-key, running a command against the SDK client, error reporting (`<code>: <message>` on stderr, exit code 1) and output (pretty JSON or raw text on stdout).
|
|
849
1142
|
*/
|
|
850
1143
|
const DEFAULT_PORT = String(API_DEFAULTS.port);
|
|
851
1144
|
const DEFAULT_HOST = API_DEFAULTS.host;
|
|
852
1145
|
/**
|
|
853
1146
|
* Add the standard `--port`, `--host` and `--api-key` options to a command.
|
|
854
1147
|
* The key has no default, so it never appears in `--help` output.
|
|
1148
|
+
*
|
|
1149
|
+
* @param cmd - The command.
|
|
1150
|
+
* @returns The command with the options added.
|
|
855
1151
|
*/
|
|
856
1152
|
function withApiOptions(cmd) {
|
|
857
1153
|
return cmd
|
|
@@ -860,43 +1156,74 @@ function withApiOptions(cmd) {
|
|
|
860
1156
|
.option('--api-key <key>', `API bearer key (defaults to the ${API_KEY_ENV_VAR} environment variable)`);
|
|
861
1157
|
}
|
|
862
1158
|
/**
|
|
863
|
-
*
|
|
1159
|
+
* Describe a failure as `<code>: <message>`. A 401 adds the key hint; a
|
|
1160
|
+
* timeout of a state-changing command adds that the operation may still
|
|
1161
|
+
* complete on the server, and how to check (D119).
|
|
864
1162
|
*
|
|
865
|
-
* @param
|
|
866
|
-
* @
|
|
1163
|
+
* @param err - What the command threw.
|
|
1164
|
+
* @param apiKey - `--api-key` value, to tell "no key" from "wrong key".
|
|
1165
|
+
* @param checkAfterTimeout - How to check a timed-out state change.
|
|
1166
|
+
* @returns The text to print (one or more lines).
|
|
867
1167
|
*/
|
|
868
|
-
function
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
}
|
|
1168
|
+
function describeError(err, apiKey, checkAfterTimeout) {
|
|
1169
|
+
if (!isWatcherError(err)) {
|
|
1170
|
+
return err instanceof Error ? err.message : String(err);
|
|
1171
|
+
}
|
|
1172
|
+
const lines = [`${err.code}: ${err.message}`];
|
|
1173
|
+
if (err.status === 401) {
|
|
1174
|
+
lines.push(unauthorizedHint(resolveApiKey(apiKey) !== undefined));
|
|
1175
|
+
}
|
|
1176
|
+
if (err.code === 'timeout' && checkAfterTimeout !== undefined) {
|
|
1177
|
+
lines.push(`The operation may still complete on the server; check with ${checkAfterTimeout}.`);
|
|
1178
|
+
}
|
|
1179
|
+
return lines.join('\n');
|
|
873
1180
|
}
|
|
874
|
-
/**
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1181
|
+
/**
|
|
1182
|
+
* Run a command against the SDK client: any failure is printed with
|
|
1183
|
+
* {@link describeError} and sets exit code 1.
|
|
1184
|
+
*
|
|
1185
|
+
* @param opts - The command's connection options.
|
|
1186
|
+
* @param fn - The command body.
|
|
1187
|
+
* @param checkAfterTimeout - For state-changing commands: how to check
|
|
1188
|
+
* whether a timed-out call completed.
|
|
1189
|
+
*/
|
|
1190
|
+
async function runApiCommand(opts, fn, checkAfterTimeout) {
|
|
1191
|
+
try {
|
|
1192
|
+
await fn(cliClient(opts));
|
|
1193
|
+
}
|
|
1194
|
+
catch (err) {
|
|
1195
|
+
console.error(describeError(err, opts.apiKey, checkAfterTimeout));
|
|
1196
|
+
process.exitCode = 1;
|
|
1197
|
+
}
|
|
885
1198
|
}
|
|
886
|
-
/**
|
|
1199
|
+
/**
|
|
1200
|
+
* Print a JSON response to stdout, pretty-printed.
|
|
1201
|
+
*
|
|
1202
|
+
* @param data - The response.
|
|
1203
|
+
*/
|
|
887
1204
|
function printJson(data) {
|
|
888
1205
|
console.log(JSON.stringify(data, null, 2));
|
|
889
1206
|
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Print a text response to stdout as received.
|
|
1209
|
+
*
|
|
1210
|
+
* @param text - The response.
|
|
1211
|
+
*/
|
|
1212
|
+
function printText(text) {
|
|
1213
|
+
process.stdout.write(text);
|
|
1214
|
+
}
|
|
890
1215
|
|
|
891
1216
|
/**
|
|
892
1217
|
* @module cli/jeeves-watcher/configCommands
|
|
893
|
-
* Re-wires core's `config` and `config apply` CLI commands through the
|
|
1218
|
+
* Re-wires core's `config` and `config apply` CLI commands through the SDK client (`config`, `configApply`), adding `--api-key` so they send the bearer key. Reads patches from a file or stdin.
|
|
894
1219
|
*
|
|
895
1220
|
* @remarks
|
|
896
1221
|
* Core's `createServiceCli` registers both commands with an HTTP helper that
|
|
897
1222
|
* cannot attach headers. This module keeps their names, arguments, options and
|
|
898
|
-
* output, adds `--api-key` and replaces only the action handlers
|
|
899
|
-
*
|
|
1223
|
+
* output, adds `--api-key` and replaces only the action handlers, which call
|
|
1224
|
+
* the SDK with the CLI's 60 s timeout. Failures print core's prefix, then
|
|
1225
|
+
* `<code>: <message>`. When the commands are absent (e.g. a bare test
|
|
1226
|
+
* program) it does nothing.
|
|
900
1227
|
*/
|
|
901
1228
|
/** Host core's config commands always address. */
|
|
902
1229
|
const CONFIG_HOST = API_DEFAULTS.host;
|
|
@@ -916,8 +1243,8 @@ function addApiKeyOption(cmd) {
|
|
|
916
1243
|
cmd.option('--api-key <key>', `API bearer key (defaults to the ${API_KEY_ENV_VAR} environment variable)`);
|
|
917
1244
|
}
|
|
918
1245
|
/** Print an error with core's prefix and set exit code 1. */
|
|
919
|
-
function reportFailure(prefix, err) {
|
|
920
|
-
console.error(`${prefix}: ${err
|
|
1246
|
+
function reportFailure(prefix, err, apiKey) {
|
|
1247
|
+
console.error(`${prefix}: ${describeError(err, apiKey)}`);
|
|
921
1248
|
process.exitCode = 1;
|
|
922
1249
|
}
|
|
923
1250
|
/**
|
|
@@ -969,13 +1296,12 @@ function authenticateConfigCommands(program) {
|
|
|
969
1296
|
configCmd.action(async () => {
|
|
970
1297
|
const opts = configOptsSchema.parse(configCmd.opts());
|
|
971
1298
|
const jsonpath = configCmd.args[0];
|
|
972
|
-
const qs = jsonpath ? `?path=${encodeURIComponent(jsonpath)}` : '';
|
|
973
1299
|
try {
|
|
974
|
-
const client =
|
|
975
|
-
printJson(await client.
|
|
1300
|
+
const client = cliClient({ ...opts, host: CONFIG_HOST });
|
|
1301
|
+
printJson(await client.config(jsonpath ? { path: jsonpath } : {}));
|
|
976
1302
|
}
|
|
977
1303
|
catch (err) {
|
|
978
|
-
reportFailure('Config query failed', err);
|
|
1304
|
+
reportFailure('Config query failed', err, opts.apiKey);
|
|
979
1305
|
}
|
|
980
1306
|
});
|
|
981
1307
|
const applyCmd = findCommand(configCmd, 'apply');
|
|
@@ -986,14 +1312,14 @@ function authenticateConfigCommands(program) {
|
|
|
986
1312
|
const opts = configOptsSchema.parse(applyCmd.opts());
|
|
987
1313
|
try {
|
|
988
1314
|
const patch = await readPatch(opts.file);
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1315
|
+
const client = cliClient({ ...opts, host: CONFIG_HOST });
|
|
1316
|
+
printJson(await client.configApply({
|
|
1317
|
+
patch,
|
|
1318
|
+
...(opts.replace ? { replace: true } : {}),
|
|
1319
|
+
}));
|
|
994
1320
|
}
|
|
995
1321
|
catch (err) {
|
|
996
|
-
reportFailure('Config apply failed', err);
|
|
1322
|
+
reportFailure('Config apply failed', err, opts.apiKey);
|
|
997
1323
|
}
|
|
998
1324
|
});
|
|
999
1325
|
}
|
|
@@ -1084,10 +1410,95 @@ function parseMetadataArgs(json, keys) {
|
|
|
1084
1410
|
return metadata;
|
|
1085
1411
|
}
|
|
1086
1412
|
|
|
1413
|
+
/**
|
|
1414
|
+
* @module cli/jeeves-watcher/vcsCommands
|
|
1415
|
+
* The `vcs` command group (#213): status, history, show, diff, revert, check and exclude, each one SDK call (`getVcsStatus`, `listVcsHistory`, `showVcsFile`, `diffVcs`, `revertVcs`, `checkVcsExclusion`, `excludeVcsPath`). JSON replies print as pretty JSON; `show` and `diff` print the raw text.
|
|
1416
|
+
*/
|
|
1417
|
+
/** How to check whether a timed-out exclusion completed (D119). */
|
|
1418
|
+
const CHECK_EXCLUDE = '`jeeves-watcher vcs check <path>` or `vcs status`';
|
|
1419
|
+
/** How to check whether a timed-out revert completed (D119). */
|
|
1420
|
+
const CHECK_REVERT = '`jeeves-watcher vcs history <glob>`';
|
|
1421
|
+
/**
|
|
1422
|
+
* Register the `vcs` command group.
|
|
1423
|
+
*
|
|
1424
|
+
* @param program - The Commander program.
|
|
1425
|
+
*/
|
|
1426
|
+
function registerVcsCommands(program) {
|
|
1427
|
+
const vcs = program
|
|
1428
|
+
.command('vcs')
|
|
1429
|
+
.description('Version history of the watched files (/vcs/*)');
|
|
1430
|
+
withApiOptions(vcs
|
|
1431
|
+
.command('status')
|
|
1432
|
+
.description('VCS state of every root (GET /vcs/status)')).action((opts) => runApiCommand(opts, async (client) => {
|
|
1433
|
+
printJson(await client.getVcsStatus());
|
|
1434
|
+
}));
|
|
1435
|
+
withApiOptions(vcs
|
|
1436
|
+
.command('history')
|
|
1437
|
+
.description('Change history of a path or glob (GET /vcs/history)')
|
|
1438
|
+
.argument('<glob>', 'Path or glob inside a VCS root')
|
|
1439
|
+
.option('--since <date>', 'Only commits after this date')
|
|
1440
|
+
.option('--until <date>', 'Only commits before this date')
|
|
1441
|
+
.option('--limit <n>', 'Most commits to return (server default 20)')).action((glob, opts) => runApiCommand(opts, async (client) => {
|
|
1442
|
+
printJson(await client.listVcsHistory({
|
|
1443
|
+
glob,
|
|
1444
|
+
since: opts.since,
|
|
1445
|
+
until: opts.until,
|
|
1446
|
+
limit: opts.limit,
|
|
1447
|
+
}));
|
|
1448
|
+
}));
|
|
1449
|
+
withApiOptions(vcs
|
|
1450
|
+
.command('show')
|
|
1451
|
+
.description('File content at a commit (GET /vcs/show)')
|
|
1452
|
+
.argument('<path>', 'File path inside a VCS root')
|
|
1453
|
+
.argument('<commit>', 'Commit to read the file from')).action((path, commit, opts) => runApiCommand(opts, async (client) => {
|
|
1454
|
+
printText(await client.showVcsFile({ path, commit }));
|
|
1455
|
+
}));
|
|
1456
|
+
withApiOptions(vcs
|
|
1457
|
+
.command('diff')
|
|
1458
|
+
.description('Unified diff from a commit (GET /vcs/diff)')
|
|
1459
|
+
.argument('<glob>', 'Path or glob inside a VCS root')
|
|
1460
|
+
.argument('<commit>', 'Commit to diff from')
|
|
1461
|
+
.option('--to <commit>', 'Commit to diff to (default: the working tree)')).action((glob, commit, opts) => runApiCommand(opts, async (client) => {
|
|
1462
|
+
printText(await client.diffVcs({ glob, commit, commitEnd: opts.to }));
|
|
1463
|
+
}));
|
|
1464
|
+
withApiOptions(vcs
|
|
1465
|
+
.command('revert')
|
|
1466
|
+
.description('Restore files from a commit, as a new commit (POST /vcs/revert)')
|
|
1467
|
+
.argument('<glob>', 'Path or glob inside a VCS root')
|
|
1468
|
+
.argument('<commit>', 'Commit to restore from')
|
|
1469
|
+
.option('--existing-only', 'Restore only files that exist now')).action((glob, commit, opts) => runApiCommand(opts, async (client) => {
|
|
1470
|
+
printJson(await client.revertVcs({
|
|
1471
|
+
glob,
|
|
1472
|
+
commit,
|
|
1473
|
+
...(opts.existingOnly ? { existingOnly: true } : {}),
|
|
1474
|
+
}));
|
|
1475
|
+
}, CHECK_REVERT));
|
|
1476
|
+
withApiOptions(vcs
|
|
1477
|
+
.command('check')
|
|
1478
|
+
.description('Whether git ignores a path, and the rule, file and line (GET /vcs/check-exclusion)')
|
|
1479
|
+
.argument('<path>', 'Path inside a VCS root')).action((path, opts) => runApiCommand(opts, async (client) => {
|
|
1480
|
+
printJson(await client.checkVcsExclusion({ path }));
|
|
1481
|
+
}));
|
|
1482
|
+
withApiOptions(vcs
|
|
1483
|
+
.command('exclude')
|
|
1484
|
+
.description('Exclude a gitignore pattern from version history (it stays in search) and untrack the files it matches, or remove the exclusion (POST /vcs/exclude)')
|
|
1485
|
+
.argument('<glob>', 'One gitignore pattern (escape brackets to name a file: Report \\[draft\\].docx)')
|
|
1486
|
+
.option('--remove', 'Remove the exclusion instead of adding it')
|
|
1487
|
+
.option('--root <root>', 'VCS root the glob is relative to')
|
|
1488
|
+
.option('--confirm', `Allow untracking more than ${String(VCS_EXCLUDE_CONFIRM_THRESHOLD)} files`)).action((glob, opts) => runApiCommand(opts, async (client) => {
|
|
1489
|
+
printJson(await client.excludeVcsPath({
|
|
1490
|
+
glob,
|
|
1491
|
+
root: opts.root,
|
|
1492
|
+
...(opts.remove ? { remove: true } : {}),
|
|
1493
|
+
...(opts.confirm ? { confirm: true } : {}),
|
|
1494
|
+
}));
|
|
1495
|
+
}, CHECK_EXCLUDE));
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1087
1498
|
/**
|
|
1088
1499
|
* @module cli/jeeves-watcher/customCommands
|
|
1089
1500
|
* Domain-specific CLI commands registered via the descriptor's customCliCommands.
|
|
1090
|
-
* Every API call
|
|
1501
|
+
* Every API call is one SDK client call (D37) with a 60 s timeout (`cliClient`), sending the bearer key when one is resolved.
|
|
1091
1502
|
*/
|
|
1092
1503
|
/**
|
|
1093
1504
|
* Register all domain-specific commands on the given Commander program.
|
|
@@ -1102,33 +1513,21 @@ function registerCustomCommands(program) {
|
|
|
1102
1513
|
.command('search')
|
|
1103
1514
|
.description('Search the vector store (POST /search)')
|
|
1104
1515
|
.argument('<query>', 'Search query')
|
|
1105
|
-
.option('-l, --limit <limit>', 'Max results', '10')).action(
|
|
1106
|
-
await
|
|
1107
|
-
|
|
1108
|
-
query,
|
|
1109
|
-
limit: Number(opts.limit),
|
|
1110
|
-
});
|
|
1111
|
-
printJson(result);
|
|
1112
|
-
})();
|
|
1113
|
-
});
|
|
1516
|
+
.option('-l, --limit <limit>', 'Max results', '10')).action((query, opts) => runApiCommand(opts, async (client) => {
|
|
1517
|
+
printJson(await client.search({ query, limit: Number(opts.limit) }));
|
|
1518
|
+
}));
|
|
1114
1519
|
// --- enrich ---
|
|
1115
1520
|
withApiOptions(program
|
|
1116
1521
|
.command('enrich')
|
|
1117
1522
|
.description('Enrich document metadata (POST /metadata)')
|
|
1118
1523
|
.argument('<path>', 'File path to enrich')
|
|
1119
1524
|
.option('-k, --key <key=value...>', 'Metadata key-value pairs (repeatable)', [])
|
|
1120
|
-
.option('-j, --json <json>', 'Metadata as JSON string (e.g., \'{"key":"value"}\')')).action(
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
path,
|
|
1127
|
-
metadata,
|
|
1128
|
-
});
|
|
1129
|
-
printJson(result);
|
|
1130
|
-
})();
|
|
1131
|
-
});
|
|
1525
|
+
.option('-j, --json <json>', 'Metadata as JSON string (e.g., \'{"key":"value"}\')')).action((path, opts) => runApiCommand(opts, async (client) => {
|
|
1526
|
+
const metadata = parseMetadataArgs(opts.json, opts.key);
|
|
1527
|
+
if (!metadata)
|
|
1528
|
+
return;
|
|
1529
|
+
printJson(await client.setMetadata({ path, metadata }));
|
|
1530
|
+
}));
|
|
1132
1531
|
// --- scan ---
|
|
1133
1532
|
withApiOptions(program
|
|
1134
1533
|
.command('scan')
|
|
@@ -1137,85 +1536,80 @@ function registerCustomCommands(program) {
|
|
|
1137
1536
|
.option('-l, --limit <limit>', 'Max results', '100')
|
|
1138
1537
|
.option('-c, --cursor <cursor>', 'Cursor from previous response')
|
|
1139
1538
|
.option('--fields <fields>', 'Fields to return (comma-separated)')
|
|
1140
|
-
.option('--count-only', 'Return count only')).action(async (
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1539
|
+
.option('--count-only', 'Return count only')).action((opts) => runApiCommand(opts, async (client) => {
|
|
1540
|
+
const filter = JSON.parse(opts.filter);
|
|
1541
|
+
printJson(await client.scan({
|
|
1542
|
+
filter,
|
|
1543
|
+
limit: Number(opts.limit),
|
|
1544
|
+
cursor: opts.cursor,
|
|
1545
|
+
fields: opts.fields
|
|
1546
|
+
? opts.fields.split(',').map((f) => f.trim())
|
|
1547
|
+
: undefined,
|
|
1548
|
+
countOnly: opts.countOnly,
|
|
1549
|
+
}));
|
|
1550
|
+
}));
|
|
1551
|
+
// --- walk ---
|
|
1552
|
+
withApiOptions(program
|
|
1553
|
+
.command('walk')
|
|
1554
|
+
.description('List watched files matching globs (POST /walk)')
|
|
1555
|
+
.argument('<globs...>', 'Glob patterns to intersect with watch paths')).action((globs, opts) => runApiCommand(opts, async (client) => {
|
|
1556
|
+
printJson(await client.walk({ globs }));
|
|
1557
|
+
}));
|
|
1155
1558
|
// --- reindex ---
|
|
1156
1559
|
withApiOptions(program
|
|
1157
1560
|
.command('reindex')
|
|
1158
1561
|
.description('Trigger a reindex operation (POST /reindex)')
|
|
1159
1562
|
.option('-s, --scope <scope>', 'Reindex scope (issues|full|rules|path|prune)', 'rules')
|
|
1160
|
-
.option('-t, --path <paths...>', 'Target path(s) for path or rules scope')
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1563
|
+
.option('-t, --path <paths...>', 'Target path(s) for path or rules scope')
|
|
1564
|
+
.option('--dry-run', 'Return the plan without running it')).action((opts) => runApiCommand(opts, async (client) => {
|
|
1565
|
+
const scope = reindexScopeSchema.safeParse(opts.scope);
|
|
1566
|
+
if (!scope.success) {
|
|
1567
|
+
console.error(`Invalid scope "${opts.scope}". Must be one of: ${reindexScopeSchema.options.join(', ')}`);
|
|
1568
|
+
process.exitCode = 1;
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
printJson(await client.reindex({
|
|
1572
|
+
scope: scope.data,
|
|
1573
|
+
...(opts.path
|
|
1574
|
+
? { path: opts.path.length === 1 ? opts.path[0] : opts.path }
|
|
1575
|
+
: {}),
|
|
1576
|
+
...(opts.dryRun ? { dryRun: true } : {}),
|
|
1577
|
+
}));
|
|
1578
|
+
}));
|
|
1175
1579
|
// --- rebuild-metadata ---
|
|
1176
1580
|
withApiOptions(program
|
|
1177
1581
|
.command('rebuild-metadata')
|
|
1178
|
-
.description('Rebuild metadata store from Qdrant (POST /rebuild-metadata)')).action(async (
|
|
1179
|
-
await
|
|
1180
|
-
|
|
1181
|
-
printJson(result);
|
|
1182
|
-
})();
|
|
1183
|
-
});
|
|
1582
|
+
.description('Rebuild metadata store from Qdrant (POST /rebuild-metadata)')).action((opts) => runApiCommand(opts, async (client) => {
|
|
1583
|
+
printJson(await client.rebuildMetadata({}));
|
|
1584
|
+
}));
|
|
1184
1585
|
// --- issues ---
|
|
1185
1586
|
withApiOptions(program
|
|
1186
1587
|
.command('issues')
|
|
1187
|
-
.description('List current processing issues (GET /issues)')).action(async (
|
|
1188
|
-
await
|
|
1189
|
-
|
|
1190
|
-
printJson(result);
|
|
1191
|
-
})();
|
|
1192
|
-
});
|
|
1588
|
+
.description('List current processing issues (GET /issues)')).action((opts) => runApiCommand(opts, async (client) => {
|
|
1589
|
+
printJson(await client.listIssues());
|
|
1590
|
+
}));
|
|
1193
1591
|
// --- helpers ---
|
|
1194
|
-
withApiOptions(program.command('helpers').description('List available helper functions')).action(async (
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
console.log(sections.join('\n\n'));
|
|
1216
|
-
}
|
|
1217
|
-
})();
|
|
1218
|
-
});
|
|
1592
|
+
withApiOptions(program.command('helpers').description('List available helper functions')).action((opts) => runApiCommand(opts, async (client) => {
|
|
1593
|
+
const query = (path) => client.config({ path });
|
|
1594
|
+
const [mapResult, tplResult] = await Promise.all([
|
|
1595
|
+
query('$.mapHelpers'),
|
|
1596
|
+
query('$.templateHelpers'),
|
|
1597
|
+
]);
|
|
1598
|
+
const mapData = mapResult.result[0];
|
|
1599
|
+
const tplData = tplResult.result[0];
|
|
1600
|
+
const sections = [];
|
|
1601
|
+
const mapSection = formatHelperSection('JsonMap lib functions', mapData);
|
|
1602
|
+
if (mapSection)
|
|
1603
|
+
sections.push(mapSection);
|
|
1604
|
+
const tplSection = formatHelperSection('Handlebars helpers', tplData);
|
|
1605
|
+
if (tplSection)
|
|
1606
|
+
sections.push(tplSection);
|
|
1607
|
+
console.log(sections.length === 0
|
|
1608
|
+
? 'No helpers configured.'
|
|
1609
|
+
: sections.join('\n\n'));
|
|
1610
|
+
}));
|
|
1611
|
+
// --- vcs ---
|
|
1612
|
+
registerVcsCommands(program);
|
|
1219
1613
|
}
|
|
1220
1614
|
|
|
1221
1615
|
/**
|
|
@@ -1252,6 +1646,9 @@ const watcherDescriptor = {
|
|
|
1252
1646
|
configSchema: jeevesWatcherConfigSchema,
|
|
1253
1647
|
configFileName: 'config.json',
|
|
1254
1648
|
initTemplate: () => ({ ...INIT_CONFIG_TEMPLATE }),
|
|
1649
|
+
// Agent-facing text, shared with the plugin through watcher-core.
|
|
1650
|
+
promptRules: WATCHER_PROMPT_RULES,
|
|
1651
|
+
skill: WATCHER_SKILL,
|
|
1255
1652
|
// onConfigApply is overridden in createApiServer (api/index.ts) via the
|
|
1256
1653
|
// descriptor passed as a dependency, where it has access to the live
|
|
1257
1654
|
// reindex tracker and config getter.
|
|
@@ -1280,7 +1677,8 @@ const watcherDescriptor = {
|
|
|
1280
1677
|
* jeeves-watcher CLI entrypoint.
|
|
1281
1678
|
* Uses core's createServiceCli factory with the watcher descriptor.
|
|
1282
1679
|
*/
|
|
1680
|
+
// First: libuv reads UV_THREADPOOL_SIZE when its threadpool starts (D170).
|
|
1283
1681
|
const program = createServiceCli(watcherDescriptor);
|
|
1284
1682
|
program.parse();
|
|
1285
1683
|
|
|
1286
|
-
export { validateFacetTypes as a, mergeInferenceRules as b, findReservedFieldWarnings as c,
|
|
1684
|
+
export { validateFacetTypes as a, mergeInferenceRules as b, findReservedFieldWarnings as c, shutdownComponents as d, startFromConfig as e, findUnknownSourceKeys as f, loadConfig as l, mergeSchemas as m, resolveAndCoerce as r, settlesWithin as s, validateSchemaCompleteness as v };
|