opencode-rag-plugin 1.19.5 → 1.20.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/chunker/image.d.ts +1 -1
- package/dist/chunker/image.js +35 -22
- package/dist/cli/commands/backend-detect.d.ts +61 -0
- package/dist/cli/commands/backend-detect.js +119 -0
- package/dist/cli/commands/describe-image.js +2 -1
- package/dist/cli/commands/index-command.js +11 -0
- package/dist/cli/commands/init-helpers.d.ts +4 -1
- package/dist/cli/commands/init-helpers.js +10 -5
- package/dist/cli/commands/init.js +30 -3
- package/dist/cli/commands/setup.js +7 -1
- package/dist/cli/types.d.ts +2 -0
- package/dist/core/config.d.ts +27 -1
- package/dist/core/config.js +10 -2
- package/dist/core/interfaces.d.ts +32 -4
- package/dist/core/manifest.js +1 -1
- package/dist/describer/describer.d.ts +20 -0
- package/dist/describer/describer.js +117 -14
- package/dist/describer/shared.d.ts +28 -0
- package/dist/describer/shared.js +60 -0
- package/dist/embedder/factory.js +1 -1
- package/dist/embedder/ollama.d.ts +3 -1
- package/dist/embedder/ollama.js +12 -2
- package/dist/indexer/pipeline.js +187 -99
- package/dist/mcp/handlers.d.ts +2 -0
- package/dist/mcp/handlers.js +1 -1
- package/dist/mcp/server.js +2 -1
- package/dist/opencode/system-guidance.js +4 -4
- package/dist/opencode/tools.js +4 -1
- package/dist/vectorstore/lancedb.d.ts +79 -7
- package/dist/vectorstore/lancedb.js +200 -72
- package/dist/vectorstore/memory.d.ts +8 -3
- package/dist/vectorstore/memory.js +22 -3
- package/dist/watcher.d.ts +8 -0
- package/dist/watcher.js +222 -96
- package/dist/web/api.js +24 -15
- package/dist/web/pca.d.ts +5 -2
- package/dist/web/pca.js +75 -20
- package/dist/web/ui/assets/ScatterPlot3D-BFWO5sAH.js +4116 -0
- package/dist/web/ui/assets/index-BLzCza1W.css +1 -0
- package/dist/web/ui/assets/index-BdPHzjQh.js +4 -0
- package/dist/web/ui/index.html +2 -2
- package/package.json +4 -1
- package/dist/web/ui/assets/index-BDPYdtA1.js +0 -3
- package/dist/web/ui/assets/index-CKdp79Tw.css +0 -1
package/dist/chunker/image.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare function getMimeType(ext: string): string;
|
|
|
18
18
|
* OpenAI, Anthropic, or Google Gemini).
|
|
19
19
|
*/
|
|
20
20
|
export interface ImageVisionProvider {
|
|
21
|
-
describeImage(imageBase64: string, mimeType: string, prompt: string, abort?: AbortSignal): Promise<string>;
|
|
21
|
+
describeImage(imageBase64: string, mimeType: string, prompt: string, systemPrompt?: string, abort?: AbortSignal): Promise<string>;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Factory function that creates the appropriate {@link ImageVisionProvider}
|
package/dist/chunker/image.js
CHANGED
|
@@ -42,6 +42,7 @@ class OllamaImageVisionProvider {
|
|
|
42
42
|
timeoutMs;
|
|
43
43
|
think;
|
|
44
44
|
numCtx;
|
|
45
|
+
keepAlive;
|
|
45
46
|
proxy;
|
|
46
47
|
constructor(config) {
|
|
47
48
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
@@ -49,22 +50,25 @@ class OllamaImageVisionProvider {
|
|
|
49
50
|
this.timeoutMs = config.timeoutMs;
|
|
50
51
|
this.think = config.think ?? false;
|
|
51
52
|
this.numCtx = config.numCtx;
|
|
53
|
+
this.keepAlive = config.keepAlive;
|
|
52
54
|
this.proxy = config.proxy;
|
|
53
55
|
}
|
|
54
|
-
async describeImage(imageBase64, _mimeType, prompt, abort) {
|
|
56
|
+
async describeImage(imageBase64, _mimeType, prompt, systemPrompt, abort) {
|
|
57
|
+
const messages = [];
|
|
58
|
+
if (systemPrompt && systemPrompt.trim().length > 0) {
|
|
59
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
60
|
+
}
|
|
61
|
+
messages.push({ role: "user", content: prompt, images: [imageBase64] });
|
|
55
62
|
const body = {
|
|
56
63
|
model: this.model,
|
|
57
|
-
messages
|
|
58
|
-
{
|
|
59
|
-
role: "user",
|
|
60
|
-
content: prompt,
|
|
61
|
-
images: [imageBase64],
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
|
+
messages,
|
|
64
65
|
stream: false,
|
|
65
66
|
think: this.think,
|
|
66
67
|
options: { num_ctx: this.numCtx },
|
|
67
68
|
};
|
|
69
|
+
if (this.keepAlive) {
|
|
70
|
+
body.keep_alive = this.keepAlive;
|
|
71
|
+
}
|
|
68
72
|
let lastError;
|
|
69
73
|
for (let attempt = 0; attempt <= VISION_RETRY_MAX; attempt++) {
|
|
70
74
|
const response = await postJson(`${this.baseUrl}/chat`, body, {}, this.timeoutMs, this.proxy, abort);
|
|
@@ -109,22 +113,25 @@ class OpenAIImageVisionProvider {
|
|
|
109
113
|
this.timeoutMs = config.timeoutMs;
|
|
110
114
|
this.proxy = config.proxy;
|
|
111
115
|
}
|
|
112
|
-
async describeImage(imageBase64, mimeType, prompt, abort) {
|
|
116
|
+
async describeImage(imageBase64, mimeType, prompt, systemPrompt, abort) {
|
|
113
117
|
const url = `${this.baseUrl}${this.baseUrl.endsWith("/v1") ? "" : "/v1"}/chat/completions`;
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
messages:
|
|
118
|
+
const messages = [];
|
|
119
|
+
if (systemPrompt && systemPrompt.trim().length > 0) {
|
|
120
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
121
|
+
}
|
|
122
|
+
messages.push({
|
|
123
|
+
role: "user",
|
|
124
|
+
content: [
|
|
125
|
+
{ type: "text", text: prompt },
|
|
117
126
|
{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
{ type: "text", text: prompt },
|
|
121
|
-
{
|
|
122
|
-
type: "image_url",
|
|
123
|
-
image_url: { url: `data:${mimeType};base64,${imageBase64}` },
|
|
124
|
-
},
|
|
125
|
-
],
|
|
127
|
+
type: "image_url",
|
|
128
|
+
image_url: { url: `data:${mimeType};base64,${imageBase64}` },
|
|
126
129
|
},
|
|
127
130
|
],
|
|
131
|
+
});
|
|
132
|
+
const body = {
|
|
133
|
+
model: this.model,
|
|
134
|
+
messages,
|
|
128
135
|
max_tokens: 2048,
|
|
129
136
|
};
|
|
130
137
|
const headers = {
|
|
@@ -177,7 +184,7 @@ class AnthropicImageVisionProvider {
|
|
|
177
184
|
this.timeoutMs = config.timeoutMs;
|
|
178
185
|
this.proxy = config.proxy;
|
|
179
186
|
}
|
|
180
|
-
async describeImage(imageBase64, mimeType, prompt, abort) {
|
|
187
|
+
async describeImage(imageBase64, mimeType, prompt, systemPrompt, abort) {
|
|
181
188
|
const body = {
|
|
182
189
|
model: this.model,
|
|
183
190
|
max_tokens: 2048,
|
|
@@ -194,6 +201,9 @@ class AnthropicImageVisionProvider {
|
|
|
194
201
|
},
|
|
195
202
|
],
|
|
196
203
|
};
|
|
204
|
+
if (systemPrompt && systemPrompt.trim().length > 0) {
|
|
205
|
+
body.system = systemPrompt;
|
|
206
|
+
}
|
|
197
207
|
const headers = {
|
|
198
208
|
"x-api-key": this.apiKey,
|
|
199
209
|
"anthropic-version": "2023-06-01",
|
|
@@ -243,7 +253,7 @@ class GeminiImageVisionProvider {
|
|
|
243
253
|
this.timeoutMs = config.timeoutMs;
|
|
244
254
|
this.proxy = config.proxy;
|
|
245
255
|
}
|
|
246
|
-
async describeImage(imageBase64, mimeType, prompt, abort) {
|
|
256
|
+
async describeImage(imageBase64, mimeType, prompt, systemPrompt, abort) {
|
|
247
257
|
const body = {
|
|
248
258
|
contents: [
|
|
249
259
|
{
|
|
@@ -260,6 +270,9 @@ class GeminiImageVisionProvider {
|
|
|
260
270
|
},
|
|
261
271
|
],
|
|
262
272
|
};
|
|
273
|
+
if (systemPrompt && systemPrompt.trim().length > 0) {
|
|
274
|
+
body.system_instruction = { parts: [{ text: systemPrompt }] };
|
|
275
|
+
}
|
|
263
276
|
const headers = {
|
|
264
277
|
"Content-Type": "application/json",
|
|
265
278
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Auto-detect the Ollama backend (CPU vs GPU) during `init` and
|
|
3
|
+
* pick embedding batch settings tuned for the detected backend.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Auto-detect whether Ollama runs models on the GPU or on the CPU and return
|
|
7
|
+
* matching embedding batch tuning.
|
|
8
|
+
*
|
|
9
|
+
* Detection uses `GET /api/ps`: loaded models report `size_vram` (bytes
|
|
10
|
+
* resident in VRAM). `size_vram > 0` means the model is (at least partially)
|
|
11
|
+
* offloaded to the GPU. If no model is loaded yet, a minimal `/api/embed`
|
|
12
|
+
* warmup loads the default embedding model first.
|
|
13
|
+
*
|
|
14
|
+
* Tuning is derived from benchmarks (see quirk memory):
|
|
15
|
+
* - GPU: batch 40 + concurrency 4 ≈ 86 texts/s (~97% of the ~88 texts/s ceiling)
|
|
16
|
+
* - CPU: flat ~3.5 texts/s regardless of batch size → small batches (20) with
|
|
17
|
+
* concurrency 1 keep each request fast and under the 4096-token context
|
|
18
|
+
* - unreachable/unknown: defaults (100 / 3 / 100)
|
|
19
|
+
*/
|
|
20
|
+
import { type ProxyConfig } from "../../core/config.js";
|
|
21
|
+
/** Detected Ollama backend kind. */
|
|
22
|
+
export type OllamaBackend = "gpu" | "cpu" | "unreachable" | "unknown";
|
|
23
|
+
/** Embedding batch settings written into the generated config. */
|
|
24
|
+
export interface IndexingTuning {
|
|
25
|
+
embedBatchSize: number;
|
|
26
|
+
embedConcurrency: number;
|
|
27
|
+
ollamaMaxBatchSize: number;
|
|
28
|
+
}
|
|
29
|
+
/** Result of the backend detection. */
|
|
30
|
+
export interface OllamaBackendInfo {
|
|
31
|
+
backend: OllamaBackend;
|
|
32
|
+
/** Tuning to write into the generated config. */
|
|
33
|
+
tuning: IndexingTuning;
|
|
34
|
+
/** Human-readable summary for the init output. */
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
interface PsModel {
|
|
38
|
+
name: string;
|
|
39
|
+
size_vram?: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Classify loaded Ollama models into a backend + tuning profile.
|
|
43
|
+
*
|
|
44
|
+
* Prefers the configured default embedding model when it is loaded, falling
|
|
45
|
+
* back to any loaded model (a loaded GPU model means the host has a working
|
|
46
|
+
* GPU that Ollama will also use for embeddings).
|
|
47
|
+
*
|
|
48
|
+
* @param models - The `models` array from `GET /api/ps`.
|
|
49
|
+
* @returns The backend info with the matching tuning profile.
|
|
50
|
+
*/
|
|
51
|
+
export declare function classifyOllamaModels(models: PsModel[]): OllamaBackendInfo;
|
|
52
|
+
/**
|
|
53
|
+
* Detect the Ollama backend by probing `/api/ps`, warming up the default
|
|
54
|
+
* embedding model when nothing is loaded yet.
|
|
55
|
+
*
|
|
56
|
+
* @param baseUrl - Ollama API base URL (defaults to the config default).
|
|
57
|
+
* @param proxy - Optional proxy configuration.
|
|
58
|
+
* @returns Backend info; never throws.
|
|
59
|
+
*/
|
|
60
|
+
export declare function detectOllamaBackend(baseUrl?: string, proxy?: ProxyConfig): Promise<OllamaBackendInfo>;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Auto-detect the Ollama backend (CPU vs GPU) during `init` and
|
|
3
|
+
* pick embedding batch settings tuned for the detected backend.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Auto-detect whether Ollama runs models on the GPU or on the CPU and return
|
|
7
|
+
* matching embedding batch tuning.
|
|
8
|
+
*
|
|
9
|
+
* Detection uses `GET /api/ps`: loaded models report `size_vram` (bytes
|
|
10
|
+
* resident in VRAM). `size_vram > 0` means the model is (at least partially)
|
|
11
|
+
* offloaded to the GPU. If no model is loaded yet, a minimal `/api/embed`
|
|
12
|
+
* warmup loads the default embedding model first.
|
|
13
|
+
*
|
|
14
|
+
* Tuning is derived from benchmarks (see quirk memory):
|
|
15
|
+
* - GPU: batch 40 + concurrency 4 ≈ 86 texts/s (~97% of the ~88 texts/s ceiling)
|
|
16
|
+
* - CPU: flat ~3.5 texts/s regardless of batch size → small batches (20) with
|
|
17
|
+
* concurrency 1 keep each request fast and under the 4096-token context
|
|
18
|
+
* - unreachable/unknown: defaults (100 / 3 / 100)
|
|
19
|
+
*/
|
|
20
|
+
import { DEFAULT_CONFIG } from "../../core/config.js";
|
|
21
|
+
import { fetchWithProxy, postJson } from "../../embedder/http.js";
|
|
22
|
+
/** Benchmarked optimum on a GPU-backed Ollama (RTX 4090, qwen3-embedding:0.6b). */
|
|
23
|
+
const GPU_TUNING = {
|
|
24
|
+
embedBatchSize: 40,
|
|
25
|
+
embedConcurrency: 4,
|
|
26
|
+
ollamaMaxBatchSize: 40,
|
|
27
|
+
};
|
|
28
|
+
/** CPU-backed Ollama: throughput is flat, so keep batches small and sequential. */
|
|
29
|
+
const CPU_TUNING = {
|
|
30
|
+
embedBatchSize: 20,
|
|
31
|
+
embedConcurrency: 1,
|
|
32
|
+
ollamaMaxBatchSize: 20,
|
|
33
|
+
};
|
|
34
|
+
/** Fallback when Ollama is unreachable or the backend cannot be determined. */
|
|
35
|
+
const DEFAULT_TUNING = {
|
|
36
|
+
embedBatchSize: DEFAULT_CONFIG.indexing.embedBatchSize,
|
|
37
|
+
embedConcurrency: DEFAULT_CONFIG.indexing.embedConcurrency ?? 3,
|
|
38
|
+
ollamaMaxBatchSize: DEFAULT_CONFIG.indexing.ollamaMaxBatchSize ?? 100,
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Classify loaded Ollama models into a backend + tuning profile.
|
|
42
|
+
*
|
|
43
|
+
* Prefers the configured default embedding model when it is loaded, falling
|
|
44
|
+
* back to any loaded model (a loaded GPU model means the host has a working
|
|
45
|
+
* GPU that Ollama will also use for embeddings).
|
|
46
|
+
*
|
|
47
|
+
* @param models - The `models` array from `GET /api/ps`.
|
|
48
|
+
* @returns The backend info with the matching tuning profile.
|
|
49
|
+
*/
|
|
50
|
+
export function classifyOllamaModels(models) {
|
|
51
|
+
if (!models || models.length === 0) {
|
|
52
|
+
return {
|
|
53
|
+
backend: "unknown",
|
|
54
|
+
tuning: DEFAULT_TUNING,
|
|
55
|
+
message: "Could not determine the Ollama backend (no models loaded) — using default batch settings.",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const embedModel = DEFAULT_CONFIG.embedding.model;
|
|
59
|
+
const probe = models.find((m) => m.name === embedModel) ?? models[0];
|
|
60
|
+
const onGpu = probe ? (probe.size_vram ?? 0) > 0 : false;
|
|
61
|
+
if (onGpu) {
|
|
62
|
+
return {
|
|
63
|
+
backend: "gpu",
|
|
64
|
+
tuning: GPU_TUNING,
|
|
65
|
+
message: `Ollama detected on GPU — tuned embedding for batch 40 / concurrency 4 (${(probe?.size_vram ?? 0) / (1024 * 1024) | 0} MiB in VRAM).`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
backend: "cpu",
|
|
70
|
+
tuning: CPU_TUNING,
|
|
71
|
+
message: "Ollama detected on CPU — tuned embedding for batch 20 / concurrency 1.",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Fetch `GET /api/ps`, returning null when Ollama is unreachable or errors. */
|
|
75
|
+
async function getOllamaPs(baseUrl, proxy) {
|
|
76
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/ps`;
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetchWithProxy(url, { method: "GET", signal: AbortSignal.timeout(3000) }, proxy);
|
|
79
|
+
if (!res.ok)
|
|
80
|
+
return null;
|
|
81
|
+
const data = (await res.json());
|
|
82
|
+
return data.models ?? null;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Detect the Ollama backend by probing `/api/ps`, warming up the default
|
|
90
|
+
* embedding model when nothing is loaded yet.
|
|
91
|
+
*
|
|
92
|
+
* @param baseUrl - Ollama API base URL (defaults to the config default).
|
|
93
|
+
* @param proxy - Optional proxy configuration.
|
|
94
|
+
* @returns Backend info; never throws.
|
|
95
|
+
*/
|
|
96
|
+
export async function detectOllamaBackend(baseUrl = DEFAULT_CONFIG.embedding.baseUrl, proxy) {
|
|
97
|
+
let models = await getOllamaPs(baseUrl, proxy);
|
|
98
|
+
if (models === null) {
|
|
99
|
+
return {
|
|
100
|
+
backend: "unreachable",
|
|
101
|
+
tuning: DEFAULT_TUNING,
|
|
102
|
+
message: "Ollama not reachable — using default batch settings.",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (models.length === 0) {
|
|
106
|
+
// Nothing loaded yet: a minimal embed request loads the default
|
|
107
|
+
// embedding model so /api/ps can report its backend.
|
|
108
|
+
const embedUrl = `${baseUrl.replace(/\/+$/, "")}/embed`;
|
|
109
|
+
try {
|
|
110
|
+
await postJson(embedUrl, { model: DEFAULT_CONFIG.embedding.model, input: "warmup" }, {}, 15000, proxy);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Model missing or request failed — classification will stay unknown.
|
|
114
|
+
}
|
|
115
|
+
models = (await getOllamaPs(baseUrl, proxy)) ?? [];
|
|
116
|
+
}
|
|
117
|
+
return classifyOllamaModels(models);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=backend-detect.js.map
|
|
@@ -22,6 +22,7 @@ export function registerDescribeImageCommand(program) {
|
|
|
22
22
|
.description("Describe an image file using a vision model")
|
|
23
23
|
.argument("<filePath>", "path to image file")
|
|
24
24
|
.option("-c, --config <path>", "path to config file")
|
|
25
|
+
.option("-s, --system-prompt <text>", "optional system prompt to steer the description toward specific features")
|
|
25
26
|
.action(async (filePath, options) => {
|
|
26
27
|
try {
|
|
27
28
|
const cwd = process.cwd();
|
|
@@ -56,7 +57,7 @@ export function registerDescribeImageCommand(program) {
|
|
|
56
57
|
const sized = maxDimension > 0 ? await resizeImage(buffer, resolvedPath, maxDimension) : buffer;
|
|
57
58
|
const b64 = sized.toString("base64");
|
|
58
59
|
const provider = createImageVisionProvider(imageDescriptionConfig);
|
|
59
|
-
const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt);
|
|
60
|
+
const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt, options.systemPrompt);
|
|
60
61
|
logCliInfo(logFilePath, "describe-image", `\n${c.desc(description)}\n`);
|
|
61
62
|
await cleanupContext(ctx);
|
|
62
63
|
}
|
|
@@ -13,6 +13,7 @@ import readline from "node:readline";
|
|
|
13
13
|
import chokidar from "chokidar";
|
|
14
14
|
import { appendDebugLog } from "../../core/fileLogger.js";
|
|
15
15
|
import { createWatchPassScheduler, createWatchIgnore, runIndexPass, } from "../../indexer.js";
|
|
16
|
+
import { tryAcquireWatcherLock, releaseWatcherLock } from "../../watcher.js";
|
|
16
17
|
import { c, resolveCliContext, cleanupContext, logCliError, logCliInfo, logIndexSummary, formatDuration, } from "../format.js";
|
|
17
18
|
/**
|
|
18
19
|
* Build a logger that suppresses console output when watchTriggered is true.
|
|
@@ -129,6 +130,15 @@ export function registerIndexCommand(program) {
|
|
|
129
130
|
await cleanupContext(ctx);
|
|
130
131
|
process.exit(sigReceived ? 130 : 0);
|
|
131
132
|
}
|
|
133
|
+
// Only one watcher may run per workspace — a background auto-indexer
|
|
134
|
+
// in an OpenCode session (or another `index --watch`) may already own
|
|
135
|
+
// this store. The initial pass above still ran; just don't start a
|
|
136
|
+
// duplicate watcher.
|
|
137
|
+
if (!tryAcquireWatcherLock(storePath)) {
|
|
138
|
+
logCliInfo(logFilePath, "index", c.warn("Another watcher is already running for this workspace (e.g. an OpenCode session with auto-index enabled) — not starting a second one. The index is up to date."));
|
|
139
|
+
await cleanupContext(ctx);
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
132
142
|
logCliInfo(logFilePath, "index", `\n${c.heading("Watching for changes...")}`);
|
|
133
143
|
const scheduler = createWatchPassScheduler(async (changedPaths) => { await runPass(true, undefined, changedPaths); }, (error) => {
|
|
134
144
|
const message = error.message || String(error);
|
|
@@ -154,6 +164,7 @@ export function registerIndexCommand(program) {
|
|
|
154
164
|
watcher.close(),
|
|
155
165
|
new Promise((r) => setTimeout(r, 5000)),
|
|
156
166
|
]);
|
|
167
|
+
releaseWatcherLock(storePath);
|
|
157
168
|
await cleanupContext(ctx);
|
|
158
169
|
process.exit(0);
|
|
159
170
|
};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* dependency installation, and gitignore merging.
|
|
7
7
|
*/
|
|
8
8
|
import type { PackageMetadata } from "../types.js";
|
|
9
|
+
import type { IndexingTuning } from "./backend-detect.js";
|
|
9
10
|
/**
|
|
10
11
|
* Build the workspace-local `.opencode/package.json` content.
|
|
11
12
|
*
|
|
@@ -115,6 +116,8 @@ export declare function installPluginFromGlobal(opencodeDir: string, packageName
|
|
|
115
116
|
/**
|
|
116
117
|
* Generate the default `opencode-rag.json` configuration content.
|
|
117
118
|
*
|
|
119
|
+
* @param tuning - Optional embedding batch tuning (auto-detected from the
|
|
120
|
+
* Ollama backend). Falls back to `DEFAULT_CONFIG` for any omitted field.
|
|
118
121
|
* @returns A pretty-printed JSON string with all default configuration values.
|
|
119
122
|
*/
|
|
120
|
-
export declare function generateDefaultConfigJson(): string;
|
|
123
|
+
export declare function generateDefaultConfigJson(tuning?: Partial<IndexingTuning>): string;
|
|
@@ -165,7 +165,7 @@ export function generateSkillFile() {
|
|
|
165
165
|
"2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
|
|
166
166
|
"3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
|
|
167
167
|
"4. User asks a code question → `search_semantic` to gather context before answering",
|
|
168
|
-
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
168
|
+
"5. User asks about an image or visual asset → `describe_image(filePath)` (optionally pass `systemPrompt` to focus on specific features) to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
169
169
|
"6. You encounter an error or need a known pitfall → `recall_quirks(query)`",
|
|
170
170
|
"7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it",
|
|
171
171
|
"8. A recalled quirk is outdated or wrong → `update_quirk(id, ...)` to fix it, or `delete_quirk(id)` if it no longer applies",
|
|
@@ -188,7 +188,7 @@ export function generateSkillFile() {
|
|
|
188
188
|
"1. **Skeleton first** — call `get_file_skeleton(filePath)` to see structure",
|
|
189
189
|
"2. **Find usages** — call `find_usages(symbolName)` before modifying any symbol",
|
|
190
190
|
"3. **Search** — call `search_semantic(query)` to find relevant code",
|
|
191
|
-
"4. **Describe images** — call `describe_image(filePath)` when context involves an image file",
|
|
191
|
+
"4. **Describe images** — call `describe_image(filePath)` when context involves an image file (pass `systemPrompt` to focus on specific features)",
|
|
192
192
|
"5. **Read** — use the `read` tool on specific line ranges identified above",
|
|
193
193
|
"6. **Edit** — now you have full context to make safe changes",
|
|
194
194
|
"",
|
|
@@ -205,7 +205,7 @@ export function generateSkillFile() {
|
|
|
205
205
|
"- `search_semantic`: `query` (req), `pathHints?`, `languageHints?`, `topK?`",
|
|
206
206
|
"- `get_file_skeleton`: `filePath` (req)",
|
|
207
207
|
"- `find_usages`: `symbolName` (req), `pathHint?`, `topK?`",
|
|
208
|
-
"- `describe_image`: `filePath` (req)",
|
|
208
|
+
"- `describe_image`: `filePath` (req), `systemPrompt?`",
|
|
209
209
|
"- `recall_quirks`: `query` (req), `topK?`, `quirkType?`, `tags?`",
|
|
210
210
|
"- `add_quirk`: `content` (req), `quirkType?`, `tags?`, `sourceRef?`",
|
|
211
211
|
"- `update_quirk`: `id` (req) + at least one of `content?`, `quirkType?`, `tags?`, `confidence?`, `sourceRef?`",
|
|
@@ -216,6 +216,7 @@ export function generateSkillFile() {
|
|
|
216
216
|
"- Use `pathHints` to narrow searches to specific directories",
|
|
217
217
|
"- Use `languageHints` to filter by file type",
|
|
218
218
|
"- `find_usages` is essential before refactoring — it shows every reference",
|
|
219
|
+
"- Pass `systemPrompt` to `describe_image` when you need specific details (e.g. `\"focus on the chart's axes and values\"`)",
|
|
219
220
|
"- If no results appear, the workspace may not be indexed yet — run `opencode-rag index`",
|
|
220
221
|
"- Image descriptions are generated at index time using the configured vision provider; ensure `imageDescription` is configured in `opencode-rag.json` if your project includes images",
|
|
221
222
|
"",
|
|
@@ -414,9 +415,11 @@ export async function installPluginFromGlobal(opencodeDir, packageName, skipInst
|
|
|
414
415
|
/**
|
|
415
416
|
* Generate the default `opencode-rag.json` configuration content.
|
|
416
417
|
*
|
|
418
|
+
* @param tuning - Optional embedding batch tuning (auto-detected from the
|
|
419
|
+
* Ollama backend). Falls back to `DEFAULT_CONFIG` for any omitted field.
|
|
417
420
|
* @returns A pretty-printed JSON string with all default configuration values.
|
|
418
421
|
*/
|
|
419
|
-
export function generateDefaultConfigJson() {
|
|
422
|
+
export function generateDefaultConfigJson(tuning) {
|
|
420
423
|
return JSON.stringify({
|
|
421
424
|
embedding: {
|
|
422
425
|
provider: DEFAULT_CONFIG.embedding.provider,
|
|
@@ -430,7 +433,9 @@ export function generateDefaultConfigJson() {
|
|
|
430
433
|
chunkOverlap: DEFAULT_CONFIG.indexing.chunkOverlap,
|
|
431
434
|
minFileSizeBytes: DEFAULT_CONFIG.indexing.minFileSizeBytes,
|
|
432
435
|
concurrency: DEFAULT_CONFIG.indexing.concurrency,
|
|
433
|
-
embedBatchSize: DEFAULT_CONFIG.indexing.embedBatchSize,
|
|
436
|
+
embedBatchSize: tuning?.embedBatchSize ?? DEFAULT_CONFIG.indexing.embedBatchSize,
|
|
437
|
+
embedConcurrency: tuning?.embedConcurrency ?? DEFAULT_CONFIG.indexing.embedConcurrency ?? 3,
|
|
438
|
+
ollamaMaxBatchSize: tuning?.ollamaMaxBatchSize ?? DEFAULT_CONFIG.indexing.ollamaMaxBatchSize ?? 100,
|
|
434
439
|
},
|
|
435
440
|
vectorStore: {
|
|
436
441
|
path: DEFAULT_CONFIG.vectorStore.path,
|
|
@@ -17,6 +17,7 @@ import { destroyAllPooledConnections } from "../../embedder/http.js";
|
|
|
17
17
|
import { c } from "../format.js";
|
|
18
18
|
import { getPackageMetadata, readJsonObject, writeJsonFile } from "../helpers.js";
|
|
19
19
|
import { buildOpencodeConfig, buildWorkspacePackageJson, generateDefaultConfigJson, generateSkillFile, generateWorkspacePluginFile, generateWorkspaceTuiPluginFile, installPluginFromGlobal, mergeAgentsMdContent, mergeGitignoreContent, } from "./init-helpers.js";
|
|
20
|
+
import { detectOllamaBackend } from "./backend-detect.js";
|
|
20
21
|
/**
|
|
21
22
|
* Register the `init` command on the given Commander program.
|
|
22
23
|
*
|
|
@@ -29,7 +30,14 @@ import { buildOpencodeConfig, buildWorkspacePackageJson, generateDefaultConfigJs
|
|
|
29
30
|
export function registerInitCommand(program) {
|
|
30
31
|
program
|
|
31
32
|
.command("init")
|
|
32
|
-
.description("Configure
|
|
33
|
+
.description("Configure this workspace (files + auto-tuned opencode-rag.json)")
|
|
34
|
+
.addHelpText("after", "\nUse cases:\n" +
|
|
35
|
+
" - First-time workspace setup: creates .opencode/, the RAG skill file, AGENTS.md\n" +
|
|
36
|
+
" guidance, and opencode-rag.json with embedding batches auto-detected for the\n" +
|
|
37
|
+
" Ollama backend (GPU vs CPU).\n" +
|
|
38
|
+
" - Re-running in an existing workspace: re-syncs plugin/skill files and keeps the\n" +
|
|
39
|
+
" existing opencode-rag.json (overwriting requires interactive confirmation).\n" +
|
|
40
|
+
"\nWorkspace-level step — run AFTER 'opencode-rag setup' on this machine, then 'opencode-rag index'.\n")
|
|
33
41
|
.option("-f, --force", "overwrite existing files")
|
|
34
42
|
.option("--skip-install", "skip installing workspace-local plugin dependencies")
|
|
35
43
|
.option("--skip-health-check", "skip provider connectivity and model availability check")
|
|
@@ -189,8 +197,27 @@ export function registerInitCommand(program) {
|
|
|
189
197
|
console.log(` ${c.exists("Exists:")} .opencode/package.json`);
|
|
190
198
|
}
|
|
191
199
|
const configExists = existsSync(configPath);
|
|
200
|
+
// Detect the Ollama backend (CPU vs GPU) lazily — only when we are
|
|
201
|
+
// actually about to write a config — and tune embedding batches for it.
|
|
202
|
+
let detectedTuning;
|
|
203
|
+
const configContent = async () => {
|
|
204
|
+
if (!detectedTuning) {
|
|
205
|
+
try {
|
|
206
|
+
const info = await detectOllamaBackend();
|
|
207
|
+
detectedTuning = info.tuning;
|
|
208
|
+
const icon = info.backend === "gpu" ? c.success("GPU:") :
|
|
209
|
+
info.backend === "cpu" ? c.warn("CPU:") :
|
|
210
|
+
c.dim("Backend:");
|
|
211
|
+
console.log(` ${icon} ${info.message}`);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
detectedTuning = undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return generateDefaultConfigJson(detectedTuning);
|
|
218
|
+
};
|
|
192
219
|
if (!configExists) {
|
|
193
|
-
writeFileSync(configPath,
|
|
220
|
+
writeFileSync(configPath, await configContent(), "utf-8");
|
|
194
221
|
console.log(` ${c.created("Created:")} opencode-rag.json`);
|
|
195
222
|
}
|
|
196
223
|
else {
|
|
@@ -208,7 +235,7 @@ export function registerInitCommand(program) {
|
|
|
208
235
|
if (overwrite) {
|
|
209
236
|
copyFileSync(configPath, `${configPath}.bak`);
|
|
210
237
|
console.log(` ${c.dim("Backup:")} opencode-rag.json.bak`);
|
|
211
|
-
writeFileSync(configPath,
|
|
238
|
+
writeFileSync(configPath, await configContent(), "utf-8");
|
|
212
239
|
console.log(` ${c.updated("Updated:")} opencode-rag.json`);
|
|
213
240
|
}
|
|
214
241
|
else {
|
|
@@ -36,7 +36,13 @@ function checkOpenCodeRunning() {
|
|
|
36
36
|
export function registerSetupCommand(program) {
|
|
37
37
|
program
|
|
38
38
|
.command("setup")
|
|
39
|
-
.description("
|
|
39
|
+
.description("Install/update the OpenCodeRAG runtime once per machine")
|
|
40
|
+
.addHelpText("after", "\nUse cases:\n" +
|
|
41
|
+
" - First-time install: run once per machine to install the plugin runtime\n" +
|
|
42
|
+
" into ~/.opencode/ so OpenCode can discover the RAG plugin.\n" +
|
|
43
|
+
" - Updating: re-sync the runtime to the published plugin version.\n" +
|
|
44
|
+
" - Troubleshooting: use --check to inspect the runtime, or --force to reinstall.\n" +
|
|
45
|
+
"\nMachine-level step — run BEFORE 'opencode-rag init' (init configures each workspace).\n")
|
|
40
46
|
.option("--uninstall", "remove the runtime and cleanup")
|
|
41
47
|
.option("-f, --force", "force re-setup even if up-to-date")
|
|
42
48
|
.option("--check", "check whether the runtime is correctly installed")
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface CliOptions {
|
|
|
20
20
|
explain?: boolean;
|
|
21
21
|
/** Skip confirmation prompts for destructive operations. */
|
|
22
22
|
yes?: boolean;
|
|
23
|
+
/** Optional system prompt to steer an image description toward specific features. */
|
|
24
|
+
systemPrompt?: string;
|
|
23
25
|
}
|
|
24
26
|
/** Options for the `init` command. */
|
|
25
27
|
export interface InitOptions {
|
package/dist/core/config.d.ts
CHANGED
|
@@ -54,7 +54,18 @@ export interface DescriptionConfig {
|
|
|
54
54
|
proxy?: ProxyConfig;
|
|
55
55
|
/** System prompt instructing the LLM how to describe code. */
|
|
56
56
|
systemPrompt: string;
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* EXPERIMENTAL: enable multi-chunk batch description requests (Ollama
|
|
59
|
+
* provider only). Off by default — each chunk is described with its own
|
|
60
|
+
* request. When enabled, up to `batchMaxChunks` chunks share one request
|
|
61
|
+
* using ordinal labels ([CHUNK 1] ... reply "1: <desc>"); small models can
|
|
62
|
+
* mangle the structured output, so batches that fail to parse fall back to
|
|
63
|
+
* individual requests and batching auto-disables after 2 consecutive
|
|
64
|
+
* failures. Part of the description manifest fingerprint — toggling it
|
|
65
|
+
* re-describes files.
|
|
66
|
+
*/
|
|
67
|
+
batchEnabled?: boolean;
|
|
68
|
+
/** Maximum chunks per batch request. Only applies when `batchEnabled` is true. */
|
|
58
69
|
batchMaxChunks?: number;
|
|
59
70
|
/** Timeout per batch request in milliseconds. */
|
|
60
71
|
batchTimeoutMs?: number;
|
|
@@ -68,6 +79,8 @@ export interface DescriptionConfig {
|
|
|
68
79
|
think?: boolean;
|
|
69
80
|
/** Context window size for the LLM. */
|
|
70
81
|
numCtx?: number;
|
|
82
|
+
/** Ollama keep_alive value (e.g. "-1" for keep-in-memory) sent with /api/chat requests. */
|
|
83
|
+
keepAlive?: string;
|
|
71
84
|
/** Maximum content characters sent to the LLM. Chunks exceeding this use fallback descriptions. */
|
|
72
85
|
maxContentChars?: number;
|
|
73
86
|
}
|
|
@@ -91,6 +104,8 @@ export interface ImageDescriptionConfig {
|
|
|
91
104
|
think?: boolean;
|
|
92
105
|
/** Context window size. */
|
|
93
106
|
numCtx?: number;
|
|
107
|
+
/** Ollama keep_alive value (e.g. "-1" for keep-in-memory) sent with /api/chat requests. */
|
|
108
|
+
keepAlive?: string;
|
|
94
109
|
/** Proxy configuration. */
|
|
95
110
|
proxy?: ProxyConfig;
|
|
96
111
|
/** Maximum image dimension (pixels) — larger images are resized before sending. */
|
|
@@ -220,6 +235,8 @@ export interface RagConfig {
|
|
|
220
235
|
queryPrefix?: string;
|
|
221
236
|
/** Cached embedding vector dimension. Probed once on first startup, then persisted to config. */
|
|
222
237
|
vectorDimension?: number;
|
|
238
|
+
/** Ollama keep_alive value (e.g. "-1" for keep-in-memory) sent with /api/embed requests. */
|
|
239
|
+
keepAlive?: string;
|
|
223
240
|
};
|
|
224
241
|
/** Indexing pipeline controls: what to index, concurrency, batch sizes. */
|
|
225
242
|
indexing: {
|
|
@@ -268,6 +285,15 @@ export interface RagConfig {
|
|
|
268
285
|
* @default 1_048_576 (1 MB)
|
|
269
286
|
*/
|
|
270
287
|
maxSvgSizeBytes?: number;
|
|
288
|
+
/**
|
|
289
|
+
* Run vector-store compaction + version pruning every N windows during a
|
|
290
|
+
* long index pass. LanceDB keeps every committed version on disk, so
|
|
291
|
+
* without periodic maintenance the store phase slows down as the index
|
|
292
|
+
* grows (version-manifest accumulation). 0 disables mid-run optimization
|
|
293
|
+
* (the store is still optimized once at the end of a pass).
|
|
294
|
+
* @default 8
|
|
295
|
+
*/
|
|
296
|
+
optimizeIntervalWindows?: number;
|
|
271
297
|
};
|
|
272
298
|
/** Vector storage backend configuration. */
|
|
273
299
|
vectorStore: {
|
package/dist/core/config.js
CHANGED
|
@@ -115,9 +115,10 @@ export const DEFAULT_CONFIG = {
|
|
|
115
115
|
concurrency: 8,
|
|
116
116
|
embedBatchSize: 100,
|
|
117
117
|
embedConcurrency: 3,
|
|
118
|
-
ollamaMaxBatchSize:
|
|
118
|
+
ollamaMaxBatchSize: 100,
|
|
119
119
|
descriptionConcurrency: 4,
|
|
120
120
|
maxSvgSizeBytes: 1_048_576,
|
|
121
|
+
optimizeIntervalWindows: 8,
|
|
121
122
|
},
|
|
122
123
|
vectorStore: {
|
|
123
124
|
path: "./.opencode/rag_db",
|
|
@@ -169,7 +170,10 @@ export const DEFAULT_CONFIG = {
|
|
|
169
170
|
think: false,
|
|
170
171
|
numCtx: 4096,
|
|
171
172
|
timeoutMs: 60000,
|
|
172
|
-
systemPrompt: "Describe this code in
|
|
173
|
+
systemPrompt: "Describe this code in ONE concise sentence (max 20 words): purpose, key inputs/outputs. No code repetition.",
|
|
174
|
+
batchEnabled: false,
|
|
175
|
+
batchMaxChunks: 25,
|
|
176
|
+
batchTimeoutMs: 120000,
|
|
173
177
|
batchConcurrency: 1,
|
|
174
178
|
retryMax: 3,
|
|
175
179
|
retryBaseDelayMs: 1000,
|
|
@@ -427,6 +431,10 @@ export function validateConfig(config) {
|
|
|
427
431
|
if (config.description.timeoutMs != null && config.description.timeoutMs <= 0) {
|
|
428
432
|
warnings.push("description.timeoutMs must be > 0");
|
|
429
433
|
}
|
|
434
|
+
if (config.description.batchEnabled === true) {
|
|
435
|
+
warnings.push("description.batchEnabled is EXPERIMENTAL — batching several chunks into one LLM request (Ollama only) " +
|
|
436
|
+
"is unreliable on small models; disable it if descriptions look wrong");
|
|
437
|
+
}
|
|
430
438
|
}
|
|
431
439
|
if (config.imageDescription) {
|
|
432
440
|
if (config.imageDescription.enabled) {
|