opencode-rag-plugin 1.19.4 → 1.19.5
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/chunker/base.js +19 -5
- package/dist/chunker/factory.js +27 -9
- package/dist/chunker/grammar.d.ts +18 -1
- package/dist/chunker/grammar.js +48 -10
- package/dist/chunker/pdf.js +30 -14
- package/dist/cli/commands/init-helpers.js +15 -2
- package/dist/cli/commands/init.js +31 -21
- package/dist/cli/commands/quirk.js +9 -3
- package/dist/cli/commands/setup.js +5 -2
- package/dist/cli/commands/status.js +14 -5
- package/dist/cli/commands/ui.js +23 -9
- package/dist/cli/commands/update.js +4 -5
- package/dist/cli/format.d.ts +5 -2
- package/dist/cli/format.js +14 -5
- package/dist/content/image.js +33 -11
- package/dist/content/reader.js +74 -13
- package/dist/core/bootstrap.js +10 -3
- package/dist/core/config.js +31 -0
- package/dist/core/desc-cache.d.ts +8 -2
- package/dist/core/desc-cache.js +10 -3
- package/dist/core/doc-progress.js +5 -2
- package/dist/core/provider-defaults.d.ts +2 -0
- package/dist/core/provider-defaults.js +19 -4
- package/dist/core/runtime-overrides.d.ts +0 -6
- package/dist/core/version-check.d.ts +5 -0
- package/dist/core/version-check.js +8 -2
- package/dist/describer/anthropic.d.ts +2 -2
- package/dist/describer/anthropic.js +19 -5
- package/dist/describer/describer.js +15 -2
- package/dist/describer/gemini.js +25 -10
- package/dist/embedder/factory.d.ts +5 -3
- package/dist/embedder/factory.js +41 -8
- package/dist/embedder/health.js +19 -19
- package/dist/embedder/http.d.ts +14 -1
- package/dist/embedder/http.js +60 -6
- package/dist/eval/session-logger.js +7 -0
- package/dist/eval/storage.js +8 -0
- package/dist/indexer/git-diff.d.ts +1 -1
- package/dist/indexer/git-diff.js +5 -1
- package/dist/indexer/pipeline.js +421 -344
- package/dist/indexer/stats.d.ts +2 -0
- package/dist/indexer/stats.js +1 -0
- package/dist/indexer/watch.js +8 -1
- package/dist/indexer/worker.js +21 -0
- package/dist/mcp/cli.js +4 -0
- package/dist/mcp/handlers.js +16 -5
- package/dist/mcp/server.js +3 -0
- package/dist/opencode/create-read-tool.js +14 -3
- package/dist/opencode/tool-args.js +23 -1
- package/dist/plugin.js +66 -152
- package/dist/quirks/auto-capture.js +5 -0
- package/dist/quirks/quirk-store.d.ts +1 -1
- package/dist/quirks/quirk-store.js +56 -17
- package/dist/retriever/context-optimizer.js +18 -4
- package/dist/retriever/keyword-index.d.ts +2 -0
- package/dist/retriever/keyword-index.js +38 -4
- package/dist/retriever/retriever.js +6 -1
- package/dist/tui.js +41 -4
- package/dist/vectorstore/lancedb.d.ts +25 -1
- package/dist/vectorstore/lancedb.js +157 -11
- package/dist/vectorstore/memory.js +5 -1
- package/dist/watcher.js +30 -4
- package/dist/web/api.d.ts +5 -1
- package/dist/web/api.js +195 -69
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +66 -28
- package/dist/web/static.d.ts +5 -2
- package/dist/web/static.js +9 -5
- package/dist/web/ui/assets/index-BDPYdtA1.js +3 -0
- package/dist/web/ui/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/ui/assets/index-CJBvt6e0.js +0 -3
package/dist/chunker/base.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @fileoverview Abstract base class for tree-sitter based language chunkers.
|
|
3
3
|
*/
|
|
4
4
|
import { Parser } from "web-tree-sitter";
|
|
5
|
-
import { loadLanguage, loadLanguageFromPath, walkTree } from "./grammar.js";
|
|
5
|
+
import { loadLanguage, loadLanguageFromPath, walkTree, buildByteOffsetMap } from "./grammar.js";
|
|
6
6
|
import { uuid } from "./uuid.js";
|
|
7
7
|
/** Module-level parser pool shared across all TreeSitterChunker instances. */
|
|
8
8
|
const parserPool = new Map();
|
|
@@ -45,8 +45,15 @@ export class TreeSitterChunker {
|
|
|
45
45
|
if (!tree) {
|
|
46
46
|
return [];
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// tree-sitter ranges are UTF-8 byte offsets; translate for slicing
|
|
49
|
+
const offsetMap = buildByteOffsetMap(content);
|
|
50
|
+
let nodes;
|
|
51
|
+
try {
|
|
52
|
+
nodes = walkTree(tree.rootNode, types, content, 10, 0, offsetMap);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
tree.delete();
|
|
56
|
+
}
|
|
50
57
|
return nodes.map((node) => ({
|
|
51
58
|
id: uuid(),
|
|
52
59
|
content: node.text,
|
|
@@ -108,8 +115,15 @@ export class TreeSitterChunker {
|
|
|
108
115
|
if (!tree) {
|
|
109
116
|
return [];
|
|
110
117
|
}
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
// tree-sitter ranges are UTF-8 byte offsets; translate for slicing
|
|
119
|
+
const offsetMap = buildByteOffsetMap(content);
|
|
120
|
+
let nodes;
|
|
121
|
+
try {
|
|
122
|
+
nodes = walkTree(tree.rootNode, this.nodeTypes, content, 10, 0, offsetMap);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
tree.delete();
|
|
126
|
+
}
|
|
113
127
|
return nodes.map((node) => ({
|
|
114
128
|
id: uuid(),
|
|
115
129
|
content: node.text,
|
package/dist/chunker/factory.js
CHANGED
|
@@ -174,14 +174,26 @@ function splitOversized(chunks, filePath) {
|
|
|
174
174
|
}
|
|
175
175
|
return result;
|
|
176
176
|
}
|
|
177
|
-
/**
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Wrap a chunker with a per-file byte limit.
|
|
179
|
+
*
|
|
180
|
+
* Previously the limit was assigned onto the shared chunker singleton
|
|
181
|
+
* (`chunker.maxContentBytes = ...`) — under pLimit concurrency one file's
|
|
182
|
+
* limit could bleed into unrelated concurrent files, and it was never
|
|
183
|
+
* reset, so a later large XML/SVG file was silently skipped.
|
|
184
|
+
*/
|
|
185
|
+
function withByteLimit(chunker, limit) {
|
|
186
|
+
const language = chunker.language;
|
|
187
|
+
return {
|
|
188
|
+
language,
|
|
189
|
+
fileExtensions: chunker.fileExtensions,
|
|
190
|
+
async chunk(filePath, content) {
|
|
191
|
+
if (limit > 0 && Buffer.byteLength(content, "utf-8") > limit) {
|
|
192
|
+
throw new Error(`File exceeds ${limit} byte limit for ${language} chunker`);
|
|
193
|
+
}
|
|
194
|
+
return chunker.chunk(filePath, content);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
185
197
|
}
|
|
186
198
|
/**
|
|
187
199
|
* Chunk a file by looking up its registered chunker, applying node-type
|
|
@@ -203,7 +215,13 @@ export async function chunkFile(filePath, content, nodeTypesOverrides, options)
|
|
|
203
215
|
chunker = chunker.withNodeTypes(new Set(overrideTypes));
|
|
204
216
|
}
|
|
205
217
|
}
|
|
206
|
-
|
|
218
|
+
// Per-file byte limit (SVG/XML/csproj) without mutating the shared singleton
|
|
219
|
+
if (options?.maxSvgSizeBytes && chunker instanceof TreeSitterChunker) {
|
|
220
|
+
const ext = filePath.toLowerCase();
|
|
221
|
+
if (ext.endsWith(".svg") || ext.endsWith(".xml") || ext.endsWith(".csproj")) {
|
|
222
|
+
chunker = withByteLimit(chunker, options.maxSvgSizeBytes);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
207
225
|
const chunks = await chunker.chunk(filePath, content);
|
|
208
226
|
if (chunks.length === 0) {
|
|
209
227
|
return fallbackChunker.chunk(filePath, content);
|
|
@@ -57,6 +57,23 @@ export interface AstNode {
|
|
|
57
57
|
*/
|
|
58
58
|
leadingDoc?: string;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Build a byte-offset → UTF-16-index map for a source string.
|
|
62
|
+
*
|
|
63
|
+
* tree-sitter reports node ranges as UTF-8 BYTE offsets, but JavaScript
|
|
64
|
+
* `String.prototype.slice` counts UTF-16 code units. For any file containing
|
|
65
|
+
* multi-byte characters the two disagree, so slicing with raw node indices
|
|
66
|
+
* truncates/garbles chunk content and doc comments. Tree-sitter node
|
|
67
|
+
* boundaries always align to code-point boundaries, so an exact map from
|
|
68
|
+
* byte offset to UTF-16 index is sufficient.
|
|
69
|
+
*
|
|
70
|
+
* Returns `null` for pure-ASCII sources (byte offsets == UTF-16 offsets),
|
|
71
|
+
* which is the common case and avoids the allocation.
|
|
72
|
+
*
|
|
73
|
+
* @param source - The full source text.
|
|
74
|
+
* @returns A Map from UTF-8 byte offset to UTF-16 index, or `null` for ASCII.
|
|
75
|
+
*/
|
|
76
|
+
export declare function buildByteOffsetMap(source: string): Map<number, number> | null;
|
|
60
77
|
/**
|
|
61
78
|
* Recursively walk a tree-sitter AST and collect nodes matching the given
|
|
62
79
|
* type set.
|
|
@@ -75,4 +92,4 @@ export interface AstNode {
|
|
|
75
92
|
* @param depth - Current recursion depth (internal, start at 0).
|
|
76
93
|
* @returns An array of {@link AstNode} objects for matching declarations.
|
|
77
94
|
*/
|
|
78
|
-
export declare function walkTree(node: Node, nodeTypes: Set<string>, source: string, maxDepth?: number, depth?: number): AstNode[];
|
|
95
|
+
export declare function walkTree(node: Node, nodeTypes: Set<string>, source: string, maxDepth?: number, depth?: number, offsetMap?: Map<number, number> | null): AstNode[];
|
package/dist/chunker/grammar.js
CHANGED
|
@@ -120,6 +120,44 @@ const COMMENT_NODE_TYPES = new Set([
|
|
|
120
120
|
"marginalia",
|
|
121
121
|
"Comment",
|
|
122
122
|
]);
|
|
123
|
+
/**
|
|
124
|
+
* Build a byte-offset → UTF-16-index map for a source string.
|
|
125
|
+
*
|
|
126
|
+
* tree-sitter reports node ranges as UTF-8 BYTE offsets, but JavaScript
|
|
127
|
+
* `String.prototype.slice` counts UTF-16 code units. For any file containing
|
|
128
|
+
* multi-byte characters the two disagree, so slicing with raw node indices
|
|
129
|
+
* truncates/garbles chunk content and doc comments. Tree-sitter node
|
|
130
|
+
* boundaries always align to code-point boundaries, so an exact map from
|
|
131
|
+
* byte offset to UTF-16 index is sufficient.
|
|
132
|
+
*
|
|
133
|
+
* Returns `null` for pure-ASCII sources (byte offsets == UTF-16 offsets),
|
|
134
|
+
* which is the common case and avoids the allocation.
|
|
135
|
+
*
|
|
136
|
+
* @param source - The full source text.
|
|
137
|
+
* @returns A Map from UTF-8 byte offset to UTF-16 index, or `null` for ASCII.
|
|
138
|
+
*/
|
|
139
|
+
export function buildByteOffsetMap(source) {
|
|
140
|
+
if (!/[\u0080-\uffff]/u.test(source))
|
|
141
|
+
return null;
|
|
142
|
+
const map = new Map();
|
|
143
|
+
let byteOffset = 0;
|
|
144
|
+
let utf16Index = 0;
|
|
145
|
+
for (const ch of source) {
|
|
146
|
+
map.set(byteOffset, utf16Index);
|
|
147
|
+
byteOffset += Buffer.byteLength(ch, "utf-8");
|
|
148
|
+
utf16Index += ch.length;
|
|
149
|
+
}
|
|
150
|
+
map.set(byteOffset, utf16Index);
|
|
151
|
+
return map;
|
|
152
|
+
}
|
|
153
|
+
/** Slice `source` by tree-sitter byte offsets, translating through the map when present. */
|
|
154
|
+
function sliceByBytes(source, startByte, endByte, offsetMap) {
|
|
155
|
+
if (!offsetMap)
|
|
156
|
+
return source.slice(startByte, endByte);
|
|
157
|
+
const start = offsetMap.get(startByte) ?? startByte;
|
|
158
|
+
const end = offsetMap.get(endByte) ?? endByte;
|
|
159
|
+
return source.slice(start, end);
|
|
160
|
+
}
|
|
123
161
|
function cleanCommentText(text) {
|
|
124
162
|
const lines = text.split("\n");
|
|
125
163
|
if (text.startsWith("/*")) {
|
|
@@ -152,19 +190,19 @@ function cleanCommentText(text) {
|
|
|
152
190
|
let cleaned = lines.map((line) => line.replace(prefix, ""));
|
|
153
191
|
return cleaned.filter((l) => l.trim().length > 0).join("\n");
|
|
154
192
|
}
|
|
155
|
-
function extractLeadingComments(node, source) {
|
|
193
|
+
function extractLeadingComments(node, source, offsetMap) {
|
|
156
194
|
const comments = [];
|
|
157
195
|
let sibling = node.previousSibling;
|
|
158
196
|
while (sibling) {
|
|
159
197
|
if (COMMENT_NODE_TYPES.has(sibling.type)) {
|
|
160
|
-
const raw = source
|
|
198
|
+
const raw = sliceByBytes(source, sibling.startIndex, sibling.endIndex, offsetMap);
|
|
161
199
|
comments.unshift(cleanCommentText(raw));
|
|
162
200
|
sibling = sibling.previousSibling;
|
|
163
201
|
}
|
|
164
202
|
else if (sibling.type === "expression_statement") {
|
|
165
203
|
const firstChild = sibling.namedChildren[0];
|
|
166
204
|
if (firstChild?.type === "string") {
|
|
167
|
-
const raw = source
|
|
205
|
+
const raw = sliceByBytes(source, firstChild.startIndex, firstChild.endIndex, offsetMap);
|
|
168
206
|
comments.unshift(cleanCommentText(raw));
|
|
169
207
|
sibling = sibling.previousSibling;
|
|
170
208
|
}
|
|
@@ -180,7 +218,7 @@ function extractLeadingComments(node, source) {
|
|
|
180
218
|
return undefined;
|
|
181
219
|
return comments.join("\n\n");
|
|
182
220
|
}
|
|
183
|
-
function extractDocstringFromBody(node, source) {
|
|
221
|
+
function extractDocstringFromBody(node, source, offsetMap) {
|
|
184
222
|
if (node.type !== "function_definition" && node.type !== "class_definition") {
|
|
185
223
|
return undefined;
|
|
186
224
|
}
|
|
@@ -193,7 +231,7 @@ function extractDocstringFromBody(node, source) {
|
|
|
193
231
|
const stringNode = firstStmt.namedChildren[0];
|
|
194
232
|
if (!stringNode || stringNode.type !== "string")
|
|
195
233
|
return undefined;
|
|
196
|
-
return cleanCommentText(source
|
|
234
|
+
return cleanCommentText(sliceByBytes(source, stringNode.startIndex, stringNode.endIndex, offsetMap));
|
|
197
235
|
}
|
|
198
236
|
/**
|
|
199
237
|
* Recursively walk a tree-sitter AST and collect nodes matching the given
|
|
@@ -213,16 +251,16 @@ function extractDocstringFromBody(node, source) {
|
|
|
213
251
|
* @param depth - Current recursion depth (internal, start at 0).
|
|
214
252
|
* @returns An array of {@link AstNode} objects for matching declarations.
|
|
215
253
|
*/
|
|
216
|
-
export function walkTree(node, nodeTypes, source, maxDepth = 10, depth = 0) {
|
|
254
|
+
export function walkTree(node, nodeTypes, source, maxDepth = 10, depth = 0, offsetMap) {
|
|
217
255
|
const results = [];
|
|
218
256
|
if (nodeTypes.has(node.type) && depth > 0) {
|
|
219
|
-
const leadingComments = extractLeadingComments(node, source);
|
|
220
|
-
const bodyDocstring = extractDocstringFromBody(node, source);
|
|
257
|
+
const leadingComments = extractLeadingComments(node, source, offsetMap);
|
|
258
|
+
const bodyDocstring = extractDocstringFromBody(node, source, offsetMap);
|
|
221
259
|
const leadingDoc = [leadingComments, bodyDocstring]
|
|
222
260
|
.filter((d) => d !== undefined && d.length > 0)
|
|
223
261
|
.join("\n\n");
|
|
224
262
|
results.push({
|
|
225
|
-
text: source
|
|
263
|
+
text: sliceByBytes(source, node.startIndex, node.endIndex, offsetMap),
|
|
226
264
|
startLine: node.startPosition.row + 1,
|
|
227
265
|
endLine: node.endPosition.row + 1,
|
|
228
266
|
startIndex: node.startIndex,
|
|
@@ -234,7 +272,7 @@ export function walkTree(node, nodeTypes, source, maxDepth = 10, depth = 0) {
|
|
|
234
272
|
}
|
|
235
273
|
if (depth < maxDepth) {
|
|
236
274
|
for (const child of node.children) {
|
|
237
|
-
results.push(...walkTree(child, nodeTypes, source, maxDepth, depth + 1));
|
|
275
|
+
results.push(...walkTree(child, nodeTypes, source, maxDepth, depth + 1, offsetMap));
|
|
238
276
|
}
|
|
239
277
|
}
|
|
240
278
|
return results;
|
package/dist/chunker/pdf.js
CHANGED
|
@@ -22,19 +22,18 @@ function getStandardFontsUrl() {
|
|
|
22
22
|
* Create a pdfjs-dist PDF document from a buffer.
|
|
23
23
|
* Uses `@thednp/dommatrix` for DOMMatrix polyfill (lighter alternative to native canvas).
|
|
24
24
|
* @param buffer - Raw buffer of the PDF file.
|
|
25
|
-
* @returns
|
|
25
|
+
* @returns The pdfjs LoadingTask for the document.
|
|
26
26
|
*/
|
|
27
|
-
async function
|
|
27
|
+
async function getPdfLoadingTask(buffer) {
|
|
28
28
|
const { default: CSSMatrix } = await import("@thednp/dommatrix");
|
|
29
29
|
globalThis.DOMMatrix ??= CSSMatrix;
|
|
30
30
|
globalThis.DOMMatrixReadOnly ??= CSSMatrix;
|
|
31
31
|
const { getDocument } = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
32
|
-
|
|
32
|
+
return getDocument({
|
|
33
33
|
data: new Uint8Array(buffer),
|
|
34
34
|
standardFontDataUrl: getStandardFontsUrl(),
|
|
35
35
|
verbosity: 0,
|
|
36
36
|
});
|
|
37
|
-
return loadingTask.promise;
|
|
38
37
|
}
|
|
39
38
|
/**
|
|
40
39
|
* Extract the full text content from a PDF file buffer.
|
|
@@ -47,17 +46,34 @@ export async function extractPdfText(buffer) {
|
|
|
47
46
|
if (buffer.length > MAX_PDF_SIZE_BYTES) {
|
|
48
47
|
throw new Error(`PDF too large: ${(buffer.length / 1024 / 1024).toFixed(1)} MB (max 100 MB)`);
|
|
49
48
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
// loadingTask.destroy() releases the pdfjs worker/document resources —
|
|
50
|
+
// without it long-running watch/plugin processes leak them per PDF.
|
|
51
|
+
const loadingTask = await getPdfLoadingTask(buffer);
|
|
52
|
+
try {
|
|
53
|
+
const pdf = await loadingTask.promise;
|
|
54
|
+
const texts = [];
|
|
55
|
+
const pageCount = Math.min(pdf.numPages, MAX_PDF_PAGES);
|
|
56
|
+
for (let i = 1; i <= pageCount; i++) {
|
|
57
|
+
const page = await pdf.getPage(i);
|
|
58
|
+
try {
|
|
59
|
+
const content = await page.getTextContent();
|
|
60
|
+
const strings = [];
|
|
61
|
+
for (const item of content.items) {
|
|
62
|
+
if (typeof item === "object" && item !== null && "str" in item) {
|
|
63
|
+
strings.push(item.str);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
texts.push(strings.join(" "));
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
page.cleanup();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return texts.join("\n\n");
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await loadingTask.destroy().catch(() => { });
|
|
59
76
|
}
|
|
60
|
-
return texts.join("\n\n");
|
|
61
77
|
}
|
|
62
78
|
/**
|
|
63
79
|
* Chunker for PDF documents (.pdf).
|
|
@@ -379,10 +379,23 @@ export async function installPluginFromGlobal(opencodeDir, packageName, skipInst
|
|
|
379
379
|
if (existsSync(workspaceTarget)) {
|
|
380
380
|
rmSync(workspaceTarget, { recursive: true, force: true });
|
|
381
381
|
}
|
|
382
|
-
// Create directory junction from workspace → global runtime
|
|
382
|
+
// Create directory junction from workspace → global runtime.
|
|
383
|
+
// Junction creation can throw (EPERM, cross-drive on Windows) — fall back
|
|
384
|
+
// to a copy instead of aborting `init`.
|
|
383
385
|
console.log(` ${c.created("Linking:")} ${packageName} from global cache...`);
|
|
384
386
|
mkdirSync(path.dirname(workspaceTarget), { recursive: true });
|
|
385
|
-
|
|
387
|
+
try {
|
|
388
|
+
createJunction(globalPluginDir, workspaceTarget);
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
console.log(` ${c.warn("Junction failed, falling back to copy...")}`);
|
|
392
|
+
rmSync(workspaceTarget, { recursive: true, force: true });
|
|
393
|
+
const { cpSync } = await import("node:fs");
|
|
394
|
+
mkdirSync(path.dirname(workspaceTarget), { recursive: true });
|
|
395
|
+
cpSync(globalPluginDir, workspaceTarget, { recursive: true });
|
|
396
|
+
console.log(` ${c.success("Copied:")} ${packageName} from global cache`);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
386
399
|
const cliEntry = path.join(workspaceTarget, "dist", "cli.js");
|
|
387
400
|
if (!existsSync(cliEntry)) {
|
|
388
401
|
// Junction may not work (e.g. cross-drive on Windows) — fall back to copy
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
|
|
14
|
-
import { loadConfig } from "../../core/config.js";
|
|
14
|
+
import { loadConfig, findConfigFile } from "../../core/config.js";
|
|
15
15
|
import { checkProviderHealth, pullOllamaModels } from "../../embedder/health.js";
|
|
16
16
|
import { destroyAllPooledConnections } from "../../embedder/http.js";
|
|
17
17
|
import { c } from "../format.js";
|
|
@@ -37,7 +37,10 @@ export function registerInitCommand(program) {
|
|
|
37
37
|
try {
|
|
38
38
|
const cwd = process.cwd();
|
|
39
39
|
const packageMetadata = getPackageMetadata();
|
|
40
|
-
|
|
40
|
+
// Use findConfigFile so an existing config in .opencode/rag.json (or
|
|
41
|
+
// .opencode/opencode-rag.json) is respected instead of being shadowed
|
|
42
|
+
// by a new root-level opencode-rag.json.
|
|
43
|
+
const configPath = findConfigFile(cwd) ?? path.join(cwd, "opencode-rag.json");
|
|
41
44
|
const opencodeDir = path.join(cwd, ".opencode");
|
|
42
45
|
const gitignorePath = path.join(opencodeDir, ".gitignore");
|
|
43
46
|
const opencodeConfigPath = path.join(opencodeDir, "opencode.json");
|
|
@@ -253,27 +256,34 @@ export function registerInitCommand(program) {
|
|
|
253
256
|
return { model: r.model, baseUrl: ragConfig.embedding.baseUrl, proxy: ragConfig.embedding.proxy };
|
|
254
257
|
});
|
|
255
258
|
console.log(`\n ${c.warn("Models not found:")} ${pullEntries.map((e) => e.model).join(", ")}`);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
});
|
|
261
|
-
rl.close();
|
|
262
|
-
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
263
|
-
console.log();
|
|
264
|
-
try {
|
|
265
|
-
await pullOllamaModels(pullEntries, (model, line) => {
|
|
266
|
-
console.log(` ${c.value(model)}: ${line}`);
|
|
267
|
-
});
|
|
268
|
-
console.log(`\n ${c.success("Models pulled successfully.")}`);
|
|
269
|
-
}
|
|
270
|
-
catch (err) {
|
|
271
|
-
console.error(`\n ${c.error("Pull failed:")} ${err.message}`);
|
|
272
|
-
console.log(` ${c.dim("Pull manually with: ollama pull <model>")}`);
|
|
273
|
-
}
|
|
259
|
+
// Non-TTY guard: rl.question would block forever with a still-open
|
|
260
|
+
// stdin pipe (e.g. `echo y | opencode-rag init`, CI runners).
|
|
261
|
+
if (!process.stdin.isTTY) {
|
|
262
|
+
console.log(` ${c.dim("Non-interactive shell — skipping. Pull manually with: ollama pull <model>")}`);
|
|
274
263
|
}
|
|
275
264
|
else {
|
|
276
|
-
|
|
265
|
+
const readline = await import("node:readline");
|
|
266
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
267
|
+
const answer = await new Promise((resolve) => {
|
|
268
|
+
rl.question(` Pull ${pullEntries.length === 1 ? "this model" : "these models"} now? (y/n) `, resolve);
|
|
269
|
+
});
|
|
270
|
+
rl.close();
|
|
271
|
+
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
272
|
+
console.log();
|
|
273
|
+
try {
|
|
274
|
+
await pullOllamaModels(pullEntries, (model, line) => {
|
|
275
|
+
console.log(` ${c.value(model)}: ${line}`);
|
|
276
|
+
});
|
|
277
|
+
console.log(`\n ${c.success("Models pulled successfully.")}`);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
console.error(`\n ${c.error("Pull failed:")} ${err.message}`);
|
|
281
|
+
console.log(` ${c.dim("Pull manually with: ollama pull <model>")}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
console.log(` ${c.dim("Skipped. Pull manually with: ollama pull <model>")}`);
|
|
286
|
+
}
|
|
277
287
|
}
|
|
278
288
|
}
|
|
279
289
|
const hasErrors = results.some((r) => r.status === "error");
|
|
@@ -16,6 +16,7 @@ export function registerQuirkCommand(program) {
|
|
|
16
16
|
.option("-t, --type <type>", "quirk type: gotcha, preference, decision, environment-constraint")
|
|
17
17
|
.option("--tag <tags...>", "tags for filtering")
|
|
18
18
|
.option("--source-ref <path>", "source file path reference")
|
|
19
|
+
.option("-c, --config <path>", "path to config file")
|
|
19
20
|
.action(async (content, options) => {
|
|
20
21
|
try {
|
|
21
22
|
const ctx = await resolveCliContext(options, resolveLogPath());
|
|
@@ -49,6 +50,11 @@ export function registerQuirkCommand(program) {
|
|
|
49
50
|
.option("-c, --config <path>", "path to config file")
|
|
50
51
|
.action(async (id, options) => {
|
|
51
52
|
try {
|
|
53
|
+
const confidence = options.confidence;
|
|
54
|
+
if (confidence !== undefined && (Number.isNaN(confidence) || confidence < 0 || confidence > 1)) {
|
|
55
|
+
logCliError(resolveLogPath(), "quirk update", "Failed to update quirk: --confidence must be a number between 0 and 1");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
52
58
|
const ctx = await resolveCliContext(options, resolveLogPath());
|
|
53
59
|
const { config, embedder, store, keywordIndex } = ctx;
|
|
54
60
|
const tags = options.tag;
|
|
@@ -56,7 +62,7 @@ export function registerQuirkCommand(program) {
|
|
|
56
62
|
content: options.content,
|
|
57
63
|
quirkType: options.type,
|
|
58
64
|
tags: tags ? (Array.isArray(tags) ? tags : [tags]) : undefined,
|
|
59
|
-
confidence
|
|
65
|
+
confidence,
|
|
60
66
|
sourceRef: options.sourceRef,
|
|
61
67
|
});
|
|
62
68
|
logCliInfo(ctx.logFilePath, "quirk update", `\n${c.success("Quirk updated:")}`);
|
|
@@ -148,7 +154,7 @@ export function registerQuirkCommand(program) {
|
|
|
148
154
|
const { config, embedder, store, keywordIndex } = ctx;
|
|
149
155
|
const results = await recallQuirks({ embedder, store, keywordIndex: keywordIndex, cfg: config, storePath: ctx.storePath }, content, { topK: 5 });
|
|
150
156
|
if (results.length > 0) {
|
|
151
|
-
logCliInfo(ctx.logFilePath, "quirk test", c.
|
|
157
|
+
logCliInfo(ctx.logFilePath, "quirk test", c.warn("\n✗ Similar quirk(s) already exist — quirk has NOT been appended:\n"));
|
|
152
158
|
for (const r of results) {
|
|
153
159
|
const badge = r.chunk.metadata.quirkType ? `[${r.chunk.metadata.quirkType}] ` : "";
|
|
154
160
|
const tags = r.chunk.metadata.tags?.length ? ` (${r.chunk.metadata.tags.join(", ")})` : "";
|
|
@@ -159,7 +165,7 @@ export function registerQuirkCommand(program) {
|
|
|
159
165
|
}
|
|
160
166
|
}
|
|
161
167
|
else {
|
|
162
|
-
logCliInfo(ctx.logFilePath, "quirk test", c.
|
|
168
|
+
logCliInfo(ctx.logFilePath, "quirk test", c.success("\n✓ No matching quirk found — safe to append\n"));
|
|
163
169
|
}
|
|
164
170
|
await cleanupContext(ctx);
|
|
165
171
|
}
|
|
@@ -81,9 +81,12 @@ export function registerSetupCommand(program) {
|
|
|
81
81
|
if (options.uninstall) {
|
|
82
82
|
console.log(`\n${c.heading("Removing OpenCodeRAG runtime...")}\n`);
|
|
83
83
|
removeIfExists(runtimePluginDir);
|
|
84
|
-
|
|
84
|
+
// Only remove the @opencode-ai/plugin SDK package — the scope dir may
|
|
85
|
+
// be shared with other OpenCode plugins/tools.
|
|
86
|
+
removeIfExists(runtimeSdkPluginDir);
|
|
85
87
|
removeIfExists(versionFile);
|
|
86
|
-
console.log(` ${c.updated("Removed:")} ${c.file(
|
|
88
|
+
console.log(` ${c.updated("Removed:")} ${c.file(runtimePluginDir)}`);
|
|
89
|
+
console.log(` ${c.updated("Removed:")} ${c.file(runtimeSdkPluginDir)}`);
|
|
87
90
|
console.log(`\n ${c.success("Done.")} Run ${c.file("npm uninstall -g opencode-rag-plugin")} to remove the global package.\n`);
|
|
88
91
|
return;
|
|
89
92
|
}
|
|
@@ -147,14 +147,23 @@ export function registerStatusCommand(program) {
|
|
|
147
147
|
logCliInfo(logFilePath, "status", `${c.label("Runtime:")} ${c.warn("version unknown — run `opencode-rag setup`")}`);
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
-
//
|
|
151
|
-
//
|
|
150
|
+
// GitHub update check. Runs unless autoUpdate is explicitly disabled.
|
|
151
|
+
// Awaited (raced against 3s) so the result can actually print —
|
|
152
|
+
// process.exit(0) below would previously kill the promise before it
|
|
153
|
+
// resolved, making the Update: line dead code.
|
|
152
154
|
if (config.autoUpdate?.enabled) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
try {
|
|
156
|
+
const info = await Promise.race([
|
|
157
|
+
checkForUpdate(pkg.version),
|
|
158
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 3000)),
|
|
159
|
+
]);
|
|
160
|
+
if (info && info.updateAvailable) {
|
|
155
161
|
process.stdout.write(` ${c.label("Update:")} ${c.warn(`v${info.latestVersion} available — run \`opencode-rag update\` to install`)}\n`);
|
|
156
162
|
}
|
|
157
|
-
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
/* ignore network errors */
|
|
166
|
+
}
|
|
158
167
|
}
|
|
159
168
|
// Force exit — avoid LanceDB close() hanging on Windows native bindings.
|
|
160
169
|
// Status is read-only so there's no state to lose.
|
package/dist/cli/commands/ui.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `ui` command — starts a local web UI for browsing the vector database.
|
|
6
6
|
*/
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { c, resolveCliContext, logCliError, logCliInfo } from "../format.js";
|
|
8
|
+
import { c, resolveCliContext, cleanupContext, logCliError, logCliInfo } from "../format.js";
|
|
9
9
|
/**
|
|
10
10
|
* Register the `ui` command on the given Commander program.
|
|
11
11
|
*
|
|
@@ -33,9 +33,10 @@ export function registerUiCommand(program) {
|
|
|
33
33
|
const openBrowser = options.open !== false && (config.ui?.openBrowser ?? true);
|
|
34
34
|
const { startWebUi } = await import("../../web/server.js");
|
|
35
35
|
const server = await startWebUi(storePath, port, cwd, config.embedding.vectorDimension ?? 384, config);
|
|
36
|
-
const url = `http://127.0.0.1:${server.port}`;
|
|
36
|
+
const url = `http://127.0.0.1:${server.port}/?token=${server.token}`;
|
|
37
37
|
logCliInfo(logFilePath, "ui", `\n${c.heading("OpenCodeRAG Web UI")}`);
|
|
38
38
|
logCliInfo(logFilePath, "ui", ` ${c.label("URL:")} ${c.value(url)}`);
|
|
39
|
+
logCliInfo(logFilePath, "ui", ` ${c.dim("The token in the URL authenticates this session — keep it private.")}`);
|
|
39
40
|
logCliInfo(logFilePath, "ui", ` ${c.dim("Press Ctrl+C to stop")}\n`);
|
|
40
41
|
if (openBrowser) {
|
|
41
42
|
const { spawn } = await import("node:child_process");
|
|
@@ -52,14 +53,27 @@ export function registerUiCommand(program) {
|
|
|
52
53
|
console.error(c.dim(`Could not open browser automatically. Open ${url} manually.`));
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
let shuttingDown = false;
|
|
57
|
+
const shutdown = async () => {
|
|
58
|
+
if (shuttingDown)
|
|
59
|
+
return;
|
|
60
|
+
shuttingDown = true;
|
|
61
|
+
try {
|
|
62
|
+
await server.close();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// best-effort — the process is exiting anyway
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
await cleanupContext(ctx);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// best-effort cleanup
|
|
72
|
+
}
|
|
61
73
|
process.exit(0);
|
|
62
|
-
}
|
|
74
|
+
};
|
|
75
|
+
process.on("SIGINT", shutdown);
|
|
76
|
+
process.on("SIGTERM", shutdown);
|
|
63
77
|
}
|
|
64
78
|
catch (err) {
|
|
65
79
|
const message = err.message || String(err);
|
|
@@ -24,11 +24,10 @@ export function registerUpdateCommand(program) {
|
|
|
24
24
|
console.log(`\n${c.heading("OpenCodeRAG Update")}\n`);
|
|
25
25
|
console.log(` ${c.label("Current version:")} ${c.value(currentVersion)}`);
|
|
26
26
|
console.log(` ${c.label("Checking...")} `);
|
|
27
|
-
|
|
28
|
-
try
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
catch {
|
|
27
|
+
// checkForUpdate never throws (all failures collapse to "no update"),
|
|
28
|
+
// so the try/catch below is defensive only.
|
|
29
|
+
const info = await checkForUpdate(currentVersion).catch(() => null);
|
|
30
|
+
if (!info) {
|
|
32
31
|
console.log(`\n ${c.warn("Could not reach the update server. Check your network and try again.")}\n`);
|
|
33
32
|
process.exit(1);
|
|
34
33
|
return;
|
package/dist/cli/format.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ export declare const c: {
|
|
|
60
60
|
* @param message - Human-readable error message.
|
|
61
61
|
* @param error - Optional error object for structured logging.
|
|
62
62
|
*/
|
|
63
|
-
export declare function logCliError(
|
|
63
|
+
export declare function logCliError(logFilePath: string, scope: string, message: string, error?: unknown): void;
|
|
64
64
|
/**
|
|
65
65
|
* Log an informational message to stdout and optionally append to the debug log.
|
|
66
66
|
*
|
|
@@ -68,7 +68,7 @@ export declare function logCliError(_logFilePath: string, _scope: string, messag
|
|
|
68
68
|
* @param scope - Logical scope (e.g. "index", "query") for log filtering.
|
|
69
69
|
* @param message - Human-readable info message.
|
|
70
70
|
*/
|
|
71
|
-
export declare function logCliInfo(
|
|
71
|
+
export declare function logCliInfo(logFilePath: string, scope: string, message: string): void;
|
|
72
72
|
/**
|
|
73
73
|
* Resolve a full `RagContext` from CLI options and log the config details.
|
|
74
74
|
*
|
|
@@ -82,6 +82,9 @@ export declare function resolveCliContext(opt: CliOptions, logFilePath: string,
|
|
|
82
82
|
/**
|
|
83
83
|
* Gracefully close a `RagContext` — closes the vector store and destroys pooled HTTP connections.
|
|
84
84
|
*
|
|
85
|
+
* The store close is raced against a timeout because LanceDB's native `close()`
|
|
86
|
+
* can hang indefinitely on Windows; callers must never be blocked forever.
|
|
87
|
+
*
|
|
85
88
|
* @param ctx - The `RagContext` to clean up.
|
|
86
89
|
*/
|
|
87
90
|
export declare function cleanupContext(ctx: RagContext): Promise<void>;
|