opencode-rag-plugin 1.21.2 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/ui.js +5 -1
- package/dist/content/reader.d.ts +2 -2
- package/dist/content/reader.js +8 -6
- package/dist/core/config.d.ts +27 -0
- package/dist/core/config.js +33 -0
- package/dist/core/exclude.d.ts +14 -0
- package/dist/core/exclude.js +37 -0
- package/dist/indexer/watch.d.ts +3 -1
- package/dist/indexer/watch.js +6 -3
- package/dist/tui.js +9 -29
- package/dist/web/api.d.ts +5 -2
- package/dist/web/api.js +126 -7
- package/dist/web/server.d.ts +2 -1
- package/dist/web/server.js +3 -2
- package/dist/web/ui/assets/{ScatterPlot3D-CzbmvDSQ.js → ScatterPlot3D-DQLl3sKN.js} +1 -1
- package/dist/web/ui/assets/index-BZh9MitV.js +4 -0
- package/dist/web/ui/assets/index-DKhjGsql.css +1 -0
- package/dist/web/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/ui/assets/index-BLzCza1W.css +0 -1
- package/dist/web/ui/assets/index-DOSfQsyL.js +0 -4
package/dist/cli/commands/ui.js
CHANGED
|
@@ -32,7 +32,11 @@ export function registerUiCommand(program) {
|
|
|
32
32
|
const port = parseInt(options.port ?? String(config.ui?.port ?? 3210), 10);
|
|
33
33
|
const openBrowser = options.open !== false && (config.ui?.openBrowser ?? true);
|
|
34
34
|
const { startWebUi } = await import("../../web/server.js");
|
|
35
|
-
const
|
|
35
|
+
const { findConfigFile } = await import("../../core/config.js");
|
|
36
|
+
const configPath = options.config
|
|
37
|
+
? path.resolve(cwd, options.config)
|
|
38
|
+
: findConfigFile(cwd);
|
|
39
|
+
const server = await startWebUi(storePath, port, cwd, config.embedding.vectorDimension ?? 384, config, configPath);
|
|
36
40
|
const url = `http://127.0.0.1:${server.port}/?token=${server.token}`;
|
|
37
41
|
logCliInfo(logFilePath, "ui", `\n${c.heading("OpenCodeRAG Web UI")}`);
|
|
38
42
|
logCliInfo(logFilePath, "ui", ` ${c.label("URL:")} ${c.value(url)}`);
|
package/dist/content/reader.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { RagConfig } from "../core/config.js";
|
|
5
5
|
import { type FileManifest } from "../core/manifest.js";
|
|
6
|
-
import { type ExcludeMatcher } from "../core/exclude.js";
|
|
6
|
+
import { type ExcludeMatcher, type IncludedMatcher } from "../core/exclude.js";
|
|
7
7
|
import { DescriptionCache } from "../core/desc-cache.js";
|
|
8
8
|
import { type ImageVisionProvider } from "../chunker/image.js";
|
|
9
9
|
/** Metadata and extracted content for a single workspace file discovered during scanning. */
|
|
@@ -28,7 +28,7 @@ interface Logger {
|
|
|
28
28
|
* Recursively walk a directory tree and collect paths matching the given extension set,
|
|
29
29
|
* respecting exclusion lists and configurable limits for max directories and results.
|
|
30
30
|
*/
|
|
31
|
-
export declare function walkFiles(dir: string, extensions: Set<string>, excludeDirs: ExcludeMatcher, excludeFiles?: ExcludeMatcher, rootDir?: string, logger?: Logger, dirCount?: {
|
|
31
|
+
export declare function walkFiles(dir: string, extensions: Set<string>, excludeDirs: ExcludeMatcher, excludeFiles?: ExcludeMatcher, includeDirs?: IncludedMatcher, rootDir?: string, logger?: Logger, dirCount?: {
|
|
32
32
|
value: number;
|
|
33
33
|
}, maxDirs?: number, maxResults?: number): Promise<string[]>;
|
|
34
34
|
/**
|
package/dist/content/reader.js
CHANGED
|
@@ -5,7 +5,7 @@ import fs from "node:fs/promises";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import pLimit from "p-limit";
|
|
7
7
|
import { computeFileHash, computeDescriptionConfigHash, normalizeFilePath } from "../core/manifest.js";
|
|
8
|
-
import { createExcludeMatcher } from "../core/exclude.js";
|
|
8
|
+
import { createExcludeMatcher, createIncludeMatcher } from "../core/exclude.js";
|
|
9
9
|
import { DescriptionCache } from "../core/desc-cache.js";
|
|
10
10
|
import { createImageVisionProvider, } from "../chunker/image.js";
|
|
11
11
|
import * as pdfExtractor from "./pdf.js";
|
|
@@ -17,13 +17,14 @@ import * as imageExtractor from "./image.js";
|
|
|
17
17
|
* Recursively walk a directory tree and collect paths matching the given extension set,
|
|
18
18
|
* respecting exclusion lists and configurable limits for max directories and results.
|
|
19
19
|
*/
|
|
20
|
-
export async function walkFiles(dir, extensions, excludeDirs, excludeFiles, rootDir = dir, logger, dirCount, maxDirs = 10_000, maxResults = 100_000) {
|
|
20
|
+
export async function walkFiles(dir, extensions, excludeDirs, excludeFiles, includeDirs, rootDir = dir, logger, dirCount, maxDirs = 10_000, maxResults = 100_000) {
|
|
21
21
|
const results = [];
|
|
22
22
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
23
23
|
for (const entry of entries) {
|
|
24
24
|
const fullPath = path.join(dir, entry.name);
|
|
25
25
|
if (entry.isDirectory()) {
|
|
26
|
-
|
|
26
|
+
const rel = path.relative(rootDir, fullPath);
|
|
27
|
+
if ((includeDirs && !includeDirs.included(rel)) || excludeDirs.excluded(rel))
|
|
27
28
|
continue;
|
|
28
29
|
if (dirCount) {
|
|
29
30
|
dirCount.value++;
|
|
@@ -39,7 +40,7 @@ export async function walkFiles(dir, extensions, excludeDirs, excludeFiles, root
|
|
|
39
40
|
logger?.warn(`Exceeded ${maxResults} matching files — truncating walk at ${fullPath}`);
|
|
40
41
|
return results;
|
|
41
42
|
}
|
|
42
|
-
results.push(...(await walkFiles(fullPath, extensions, excludeDirs, excludeFiles, rootDir, logger, dirCount, maxDirs, maxResults)));
|
|
43
|
+
results.push(...(await walkFiles(fullPath, extensions, excludeDirs, excludeFiles, includeDirs, rootDir, logger, dirCount, maxDirs, maxResults)));
|
|
43
44
|
}
|
|
44
45
|
else if (entry.isFile()) {
|
|
45
46
|
if (results.length >= maxResults) {
|
|
@@ -104,6 +105,7 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
|
|
|
104
105
|
}
|
|
105
106
|
const excludeDirMatcher = createExcludeMatcher(config.indexing.excludeDirs);
|
|
106
107
|
const excludeFileMatcher = createExcludeMatcher(config.indexing.excludeFiles ?? []);
|
|
108
|
+
const includeDirMatcher = createIncludeMatcher(config.indexing.includeDirs ?? []);
|
|
107
109
|
let files;
|
|
108
110
|
if (filterPaths && filterPaths.length > 0) {
|
|
109
111
|
files = filterPaths
|
|
@@ -116,14 +118,14 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
|
|
|
116
118
|
const rel = path.relative(cwd, fp);
|
|
117
119
|
if (rel.startsWith(".."))
|
|
118
120
|
return false;
|
|
119
|
-
return !excludeDirMatcher.excluded(rel) && !excludeFileMatcher.excluded(rel);
|
|
121
|
+
return includeDirMatcher.included(rel) && !excludeDirMatcher.excluded(rel) && !excludeFileMatcher.excluded(rel);
|
|
120
122
|
});
|
|
121
123
|
}
|
|
122
124
|
else {
|
|
123
125
|
logger?.info("Walking directory tree...");
|
|
124
126
|
const walkStart = Date.now();
|
|
125
127
|
const dirCount = { value: 0 };
|
|
126
|
-
files = await walkFiles(cwd, extensions, excludeDirMatcher, excludeFileMatcher, cwd, logger, dirCount);
|
|
128
|
+
files = await walkFiles(cwd, extensions, excludeDirMatcher, excludeFileMatcher, includeDirMatcher, cwd, logger, dirCount);
|
|
127
129
|
const walkSec = ((Date.now() - walkStart) / 1000).toFixed(1);
|
|
128
130
|
logger?.info(`Found ${files.length} matching files in ${walkSec}s (${dirCount.value} dirs traversed)`);
|
|
129
131
|
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -265,6 +265,21 @@ export interface RagConfig {
|
|
|
265
265
|
* patterns are anchored to the workspace root.
|
|
266
266
|
*/
|
|
267
267
|
excludeFiles?: string[];
|
|
268
|
+
/**
|
|
269
|
+
* Workspace-relative folder paths to **restrict indexing to**.
|
|
270
|
+
*
|
|
271
|
+
* When non-empty, only files inside these folders (and their
|
|
272
|
+
* subfolders) are indexed; files directly in the workspace root are
|
|
273
|
+
* NOT indexed. `excludeDirs`/`excludeFiles` still apply inside the
|
|
274
|
+
* included folders. Empty or omitted = whole workspace (default).
|
|
275
|
+
*
|
|
276
|
+
* Semantics:
|
|
277
|
+
* - Entries are **anchored to the workspace root** (unlike
|
|
278
|
+
* `excludeDirs`, plain names do NOT match at any depth).
|
|
279
|
+
* - Supports globs (`docs/**`, `src/{a,b}`); use `/` as separator
|
|
280
|
+
* (converted automatically). Matching is case-insensitive.
|
|
281
|
+
*/
|
|
282
|
+
includeDirs?: string[];
|
|
268
283
|
/** Number of overlapping lines between adjacent chunks. */
|
|
269
284
|
chunkOverlap: number;
|
|
270
285
|
/** Minimum file size in bytes to index (0 = no minimum). */
|
|
@@ -415,3 +430,15 @@ export declare function loadConfig(filePath: string, validate?: boolean): RagCon
|
|
|
415
430
|
* @param dimension - The vector dimension to persist.
|
|
416
431
|
*/
|
|
417
432
|
export declare function persistProbedDimension(configPath: string, dimension: number): void;
|
|
433
|
+
/**
|
|
434
|
+
* Set a value at a dotted path inside the config JSON file, creating
|
|
435
|
+
* intermediate objects as needed, and write it back with 2-space
|
|
436
|
+
* indentation. Reads/writes the raw file, so comments or formatting of
|
|
437
|
+
* unrelated keys are preserved as parsed (the file is re-serialized).
|
|
438
|
+
*
|
|
439
|
+
* @param configPath - Absolute path to the config JSON file.
|
|
440
|
+
* @param path - Dotted path segments, e.g. `["indexing", "includeDirs"]`.
|
|
441
|
+
* @param value - Value to write at the target path.
|
|
442
|
+
* @returns `true` when the file was updated, `false` on any error.
|
|
443
|
+
*/
|
|
444
|
+
export declare function updateConfigValue(configPath: string, path: string[], value: unknown): boolean;
|
package/dist/core/config.js
CHANGED
|
@@ -617,4 +617,37 @@ export function persistProbedDimension(configPath, dimension) {
|
|
|
617
617
|
// best-effort
|
|
618
618
|
}
|
|
619
619
|
}
|
|
620
|
+
/**
|
|
621
|
+
* Set a value at a dotted path inside the config JSON file, creating
|
|
622
|
+
* intermediate objects as needed, and write it back with 2-space
|
|
623
|
+
* indentation. Reads/writes the raw file, so comments or formatting of
|
|
624
|
+
* unrelated keys are preserved as parsed (the file is re-serialized).
|
|
625
|
+
*
|
|
626
|
+
* @param configPath - Absolute path to the config JSON file.
|
|
627
|
+
* @param path - Dotted path segments, e.g. `["indexing", "includeDirs"]`.
|
|
628
|
+
* @param value - Value to write at the target path.
|
|
629
|
+
* @returns `true` when the file was updated, `false` on any error.
|
|
630
|
+
*/
|
|
631
|
+
export function updateConfigValue(configPath, path, value) {
|
|
632
|
+
try {
|
|
633
|
+
let raw = readFileSync(configPath, "utf-8");
|
|
634
|
+
if (raw.charCodeAt(0) === 0xfeff)
|
|
635
|
+
raw = raw.slice(1);
|
|
636
|
+
const data = JSON.parse(raw);
|
|
637
|
+
let target = data;
|
|
638
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
639
|
+
const key = path[i];
|
|
640
|
+
if (!target[key] || typeof target[key] !== "object") {
|
|
641
|
+
target[key] = {};
|
|
642
|
+
}
|
|
643
|
+
target = target[key];
|
|
644
|
+
}
|
|
645
|
+
target[path[path.length - 1]] = value;
|
|
646
|
+
writeFileSync(configPath, JSON.stringify(data, null, 2), "utf-8");
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
620
653
|
//# sourceMappingURL=config.js.map
|
package/dist/core/exclude.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
export interface ExcludeMatcher {
|
|
2
2
|
excluded(relPath: string): boolean;
|
|
3
3
|
}
|
|
4
|
+
export interface IncludedMatcher {
|
|
5
|
+
included(relPath: string): boolean;
|
|
6
|
+
}
|
|
4
7
|
export declare function createExcludeMatcher(patterns: string[]): ExcludeMatcher;
|
|
8
|
+
/**
|
|
9
|
+
* Create a matcher for `indexing.includeDirs`.
|
|
10
|
+
*
|
|
11
|
+
* Semantics differ from {@link createExcludeMatcher}: every pattern is
|
|
12
|
+
* **anchored to the workspace root** — it is matched as a glob against each
|
|
13
|
+
* ancestor prefix of the path, so `docs` includes `<root>/docs` and all its
|
|
14
|
+
* contents, but never a nested `docs` elsewhere in the tree. The workspace
|
|
15
|
+
* root itself (`""`) is always included so a walk can descend into it.
|
|
16
|
+
* Empty pattern lists include everything.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createIncludeMatcher(patterns: string[]): IncludedMatcher;
|
package/dist/core/exclude.js
CHANGED
|
@@ -42,4 +42,41 @@ export function createExcludeMatcher(patterns) {
|
|
|
42
42
|
}
|
|
43
43
|
return { excluded };
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Create a matcher for `indexing.includeDirs`.
|
|
47
|
+
*
|
|
48
|
+
* Semantics differ from {@link createExcludeMatcher}: every pattern is
|
|
49
|
+
* **anchored to the workspace root** — it is matched as a glob against each
|
|
50
|
+
* ancestor prefix of the path, so `docs` includes `<root>/docs` and all its
|
|
51
|
+
* contents, but never a nested `docs` elsewhere in the tree. The workspace
|
|
52
|
+
* root itself (`""`) is always included so a walk can descend into it.
|
|
53
|
+
* Empty pattern lists include everything.
|
|
54
|
+
*/
|
|
55
|
+
export function createIncludeMatcher(patterns) {
|
|
56
|
+
const matchers = [];
|
|
57
|
+
for (const pattern of patterns) {
|
|
58
|
+
const pat = pattern.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
59
|
+
if (!pat)
|
|
60
|
+
continue;
|
|
61
|
+
matchers.push(new Minimatch(pat, { nocase: true, dot: true }));
|
|
62
|
+
}
|
|
63
|
+
if (matchers.length === 0) {
|
|
64
|
+
return { included: () => true };
|
|
65
|
+
}
|
|
66
|
+
function included(relPath) {
|
|
67
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
68
|
+
if (!normalized)
|
|
69
|
+
return true;
|
|
70
|
+
let prefix = "";
|
|
71
|
+
for (const seg of normalized.split("/")) {
|
|
72
|
+
prefix = prefix ? `${prefix}/${seg}` : seg;
|
|
73
|
+
for (const mm of matchers) {
|
|
74
|
+
if (mm.match(prefix) || mm.match(`${prefix}/`))
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return { included };
|
|
81
|
+
}
|
|
45
82
|
//# sourceMappingURL=exclude.js.map
|
package/dist/indexer/watch.d.ts
CHANGED
|
@@ -28,7 +28,9 @@ export declare function createWatchPassScheduler(runPass: (filterPaths?: string[
|
|
|
28
28
|
/**
|
|
29
29
|
* Build a predicate that returns `true` for paths that should be ignored by
|
|
30
30
|
* a file watcher (store directory, manifest file, and configured exclude
|
|
31
|
-
* directories).
|
|
31
|
+
* directories). When `indexing.includeDirs` is non-empty, any path outside
|
|
32
|
+
* the included folders is ignored as well, so out-of-scope changes never
|
|
33
|
+
* trigger re-index passes.
|
|
32
34
|
*
|
|
33
35
|
* @param cwd - Workspace root directory.
|
|
34
36
|
* @param config - RAG configuration containing `indexing.excludeDirs`.
|
package/dist/indexer/watch.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { manifestPathFor } from "../core/manifest.js";
|
|
6
|
-
import { createExcludeMatcher } from "../core/exclude.js";
|
|
6
|
+
import { createExcludeMatcher, createIncludeMatcher } from "../core/exclude.js";
|
|
7
7
|
/**
|
|
8
8
|
* Create a scheduler that debounces calls to a re-index pass. While a pass is
|
|
9
9
|
* running, subsequent notifications queue a single rerun. Useful for watching
|
|
@@ -108,7 +108,9 @@ export function createWatchPassScheduler(runPass, onError, debounceMs = 300) {
|
|
|
108
108
|
/**
|
|
109
109
|
* Build a predicate that returns `true` for paths that should be ignored by
|
|
110
110
|
* a file watcher (store directory, manifest file, and configured exclude
|
|
111
|
-
* directories).
|
|
111
|
+
* directories). When `indexing.includeDirs` is non-empty, any path outside
|
|
112
|
+
* the included folders is ignored as well, so out-of-scope changes never
|
|
113
|
+
* trigger re-index passes.
|
|
112
114
|
*
|
|
113
115
|
* @param cwd - Workspace root directory.
|
|
114
116
|
* @param config - RAG configuration containing `indexing.excludeDirs`.
|
|
@@ -120,6 +122,7 @@ export function createWatchIgnore(cwd, config, storePath) {
|
|
|
120
122
|
const manifestPath = manifestPathFor(storePath);
|
|
121
123
|
const dirMatcher = createExcludeMatcher(config.indexing.excludeDirs);
|
|
122
124
|
const fileMatcher = createExcludeMatcher(config.indexing.excludeFiles ?? []);
|
|
125
|
+
const includeMatcher = createIncludeMatcher(config.indexing.includeDirs ?? []);
|
|
123
126
|
// Prefix check with a trailing separator so sibling dirs like
|
|
124
127
|
// `<storePath>2` are NOT ignored; case-insensitive on win32 so a
|
|
125
128
|
// differently-cased store path cannot cause self-triggering watch loops.
|
|
@@ -136,7 +139,7 @@ export function createWatchIgnore(cwd, config, storePath) {
|
|
|
136
139
|
const relative = path.relative(cwd, resolved);
|
|
137
140
|
if (!relative || relative.startsWith(".."))
|
|
138
141
|
return false;
|
|
139
|
-
return dirMatcher.excluded(relative) || fileMatcher.excluded(relative);
|
|
142
|
+
return !includeMatcher.included(relative) || dirMatcher.excluded(relative) || fileMatcher.excluded(relative);
|
|
140
143
|
};
|
|
141
144
|
}
|
|
142
145
|
//# sourceMappingURL=watch.js.map
|
package/dist/tui.js
CHANGED
|
@@ -8,7 +8,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { loadRuntimeOverrides, saveRuntimeOverride } from "./core/runtime-overrides.js";
|
|
10
10
|
import { PROVIDER_DEFAULTS } from "./core/provider-defaults.js";
|
|
11
|
-
import { loadConfig } from "./core/config.js";
|
|
11
|
+
import { loadConfig, updateConfigValue } from "./core/config.js";
|
|
12
12
|
import { setPendingRagInjection } from "./core/rag-injection-flag.js";
|
|
13
13
|
/** Cached plugin version string from package.json. */
|
|
14
14
|
let _version;
|
|
@@ -275,26 +275,6 @@ function resolveProviderBaseUrl(provider) {
|
|
|
275
275
|
* Write a single config value at a dotted path into the opencode-rag.json file.
|
|
276
276
|
* Creates intermediate objects as needed.
|
|
277
277
|
*/
|
|
278
|
-
function saveConfigValue(configPath, path, value) {
|
|
279
|
-
try {
|
|
280
|
-
let raw = readFileSync(configPath, "utf-8");
|
|
281
|
-
if (raw.charCodeAt(0) === 0xfeff)
|
|
282
|
-
raw = raw.slice(1);
|
|
283
|
-
const data = JSON.parse(raw);
|
|
284
|
-
let target = data;
|
|
285
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
286
|
-
const key = path[i];
|
|
287
|
-
if (!target[key] || typeof target[key] !== "object") {
|
|
288
|
-
target[key] = {};
|
|
289
|
-
}
|
|
290
|
-
target = target[key];
|
|
291
|
-
}
|
|
292
|
-
target[path[path.length - 1]] = value;
|
|
293
|
-
writeFileSync(configPath, JSON.stringify(data, null, 2), "utf-8");
|
|
294
|
-
}
|
|
295
|
-
catch {
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
278
|
/**
|
|
299
279
|
* Mirror the watcher status file that the server plugin maintains, so the
|
|
300
280
|
* sidebar reflects a watcher toggle from the settings dialog immediately:
|
|
@@ -334,12 +314,12 @@ function saveModelSelection(storePath, configPath, selectionValue, path, provide
|
|
|
334
314
|
const ragProvider = providerIdToRagProvider(providerId);
|
|
335
315
|
const baseUrl = provider ? resolveProviderBaseUrl(provider) : "";
|
|
336
316
|
saveRuntimeOverride(storePath, [section, "provider"], ragProvider);
|
|
337
|
-
|
|
317
|
+
updateConfigValue(configPath, [section, "provider"], ragProvider);
|
|
338
318
|
saveRuntimeOverride(storePath, [section, "model"], modelId);
|
|
339
|
-
|
|
319
|
+
updateConfigValue(configPath, [section, "model"], modelId);
|
|
340
320
|
if (baseUrl) {
|
|
341
321
|
saveRuntimeOverride(storePath, [section, "baseUrl"], baseUrl);
|
|
342
|
-
|
|
322
|
+
updateConfigValue(configPath, [section, "baseUrl"], baseUrl);
|
|
343
323
|
}
|
|
344
324
|
const apiKey = provider?.options?.apiKey ?? "";
|
|
345
325
|
if (apiKey) {
|
|
@@ -704,7 +684,7 @@ async function openSettingsDialog(api) {
|
|
|
704
684
|
else if (entry.type === "boolean") {
|
|
705
685
|
const newVal = !entry.currentValue;
|
|
706
686
|
saveRuntimeOverride(storePath, entry.path, newVal);
|
|
707
|
-
|
|
687
|
+
updateConfigValue(configPath, entry.path, newVal);
|
|
708
688
|
api.ui.toast({
|
|
709
689
|
variant: "success",
|
|
710
690
|
title: "Settings",
|
|
@@ -730,7 +710,7 @@ async function openSettingsDialog(api) {
|
|
|
730
710
|
const num = parseFloat(input);
|
|
731
711
|
if (!isNaN(num)) {
|
|
732
712
|
saveRuntimeOverride(storePath, entry.path, num);
|
|
733
|
-
|
|
713
|
+
updateConfigValue(configPath, entry.path, num);
|
|
734
714
|
api.ui.toast({
|
|
735
715
|
variant: "success",
|
|
736
716
|
title: "Settings",
|
|
@@ -763,7 +743,7 @@ async function openSettingsDialog(api) {
|
|
|
763
743
|
return;
|
|
764
744
|
}
|
|
765
745
|
saveRuntimeOverride(storePath, entry.path, parsed);
|
|
766
|
-
|
|
746
|
+
updateConfigValue(configPath, entry.path, parsed);
|
|
767
747
|
api.ui.toast({
|
|
768
748
|
variant: "success",
|
|
769
749
|
title: "Settings",
|
|
@@ -792,7 +772,7 @@ async function openSettingsDialog(api) {
|
|
|
792
772
|
value: String(entry.currentValue),
|
|
793
773
|
onConfirm: (input) => {
|
|
794
774
|
saveRuntimeOverride(storePath, entry.path, input);
|
|
795
|
-
|
|
775
|
+
updateConfigValue(configPath, entry.path, input);
|
|
796
776
|
api.ui.toast({
|
|
797
777
|
variant: "success",
|
|
798
778
|
title: "Settings",
|
|
@@ -828,7 +808,7 @@ async function openSettingsDialog(api) {
|
|
|
828
808
|
}
|
|
829
809
|
else if (input) {
|
|
830
810
|
saveRuntimeOverride(storePath, entry.path, input);
|
|
831
|
-
|
|
811
|
+
updateConfigValue(configPath, entry.path, input);
|
|
832
812
|
entry.currentValue = input;
|
|
833
813
|
}
|
|
834
814
|
api.onSettingsChanged?.();
|
package/dist/web/api.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
5
5
|
import { LanceDbStore } from "../vectorstore/lancedb.js";
|
|
6
6
|
import { KeywordIndex } from "../retriever/keyword-index.js";
|
|
7
|
-
import type
|
|
7
|
+
import { type RagConfig } from "../core/config.js";
|
|
8
8
|
import { type EmbeddingProvider } from "../core/interfaces.js";
|
|
9
9
|
/** Internal shape for a JSON API response: an HTTP status code and a serialisable body. */
|
|
10
10
|
interface ApiResponse {
|
|
@@ -26,9 +26,12 @@ interface ApiResponse {
|
|
|
26
26
|
* @param storePath - Filesystem path to the store directory (used by eval endpoints).
|
|
27
27
|
* @param cwd - Optional workspace root for resolving file paths.
|
|
28
28
|
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
29
|
+
* @param getEmbedder - Lazily-created embedder for /api/retrieve and reindex passes.
|
|
30
|
+
* @param token - Per-run auth token required on every request.
|
|
31
|
+
* @param configPath - Path to the opencode-rag.json config file, used by PUT /api/config.
|
|
29
32
|
* @returns An async handler that returns `true` when a route matched or `false` otherwise.
|
|
30
33
|
*/
|
|
31
|
-
export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string, cfg?: RagConfig, getEmbedder?: () => Promise<EmbeddingProvider>, token?: string): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
34
|
+
export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string, cfg?: RagConfig, getEmbedder?: () => Promise<EmbeddingProvider>, token?: string, configPath?: string): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
32
35
|
/**
|
|
33
36
|
* Perform token-usage analysis for a single evaluation session.
|
|
34
37
|
*
|
package/dist/web/api.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { extname, join, resolve as resolvePathModule } from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { listSessions, getSession, deleteSession, compareSessions, validateSessionID } from "../eval/storage.js";
|
|
5
5
|
import { analyzeTokenUsage, compareTokenAnalyses, projectTokenSavings } from "../eval/token-analysis.js";
|
|
6
6
|
import { listQuirks, lintQuirks, removeQuirk } from "../quirks/quirk-store.js";
|
|
7
7
|
import { retrieve } from "../retriever/retriever.js";
|
|
8
|
+
import { updateConfigValue } from "../core/config.js";
|
|
9
|
+
import { createExcludeMatcher } from "../core/exclude.js";
|
|
8
10
|
import { CODE_SEARCH_FILTER } from "../core/interfaces.js";
|
|
9
11
|
const FILE_MIME_TYPES = {
|
|
10
12
|
".png": "image/png",
|
|
@@ -94,9 +96,16 @@ function sendJson(res, response, origin) {
|
|
|
94
96
|
* @param storePath - Filesystem path to the store directory (used by eval endpoints).
|
|
95
97
|
* @param cwd - Optional workspace root for resolving file paths.
|
|
96
98
|
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
99
|
+
* @param getEmbedder - Lazily-created embedder for /api/retrieve and reindex passes.
|
|
100
|
+
* @param token - Per-run auth token required on every request.
|
|
101
|
+
* @param configPath - Path to the opencode-rag.json config file, used by PUT /api/config.
|
|
97
102
|
* @returns An async handler that returns `true` when a route matched or `false` otherwise.
|
|
98
103
|
*/
|
|
99
|
-
export function createApiHandler(store, keywordIndex, storePath, cwd, cfg, getEmbedder, token) {
|
|
104
|
+
export function createApiHandler(store, keywordIndex, storePath, cwd, cfg, getEmbedder, token, configPath) {
|
|
105
|
+
// Mutable config reference so PUT /api/config can update the in-memory
|
|
106
|
+
// config used by subsequent requests (retrieve, reindex, quirk endpoints).
|
|
107
|
+
const configRef = { current: cfg };
|
|
108
|
+
const effectiveConfig = () => configRef.current ?? cfg ?? {};
|
|
100
109
|
return async (req, res) => {
|
|
101
110
|
const url = req.url ?? "/";
|
|
102
111
|
const method = req.method ?? "GET";
|
|
@@ -138,7 +147,7 @@ export function createApiHandler(store, keywordIndex, storePath, cwd, cfg, getEm
|
|
|
138
147
|
embedder: stubEmbedder,
|
|
139
148
|
store,
|
|
140
149
|
keywordIndex,
|
|
141
|
-
cfg:
|
|
150
|
+
cfg: effectiveConfig(),
|
|
142
151
|
storePath,
|
|
143
152
|
};
|
|
144
153
|
let response;
|
|
@@ -150,6 +159,9 @@ export function createApiHandler(store, keywordIndex, storePath, cwd, cfg, getEm
|
|
|
150
159
|
else if (path === "/api/files") {
|
|
151
160
|
response = await handleFiles(store);
|
|
152
161
|
}
|
|
162
|
+
else if (path === "/api/tree" && method === "GET") {
|
|
163
|
+
response = handleTree(cwd, effectiveConfig());
|
|
164
|
+
}
|
|
153
165
|
else if (path === "/api/chunks" && !path.includes("/api/chunks/")) {
|
|
154
166
|
response = await handleChunks(store, params);
|
|
155
167
|
}
|
|
@@ -164,16 +176,20 @@ export function createApiHandler(store, keywordIndex, storePath, cwd, cfg, getEm
|
|
|
164
176
|
response = await handleCompare(store, params);
|
|
165
177
|
}
|
|
166
178
|
else if (path === "/api/retrieve") {
|
|
167
|
-
response = await handleRetrieve(store, keywordIndex, getEmbedder,
|
|
179
|
+
response = await handleRetrieve(store, keywordIndex, getEmbedder, effectiveConfig(), params);
|
|
168
180
|
}
|
|
169
181
|
else if (path === "/api/indexing/status") {
|
|
170
182
|
response = await handleIndexingStatus(storePath, cwd);
|
|
171
183
|
}
|
|
172
184
|
else if (path === "/api/indexing/reindex" && method === "POST") {
|
|
173
|
-
response = await handleReindex(cwd,
|
|
185
|
+
response = await handleReindex(cwd, effectiveConfig(), storePath, store, getEmbedder);
|
|
186
|
+
}
|
|
187
|
+
else if (path === "/api/config" && method === "GET") {
|
|
188
|
+
response = await handleConfig(effectiveConfig());
|
|
174
189
|
}
|
|
175
|
-
else if (path === "/api/config") {
|
|
176
|
-
|
|
190
|
+
else if (path === "/api/config" && method === "PUT") {
|
|
191
|
+
const body = await readBody(req);
|
|
192
|
+
response = handleConfigUpdate(configPath, configRef, body);
|
|
177
193
|
}
|
|
178
194
|
else if (path === "/api/embeddings/projection") {
|
|
179
195
|
response = await handleEmbeddingProjection(store, params);
|
|
@@ -301,6 +317,47 @@ async function handleFiles(store) {
|
|
|
301
317
|
const files = await store.listFiles();
|
|
302
318
|
return { status: 200, body: files };
|
|
303
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Respond with the workspace **directory** tree (dirs only), used by the UI's
|
|
322
|
+
* indexing-scope folder selector. `indexing.excludeDirs` are pruned so the
|
|
323
|
+
* tree stays clean (node_modules, build output, ...); `includeDirs` are NOT
|
|
324
|
+
* applied — users must be able to see and select every selectable folder.
|
|
325
|
+
*/
|
|
326
|
+
function handleTree(cwd, cfg) {
|
|
327
|
+
if (!cwd) {
|
|
328
|
+
return { status: 400, body: { error: "Workspace path not configured" } };
|
|
329
|
+
}
|
|
330
|
+
const excludeMatcher = createExcludeMatcher(cfg.indexing.excludeDirs);
|
|
331
|
+
const MAX_DIRS = 10_000;
|
|
332
|
+
let visited = 0;
|
|
333
|
+
function walk(dir, rel) {
|
|
334
|
+
const children = [];
|
|
335
|
+
let entries;
|
|
336
|
+
try {
|
|
337
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return children;
|
|
341
|
+
}
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
if (!entry.isDirectory())
|
|
344
|
+
continue;
|
|
345
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
346
|
+
if (excludeMatcher.excluded(childRel))
|
|
347
|
+
continue;
|
|
348
|
+
if (++visited > MAX_DIRS)
|
|
349
|
+
continue;
|
|
350
|
+
children.push({
|
|
351
|
+
name: entry.name,
|
|
352
|
+
path: childRel,
|
|
353
|
+
children: walk(join(dir, entry.name), childRel),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
children.sort((a, b) => a.name.localeCompare(b.name));
|
|
357
|
+
return children;
|
|
358
|
+
}
|
|
359
|
+
return { status: 200, body: { tree: walk(cwd, "") } };
|
|
360
|
+
}
|
|
304
361
|
// ── Quirk Memory API ─────────────────────────────────────────────────
|
|
305
362
|
/** Respond with all stored quirks, sorted by last-observed time (most recent first). */
|
|
306
363
|
async function handleQuirks(deps) {
|
|
@@ -603,6 +660,68 @@ function redactKeys(obj) {
|
|
|
603
660
|
}
|
|
604
661
|
}
|
|
605
662
|
}
|
|
663
|
+
/**
|
|
664
|
+
* Config keys under `indexing` that the UI may write. Values must be arrays
|
|
665
|
+
* of strings. Add keys here when the UI learns to edit more settings.
|
|
666
|
+
*/
|
|
667
|
+
const INDEXING_STRING_ARRAY_KEYS = new Set([
|
|
668
|
+
"includeExtensions",
|
|
669
|
+
"excludeDirs",
|
|
670
|
+
"excludeFiles",
|
|
671
|
+
"includeDirs",
|
|
672
|
+
]);
|
|
673
|
+
/**
|
|
674
|
+
* Apply a validated config patch (`{ indexing: { includeDirs: [...] } }`)
|
|
675
|
+
* to the on-disk config file and refresh the in-memory config used by
|
|
676
|
+
* subsequent API requests. Only known sections/keys with validated types
|
|
677
|
+
* are accepted; anything else is rejected with a 400.
|
|
678
|
+
*/
|
|
679
|
+
function handleConfigUpdate(configPath, configRef, body) {
|
|
680
|
+
if (!configPath) {
|
|
681
|
+
return {
|
|
682
|
+
status: 400,
|
|
683
|
+
body: { error: "Config file path unavailable — the UI server was started without a config file" },
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
const patch = body;
|
|
687
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
|
|
688
|
+
return { status: 400, body: { error: "Invalid config patch" } };
|
|
689
|
+
}
|
|
690
|
+
const otherSections = Object.keys(patch).filter((k) => k !== "indexing");
|
|
691
|
+
if (otherSections.length > 0) {
|
|
692
|
+
return { status: 400, body: { error: `Unsupported config section(s): ${otherSections.join(", ")}` } };
|
|
693
|
+
}
|
|
694
|
+
const indexingPatch = patch.indexing;
|
|
695
|
+
if (indexingPatch !== undefined) {
|
|
696
|
+
if (typeof indexingPatch !== "object" || indexingPatch === null || Array.isArray(indexingPatch)) {
|
|
697
|
+
return { status: 400, body: { error: "Invalid 'indexing' section" } };
|
|
698
|
+
}
|
|
699
|
+
for (const [key, value] of Object.entries(indexingPatch)) {
|
|
700
|
+
if (!INDEXING_STRING_ARRAY_KEYS.has(key)) {
|
|
701
|
+
return { status: 400, body: { error: `Unsupported indexing key '${key}'` } };
|
|
702
|
+
}
|
|
703
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
|
|
704
|
+
return { status: 400, body: { error: `'indexing.${key}' must be an array of strings` } };
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (indexingPatch) {
|
|
709
|
+
for (const [key, value] of Object.entries(indexingPatch)) {
|
|
710
|
+
const ok = updateConfigValue(configPath, ["indexing", key], value);
|
|
711
|
+
if (!ok) {
|
|
712
|
+
return { status: 500, body: { error: `Failed to write 'indexing.${key}' to ${configPath}` } };
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const current = configRef.current ?? {};
|
|
716
|
+
configRef.current = {
|
|
717
|
+
...current,
|
|
718
|
+
indexing: { ...current.indexing, ...indexingPatch },
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const redacted = JSON.parse(JSON.stringify(configRef.current));
|
|
722
|
+
redactKeys(redacted);
|
|
723
|
+
return { status: 200, body: { config: redacted } };
|
|
724
|
+
}
|
|
606
725
|
/**
|
|
607
726
|
* Project chunk embeddings to 2D/3D via PCA for the Embedding Space Explorer.
|
|
608
727
|
* Capped at 5000 chunks and memoized per (maxChunks, dims) so the
|
package/dist/web/server.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface WebUiServer {
|
|
|
18
18
|
* @param cwd - Optional workspace root used to resolve file paths for the file API.
|
|
19
19
|
* @param vectorDimension - Embedding vector dimension (default 384).
|
|
20
20
|
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
21
|
+
* @param configPath - Path to the opencode-rag.json config file (used by PUT /api/config).
|
|
21
22
|
* @returns A {@link WebUiServer} handle for the running server.
|
|
22
23
|
*/
|
|
23
|
-
export declare function startWebUi(storePath: string, port: number, cwd?: string, vectorDimension?: number, cfg?: import("../core/config.js").RagConfig): Promise<WebUiServer>;
|
|
24
|
+
export declare function startWebUi(storePath: string, port: number, cwd?: string, vectorDimension?: number, cfg?: import("../core/config.js").RagConfig, configPath?: string): Promise<WebUiServer>;
|
package/dist/web/server.js
CHANGED
|
@@ -66,9 +66,10 @@ function serveUiAsset(res, filePath) {
|
|
|
66
66
|
* @param cwd - Optional workspace root used to resolve file paths for the file API.
|
|
67
67
|
* @param vectorDimension - Embedding vector dimension (default 384).
|
|
68
68
|
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
69
|
+
* @param configPath - Path to the opencode-rag.json config file (used by PUT /api/config).
|
|
69
70
|
* @returns A {@link WebUiServer} handle for the running server.
|
|
70
71
|
*/
|
|
71
|
-
export async function startWebUi(storePath, port, cwd, vectorDimension = 384, cfg) {
|
|
72
|
+
export async function startWebUi(storePath, port, cwd, vectorDimension = 384, cfg, configPath) {
|
|
72
73
|
const store = new LanceDbStore(storePath, vectorDimension);
|
|
73
74
|
const keywordIndex = await KeywordIndex.load(storePath);
|
|
74
75
|
// Lazy embedder for /api/retrieve — initialized on first use
|
|
@@ -82,7 +83,7 @@ export async function startWebUi(storePath, port, cwd, vectorDimension = 384, cf
|
|
|
82
83
|
}
|
|
83
84
|
const html = getStaticHtml();
|
|
84
85
|
const token = randomBytes(24).toString("hex");
|
|
85
|
-
const apiHandler = createApiHandler(store, keywordIndex, storePath, cwd, cfg, getEmbedder, token);
|
|
86
|
+
const apiHandler = createApiHandler(store, keywordIndex, storePath, cwd, cfg, getEmbedder, token, configPath);
|
|
86
87
|
const server = createServer(async (req, res) => {
|
|
87
88
|
try {
|
|
88
89
|
// Strip the query string before routing — the auth token arrives as
|