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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { SearchResult } from './types.js';
|
|
2
|
+
|
|
3
|
+
export type SearchCandidate = {
|
|
4
|
+
id: string;
|
|
5
|
+
type: string;
|
|
6
|
+
sessionID?: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
snippet: string;
|
|
9
|
+
content: string;
|
|
10
|
+
sourceKind: 'message' | 'summary' | 'artifact';
|
|
11
|
+
sourceOrder: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function escapeRegExp(value: string): string {
|
|
15
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function tokenizeQuery(query: string): string[] {
|
|
19
|
+
return [...new Set(query.toLowerCase().match(/[a-z0-9_]+/g) ?? [])];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildRecencyRange(candidates: SearchCandidate[]): { oldest: number; newest: number } {
|
|
23
|
+
let oldest = Number.POSITIVE_INFINITY;
|
|
24
|
+
let newest = Number.NEGATIVE_INFINITY;
|
|
25
|
+
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
oldest = Math.min(oldest, candidate.timestamp);
|
|
28
|
+
newest = Math.max(newest, candidate.timestamp);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!Number.isFinite(oldest) || !Number.isFinite(newest)) {
|
|
32
|
+
return { oldest: 0, newest: 0 };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { oldest, newest };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const MESSAGE_BASE_SCORE = 135;
|
|
39
|
+
const ARTIFACT_BASE_SCORE = 96;
|
|
40
|
+
const SUMMARY_BASE_SCORE = 78;
|
|
41
|
+
const USER_ROLE_BONUS = 22;
|
|
42
|
+
const ASSISTANT_ROLE_BONUS = 12;
|
|
43
|
+
const EXACT_PHRASE_BONUS = 90;
|
|
44
|
+
const COVERAGE_MULTIPLIER = 70;
|
|
45
|
+
const TOKEN_MATCH_MULTIPLIER = 12;
|
|
46
|
+
const TOTAL_HIT_MULTIPLIER = 2;
|
|
47
|
+
const BOUNDARY_HIT_MULTIPLIER = 4;
|
|
48
|
+
const SNIPPET_EXACT_BONUS = 24;
|
|
49
|
+
const SOURCE_ORDER_DECAY_BASE = 18;
|
|
50
|
+
const RECENCY_MULTIPLIER = 28;
|
|
51
|
+
|
|
52
|
+
function scoreSearchCandidate(
|
|
53
|
+
candidate: SearchCandidate,
|
|
54
|
+
query: string,
|
|
55
|
+
tokens: string[],
|
|
56
|
+
recencyRange: { oldest: number; newest: number },
|
|
57
|
+
): number {
|
|
58
|
+
const content = candidate.content.toLowerCase();
|
|
59
|
+
const snippet = candidate.snippet.toLowerCase();
|
|
60
|
+
const exactPhrase = content.includes(query);
|
|
61
|
+
const base =
|
|
62
|
+
candidate.sourceKind === 'message'
|
|
63
|
+
? MESSAGE_BASE_SCORE
|
|
64
|
+
: candidate.sourceKind === 'artifact'
|
|
65
|
+
? ARTIFACT_BASE_SCORE
|
|
66
|
+
: SUMMARY_BASE_SCORE;
|
|
67
|
+
|
|
68
|
+
let matchedTokens = 0;
|
|
69
|
+
let totalHits = 0;
|
|
70
|
+
let boundaryHits = 0;
|
|
71
|
+
|
|
72
|
+
for (const token of tokens) {
|
|
73
|
+
const hasToken = content.includes(token);
|
|
74
|
+
if (hasToken) matchedTokens += 1;
|
|
75
|
+
|
|
76
|
+
const boundaryPattern = new RegExp(`\\b${escapeRegExp(token)}\\b`, 'g');
|
|
77
|
+
const matches = content.match(boundaryPattern)?.length ?? 0;
|
|
78
|
+
boundaryHits += matches;
|
|
79
|
+
totalHits += matches > 0 ? matches : hasToken ? 1 : 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const coverage = tokens.length > 0 ? matchedTokens / tokens.length : 0;
|
|
83
|
+
let score = base;
|
|
84
|
+
if (candidate.sourceKind === 'message') {
|
|
85
|
+
score +=
|
|
86
|
+
candidate.type === 'user'
|
|
87
|
+
? USER_ROLE_BONUS
|
|
88
|
+
: candidate.type === 'assistant'
|
|
89
|
+
? ASSISTANT_ROLE_BONUS
|
|
90
|
+
: 0;
|
|
91
|
+
}
|
|
92
|
+
score += exactPhrase ? EXACT_PHRASE_BONUS : 0;
|
|
93
|
+
score += coverage * COVERAGE_MULTIPLIER;
|
|
94
|
+
score += matchedTokens * TOKEN_MATCH_MULTIPLIER;
|
|
95
|
+
score += Math.min(totalHits, matchedTokens + 2) * TOTAL_HIT_MULTIPLIER;
|
|
96
|
+
score += Math.min(boundaryHits, matchedTokens) * BOUNDARY_HIT_MULTIPLIER;
|
|
97
|
+
score += snippet.includes(query) ? SNIPPET_EXACT_BONUS : 0;
|
|
98
|
+
score += Math.max(0, SOURCE_ORDER_DECAY_BASE - candidate.sourceOrder);
|
|
99
|
+
if (recencyRange.newest > recencyRange.oldest) {
|
|
100
|
+
const recencyRatio =
|
|
101
|
+
(candidate.timestamp - recencyRange.oldest) / (recencyRange.newest - recencyRange.oldest);
|
|
102
|
+
score += recencyRatio * RECENCY_MULTIPLIER;
|
|
103
|
+
}
|
|
104
|
+
return score;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function rankSearchCandidates(
|
|
108
|
+
candidates: SearchCandidate[],
|
|
109
|
+
query: string,
|
|
110
|
+
limit: number,
|
|
111
|
+
): SearchResult[] {
|
|
112
|
+
const exactQuery = query.toLowerCase();
|
|
113
|
+
const tokens = tokenizeQuery(query);
|
|
114
|
+
const deduped = new Map<string, SearchCandidate & { score: number }>();
|
|
115
|
+
const recencyRange = buildRecencyRange(candidates);
|
|
116
|
+
|
|
117
|
+
for (const candidate of candidates) {
|
|
118
|
+
const score = scoreSearchCandidate(candidate, exactQuery, tokens, recencyRange);
|
|
119
|
+
const key = `${candidate.type}:${candidate.id}`;
|
|
120
|
+
const existing = deduped.get(key);
|
|
121
|
+
if (
|
|
122
|
+
!existing ||
|
|
123
|
+
score > existing.score ||
|
|
124
|
+
(score === existing.score && candidate.timestamp > existing.timestamp)
|
|
125
|
+
) {
|
|
126
|
+
deduped.set(key, {
|
|
127
|
+
...candidate,
|
|
128
|
+
score,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return [...deduped.values()]
|
|
134
|
+
.sort((a, b) => b.score - a.score || b.timestamp - a.timestamp)
|
|
135
|
+
.slice(0, limit)
|
|
136
|
+
.map(
|
|
137
|
+
({
|
|
138
|
+
content: _content,
|
|
139
|
+
sourceKind: _sourceKind,
|
|
140
|
+
sourceOrder: _sourceOrder,
|
|
141
|
+
score: _score,
|
|
142
|
+
...result
|
|
143
|
+
}) => result,
|
|
144
|
+
);
|
|
145
|
+
}
|
package/src/sql-utils.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { SqlDatabaseLike, SqlStatementLike } from './store-types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates that a SQL statement result contains the expected number of rows.
|
|
5
|
+
* Throws a descriptive error if the constraint is violated.
|
|
6
|
+
*/
|
|
7
|
+
export function assertSingleRow(
|
|
8
|
+
result: unknown,
|
|
9
|
+
operation: string,
|
|
10
|
+
): asserts result is Record<string, unknown> {
|
|
11
|
+
if (!result || typeof result !== 'object') {
|
|
12
|
+
throw new Error(`${operation}: expected a single row, got ${typeof result}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validates that a SQL statement affected at least one row.
|
|
18
|
+
* Useful for UPDATE/DELETE operations where zero rows may indicate a bug.
|
|
19
|
+
*/
|
|
20
|
+
export function assertAffectedRows(
|
|
21
|
+
statement: SqlStatementLike,
|
|
22
|
+
args: unknown[],
|
|
23
|
+
operation: string,
|
|
24
|
+
minRows = 1,
|
|
25
|
+
): void {
|
|
26
|
+
const result = statement.run(...args) as { changes: number } | undefined;
|
|
27
|
+
const changes = result?.changes ?? 0;
|
|
28
|
+
if (changes < minRows) {
|
|
29
|
+
throw new Error(`${operation}: expected at least ${minRows} affected rows, got ${changes}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Safely executes a SQL statement and returns typed results.
|
|
35
|
+
* Wraps the raw SQLite call with validation to catch schema mismatches early.
|
|
36
|
+
*/
|
|
37
|
+
export function safeQuery<T extends Record<string, unknown>>(
|
|
38
|
+
statement: SqlStatementLike,
|
|
39
|
+
args: unknown[],
|
|
40
|
+
operation: string,
|
|
41
|
+
): T[] {
|
|
42
|
+
const result = statement.all(...args);
|
|
43
|
+
if (!Array.isArray(result)) {
|
|
44
|
+
throw new Error(`${operation}: expected array result, got ${typeof result}`);
|
|
45
|
+
}
|
|
46
|
+
return result as T[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Safely executes a SQL statement that should return exactly one row.
|
|
51
|
+
*/
|
|
52
|
+
export function safeQueryOne<T extends Record<string, unknown>>(
|
|
53
|
+
statement: SqlStatementLike,
|
|
54
|
+
args: unknown[],
|
|
55
|
+
operation: string,
|
|
56
|
+
): T | undefined {
|
|
57
|
+
const result = statement.get(...args) as Record<string, unknown> | undefined;
|
|
58
|
+
if (result && typeof result !== 'object') {
|
|
59
|
+
throw new Error(`${operation}: expected object or undefined, got ${typeof result}`);
|
|
60
|
+
}
|
|
61
|
+
return result as T | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Wraps database operations in a transaction with proper error handling.
|
|
66
|
+
* Automatically rolls back on failure.
|
|
67
|
+
*/
|
|
68
|
+
export function withTransaction(db: SqlDatabaseLike, operation: string, fn: () => void): void {
|
|
69
|
+
db.exec('BEGIN');
|
|
70
|
+
try {
|
|
71
|
+
fn();
|
|
72
|
+
db.exec('COMMIT');
|
|
73
|
+
} catch (error) {
|
|
74
|
+
db.exec('ROLLBACK');
|
|
75
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
+
throw new Error(`${operation} transaction failed: ${message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Lightweight runtime validator for SQL row results.
|
|
82
|
+
* Checks that required keys exist and have the expected types.
|
|
83
|
+
* Returns the row as-is if valid, throws if not.
|
|
84
|
+
*/
|
|
85
|
+
export function validateRow<T extends Record<string, unknown>>(
|
|
86
|
+
row: unknown,
|
|
87
|
+
schema: Record<keyof T, 'string' | 'number' | 'boolean' | 'object' | 'nullable'>,
|
|
88
|
+
operation: string,
|
|
89
|
+
): T {
|
|
90
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) {
|
|
91
|
+
throw new Error(`${operation}: expected an object row, got ${typeof row}`);
|
|
92
|
+
}
|
|
93
|
+
const record = row as Record<string, unknown>;
|
|
94
|
+
for (const [key, expectedType] of Object.entries(schema) as Array<[string, string]>) {
|
|
95
|
+
const value = record[key];
|
|
96
|
+
if (expectedType === 'nullable') continue;
|
|
97
|
+
if (value === null || value === undefined) {
|
|
98
|
+
throw new Error(`${operation}: missing required column "${key}"`);
|
|
99
|
+
}
|
|
100
|
+
if (typeof value !== expectedType) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`${operation}: column "${key}" expected ${expectedType}, got ${typeof value}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return record as T;
|
|
107
|
+
}
|