lancedb-opencode-pro 0.2.2 → 0.2.4
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/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/config.js +48 -0
- package/dist/index.js +22 -7
- package/dist/summarize.d.ts +52 -0
- package/dist/summarize.js +350 -0
- package/dist/types.d.ts +45 -0
- package/package.json +2 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jonathan Tsai <tryweb@ichiayi.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -240,6 +240,17 @@ Supported environment variables:
|
|
|
240
240
|
- `LANCEDB_OPENCODE_PRO_UNUSED_DAYS_THRESHOLD`
|
|
241
241
|
- `LANCEDB_OPENCODE_PRO_MIN_CAPTURE_CHARS`
|
|
242
242
|
- `LANCEDB_OPENCODE_PRO_MAX_ENTRIES_PER_SCOPE`
|
|
243
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MODE`
|
|
244
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MAX_MEMORIES`
|
|
245
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MIN_MEMORIES`
|
|
246
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_BUDGET_TOKENS`
|
|
247
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MAX_CHARS_PER_MEMORY`
|
|
248
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_SUMMARIZATION`
|
|
249
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_SUMMARY_TARGET_CHARS`
|
|
250
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_SCORE_DROP_TOLERANCE`
|
|
251
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_INJECTION_FLOOR`
|
|
252
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_CODE_SUMMARIZATION_MODE`
|
|
253
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_CODE_SUMMARIZATION_PRESERVE_STRUCTURE`
|
|
243
254
|
|
|
244
255
|
## What It Provides
|
|
245
256
|
|
|
@@ -357,6 +368,127 @@ Recommended review order in low-feedback environments:
|
|
|
357
368
|
3. Check whether users still needed manual rescue through `memory_search` or issued correction-like responses.
|
|
358
369
|
4. Run a bounded audit of recalled memories or skipped captures before concluding the system is helping.
|
|
359
370
|
|
|
371
|
+
## Injection Control
|
|
372
|
+
|
|
373
|
+
This provider supports configurable memory injection behavior, allowing you to control how recalled memories are processed before being injected into the LLM prompt.
|
|
374
|
+
|
|
375
|
+
### Configuration
|
|
376
|
+
|
|
377
|
+
Add an `injection` block to your sidecar config:
|
|
378
|
+
|
|
379
|
+
```json
|
|
380
|
+
{
|
|
381
|
+
"provider": "lancedb-opencode-pro",
|
|
382
|
+
"injection": {
|
|
383
|
+
"mode": "fixed",
|
|
384
|
+
"maxMemories": 3,
|
|
385
|
+
"minMemories": 1,
|
|
386
|
+
"budgetTokens": 2000,
|
|
387
|
+
"maxCharsPerMemory": 1200,
|
|
388
|
+
"summarization": "none",
|
|
389
|
+
"summaryTargetChars": 400,
|
|
390
|
+
"scoreDropTolerance": 0.15,
|
|
391
|
+
"injectionFloor": 0.3,
|
|
392
|
+
"codeSummarization": {
|
|
393
|
+
"mode": "truncate",
|
|
394
|
+
"preserveStructure": true
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Injection Modes
|
|
401
|
+
|
|
402
|
+
- **`fixed`** (default) — Always inject up to `maxMemories` memories regardless of content size. This preserves backward-compatible behavior.
|
|
403
|
+
- **`budget`** — Limit total injected tokens to `budgetTokens`. The provider accumulates memories until the token budget is exhausted.
|
|
404
|
+
- **`adaptive`** — Dynamically adjust injection count based on score drops. Stop injection when scores drop below `scoreDropTolerance` relative to the highest-scored memory.
|
|
405
|
+
|
|
406
|
+
### Summarization Modes
|
|
407
|
+
|
|
408
|
+
When `summarization` is set to `truncate` or `extract`, memories are summarized before injection:
|
|
409
|
+
|
|
410
|
+
- **`none`** (default) — No summarization; inject full text.
|
|
411
|
+
- **`truncate`** — Simple truncation to `summaryTargetChars` with ellipsis.
|
|
412
|
+
- **`extract`** — Key sentence extraction for text, structure-preserving truncation for code.
|
|
413
|
+
- **`auto`** — Content-aware summarization (truncate for text, preserve structure for code).
|
|
414
|
+
|
|
415
|
+
### Code Handling
|
|
416
|
+
|
|
417
|
+
The `codeSummarization` config controls how code snippets are processed:
|
|
418
|
+
|
|
419
|
+
- **`mode`**: `"truncate"` | `"preserve"` | `"auto"` (default: `"truncate"`)
|
|
420
|
+
- **`preserveStructure`**: When `true`, code truncation attempts to balance brackets and preserve syntactic validity.
|
|
421
|
+
|
|
422
|
+
### Environment Variables
|
|
423
|
+
|
|
424
|
+
All injection options can be overridden via environment variables:
|
|
425
|
+
|
|
426
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MODE`
|
|
427
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MAX_MEMORIES`
|
|
428
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MIN_MEMORIES`
|
|
429
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_BUDGET_TOKENS`
|
|
430
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MAX_CHARS_PER_MEMORY`
|
|
431
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_SUMMARIZATION`
|
|
432
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_SUMMARY_TARGET_CHARS`
|
|
433
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_SCORE_DROP_TOLERANCE`
|
|
434
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_INJECTION_FLOOR`
|
|
435
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_CODE_SUMMARIZATION_MODE`
|
|
436
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_CODE_SUMMARIZATION_PRESERVE_STRUCTURE`
|
|
437
|
+
|
|
438
|
+
### Default Behavior
|
|
439
|
+
|
|
440
|
+
The default configuration preserves backward compatibility:
|
|
441
|
+
|
|
442
|
+
- `mode`: `"fixed"`
|
|
443
|
+
- `maxMemories`: `3`
|
|
444
|
+
- `summarization`: `"none"`
|
|
445
|
+
|
|
446
|
+
This means without any `injection` configuration, the provider behaves identically to previous versions: always inject up to 3 memories with full text.
|
|
447
|
+
|
|
448
|
+
### Example: Token Budget Mode
|
|
449
|
+
|
|
450
|
+
For token-sensitive deployments, use budget mode to limit context size:
|
|
451
|
+
|
|
452
|
+
```json
|
|
453
|
+
{
|
|
454
|
+
"injection": {
|
|
455
|
+
"mode": "budget",
|
|
456
|
+
"budgetTokens": 1500,
|
|
457
|
+
"summarization": "truncate",
|
|
458
|
+
"summaryTargetChars": 400
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
This configuration:
|
|
464
|
+
1. Accumulates memories until total estimated tokens reach ~1500
|
|
465
|
+
2. Truncates each memory to ~400 characters before injection
|
|
466
|
+
3. Guarantees at least 1 memory is always included
|
|
467
|
+
|
|
468
|
+
### Example: Adaptive Mode
|
|
469
|
+
|
|
470
|
+
For quality-sensitive scenarios where you want to avoid low-relevance memories:
|
|
471
|
+
|
|
472
|
+
```json
|
|
473
|
+
{
|
|
474
|
+
"injection": {
|
|
475
|
+
"mode": "adaptive",
|
|
476
|
+
"maxMemories": 5,
|
|
477
|
+
"minMemories": 1,
|
|
478
|
+
"scoreDropTolerance": 0.15,
|
|
479
|
+
"injectionFloor": 0.3
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
This configuration:
|
|
485
|
+
1. Starts with up to 5 candidate memories
|
|
486
|
+
2. Stops adding memories when score drops >15% from the top
|
|
487
|
+
3. Ensures minimum score threshold (floor) prevents low-quality injection
|
|
488
|
+
4. Always includes at least 1 memory
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
360
492
|
## OpenAI Embedding Configuration
|
|
361
493
|
|
|
362
494
|
Default behavior stays on Ollama. To use OpenAI embeddings, set `embedding.provider` to `openai` and provide API key + model.
|
package/dist/config.js
CHANGED
|
@@ -38,6 +38,7 @@ export function resolveMemoryConfig(config, worktree) {
|
|
|
38
38
|
? process.env.LANCEDB_OPENCODE_PRO_OPENAI_TIMEOUT_MS ?? process.env.LANCEDB_OPENCODE_PRO_EMBEDDING_TIMEOUT_MS
|
|
39
39
|
: process.env.LANCEDB_OPENCODE_PRO_EMBEDDING_TIMEOUT_MS;
|
|
40
40
|
const timeoutRaw = timeoutEnv ?? embeddingRaw.timeoutMs;
|
|
41
|
+
const injection = resolveInjectionConfig(raw, process.env);
|
|
41
42
|
const resolvedConfig = {
|
|
42
43
|
provider,
|
|
43
44
|
dbPath,
|
|
@@ -58,6 +59,7 @@ export function resolveMemoryConfig(config, worktree) {
|
|
|
58
59
|
recencyHalfLifeHours,
|
|
59
60
|
importanceWeight,
|
|
60
61
|
},
|
|
62
|
+
injection,
|
|
61
63
|
includeGlobalScope: toBoolean(process.env.LANCEDB_OPENCODE_PRO_INCLUDE_GLOBAL_SCOPE ?? raw.includeGlobalScope, true),
|
|
62
64
|
globalDetectionThreshold: Math.max(1, Math.floor(toNumber(process.env.LANCEDB_OPENCODE_PRO_GLOBAL_DETECTION_THRESHOLD ?? raw.globalDetectionThreshold, 2))),
|
|
63
65
|
globalDiscountFactor: clamp(toNumber(process.env.LANCEDB_OPENCODE_PRO_GLOBAL_DISCOUNT_FACTOR ?? raw.globalDiscountFactor, 0.7), 0, 1),
|
|
@@ -75,6 +77,44 @@ function resolveEmbeddingProvider(raw) {
|
|
|
75
77
|
return "openai";
|
|
76
78
|
throw new Error(`[lancedb-opencode-pro] Invalid embedding provider "${raw}". Expected "ollama" or "openai".`);
|
|
77
79
|
}
|
|
80
|
+
function resolveInjectionMode(raw) {
|
|
81
|
+
if (raw === "fixed" || raw === "budget" || raw === "adaptive")
|
|
82
|
+
return raw;
|
|
83
|
+
return "fixed";
|
|
84
|
+
}
|
|
85
|
+
function resolveSummarizationMode(raw) {
|
|
86
|
+
if (raw === "none" || raw === "truncate" || raw === "extract" || raw === "auto")
|
|
87
|
+
return raw;
|
|
88
|
+
return "none";
|
|
89
|
+
}
|
|
90
|
+
function resolveCodeTruncationMode(raw) {
|
|
91
|
+
if (raw === "smart" || raw === "signature" || raw === "preserve")
|
|
92
|
+
return raw;
|
|
93
|
+
return "smart";
|
|
94
|
+
}
|
|
95
|
+
function resolveInjectionConfig(raw, env) {
|
|
96
|
+
const injectionRaw = (raw.injection ?? {});
|
|
97
|
+
const codeSummarizationRaw = (injectionRaw.codeSummarization ?? {});
|
|
98
|
+
return {
|
|
99
|
+
mode: resolveInjectionMode(env.LANCEDB_OPENCODE_PRO_INJECTION_MODE ?? injectionRaw.mode),
|
|
100
|
+
maxMemories: Math.max(1, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_MAX_MEMORIES ?? injectionRaw.maxMemories, 3))),
|
|
101
|
+
minMemories: Math.max(1, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_MIN_MEMORIES ?? injectionRaw.minMemories, 1))),
|
|
102
|
+
budgetTokens: Math.max(256, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_BUDGET_TOKENS ?? injectionRaw.budgetTokens, 4096))),
|
|
103
|
+
maxCharsPerMemory: Math.max(100, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_MAX_CHARS ?? injectionRaw.maxCharsPerMemory, 1200))),
|
|
104
|
+
summarization: resolveSummarizationMode(env.LANCEDB_OPENCODE_PRO_INJECTION_SUMMARIZATION ?? injectionRaw.summarization),
|
|
105
|
+
summaryTargetChars: Math.max(50, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_SUMMARY_TARGET_CHARS ?? injectionRaw.summaryTargetChars, 300))),
|
|
106
|
+
scoreDropTolerance: clamp(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_SCORE_DROP_TOLERANCE ?? injectionRaw.scoreDropTolerance, 0.15), 0, 1),
|
|
107
|
+
injectionFloor: clamp(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_FLOOR ?? injectionRaw.injectionFloor, 0.2), 0, 1),
|
|
108
|
+
codeSummarization: {
|
|
109
|
+
enabled: toBoolean(env.LANCEDB_OPENCODE_PRO_CODE_SUMMARIZATION_ENABLED ?? codeSummarizationRaw.enabled, true),
|
|
110
|
+
pureCodeThreshold: Math.max(100, Math.floor(toNumber(codeSummarizationRaw.pureCodeThreshold, 500))),
|
|
111
|
+
maxCodeLines: Math.max(5, Math.floor(toNumber(codeSummarizationRaw.maxCodeLines, 15))),
|
|
112
|
+
codeTruncationMode: resolveCodeTruncationMode(codeSummarizationRaw.codeTruncationMode),
|
|
113
|
+
preserveComments: toBoolean(codeSummarizationRaw.preserveComments, true),
|
|
114
|
+
preserveImports: toBoolean(codeSummarizationRaw.preserveImports, false),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
78
118
|
function validateEmbeddingConfig(embedding) {
|
|
79
119
|
if (embedding.provider !== "openai")
|
|
80
120
|
return;
|
|
@@ -130,6 +170,14 @@ function mergeMemoryConfig(base, override) {
|
|
|
130
170
|
...(base.retrieval ?? {}),
|
|
131
171
|
...(override.retrieval ?? {}),
|
|
132
172
|
},
|
|
173
|
+
injection: {
|
|
174
|
+
...(base.injection ?? {}),
|
|
175
|
+
...(override.injection ?? {}),
|
|
176
|
+
codeSummarization: {
|
|
177
|
+
...((base.injection ?? {}).codeSummarization ?? {}),
|
|
178
|
+
...((override.injection ?? {}).codeSummarization ?? {}),
|
|
179
|
+
},
|
|
180
|
+
},
|
|
133
181
|
};
|
|
134
182
|
}
|
|
135
183
|
function firstString(...values) {
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { isTcpPortAvailable, parsePortReservations, planPorts, reservationKey }
|
|
|
6
6
|
import { buildScopeFilter, deriveProjectScope } from "./scope.js";
|
|
7
7
|
import { MemoryStore } from "./store.js";
|
|
8
8
|
import { generateId } from "./utils.js";
|
|
9
|
+
import { calculateInjectionLimit, createSummarizationConfig, summarizeContent } from "./summarize.js";
|
|
9
10
|
const SCHEMA_VERSION = 1;
|
|
10
11
|
const plugin = async (input) => {
|
|
11
12
|
const state = await createRuntimeState(input);
|
|
@@ -52,16 +53,19 @@ const plugin = async (input) => {
|
|
|
52
53
|
query,
|
|
53
54
|
queryVector,
|
|
54
55
|
scopes,
|
|
55
|
-
limit:
|
|
56
|
+
limit: state.config.injection.maxMemories * 2, // Fetch more than needed for filtering
|
|
56
57
|
vectorWeight: state.config.retrieval.mode === "vector" ? 1 : state.config.retrieval.vectorWeight,
|
|
57
58
|
bm25Weight: state.config.retrieval.mode === "vector" ? 0 : state.config.retrieval.bm25Weight,
|
|
58
|
-
minScore: state.config.retrieval.minScore,
|
|
59
|
+
minScore: Math.max(state.config.retrieval.minScore, state.config.injection.injectionFloor),
|
|
59
60
|
rrfK: state.config.retrieval.rrfK,
|
|
60
61
|
recencyBoost: state.config.retrieval.recencyBoost,
|
|
61
62
|
recencyHalfLifeHours: state.config.retrieval.recencyHalfLifeHours,
|
|
62
63
|
importanceWeight: state.config.retrieval.importanceWeight,
|
|
63
64
|
globalDiscountFactor: state.config.globalDiscountFactor,
|
|
64
65
|
});
|
|
66
|
+
// Apply injection control
|
|
67
|
+
const injectionLimit = calculateInjectionLimit(results, state.config.injection);
|
|
68
|
+
const limitedResults = results.slice(0, injectionLimit);
|
|
65
69
|
await state.store.putEvent({
|
|
66
70
|
id: generateId(),
|
|
67
71
|
type: "recall",
|
|
@@ -69,21 +73,32 @@ const plugin = async (input) => {
|
|
|
69
73
|
scope: activeScope,
|
|
70
74
|
sessionID: eventInput.sessionID,
|
|
71
75
|
timestamp: Date.now(),
|
|
72
|
-
resultCount:
|
|
73
|
-
injected:
|
|
76
|
+
resultCount: limitedResults.length,
|
|
77
|
+
injected: limitedResults.length > 0,
|
|
74
78
|
metadataJson: JSON.stringify({
|
|
75
79
|
source: "system-transform",
|
|
76
80
|
includeGlobalScope: state.config.includeGlobalScope,
|
|
81
|
+
injectionMode: state.config.injection.mode,
|
|
82
|
+
injectionLimit: injectionLimit,
|
|
77
83
|
}),
|
|
78
84
|
});
|
|
79
|
-
if (
|
|
85
|
+
if (limitedResults.length === 0)
|
|
80
86
|
return;
|
|
81
|
-
for (const result of
|
|
87
|
+
for (const result of limitedResults) {
|
|
82
88
|
state.store.updateMemoryUsage(result.record.id, activeScope, scopes).catch(() => { });
|
|
83
89
|
}
|
|
90
|
+
// Apply summarization if configured
|
|
91
|
+
const summarizationConfig = createSummarizationConfig(state.config.injection);
|
|
92
|
+
const processedResults = limitedResults.map((item) => {
|
|
93
|
+
if (state.config.injection.summarization === "none") {
|
|
94
|
+
return { ...item, text: item.record.text };
|
|
95
|
+
}
|
|
96
|
+
const summarized = summarizeContent(item.record.text, summarizationConfig);
|
|
97
|
+
return { ...item, text: summarized.content };
|
|
98
|
+
});
|
|
84
99
|
const memoryBlock = [
|
|
85
100
|
"[Memory Recall - optional historical context]",
|
|
86
|
-
...
|
|
101
|
+
...processedResults.map((item, index) => `${index + 1}. [${item.record.id}] (${item.record.scope}) ${item.text}`),
|
|
87
102
|
"Use these as optional hints only; prioritize current user intent and current repo state.",
|
|
88
103
|
].join("\n");
|
|
89
104
|
eventOutput.system.push(memoryBlock);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ContentType, ContentDetection, SummarizedContent, SummarizationConfig, InjectionConfig, SearchResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Detects whether content contains code and its type
|
|
4
|
+
*/
|
|
5
|
+
export declare function detectContentType(text: string): ContentDetection;
|
|
6
|
+
/**
|
|
7
|
+
* Calculates bracket balance for code detection
|
|
8
|
+
*/
|
|
9
|
+
export declare function calculateBracketBalance(text: string): number;
|
|
10
|
+
/**
|
|
11
|
+
* Counts code-related keywords
|
|
12
|
+
*/
|
|
13
|
+
export declare function countCodeKeywords(text: string): number;
|
|
14
|
+
/**
|
|
15
|
+
* Calculates ratio of indented lines
|
|
16
|
+
*/
|
|
17
|
+
export declare function calculateIndentationRatio(text: string): number;
|
|
18
|
+
/**
|
|
19
|
+
* Estimates token count for content
|
|
20
|
+
*/
|
|
21
|
+
export declare function estimateTokens(text: string, contentType: ContentType): number;
|
|
22
|
+
/**
|
|
23
|
+
* Truncates text to max characters
|
|
24
|
+
*/
|
|
25
|
+
export declare function truncateText(text: string, maxChars: number): string;
|
|
26
|
+
/**
|
|
27
|
+
* Smart truncation for code - finds complete statement boundaries
|
|
28
|
+
*/
|
|
29
|
+
export declare function smartTruncateCode(code: string, maxLines: number, config?: {
|
|
30
|
+
preserveComments?: boolean;
|
|
31
|
+
preserveImports?: boolean;
|
|
32
|
+
}): string;
|
|
33
|
+
/**
|
|
34
|
+
* Extracts key sentences from text
|
|
35
|
+
*/
|
|
36
|
+
export declare function extractKeySentences(text: string, targetChars: number): string;
|
|
37
|
+
export declare function splitCodeAndText(text: string): Array<{
|
|
38
|
+
type: "code" | "text";
|
|
39
|
+
content: string;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Main summarization function
|
|
43
|
+
*/
|
|
44
|
+
export declare function summarizeContent(text: string, config: SummarizationConfig): SummarizedContent;
|
|
45
|
+
/**
|
|
46
|
+
* Calculates injection limit based on mode
|
|
47
|
+
*/
|
|
48
|
+
export declare function calculateInjectionLimit(results: SearchResult[], config: InjectionConfig): number;
|
|
49
|
+
/**
|
|
50
|
+
* Creates default summarization config from injection config
|
|
51
|
+
*/
|
|
52
|
+
export declare function createSummarizationConfig(injection: InjectionConfig): SummarizationConfig;
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// Code keywords used for content detection
|
|
2
|
+
const CODE_KEYWORDS = [
|
|
3
|
+
"function", "async", "await", "const", "let", "var", "return", "class", "interface", "type",
|
|
4
|
+
"import", "export", "from", "default", "extends", "implements", "new", "this", "super",
|
|
5
|
+
"def ", "async def", "func ", "fn ", "pub fn", "impl ", "struct ", "enum ",
|
|
6
|
+
"=>", "->", "::", "if (", "for (", "while (", "try {", "catch (", "throw ",
|
|
7
|
+
];
|
|
8
|
+
// Keywords for key sentence extraction
|
|
9
|
+
const KEY_SENTENCE_PATTERNS = [
|
|
10
|
+
/(?:fixed|resolved|works?\s+now|successful|done|完成|已解決|修復|成功)/i,
|
|
11
|
+
/(?:probleme|issue|bug|error|fail|錯誤|問題|失敗)/i,
|
|
12
|
+
/(?:solution|fix|resolve|解決方案|修正)/i,
|
|
13
|
+
/(?:because|root\s+cause|原因|由於)/i,
|
|
14
|
+
/(?:decide|decision|tradeoff|architecture|決定|架構|採用)/i,
|
|
15
|
+
/(?:prefer|preference|偏好|習慣)/i,
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Detects whether content contains code and its type
|
|
19
|
+
*/
|
|
20
|
+
export function detectContentType(text) {
|
|
21
|
+
const hasMarkdownCode = /```[\s\S]*?```/.test(text);
|
|
22
|
+
const bracketBalance = calculateBracketBalance(text);
|
|
23
|
+
const codeKeywords = countCodeKeywords(text);
|
|
24
|
+
const indentationRatio = calculateIndentationRatio(text);
|
|
25
|
+
const codeScore = (hasMarkdownCode ? 2 : 0) +
|
|
26
|
+
(bracketBalance > 3 ? 1 : 0) +
|
|
27
|
+
(codeKeywords > 5 ? 1 : 0) +
|
|
28
|
+
(indentationRatio > 0.3 ? 1 : 0);
|
|
29
|
+
if (codeScore >= 5) {
|
|
30
|
+
return { hasCode: true, isPureCode: true };
|
|
31
|
+
}
|
|
32
|
+
if (codeScore >= 3) {
|
|
33
|
+
return { hasCode: true, isPureCode: false };
|
|
34
|
+
}
|
|
35
|
+
if (hasMarkdownCode || codeKeywords > 10) {
|
|
36
|
+
return { hasCode: true, isPureCode: false };
|
|
37
|
+
}
|
|
38
|
+
return { hasCode: false, isPureCode: false };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Calculates bracket balance for code detection
|
|
42
|
+
*/
|
|
43
|
+
export function calculateBracketBalance(text) {
|
|
44
|
+
const openBrackets = (text.match(/[{([]/g) || []).length;
|
|
45
|
+
const closeBrackets = (text.match(/[})\]]/g) || []).length;
|
|
46
|
+
return Math.abs(openBrackets - closeBrackets) + Math.min(openBrackets, closeBrackets);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Counts code-related keywords
|
|
50
|
+
*/
|
|
51
|
+
export function countCodeKeywords(text) {
|
|
52
|
+
const lower = text.toLowerCase();
|
|
53
|
+
let count = 0;
|
|
54
|
+
for (const keyword of CODE_KEYWORDS) {
|
|
55
|
+
if (lower.includes(keyword.toLowerCase())) {
|
|
56
|
+
count += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return count;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Calculates ratio of indented lines
|
|
63
|
+
*/
|
|
64
|
+
export function calculateIndentationRatio(text) {
|
|
65
|
+
const lines = text.split("\n");
|
|
66
|
+
if (lines.length === 0)
|
|
67
|
+
return 0;
|
|
68
|
+
const indentedLines = lines.filter((line) => /^\s{2,}/.test(line));
|
|
69
|
+
return indentedLines.length / lines.length;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Estimates token count for content
|
|
73
|
+
*/
|
|
74
|
+
export function estimateTokens(text, contentType) {
|
|
75
|
+
// Count Chinese characters
|
|
76
|
+
const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
|
|
77
|
+
const nonChineseChars = text.length - chineseChars;
|
|
78
|
+
// Chinese ~2 chars/token, English/other ~4 chars/token
|
|
79
|
+
const baseTokens = Math.ceil(chineseChars / 2 + nonChineseChars / 4);
|
|
80
|
+
// Code has higher token density
|
|
81
|
+
if (contentType === "code") {
|
|
82
|
+
return Math.ceil(baseTokens * 1.2);
|
|
83
|
+
}
|
|
84
|
+
return baseTokens;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Truncates text to max characters
|
|
88
|
+
*/
|
|
89
|
+
export function truncateText(text, maxChars) {
|
|
90
|
+
if (text.length <= maxChars)
|
|
91
|
+
return text;
|
|
92
|
+
return `${text.slice(0, maxChars - 3)}...`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Smart truncation for code - finds complete statement boundaries
|
|
96
|
+
*/
|
|
97
|
+
export function smartTruncateCode(code, maxLines, config) {
|
|
98
|
+
const lines = code.split("\n");
|
|
99
|
+
if (lines.length <= maxLines)
|
|
100
|
+
return code;
|
|
101
|
+
let braceBalance = 0;
|
|
102
|
+
let lastCompleteIndex = maxLines;
|
|
103
|
+
let foundComplete = false;
|
|
104
|
+
// Calculate brace balance and find last complete statement
|
|
105
|
+
for (let i = 0; i < Math.min(lines.length, maxLines + 10); i++) {
|
|
106
|
+
const line = lines[i];
|
|
107
|
+
braceBalance += (line.match(/{/g) || []).length;
|
|
108
|
+
braceBalance -= (line.match(/}/g) || []).length;
|
|
109
|
+
if (i >= maxLines - 5 && braceBalance === 0 && i < lines.length - 1) {
|
|
110
|
+
lastCompleteIndex = i + 1;
|
|
111
|
+
foundComplete = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// If no complete boundary found, use maxLines
|
|
116
|
+
if (!foundComplete) {
|
|
117
|
+
lastCompleteIndex = maxLines;
|
|
118
|
+
}
|
|
119
|
+
// Build truncated code
|
|
120
|
+
let result = lines.slice(0, lastCompleteIndex).join("\n");
|
|
121
|
+
// Add truncation indicator
|
|
122
|
+
result += "\n// ... (truncated)";
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Extracts key sentences from text
|
|
127
|
+
*/
|
|
128
|
+
export function extractKeySentences(text, targetChars) {
|
|
129
|
+
const sentences = text.split(/[。.!?\n]+/).filter((s) => s.trim().length > 0);
|
|
130
|
+
const keySentences = [];
|
|
131
|
+
let currentLength = 0;
|
|
132
|
+
// First pass: sentences matching key patterns
|
|
133
|
+
for (const sentence of sentences) {
|
|
134
|
+
const trimmed = sentence.trim();
|
|
135
|
+
if (KEY_SENTENCE_PATTERNS.some((pattern) => pattern.test(trimmed))) {
|
|
136
|
+
if (currentLength + trimmed.length > targetChars && keySentences.length > 0) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
keySentences.push(trimmed);
|
|
140
|
+
currentLength += trimmed.length + 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Second pass: fill remaining with first sentences if needed
|
|
144
|
+
if (currentLength < targetChars * 0.5) {
|
|
145
|
+
for (const sentence of sentences) {
|
|
146
|
+
const trimmed = sentence.trim();
|
|
147
|
+
if (!keySentences.includes(trimmed)) {
|
|
148
|
+
if (currentLength + trimmed.length > targetChars) {
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
keySentences.push(trimmed);
|
|
152
|
+
currentLength += trimmed.length + 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return keySentences.join(" → ");
|
|
157
|
+
}
|
|
158
|
+
export function splitCodeAndText(text) {
|
|
159
|
+
const parts = [];
|
|
160
|
+
const codeBlockRegex = /```[\s\S]*?```/g;
|
|
161
|
+
let lastIndex = 0;
|
|
162
|
+
let match = codeBlockRegex.exec(text);
|
|
163
|
+
while (match !== null) {
|
|
164
|
+
if (match.index > lastIndex) {
|
|
165
|
+
const textPart = text.slice(lastIndex, match.index).trim();
|
|
166
|
+
if (textPart) {
|
|
167
|
+
parts.push({ type: "text", content: textPart });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
parts.push({ type: "code", content: match[0] });
|
|
171
|
+
lastIndex = match.index + match[0].length;
|
|
172
|
+
match = codeBlockRegex.exec(text);
|
|
173
|
+
}
|
|
174
|
+
if (lastIndex < text.length) {
|
|
175
|
+
const remaining = text.slice(lastIndex).trim();
|
|
176
|
+
if (remaining) {
|
|
177
|
+
parts.push({ type: "text", content: remaining });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return parts;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Main summarization function
|
|
184
|
+
*/
|
|
185
|
+
export function summarizeContent(text, config) {
|
|
186
|
+
const detection = detectContentType(text);
|
|
187
|
+
const originalLength = text.length;
|
|
188
|
+
// Determine content type
|
|
189
|
+
const contentType = detection.isPureCode
|
|
190
|
+
? "code"
|
|
191
|
+
: detection.hasCode
|
|
192
|
+
? "mixed"
|
|
193
|
+
: "text";
|
|
194
|
+
// No summarization
|
|
195
|
+
if (config.mode === "none") {
|
|
196
|
+
return {
|
|
197
|
+
type: "kept",
|
|
198
|
+
content: truncateText(text, config.textThreshold * 4), // Max chars limit
|
|
199
|
+
originalLength,
|
|
200
|
+
estimatedTokens: estimateTokens(text, contentType),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// Pure text
|
|
204
|
+
if (contentType === "text") {
|
|
205
|
+
if (text.length <= config.textThreshold) {
|
|
206
|
+
return {
|
|
207
|
+
type: "kept",
|
|
208
|
+
content: text,
|
|
209
|
+
originalLength,
|
|
210
|
+
estimatedTokens: estimateTokens(text, contentType),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (config.mode === "truncate") {
|
|
214
|
+
const truncated = truncateText(text, config.summaryTargetChars);
|
|
215
|
+
return {
|
|
216
|
+
type: "truncated",
|
|
217
|
+
content: truncated,
|
|
218
|
+
originalLength,
|
|
219
|
+
estimatedTokens: estimateTokens(truncated, contentType),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const extracted = extractKeySentences(text, config.summaryTargetChars);
|
|
223
|
+
return {
|
|
224
|
+
type: "summarized",
|
|
225
|
+
content: extracted,
|
|
226
|
+
originalLength,
|
|
227
|
+
estimatedTokens: estimateTokens(extracted, contentType),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Pure code
|
|
231
|
+
if (contentType === "code") {
|
|
232
|
+
if (text.length <= config.codeThreshold) {
|
|
233
|
+
return {
|
|
234
|
+
type: "kept",
|
|
235
|
+
content: text,
|
|
236
|
+
originalLength,
|
|
237
|
+
estimatedTokens: estimateTokens(text, contentType),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const truncated = smartTruncateCode(text, config.maxCodeLines, {
|
|
241
|
+
preserveComments: config.preserveComments,
|
|
242
|
+
preserveImports: config.preserveImports,
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
type: "truncated",
|
|
246
|
+
content: truncated,
|
|
247
|
+
originalLength,
|
|
248
|
+
estimatedTokens: estimateTokens(truncated, contentType),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// Mixed content
|
|
252
|
+
if (config.mode === "auto" || config.mode === "extract") {
|
|
253
|
+
const parts = splitCodeAndText(text);
|
|
254
|
+
const summarizedParts = [];
|
|
255
|
+
for (const part of parts) {
|
|
256
|
+
if (part.type === "text") {
|
|
257
|
+
if (part.content.length <= config.textThreshold) {
|
|
258
|
+
summarizedParts.push(part.content);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
summarizedParts.push(extractKeySentences(part.content, config.summaryTargetChars / 2));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
if (part.content.length <= config.codeThreshold) {
|
|
266
|
+
summarizedParts.push(part.content);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
summarizedParts.push(smartTruncateCode(part.content, config.maxCodeLines));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
type: "mixed",
|
|
275
|
+
content: summarizedParts.join("\n\n"),
|
|
276
|
+
originalLength,
|
|
277
|
+
estimatedTokens: estimateTokens(summarizedParts.join("\n\n"), contentType),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// Fallback: truncate
|
|
281
|
+
return {
|
|
282
|
+
type: "truncated",
|
|
283
|
+
content: truncateText(text, config.summaryTargetChars),
|
|
284
|
+
originalLength,
|
|
285
|
+
estimatedTokens: estimateTokens(truncateText(text, config.summaryTargetChars), contentType),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Calculates injection limit based on mode
|
|
290
|
+
*/
|
|
291
|
+
export function calculateInjectionLimit(results, config) {
|
|
292
|
+
// Filter by injection floor
|
|
293
|
+
const filteredResults = results.filter((r) => r.score >= config.injectionFloor);
|
|
294
|
+
// Fixed mode: simple limit
|
|
295
|
+
if (config.mode === "fixed") {
|
|
296
|
+
return Math.min(config.maxMemories, filteredResults.length);
|
|
297
|
+
}
|
|
298
|
+
// Budget mode: accumulate until budget exhausted
|
|
299
|
+
if (config.mode === "budget") {
|
|
300
|
+
let accumulatedTokens = 0;
|
|
301
|
+
let count = 0;
|
|
302
|
+
for (const result of filteredResults) {
|
|
303
|
+
const tokens = estimateTokens(result.record.text, detectContentType(result.record.text).isPureCode ? "code" : "text");
|
|
304
|
+
if (accumulatedTokens + tokens > config.budgetTokens && count >= config.minMemories) {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
accumulatedTokens += tokens;
|
|
308
|
+
count += 1;
|
|
309
|
+
if (count >= config.maxMemories) {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return Math.max(config.minMemories, Math.min(count, config.maxMemories));
|
|
314
|
+
}
|
|
315
|
+
// Adaptive mode: stop on score drop
|
|
316
|
+
if (config.mode === "adaptive") {
|
|
317
|
+
let count = 0;
|
|
318
|
+
let prevScore = filteredResults[0]?.score ?? 0;
|
|
319
|
+
for (const result of filteredResults) {
|
|
320
|
+
const scoreDrop = prevScore - result.score;
|
|
321
|
+
// Stop if score drops below tolerance (but respect minimum)
|
|
322
|
+
if (scoreDrop > config.scoreDropTolerance && count >= config.minMemories) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
count += 1;
|
|
326
|
+
prevScore = result.score;
|
|
327
|
+
if (count >= config.maxMemories) {
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return Math.max(config.minMemories, Math.min(count, filteredResults.length));
|
|
332
|
+
}
|
|
333
|
+
// Fallback
|
|
334
|
+
return Math.min(config.maxMemories, filteredResults.length);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Creates default summarization config from injection config
|
|
338
|
+
*/
|
|
339
|
+
export function createSummarizationConfig(injection) {
|
|
340
|
+
return {
|
|
341
|
+
mode: injection.summarization,
|
|
342
|
+
textThreshold: 300,
|
|
343
|
+
codeThreshold: injection.codeSummarization.pureCodeThreshold,
|
|
344
|
+
summaryTargetChars: injection.summaryTargetChars,
|
|
345
|
+
maxCodeLines: injection.codeSummarization.maxCodeLines,
|
|
346
|
+
codeTruncationMode: injection.codeSummarization.codeTruncationMode,
|
|
347
|
+
preserveComments: injection.codeSummarization.preserveComments,
|
|
348
|
+
preserveImports: injection.codeSummarization.preserveImports,
|
|
349
|
+
};
|
|
350
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
export type EmbeddingProvider = "ollama" | "openai";
|
|
2
2
|
export type RetrievalMode = "hybrid" | "vector";
|
|
3
|
+
export type InjectionMode = "fixed" | "budget" | "adaptive";
|
|
4
|
+
export type SummarizationMode = "none" | "truncate" | "extract" | "auto";
|
|
5
|
+
export type CodeTruncationMode = "smart" | "signature" | "preserve";
|
|
6
|
+
export type ContentType = "text" | "code" | "mixed";
|
|
7
|
+
export interface ContentDetection {
|
|
8
|
+
hasCode: boolean;
|
|
9
|
+
isPureCode: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SummarizedContent {
|
|
12
|
+
type: "kept" | "truncated" | "summarized" | "mixed";
|
|
13
|
+
content: string;
|
|
14
|
+
originalLength: number;
|
|
15
|
+
estimatedTokens: number;
|
|
16
|
+
}
|
|
3
17
|
export type MemoryCategory = "preference" | "fact" | "decision" | "entity" | "other";
|
|
4
18
|
export type CaptureOutcome = "considered" | "skipped" | "stored";
|
|
5
19
|
export type CaptureSkipReason = "empty-buffer" | "below-min-chars" | "no-positive-signal" | "initialization-unavailable" | "embedding-unavailable" | "empty-embedding";
|
|
@@ -23,11 +37,42 @@ export interface RetrievalConfig {
|
|
|
23
37
|
recencyHalfLifeHours: number;
|
|
24
38
|
importanceWeight: number;
|
|
25
39
|
}
|
|
40
|
+
export interface CodeSummarizationConfig {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
pureCodeThreshold: number;
|
|
43
|
+
maxCodeLines: number;
|
|
44
|
+
codeTruncationMode: CodeTruncationMode;
|
|
45
|
+
preserveComments: boolean;
|
|
46
|
+
preserveImports: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface InjectionConfig {
|
|
49
|
+
mode: InjectionMode;
|
|
50
|
+
maxMemories: number;
|
|
51
|
+
minMemories: number;
|
|
52
|
+
budgetTokens: number;
|
|
53
|
+
maxCharsPerMemory: number;
|
|
54
|
+
summarization: SummarizationMode;
|
|
55
|
+
summaryTargetChars: number;
|
|
56
|
+
scoreDropTolerance: number;
|
|
57
|
+
injectionFloor: number;
|
|
58
|
+
codeSummarization: CodeSummarizationConfig;
|
|
59
|
+
}
|
|
60
|
+
export interface SummarizationConfig {
|
|
61
|
+
mode: SummarizationMode;
|
|
62
|
+
textThreshold: number;
|
|
63
|
+
codeThreshold: number;
|
|
64
|
+
summaryTargetChars: number;
|
|
65
|
+
maxCodeLines: number;
|
|
66
|
+
codeTruncationMode: CodeTruncationMode;
|
|
67
|
+
preserveComments: boolean;
|
|
68
|
+
preserveImports: boolean;
|
|
69
|
+
}
|
|
26
70
|
export interface MemoryRuntimeConfig {
|
|
27
71
|
provider: string;
|
|
28
72
|
dbPath: string;
|
|
29
73
|
embedding: EmbeddingConfig;
|
|
30
74
|
retrieval: RetrievalConfig;
|
|
75
|
+
injection: InjectionConfig;
|
|
31
76
|
includeGlobalScope: boolean;
|
|
32
77
|
globalDetectionThreshold: number;
|
|
33
78
|
globalDiscountFactor: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lancedb-opencode-pro",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "LanceDB-backed long-term memory provider for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"prepublishOnly": "npm run verify:full"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@lancedb/lancedb": "^0.
|
|
59
|
+
"@lancedb/lancedb": "^0.27.1",
|
|
60
60
|
"@opencode-ai/plugin": "1.2.25",
|
|
61
61
|
"@opencode-ai/sdk": "1.2.25"
|
|
62
62
|
},
|