@fishdivinity/dsh-local-search 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/config.js ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Config access shared by activation and every tool call.
3
+ *
4
+ * The fields the Plugins page may edit are declared `.volatile()` in the
5
+ * schema, so the Host hands `apply` a live reference instead of the value
6
+ * frozen at activation. A save on the page updates that reference in place;
7
+ * only `get()` sees it. Nothing downstream may keep such a reference, so every
8
+ * operation starts from {@link readConfig}'s plain snapshot.
9
+ *
10
+ * `timeoutMs` is deliberately not in {@link LIVE_KEYS}: `defineTool` fixes each
11
+ * tool's deadline when the tool is registered, so a new value could not take
12
+ * effect without reloading the row. It stays a profile-YAML setting.
13
+ */
14
+
15
+ /** Config keys the Plugins page edits. */
16
+ export const LIVE_KEYS = [
17
+ 'esPath',
18
+ 'esEncoding',
19
+ 'anytxtUrl',
20
+ 'workspaceRoot',
21
+ 'workRoot',
22
+ 'personalRoot',
23
+ 'allRoots',
24
+ 'maxResults',
25
+ 'readMaxChars',
26
+ ]
27
+
28
+ /**
29
+ * Read a value that may still be a live volatile reference.
30
+ * @param {unknown} value - plain value, or a reference carrying `get()`
31
+ * @returns {unknown} the current value
32
+ */
33
+ export function liveValue(value) {
34
+ return typeof value?.get === 'function' ? value.get() : value
35
+ }
36
+
37
+ /**
38
+ * Snapshot the whole config as plain values.
39
+ * @param {object} config - schema output, volatile references included
40
+ * @returns {object} plain values for this operation
41
+ */
42
+ export function readConfig(config) {
43
+ const snapshot = {}
44
+ for (const key of [...LIVE_KEYS, 'timeoutMs']) snapshot[key] = liveValue(config[key])
45
+ return snapshot
46
+ }
47
+
48
+ function assertPositiveInteger(field, value) {
49
+ if (!Number.isInteger(value) || value < 1) {
50
+ throw new Error(`dsh-local-search: ${field} must be a positive integer`)
51
+ }
52
+ }
53
+
54
+ /** Hosts that name this machine. */
55
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost'])
56
+
57
+ /**
58
+ * Whether a value is an `http(s)` URL that names this machine.
59
+ *
60
+ * Both indexes describe the local machine: `es.exe` is spawned here, and every
61
+ * Anytxt result carries a path that only resolves here. A remote endpoint would
62
+ * answer with another machine's paths, so it is refused rather than supported —
63
+ * tunnelling one in lands it on loopback anyway.
64
+ * @param {unknown} value - candidate endpoint
65
+ * @returns {boolean} true for a loopback `http(s)` URL
66
+ */
67
+ export function isLoopbackUrl(value) {
68
+ let parsed
69
+ try {
70
+ parsed = new URL(String(value))
71
+ } catch {
72
+ return false
73
+ }
74
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false
75
+ return LOOPBACK_HOSTS.has(parsed.hostname.replace(/^\[|\]$/g, ''))
76
+ }
77
+
78
+ /**
79
+ * Validate a candidate config and return its plain snapshot. Run once at
80
+ * activation; the page's writes are validated by the Host against the Config
81
+ * schema before they are persisted, so the range checks that matter live there
82
+ * (`lib/index.js`) and this function covers what the schema cannot express.
83
+ *
84
+ * The endpoint's loopback rule is deliberately *not* enforced here. A profile
85
+ * that 0.1 accepted must still activate: a wrong address should fail the content
86
+ * engine with a readable error, not take file-name search down with it. The
87
+ * rule lives where the endpoint is used (`lib/anytxt.js`).
88
+ * @param {object} config - schema output, volatile references included
89
+ * @param {number} [maxTimerDelayMs] - largest deadline the timer API accepts
90
+ * @returns {object} the validated plain snapshot
91
+ */
92
+ export function validateConfig(config, maxTimerDelayMs) {
93
+ const values = readConfig(config)
94
+ assertPositiveInteger('maxResults', values.maxResults)
95
+ assertPositiveInteger('timeoutMs', values.timeoutMs)
96
+ assertPositiveInteger('readMaxChars', values.readMaxChars)
97
+ if (maxTimerDelayMs !== undefined && values.timeoutMs > maxTimerDelayMs) {
98
+ throw new Error(`dsh-local-search: timeoutMs must be no greater than ${maxTimerDelayMs}`)
99
+ }
100
+ return values
101
+ }
package/lib/engines.js CHANGED
@@ -1,85 +1,85 @@
1
- import { EngineError } from './errors.js'
2
- import { getEsVersion, resolveEsPath, searchFiles } from './everything.js'
3
- import { anytxtStatus, getFragments, getRawText, searchContent } from './anytxt.js'
4
- import { assertRefEngine, formatRef, parseRef } from './ref.js'
5
-
6
- /**
7
- * Engine registry. A new filename or content backend is one entry here plus
8
- * its module; the tool surface and `engine` values stay unchanged.
9
- *
10
- * files: { name, search(args), status(args) }
11
- * content: { name, search(args), fragments(args), read(args), status(args) }
12
- */
13
- const filesEngines = new Map([
14
- [
15
- 'everything',
16
- {
17
- name: 'everything',
18
- search: (args) => searchFiles(args),
19
- status: async (args) => ({
20
- ok: true,
21
- version: await getEsVersion(args),
22
- path: resolveEsPath(args.config ?? {}),
23
- }),
24
- },
25
- ],
26
- ])
27
-
28
- const contentEngines = new Map([
29
- [
30
- 'anytxt',
31
- {
32
- name: 'anytxt',
33
- async search(args) {
34
- const result = await searchContent(args)
35
- return {
36
- ...result,
37
- items: result.items.map((item) => ({
38
- ...item,
39
- engine: 'anytxt',
40
- ref: formatRef('anytxt', item.ref),
41
- })),
42
- }
43
- },
44
- fragments(args) {
45
- const parsed = assertRefEngine(parseRef(args.ref), 'anytxt')
46
- return getFragments({ ...args, ref: parsed.id })
47
- },
48
- read(args) {
49
- const parsed = assertRefEngine(parseRef(args.ref), 'anytxt')
50
- return getRawText({ ...args, ref: parsed.id })
51
- },
52
- status: (args) => anytxtStatus(args),
53
- },
54
- ],
55
- ])
56
-
57
- function resolve(registry, kind, requested) {
58
- const name = requested || 'auto'
59
- if (name === 'auto') return registry.values().next().value
60
- const engine = registry.get(name)
61
- if (!engine) {
62
- throw new EngineError(`unknown ${kind} engine: ${name}`, 'ENGINE_UNSUPPORTED', {
63
- hint: `Use auto, ${[...registry.keys()].join(', ')}.`,
64
- })
65
- }
66
- return engine
67
- }
68
-
69
- /** Resolve a filename engine; `auto` selects the first registered engine. */
70
- export function resolveFilesEngine(requested = 'auto') {
71
- return resolve(filesEngines, 'files', requested)
72
- }
73
-
74
- /** Resolve a content engine; `auto` selects the first registered engine. */
75
- export function resolveContentEngine(requested = 'auto') {
76
- return resolve(contentEngines, 'content', requested)
77
- }
78
-
79
- /** Engine names for status output and future config pages. */
80
- export function engineNames() {
81
- return {
82
- files: [...filesEngines.keys()],
83
- content: [...contentEngines.keys()],
84
- }
85
- }
1
+ import { EngineError } from './errors.js'
2
+ import { getEsVersion, resolveEsPath, searchFiles } from './everything.js'
3
+ import { anytxtStatus, getFragments, getRawText, searchContent } from './anytxt.js'
4
+ import { assertRefEngine, formatRef, parseRef } from './ref.js'
5
+
6
+ /**
7
+ * Engine registry. A new filename or content backend is one entry here plus
8
+ * its module; the tool surface and `engine` values stay unchanged.
9
+ *
10
+ * files: { name, search(args), status(args) }
11
+ * content: { name, search(args), fragments(args), read(args), status(args) }
12
+ */
13
+ const filesEngines = new Map([
14
+ [
15
+ 'everything',
16
+ {
17
+ name: 'everything',
18
+ search: (args) => searchFiles(args),
19
+ status: async (args) => ({
20
+ ok: true,
21
+ version: await getEsVersion(args),
22
+ path: resolveEsPath(args.config ?? {}),
23
+ }),
24
+ },
25
+ ],
26
+ ])
27
+
28
+ const contentEngines = new Map([
29
+ [
30
+ 'anytxt',
31
+ {
32
+ name: 'anytxt',
33
+ async search(args) {
34
+ const result = await searchContent(args)
35
+ return {
36
+ ...result,
37
+ items: result.items.map((item) => ({
38
+ ...item,
39
+ engine: 'anytxt',
40
+ ref: formatRef('anytxt', item.ref),
41
+ })),
42
+ }
43
+ },
44
+ fragments(args) {
45
+ const parsed = assertRefEngine(parseRef(args.ref), 'anytxt')
46
+ return getFragments({ ...args, ref: parsed.id })
47
+ },
48
+ read(args) {
49
+ const parsed = assertRefEngine(parseRef(args.ref), 'anytxt')
50
+ return getRawText({ ...args, ref: parsed.id })
51
+ },
52
+ status: (args) => anytxtStatus(args),
53
+ },
54
+ ],
55
+ ])
56
+
57
+ function resolve(registry, kind, requested) {
58
+ const name = requested || 'auto'
59
+ if (name === 'auto') return registry.values().next().value
60
+ const engine = registry.get(name)
61
+ if (!engine) {
62
+ throw new EngineError(`unknown ${kind} engine: ${name}`, 'ENGINE_UNSUPPORTED', {
63
+ hint: `Use auto, ${[...registry.keys()].join(', ')}.`,
64
+ })
65
+ }
66
+ return engine
67
+ }
68
+
69
+ /** Resolve a filename engine; `auto` selects the first registered engine. */
70
+ export function resolveFilesEngine(requested = 'auto') {
71
+ return resolve(filesEngines, 'files', requested)
72
+ }
73
+
74
+ /** Resolve a content engine; `auto` selects the first registered engine. */
75
+ export function resolveContentEngine(requested = 'auto') {
76
+ return resolve(contentEngines, 'content', requested)
77
+ }
78
+
79
+ /** Engine names for status output and future config pages. */
80
+ export function engineNames() {
81
+ return {
82
+ files: [...filesEngines.keys()],
83
+ content: [...contentEngines.keys()],
84
+ }
85
+ }
package/lib/errors.js CHANGED
@@ -1,18 +1,18 @@
1
- /**
2
- * Engine-level error with a stable code. Kept independent of the DSH runtime
3
- * so engine modules and their unit tests can run under plain Node; the tool
4
- * layer converts these to `HarnessError` before returning to the model.
5
- */
6
- export class EngineError extends Error {
7
- /**
8
- * @param {string} message
9
- * @param {string} code
10
- * @param {{ cause?: unknown, hint?: string }} [options]
11
- */
12
- constructor(message, code, options = {}) {
13
- super(message, { cause: options.cause })
14
- this.name = 'EngineError'
15
- this.code = code
16
- this.hint = options.hint
17
- }
18
- }
1
+ /**
2
+ * Engine-level error with a stable code. Kept independent of the DSH runtime
3
+ * so engine modules and their unit tests can run under plain Node; the tool
4
+ * layer converts these to `HarnessError` before returning to the model.
5
+ */
6
+ export class EngineError extends Error {
7
+ /**
8
+ * @param {string} message
9
+ * @param {string} code
10
+ * @param {{ cause?: unknown, hint?: string }} [options]
11
+ */
12
+ constructor(message, code, options = {}) {
13
+ super(message, { cause: options.cause })
14
+ this.name = 'EngineError'
15
+ this.code = code
16
+ this.hint = options.hint
17
+ }
18
+ }