opencode-lcm 0.11.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/CHANGELOG.md +83 -0
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/archive-transform.d.ts +45 -0
- package/dist/archive-transform.js +81 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +16 -0
- package/dist/doctor.d.ts +22 -0
- package/dist/doctor.js +44 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +306 -0
- package/dist/logging.d.ts +14 -0
- package/dist/logging.js +28 -0
- package/dist/options.d.ts +3 -0
- package/dist/options.js +217 -0
- package/dist/preview-providers.d.ts +20 -0
- package/dist/preview-providers.js +246 -0
- package/dist/privacy.d.ts +16 -0
- package/dist/privacy.js +92 -0
- package/dist/search-ranking.d.ts +12 -0
- package/dist/search-ranking.js +98 -0
- package/dist/sql-utils.d.ts +31 -0
- package/dist/sql-utils.js +80 -0
- package/dist/store-artifacts.d.ts +50 -0
- package/dist/store-artifacts.js +374 -0
- package/dist/store-retention.d.ts +39 -0
- package/dist/store-retention.js +90 -0
- package/dist/store-search.d.ts +37 -0
- package/dist/store-search.js +298 -0
- package/dist/store-snapshot.d.ts +133 -0
- package/dist/store-snapshot.js +325 -0
- package/dist/store-types.d.ts +14 -0
- package/dist/store-types.js +5 -0
- package/dist/store.d.ts +316 -0
- package/dist/store.js +3673 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +35 -0
- package/dist/utils.js +414 -0
- package/dist/workspace-path.d.ts +1 -0
- package/dist/workspace-path.js +15 -0
- package/dist/worktree-key.d.ts +1 -0
- package/dist/worktree-key.js +6 -0
- package/package.json +61 -0
- package/src/archive-transform.ts +147 -0
- package/src/bun-sqlite.d.ts +18 -0
- package/src/constants.ts +20 -0
- package/src/doctor.ts +83 -0
- package/src/index.ts +330 -0
- package/src/logging.ts +41 -0
- package/src/options.ts +297 -0
- package/src/preview-providers.ts +298 -0
- package/src/privacy.ts +122 -0
- package/src/search-ranking.ts +145 -0
- package/src/sql-utils.ts +107 -0
- package/src/store-artifacts.ts +666 -0
- package/src/store-retention.ts +152 -0
- package/src/store-search.ts +440 -0
- package/src/store-snapshot.ts +582 -0
- package/src/store-types.ts +16 -0
- package/src/store.ts +4926 -0
- package/src/types.ts +132 -0
- package/src/utils.ts +444 -0
- package/src/workspace-path.ts +20 -0
- package/src/worktree-key.ts +5 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Message, Part } from '@opencode-ai/sdk';
|
|
2
|
+
export type InteropOptions = {
|
|
3
|
+
contextMode: boolean;
|
|
4
|
+
neverOverrideCompactionPrompt: boolean;
|
|
5
|
+
ignoreToolPrefixes: string[];
|
|
6
|
+
};
|
|
7
|
+
export type ScopeName = 'session' | 'root' | 'worktree' | 'all';
|
|
8
|
+
export type ScopeDefaults = {
|
|
9
|
+
grep: ScopeName;
|
|
10
|
+
describe: ScopeName;
|
|
11
|
+
};
|
|
12
|
+
export type ScopeProfile = {
|
|
13
|
+
worktree: string;
|
|
14
|
+
grep?: ScopeName;
|
|
15
|
+
describe?: ScopeName;
|
|
16
|
+
};
|
|
17
|
+
export type RetentionPolicyOptions = {
|
|
18
|
+
staleSessionDays?: number;
|
|
19
|
+
deletedSessionDays?: number;
|
|
20
|
+
orphanBlobDays?: number;
|
|
21
|
+
};
|
|
22
|
+
export type PrivacyOptions = {
|
|
23
|
+
excludeToolPrefixes: string[];
|
|
24
|
+
excludePathPatterns: string[];
|
|
25
|
+
redactPatterns: string[];
|
|
26
|
+
};
|
|
27
|
+
export type AutomaticRetrievalScopeBudgets = {
|
|
28
|
+
session: number;
|
|
29
|
+
root: number;
|
|
30
|
+
worktree: number;
|
|
31
|
+
all: number;
|
|
32
|
+
};
|
|
33
|
+
export type AutomaticRetrievalStopOptions = {
|
|
34
|
+
targetHits: number;
|
|
35
|
+
stopOnFirstScopeWithHits: boolean;
|
|
36
|
+
};
|
|
37
|
+
export type AutomaticRetrievalOptions = {
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
maxChars: number;
|
|
40
|
+
minTokens: number;
|
|
41
|
+
maxMessageHits: number;
|
|
42
|
+
maxSummaryHits: number;
|
|
43
|
+
maxArtifactHits: number;
|
|
44
|
+
scopeOrder: ScopeName[];
|
|
45
|
+
scopeBudgets: AutomaticRetrievalScopeBudgets;
|
|
46
|
+
stop: AutomaticRetrievalStopOptions;
|
|
47
|
+
};
|
|
48
|
+
export type OpencodeLcmOptions = {
|
|
49
|
+
interop: InteropOptions;
|
|
50
|
+
scopeDefaults: ScopeDefaults;
|
|
51
|
+
scopeProfiles: ScopeProfile[];
|
|
52
|
+
retention: RetentionPolicyOptions;
|
|
53
|
+
privacy: PrivacyOptions;
|
|
54
|
+
automaticRetrieval: AutomaticRetrievalOptions;
|
|
55
|
+
compactContextLimit: number;
|
|
56
|
+
systemHint: boolean;
|
|
57
|
+
storeDir?: string;
|
|
58
|
+
freshTailMessages: number;
|
|
59
|
+
minMessagesForTransform: number;
|
|
60
|
+
summaryCharBudget: number;
|
|
61
|
+
partCharBudget: number;
|
|
62
|
+
largeContentThreshold: number;
|
|
63
|
+
artifactPreviewChars: number;
|
|
64
|
+
artifactViewChars: number;
|
|
65
|
+
binaryPreviewProviders: string[];
|
|
66
|
+
previewBytePeek: number;
|
|
67
|
+
};
|
|
68
|
+
export type CapturedEvent = {
|
|
69
|
+
id: string;
|
|
70
|
+
type: string;
|
|
71
|
+
sessionID?: string;
|
|
72
|
+
timestamp: number;
|
|
73
|
+
payload: unknown;
|
|
74
|
+
};
|
|
75
|
+
export type SearchResult = {
|
|
76
|
+
id: string;
|
|
77
|
+
type: string;
|
|
78
|
+
sessionID?: string;
|
|
79
|
+
timestamp: number;
|
|
80
|
+
snippet: string;
|
|
81
|
+
};
|
|
82
|
+
export type StoreStats = {
|
|
83
|
+
schemaVersion: number;
|
|
84
|
+
totalEvents: number;
|
|
85
|
+
sessionCount: number;
|
|
86
|
+
latestEventAt?: number;
|
|
87
|
+
eventTypes: Record<string, number>;
|
|
88
|
+
summaryNodeCount: number;
|
|
89
|
+
summaryStateCount: number;
|
|
90
|
+
rootSessionCount: number;
|
|
91
|
+
branchedSessionCount: number;
|
|
92
|
+
artifactCount: number;
|
|
93
|
+
artifactBlobCount: number;
|
|
94
|
+
sharedArtifactBlobCount: number;
|
|
95
|
+
orphanArtifactBlobCount: number;
|
|
96
|
+
worktreeCount: number;
|
|
97
|
+
pinnedSessionCount: number;
|
|
98
|
+
};
|
|
99
|
+
export type ConversationMessage = {
|
|
100
|
+
info: Message;
|
|
101
|
+
parts: Part[];
|
|
102
|
+
};
|
|
103
|
+
export type NormalizedSession = {
|
|
104
|
+
sessionID: string;
|
|
105
|
+
title?: string;
|
|
106
|
+
directory?: string;
|
|
107
|
+
parentSessionID?: string;
|
|
108
|
+
rootSessionID?: string;
|
|
109
|
+
lineageDepth?: number;
|
|
110
|
+
pinned?: boolean;
|
|
111
|
+
pinReason?: string;
|
|
112
|
+
updatedAt: number;
|
|
113
|
+
compactedAt?: number;
|
|
114
|
+
deleted?: boolean;
|
|
115
|
+
eventCount: number;
|
|
116
|
+
messages: ConversationMessage[];
|
|
117
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared pure utility functions used across the LCM codebase.
|
|
3
|
+
* These functions have no dependencies on store internals or external types.
|
|
4
|
+
*/
|
|
5
|
+
export declare function asRecord(value: unknown): Record<string, unknown> | undefined;
|
|
6
|
+
export declare function firstFiniteNumber(value: unknown): number | undefined;
|
|
7
|
+
export declare function truncate(text: string, limit: number): string;
|
|
8
|
+
export declare function shortNodeID(nodeID: string): string;
|
|
9
|
+
export declare function parseJson<T>(value: string): T;
|
|
10
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
11
|
+
export declare function hashContent(content: string): string;
|
|
12
|
+
export declare function tokenizeQuery(query: string): string[];
|
|
13
|
+
/** FTS5 reserved words that would be interpreted as operators/syntax inside MATCH. */
|
|
14
|
+
export declare const FTS5_RESERVED: Set<string>;
|
|
15
|
+
/** Drop FTS5-reserved words and too-short tokens to keep MATCH queries safe. */
|
|
16
|
+
export declare function sanitizeFtsTokens(tokens: string[]): string[];
|
|
17
|
+
export declare function buildSnippet(content: string, query?: string, limit?: number): string;
|
|
18
|
+
export declare function sanitizeAutomaticRetrievalSourceText(text: string): string;
|
|
19
|
+
export declare function isAutomaticRetrievalNoise(text: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Fast-path filter: drops only grammatical function words.
|
|
22
|
+
* Corpus-aware TF-IDF weighting should be applied downstream for full filtering.
|
|
23
|
+
*/
|
|
24
|
+
export declare function filterIntentTokens(tokens: string[]): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Returns the minimal grammatical stoplist for use in TF-IDF scoring.
|
|
27
|
+
* Tokens in this set are always dropped without hitting the FTS index.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getGrammaticalStopwords(): Set<string>;
|
|
30
|
+
export declare function shouldSuppressLowSignalAutomaticRetrievalAnchor(anchorText: string, anchorSignalCount: number, minTokens: number, anchorFileCount: number): boolean;
|
|
31
|
+
export declare function inferUrlScheme(url?: string): string | undefined;
|
|
32
|
+
export declare function inferFileExtension(file?: string): string | undefined;
|
|
33
|
+
export declare function classifyFileCategory(mime?: string, extension?: string): string;
|
|
34
|
+
export declare function formatMetadataValue(value: unknown): string | undefined;
|
|
35
|
+
export declare function formatRetentionDays(value?: number): string;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Shared pure utility functions used across the LCM codebase.
|
|
4
|
+
* These functions have no dependencies on store internals or external types.
|
|
5
|
+
*/
|
|
6
|
+
// --- Type guards ---
|
|
7
|
+
export function asRecord(value) {
|
|
8
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
9
|
+
return undefined;
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
export function firstFiniteNumber(value) {
|
|
13
|
+
const record = asRecord(value);
|
|
14
|
+
if (!record)
|
|
15
|
+
return undefined;
|
|
16
|
+
for (const entry of Object.values(record)) {
|
|
17
|
+
if (typeof entry === 'number' && Number.isFinite(entry))
|
|
18
|
+
return entry;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
// --- String utilities ---
|
|
23
|
+
export function truncate(text, limit) {
|
|
24
|
+
return text.length <= limit ? text : `${text.slice(0, Math.max(0, limit - 3))}...`;
|
|
25
|
+
}
|
|
26
|
+
export function shortNodeID(nodeID) {
|
|
27
|
+
return nodeID.length <= 32 ? nodeID : `${nodeID.slice(0, 20)}...${nodeID.slice(-8)}`;
|
|
28
|
+
}
|
|
29
|
+
export function parseJson(value) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(value);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
+
throw new Error(`Failed to parse JSON: ${message}\nInput: ${value.slice(0, 120)}${value.length > 120 ? '...' : ''}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// --- Number utilities ---
|
|
39
|
+
export function clamp(value, min, max) {
|
|
40
|
+
return Math.min(max, Math.max(min, value));
|
|
41
|
+
}
|
|
42
|
+
// --- Hashing ---
|
|
43
|
+
export function hashContent(content) {
|
|
44
|
+
return createHash('sha256').update(content).digest('hex');
|
|
45
|
+
}
|
|
46
|
+
// --- Query tokenization ---
|
|
47
|
+
export function tokenizeQuery(query) {
|
|
48
|
+
return [...new Set(query.toLowerCase().match(/[a-z0-9_]+/g) ?? [])];
|
|
49
|
+
}
|
|
50
|
+
/** FTS5 reserved words that would be interpreted as operators/syntax inside MATCH. */
|
|
51
|
+
export const FTS5_RESERVED = new Set([
|
|
52
|
+
'and',
|
|
53
|
+
'or',
|
|
54
|
+
'not',
|
|
55
|
+
'near',
|
|
56
|
+
'order',
|
|
57
|
+
'by',
|
|
58
|
+
'asc',
|
|
59
|
+
'desc',
|
|
60
|
+
'limit',
|
|
61
|
+
'offset',
|
|
62
|
+
'match',
|
|
63
|
+
'rank',
|
|
64
|
+
'rowid',
|
|
65
|
+
'bm25',
|
|
66
|
+
'highlight',
|
|
67
|
+
'snippet',
|
|
68
|
+
'replace',
|
|
69
|
+
'delete',
|
|
70
|
+
'insert',
|
|
71
|
+
'update',
|
|
72
|
+
'select',
|
|
73
|
+
'from',
|
|
74
|
+
'where',
|
|
75
|
+
'group',
|
|
76
|
+
'having',
|
|
77
|
+
]);
|
|
78
|
+
/** Drop FTS5-reserved words and too-short tokens to keep MATCH queries safe. */
|
|
79
|
+
export function sanitizeFtsTokens(tokens) {
|
|
80
|
+
return tokens.filter((t) => t.length >= 2 && !FTS5_RESERVED.has(t));
|
|
81
|
+
}
|
|
82
|
+
// --- Snippet extraction ---
|
|
83
|
+
export function buildSnippet(content, query, limit = 280) {
|
|
84
|
+
const normalized = content.replace(/\s+/g, ' ').trim();
|
|
85
|
+
if (!normalized)
|
|
86
|
+
return '';
|
|
87
|
+
if (!query)
|
|
88
|
+
return truncate(normalized, limit);
|
|
89
|
+
const lower = normalized.toLowerCase();
|
|
90
|
+
const exact = query.toLowerCase();
|
|
91
|
+
let index = lower.indexOf(exact);
|
|
92
|
+
if (index < 0) {
|
|
93
|
+
for (const token of tokenizeQuery(query)) {
|
|
94
|
+
index = lower.indexOf(token);
|
|
95
|
+
if (index >= 0)
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (index < 0)
|
|
100
|
+
return truncate(normalized, limit);
|
|
101
|
+
const start = Math.max(0, index - 90);
|
|
102
|
+
const end = Math.min(normalized.length, index + Math.max(exact.length, 32) + 150);
|
|
103
|
+
return truncate(normalized.slice(start, end), limit);
|
|
104
|
+
}
|
|
105
|
+
// --- Automatic retrieval text sanitization ---
|
|
106
|
+
const AUTOMATIC_RETRIEVAL_NOISE_PATTERNS = [
|
|
107
|
+
/<system-reminder>/i,
|
|
108
|
+
/system[-\s[\]]*reminder/i,
|
|
109
|
+
/\[archived by opencode-lcm:/i,
|
|
110
|
+
/archived hits:/i,
|
|
111
|
+
/summary roots:/i,
|
|
112
|
+
/\b(?:message|summary|artifact) session=[^\s]+ id=[^\s]+/i,
|
|
113
|
+
/\bsum_[a-f0-9]{12}_l\d+_p\d+:/i,
|
|
114
|
+
/recall telemetry:/i,
|
|
115
|
+
/recalled context:/i,
|
|
116
|
+
/your operational mode has changed/i,
|
|
117
|
+
/opencode-lcm automatically recalled/i,
|
|
118
|
+
/recalled \d+ archived hit/i,
|
|
119
|
+
/compacted \d+ older conversation turn/i,
|
|
120
|
+
/treat recalled archive as supporting context/i,
|
|
121
|
+
/use lcm_(describe|grep|resume|expand|artifact)/i,
|
|
122
|
+
];
|
|
123
|
+
/**
|
|
124
|
+
* Minimal grammatical stoplist — function words and common verbs that are almost never
|
|
125
|
+
* informative for retrieval. Corpus-aware TF-IDF weighting handles the rest in store.ts.
|
|
126
|
+
* This list exists purely to avoid a DB round-trip for obvious determiners/pronouns/verbs.
|
|
127
|
+
*/
|
|
128
|
+
const GRAMMATICAL_STOPWORDS = new Set([
|
|
129
|
+
// Determiners/articles
|
|
130
|
+
'a',
|
|
131
|
+
'an',
|
|
132
|
+
'the',
|
|
133
|
+
// Pronouns
|
|
134
|
+
'i',
|
|
135
|
+
'me',
|
|
136
|
+
'my',
|
|
137
|
+
'we',
|
|
138
|
+
'us',
|
|
139
|
+
'our',
|
|
140
|
+
'you',
|
|
141
|
+
'your',
|
|
142
|
+
'it',
|
|
143
|
+
'its',
|
|
144
|
+
'this',
|
|
145
|
+
'that',
|
|
146
|
+
'these',
|
|
147
|
+
'those',
|
|
148
|
+
// Copula/auxiliary verbs
|
|
149
|
+
'is',
|
|
150
|
+
'are',
|
|
151
|
+
'was',
|
|
152
|
+
'were',
|
|
153
|
+
'be',
|
|
154
|
+
'been',
|
|
155
|
+
'am',
|
|
156
|
+
'do',
|
|
157
|
+
'does',
|
|
158
|
+
'did',
|
|
159
|
+
'has',
|
|
160
|
+
'have',
|
|
161
|
+
'had',
|
|
162
|
+
'can',
|
|
163
|
+
'could',
|
|
164
|
+
'will',
|
|
165
|
+
'would',
|
|
166
|
+
'shall',
|
|
167
|
+
'should',
|
|
168
|
+
'may',
|
|
169
|
+
'might',
|
|
170
|
+
'must',
|
|
171
|
+
// Common verbs that are rarely informative for retrieval
|
|
172
|
+
'say',
|
|
173
|
+
'said',
|
|
174
|
+
'tell',
|
|
175
|
+
'told',
|
|
176
|
+
'show',
|
|
177
|
+
'shown',
|
|
178
|
+
'get',
|
|
179
|
+
'got',
|
|
180
|
+
'give',
|
|
181
|
+
'gave',
|
|
182
|
+
'make',
|
|
183
|
+
'made',
|
|
184
|
+
'take',
|
|
185
|
+
'took',
|
|
186
|
+
'come',
|
|
187
|
+
'came',
|
|
188
|
+
'go',
|
|
189
|
+
'went',
|
|
190
|
+
'see',
|
|
191
|
+
'saw',
|
|
192
|
+
'know',
|
|
193
|
+
'knew',
|
|
194
|
+
'think',
|
|
195
|
+
'thought',
|
|
196
|
+
'want',
|
|
197
|
+
'need',
|
|
198
|
+
'use',
|
|
199
|
+
'used',
|
|
200
|
+
// Prepositions/conjunctions
|
|
201
|
+
'in',
|
|
202
|
+
'on',
|
|
203
|
+
'at',
|
|
204
|
+
'to',
|
|
205
|
+
'for',
|
|
206
|
+
'from',
|
|
207
|
+
'by',
|
|
208
|
+
'with',
|
|
209
|
+
'about',
|
|
210
|
+
'into',
|
|
211
|
+
'and',
|
|
212
|
+
'or',
|
|
213
|
+
'but',
|
|
214
|
+
'so',
|
|
215
|
+
'if',
|
|
216
|
+
'then',
|
|
217
|
+
'than',
|
|
218
|
+
'as',
|
|
219
|
+
'of',
|
|
220
|
+
// Adverbs/misc
|
|
221
|
+
'just',
|
|
222
|
+
'only',
|
|
223
|
+
'also',
|
|
224
|
+
'still',
|
|
225
|
+
'already',
|
|
226
|
+
'even',
|
|
227
|
+
'very',
|
|
228
|
+
'yes',
|
|
229
|
+
'no',
|
|
230
|
+
'ok',
|
|
231
|
+
'okay',
|
|
232
|
+
'not',
|
|
233
|
+
'again',
|
|
234
|
+
'more',
|
|
235
|
+
'some',
|
|
236
|
+
'any',
|
|
237
|
+
'all',
|
|
238
|
+
'every',
|
|
239
|
+
'each',
|
|
240
|
+
// Retrieval-specific noise
|
|
241
|
+
'recall',
|
|
242
|
+
'remind',
|
|
243
|
+
'reply',
|
|
244
|
+
'mention',
|
|
245
|
+
'mentioned',
|
|
246
|
+
'where',
|
|
247
|
+
'when',
|
|
248
|
+
'what',
|
|
249
|
+
'which',
|
|
250
|
+
'who',
|
|
251
|
+
'how',
|
|
252
|
+
'why',
|
|
253
|
+
]);
|
|
254
|
+
export function sanitizeAutomaticRetrievalSourceText(text) {
|
|
255
|
+
return text
|
|
256
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, ' ')
|
|
257
|
+
.replace(/<system-reminder>/gi, ' ')
|
|
258
|
+
.replace(/<\/system-reminder>/gi, ' ')
|
|
259
|
+
.replace(/\[Archived by opencode-lcm:[^\]]*\]/gi, ' ')
|
|
260
|
+
.replace(/Archived hits:[^\n]*/gi, ' ')
|
|
261
|
+
.replace(/Summary roots:[^\n]*/gi, ' ')
|
|
262
|
+
.replace(/Recall telemetry:[^\n]*/gi, ' ')
|
|
263
|
+
.replace(/Recalled context:/gi, ' ')
|
|
264
|
+
.replace(/Archived roots:/gi, ' ')
|
|
265
|
+
.replace(/Treat recalled archive as supporting context[^\n]*/gi, ' ')
|
|
266
|
+
.replace(/Use lcm_(describe|grep|resume|expand|artifact)[^\n]*/gi, ' ')
|
|
267
|
+
.replace(/opencode-lcm automatically recalled[^\n]*/gi, ' ')
|
|
268
|
+
.replace(/\s+/g, ' ')
|
|
269
|
+
.trim();
|
|
270
|
+
}
|
|
271
|
+
export function isAutomaticRetrievalNoise(text) {
|
|
272
|
+
return AUTOMATIC_RETRIEVAL_NOISE_PATTERNS.some((pattern) => pattern.test(text));
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Fast-path filter: drops only grammatical function words.
|
|
276
|
+
* Corpus-aware TF-IDF weighting should be applied downstream for full filtering.
|
|
277
|
+
*/
|
|
278
|
+
export function filterIntentTokens(tokens) {
|
|
279
|
+
return tokens.filter((token) => {
|
|
280
|
+
if (GRAMMATICAL_STOPWORDS.has(token))
|
|
281
|
+
return false;
|
|
282
|
+
if (token.length >= 3)
|
|
283
|
+
return true;
|
|
284
|
+
return /\d/.test(token) || token.includes('_');
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Returns the minimal grammatical stoplist for use in TF-IDF scoring.
|
|
289
|
+
* Tokens in this set are always dropped without hitting the FTS index.
|
|
290
|
+
*/
|
|
291
|
+
export function getGrammaticalStopwords() {
|
|
292
|
+
return GRAMMATICAL_STOPWORDS;
|
|
293
|
+
}
|
|
294
|
+
export function shouldSuppressLowSignalAutomaticRetrievalAnchor(anchorText, anchorSignalCount, minTokens, anchorFileCount) {
|
|
295
|
+
if (anchorSignalCount >= minTokens)
|
|
296
|
+
return false;
|
|
297
|
+
if (anchorFileCount > 0)
|
|
298
|
+
return false;
|
|
299
|
+
if (anchorText.includes('?'))
|
|
300
|
+
return false;
|
|
301
|
+
const rawTokenCount = tokenizeQuery(anchorText).length;
|
|
302
|
+
return rawTokenCount <= 4;
|
|
303
|
+
}
|
|
304
|
+
// --- URL and file utilities ---
|
|
305
|
+
export function inferUrlScheme(url) {
|
|
306
|
+
if (!url)
|
|
307
|
+
return undefined;
|
|
308
|
+
const index = url.indexOf(':');
|
|
309
|
+
if (index <= 0)
|
|
310
|
+
return undefined;
|
|
311
|
+
return url.slice(0, index).toLowerCase();
|
|
312
|
+
}
|
|
313
|
+
export function inferFileExtension(file) {
|
|
314
|
+
if (!file)
|
|
315
|
+
return undefined;
|
|
316
|
+
const cleaned = file.replace(/\\/g, '/').split('/').pop() ?? file;
|
|
317
|
+
const index = cleaned.lastIndexOf('.');
|
|
318
|
+
if (index <= 0 || index === cleaned.length - 1)
|
|
319
|
+
return undefined;
|
|
320
|
+
return cleaned.slice(index + 1).toLowerCase();
|
|
321
|
+
}
|
|
322
|
+
export function classifyFileCategory(mime, extension) {
|
|
323
|
+
const kind = mime?.toLowerCase() ?? '';
|
|
324
|
+
const ext = extension?.toLowerCase() ?? '';
|
|
325
|
+
if (kind.startsWith('image/'))
|
|
326
|
+
return 'image';
|
|
327
|
+
if (kind === 'application/pdf' || ext === 'pdf')
|
|
328
|
+
return 'pdf';
|
|
329
|
+
if (kind.startsWith('audio/'))
|
|
330
|
+
return 'audio';
|
|
331
|
+
if (kind.startsWith('video/'))
|
|
332
|
+
return 'video';
|
|
333
|
+
if (kind.includes('zip') ||
|
|
334
|
+
kind.includes('tar') ||
|
|
335
|
+
kind.includes('gzip') ||
|
|
336
|
+
['zip', 'tar', 'gz', 'tgz', 'rar', '7z'].includes(ext))
|
|
337
|
+
return 'archive';
|
|
338
|
+
if (kind.includes('spreadsheet') || ['xls', 'xlsx', 'ods', 'csv', 'tsv'].includes(ext))
|
|
339
|
+
return 'spreadsheet';
|
|
340
|
+
if (kind.includes('presentation') || ['ppt', 'pptx', 'odp', 'key'].includes(ext))
|
|
341
|
+
return 'presentation';
|
|
342
|
+
if (kind.includes('word') ||
|
|
343
|
+
kind.includes('document') ||
|
|
344
|
+
['doc', 'docx', 'odt', 'rtf'].includes(ext))
|
|
345
|
+
return 'document';
|
|
346
|
+
if (kind.startsWith('text/') || ['txt', 'md', 'rst', 'log'].includes(ext))
|
|
347
|
+
return 'text';
|
|
348
|
+
if ([
|
|
349
|
+
'ts',
|
|
350
|
+
'tsx',
|
|
351
|
+
'js',
|
|
352
|
+
'jsx',
|
|
353
|
+
'py',
|
|
354
|
+
'rb',
|
|
355
|
+
'go',
|
|
356
|
+
'rs',
|
|
357
|
+
'java',
|
|
358
|
+
'kt',
|
|
359
|
+
'c',
|
|
360
|
+
'cpp',
|
|
361
|
+
'h',
|
|
362
|
+
'hpp',
|
|
363
|
+
'cs',
|
|
364
|
+
'php',
|
|
365
|
+
'swift',
|
|
366
|
+
'scala',
|
|
367
|
+
'pl',
|
|
368
|
+
'pm',
|
|
369
|
+
'sh',
|
|
370
|
+
'bash',
|
|
371
|
+
'zsh',
|
|
372
|
+
'fish',
|
|
373
|
+
'ps1',
|
|
374
|
+
'bat',
|
|
375
|
+
'cmd',
|
|
376
|
+
].includes(ext))
|
|
377
|
+
return 'code';
|
|
378
|
+
if ([
|
|
379
|
+
'html',
|
|
380
|
+
'htm',
|
|
381
|
+
'xml',
|
|
382
|
+
'json',
|
|
383
|
+
'yaml',
|
|
384
|
+
'yml',
|
|
385
|
+
'toml',
|
|
386
|
+
'ini',
|
|
387
|
+
'cfg',
|
|
388
|
+
'conf',
|
|
389
|
+
'env',
|
|
390
|
+
'sql',
|
|
391
|
+
'graphql',
|
|
392
|
+
'gql',
|
|
393
|
+
].includes(ext))
|
|
394
|
+
return 'structured-data';
|
|
395
|
+
return 'binary';
|
|
396
|
+
}
|
|
397
|
+
// --- Formatting utilities ---
|
|
398
|
+
export function formatMetadataValue(value) {
|
|
399
|
+
if (typeof value === 'string')
|
|
400
|
+
return value;
|
|
401
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
402
|
+
return String(value);
|
|
403
|
+
if (Array.isArray(value)) {
|
|
404
|
+
const items = value.map((item) => formatMetadataValue(item)).filter(Boolean);
|
|
405
|
+
return items.length > 0 ? items.join(', ') : undefined;
|
|
406
|
+
}
|
|
407
|
+
if (value && typeof value === 'object') {
|
|
408
|
+
return JSON.stringify(value);
|
|
409
|
+
}
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
export function formatRetentionDays(value) {
|
|
413
|
+
return value === undefined ? 'disabled' : String(value);
|
|
414
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveWorkspacePath(workspaceDirectory: string, inputPath: string): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export function resolveWorkspacePath(workspaceDirectory, inputPath) {
|
|
3
|
+
const workspaceRoot = path.resolve(workspaceDirectory);
|
|
4
|
+
const resolvedPath = path.isAbsolute(inputPath)
|
|
5
|
+
? path.normalize(inputPath)
|
|
6
|
+
: path.resolve(workspaceRoot, inputPath);
|
|
7
|
+
const relativePath = path.relative(workspaceRoot, resolvedPath);
|
|
8
|
+
const escapesWorkspace = relativePath === '..' ||
|
|
9
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
10
|
+
path.isAbsolute(relativePath);
|
|
11
|
+
if (!escapesWorkspace) {
|
|
12
|
+
return resolvedPath;
|
|
13
|
+
}
|
|
14
|
+
throw new Error(`Path must stay within the workspace: ${inputPath}`);
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function normalizeWorktreeKey(directory?: string): string | undefined;
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-lcm",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "Long-memory plugin for OpenCode with context-mode interop",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/",
|
|
16
|
+
"src/",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"author": "Isaac Grumberg",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/Plutarch01/opencode-lcm.git"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/Plutarch01/opencode-lcm#readme",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/Plutarch01/opencode-lcm/issues"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"opencode",
|
|
36
|
+
"plugin",
|
|
37
|
+
"memory",
|
|
38
|
+
"sqlite",
|
|
39
|
+
"context-mode",
|
|
40
|
+
"lcm"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"build": "tsc -p tsconfig.json",
|
|
45
|
+
"lint": "biome check src tests",
|
|
46
|
+
"format": "biome format --write src tests",
|
|
47
|
+
"test": "npm run build && tsc -p tests/tsconfig.json && node --test tests/*.test.mjs dist-tests/*.test.js",
|
|
48
|
+
"perf:archive": "npm run build && node scripts/perf-archive.mjs",
|
|
49
|
+
"dogfood:opencode": "npm run build && node scripts/dogfood-opencode.mjs",
|
|
50
|
+
"prepublishOnly": "npm run typecheck && npm run lint && npm run test"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@opencode-ai/plugin": "*"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@biomejs/biome": "^2.4.10",
|
|
57
|
+
"@opencode-ai/plugin": "^1.3.3",
|
|
58
|
+
"@types/node": "^25.5.0",
|
|
59
|
+
"typescript": "^5.8.3"
|
|
60
|
+
}
|
|
61
|
+
}
|