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.
Files changed (65) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/LICENSE +21 -0
  3. package/README.md +207 -0
  4. package/dist/archive-transform.d.ts +45 -0
  5. package/dist/archive-transform.js +81 -0
  6. package/dist/constants.d.ts +12 -0
  7. package/dist/constants.js +16 -0
  8. package/dist/doctor.d.ts +22 -0
  9. package/dist/doctor.js +44 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.js +306 -0
  12. package/dist/logging.d.ts +14 -0
  13. package/dist/logging.js +28 -0
  14. package/dist/options.d.ts +3 -0
  15. package/dist/options.js +217 -0
  16. package/dist/preview-providers.d.ts +20 -0
  17. package/dist/preview-providers.js +246 -0
  18. package/dist/privacy.d.ts +16 -0
  19. package/dist/privacy.js +92 -0
  20. package/dist/search-ranking.d.ts +12 -0
  21. package/dist/search-ranking.js +98 -0
  22. package/dist/sql-utils.d.ts +31 -0
  23. package/dist/sql-utils.js +80 -0
  24. package/dist/store-artifacts.d.ts +50 -0
  25. package/dist/store-artifacts.js +374 -0
  26. package/dist/store-retention.d.ts +39 -0
  27. package/dist/store-retention.js +90 -0
  28. package/dist/store-search.d.ts +37 -0
  29. package/dist/store-search.js +298 -0
  30. package/dist/store-snapshot.d.ts +133 -0
  31. package/dist/store-snapshot.js +325 -0
  32. package/dist/store-types.d.ts +14 -0
  33. package/dist/store-types.js +5 -0
  34. package/dist/store.d.ts +316 -0
  35. package/dist/store.js +3673 -0
  36. package/dist/types.d.ts +117 -0
  37. package/dist/types.js +1 -0
  38. package/dist/utils.d.ts +35 -0
  39. package/dist/utils.js +414 -0
  40. package/dist/workspace-path.d.ts +1 -0
  41. package/dist/workspace-path.js +15 -0
  42. package/dist/worktree-key.d.ts +1 -0
  43. package/dist/worktree-key.js +6 -0
  44. package/package.json +61 -0
  45. package/src/archive-transform.ts +147 -0
  46. package/src/bun-sqlite.d.ts +18 -0
  47. package/src/constants.ts +20 -0
  48. package/src/doctor.ts +83 -0
  49. package/src/index.ts +330 -0
  50. package/src/logging.ts +41 -0
  51. package/src/options.ts +297 -0
  52. package/src/preview-providers.ts +298 -0
  53. package/src/privacy.ts +122 -0
  54. package/src/search-ranking.ts +145 -0
  55. package/src/sql-utils.ts +107 -0
  56. package/src/store-artifacts.ts +666 -0
  57. package/src/store-retention.ts +152 -0
  58. package/src/store-search.ts +440 -0
  59. package/src/store-snapshot.ts +582 -0
  60. package/src/store-types.ts +16 -0
  61. package/src/store.ts +4926 -0
  62. package/src/types.ts +132 -0
  63. package/src/utils.ts +444 -0
  64. package/src/workspace-path.ts +20 -0
  65. package/src/worktree-key.ts +5 -0
@@ -0,0 +1,246 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { getLogger } from './logging.js';
4
+ import { resolveWorkspacePath } from './workspace-path.js';
5
+ function inferLocalPath(workspaceDirectory, file) {
6
+ const sourcePath = file.source && 'path' in file.source ? file.source.path : undefined;
7
+ if (!sourcePath)
8
+ return undefined;
9
+ try {
10
+ return resolveWorkspacePath(workspaceDirectory, sourcePath);
11
+ }
12
+ catch (error) {
13
+ getLogger().debug('Failed to resolve workspace path', { sourcePath, error });
14
+ return undefined;
15
+ }
16
+ }
17
+ function toHexPreview(buffer, bytes) {
18
+ if (buffer.length === 0)
19
+ return undefined;
20
+ return [...buffer.subarray(0, Math.max(1, bytes))]
21
+ .map((byte) => byte.toString(16).padStart(2, '0'))
22
+ .join(' ');
23
+ }
24
+ function parsePngDimensions(buffer) {
25
+ if (buffer.length < 24)
26
+ return undefined;
27
+ if (!buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
28
+ return undefined;
29
+ return {
30
+ width: buffer.readUInt32BE(16),
31
+ height: buffer.readUInt32BE(20),
32
+ };
33
+ }
34
+ function parseGifDimensions(buffer) {
35
+ if (buffer.length < 10)
36
+ return undefined;
37
+ const header = buffer.subarray(0, 6).toString('ascii');
38
+ if (header !== 'GIF87a' && header !== 'GIF89a')
39
+ return undefined;
40
+ return {
41
+ width: buffer.readUInt16LE(6),
42
+ height: buffer.readUInt16LE(8),
43
+ };
44
+ }
45
+ function parseJpegDimensions(buffer) {
46
+ if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8)
47
+ return undefined;
48
+ let offset = 2;
49
+ while (offset + 9 < buffer.length) {
50
+ if (buffer[offset] !== 0xff) {
51
+ offset += 1;
52
+ continue;
53
+ }
54
+ const marker = buffer[offset + 1];
55
+ if (marker === 0xd9 || marker === 0xda)
56
+ break;
57
+ const size = buffer.readUInt16BE(offset + 2);
58
+ if (size < 2 || offset + 2 + size > buffer.length)
59
+ break;
60
+ if ((marker >= 0xc0 && marker <= 0xc3) ||
61
+ (marker >= 0xc5 && marker <= 0xc7) ||
62
+ (marker >= 0xc9 && marker <= 0xcb) ||
63
+ (marker >= 0xcd && marker <= 0xcf)) {
64
+ return {
65
+ height: buffer.readUInt16BE(offset + 5),
66
+ width: buffer.readUInt16BE(offset + 7),
67
+ };
68
+ }
69
+ offset += 2 + size;
70
+ }
71
+ return undefined;
72
+ }
73
+ function estimatePdfPages(buffer) {
74
+ const text = buffer.toString('latin1');
75
+ const matches = text.match(/\/Type\s*\/Page([^s]|$)/g);
76
+ return matches && matches.length > 0 ? matches.length : undefined;
77
+ }
78
+ function estimateZipEntries(buffer) {
79
+ if (buffer.length < 4)
80
+ return undefined;
81
+ const localFileHeaderSignature = 0x04034b50;
82
+ const endOfCentralDirectorySignature = 0x06054b50;
83
+ const firstSignature = buffer.readUInt32LE(0);
84
+ if (firstSignature !== localFileHeaderSignature &&
85
+ firstSignature !== endOfCentralDirectorySignature) {
86
+ return undefined;
87
+ }
88
+ const searchStart = Math.max(0, buffer.length - 65557);
89
+ for (let offset = buffer.length - 22; offset >= searchStart; offset -= 1) {
90
+ if (buffer.readUInt32LE(offset) !== endOfCentralDirectorySignature)
91
+ continue;
92
+ return buffer.readUInt16LE(offset + 10);
93
+ }
94
+ let entries = 0;
95
+ for (let offset = 0; offset <= buffer.length - 4; offset += 1) {
96
+ if (buffer.readUInt32LE(offset) === localFileHeaderSignature)
97
+ entries += 1;
98
+ }
99
+ return entries > 0 ? entries : undefined;
100
+ }
101
+ const fingerprintProvider = {
102
+ name: 'fingerprint',
103
+ apply(_context, helpers) {
104
+ const filePath = helpers.resolvePath();
105
+ const bytes = helpers.readBytes();
106
+ if (!filePath || !bytes)
107
+ return { metadata: {}, lines: [], summaryBits: [] };
108
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
109
+ const sizeBytes = bytes.length;
110
+ return {
111
+ metadata: {
112
+ previewLocalPath: filePath,
113
+ previewSha256: sha256,
114
+ previewSizeBytes: sizeBytes,
115
+ },
116
+ lines: [`Fingerprint: sha256 ${sha256} (${sizeBytes} bytes)`],
117
+ summaryBits: [`sha256 ${sha256.slice(0, 12)}`, `${sizeBytes} bytes`],
118
+ };
119
+ },
120
+ };
121
+ const bytePeekProvider = {
122
+ name: 'byte-peek',
123
+ apply(context, helpers) {
124
+ const bytes = helpers.readBytes();
125
+ const preview = bytes ? toHexPreview(bytes, context.bytePeek) : undefined;
126
+ if (!preview)
127
+ return { metadata: {}, lines: [], summaryBits: [] };
128
+ return {
129
+ metadata: {
130
+ previewBytePeekHex: preview,
131
+ },
132
+ lines: [`Byte peek: ${preview}`],
133
+ summaryBits: [`peek ${preview}`],
134
+ };
135
+ },
136
+ };
137
+ const imageDimensionsProvider = {
138
+ name: 'image-dimensions',
139
+ apply(context, helpers) {
140
+ if (context.category !== 'image')
141
+ return { metadata: {}, lines: [], summaryBits: [] };
142
+ const bytes = helpers.readBytes();
143
+ if (!bytes)
144
+ return { metadata: {}, lines: [], summaryBits: [] };
145
+ const dimensions = parsePngDimensions(bytes) ?? parseGifDimensions(bytes) ?? parseJpegDimensions(bytes);
146
+ if (!dimensions)
147
+ return { metadata: {}, lines: [], summaryBits: [] };
148
+ return {
149
+ metadata: {
150
+ previewImageWidth: dimensions.width,
151
+ previewImageHeight: dimensions.height,
152
+ },
153
+ lines: [`Image dimensions: ${dimensions.width}x${dimensions.height}`],
154
+ summaryBits: [`${dimensions.width}x${dimensions.height}`],
155
+ };
156
+ },
157
+ };
158
+ const pdfMetadataProvider = {
159
+ name: 'pdf-metadata',
160
+ apply(context, helpers) {
161
+ if (context.category !== 'pdf')
162
+ return { metadata: {}, lines: [], summaryBits: [] };
163
+ const bytes = helpers.readBytes();
164
+ if (!bytes)
165
+ return { metadata: {}, lines: [], summaryBits: [] };
166
+ const pageEstimate = estimatePdfPages(bytes);
167
+ if (!pageEstimate)
168
+ return { metadata: {}, lines: [], summaryBits: [] };
169
+ return {
170
+ metadata: {
171
+ previewPdfPageEstimate: pageEstimate,
172
+ },
173
+ lines: [`PDF page estimate: ${pageEstimate}`],
174
+ summaryBits: [`${pageEstimate} page${pageEstimate === 1 ? '' : 's'}`],
175
+ };
176
+ },
177
+ };
178
+ const zipMetadataProvider = {
179
+ name: 'zip-metadata',
180
+ apply(_context, helpers) {
181
+ const bytes = helpers.readBytes();
182
+ if (!bytes)
183
+ return { metadata: {}, lines: [], summaryBits: [] };
184
+ const entryCount = estimateZipEntries(bytes);
185
+ if (entryCount === undefined)
186
+ return { metadata: {}, lines: [], summaryBits: [] };
187
+ return {
188
+ metadata: {
189
+ previewZipEntryCount: entryCount,
190
+ },
191
+ lines: [`ZIP entries: ${entryCount}`],
192
+ summaryBits: [`${entryCount} entr${entryCount === 1 ? 'y' : 'ies'}`],
193
+ };
194
+ },
195
+ };
196
+ const PROVIDERS = [
197
+ fingerprintProvider,
198
+ bytePeekProvider,
199
+ imageDimensionsProvider,
200
+ pdfMetadataProvider,
201
+ zipMetadataProvider,
202
+ ];
203
+ export async function runBinaryPreviewProviders(context) {
204
+ const localPath = inferLocalPath(context.workspaceDirectory, context.file);
205
+ let resolvedPath;
206
+ let resolvedBytes;
207
+ if (localPath) {
208
+ try {
209
+ resolvedBytes = await readFile(localPath);
210
+ resolvedPath = localPath;
211
+ }
212
+ catch (error) {
213
+ getLogger().debug('Failed to read file bytes for preview', { filePath: localPath, error });
214
+ }
215
+ }
216
+ const helpers = {
217
+ resolvePath() {
218
+ return resolvedPath;
219
+ },
220
+ readBytes() {
221
+ return resolvedBytes;
222
+ },
223
+ };
224
+ const enabled = new Set(context.enabledProviders);
225
+ const outputs = PROVIDERS.filter((provider) => enabled.has(provider.name)).map((provider) => ({
226
+ name: provider.name,
227
+ output: provider.apply(context, helpers),
228
+ }));
229
+ const metadata = {
230
+ previewProviders: outputs
231
+ .filter((entry) => Object.keys(entry.output.metadata).length > 0 || entry.output.lines.length > 0)
232
+ .map((entry) => entry.name),
233
+ };
234
+ const lines = [];
235
+ const summaryBits = [];
236
+ for (const entry of outputs) {
237
+ Object.assign(metadata, entry.output.metadata);
238
+ lines.push(...entry.output.lines);
239
+ summaryBits.push(...entry.output.summaryBits);
240
+ }
241
+ return {
242
+ metadata,
243
+ lines,
244
+ summaryBits,
245
+ };
246
+ }
@@ -0,0 +1,16 @@
1
+ import type { PrivacyOptions } from './types.js';
2
+ export declare const PRIVACY_REDACTION_TEXT = "[REDACTED]";
3
+ export declare const PRIVACY_REDACTED_PATH_TEXT = "[REDACTED_PATH]";
4
+ export declare const PRIVACY_EXCLUDED_TOOL_OUTPUT = "[Excluded tool payload by opencode-lcm privacy policy.]";
5
+ export declare const PRIVACY_EXCLUDED_FILE_CONTENT = "[Excluded file content by opencode-lcm privacy policy.]";
6
+ export declare const PRIVACY_EXCLUDED_FILE_REFERENCE = "[Excluded file reference by opencode-lcm privacy policy.]";
7
+ export type CompiledPrivacyOptions = {
8
+ excludeToolPrefixes: string[];
9
+ excludePathPatterns: RegExp[];
10
+ redactPatterns: RegExp[];
11
+ };
12
+ export declare function compilePrivacyOptions(options: PrivacyOptions): CompiledPrivacyOptions;
13
+ export declare function redactText(value: string, privacy: CompiledPrivacyOptions): string;
14
+ export declare function redactStructuredValue<T>(value: T, privacy: CompiledPrivacyOptions, currentKey?: string): T;
15
+ export declare function isExcludedTool(toolName: string, privacy: CompiledPrivacyOptions): boolean;
16
+ export declare function matchesExcludedPath(candidates: Array<string | undefined>, privacy: CompiledPrivacyOptions): boolean;
@@ -0,0 +1,92 @@
1
+ export const PRIVACY_REDACTION_TEXT = '[REDACTED]';
2
+ export const PRIVACY_REDACTED_PATH_TEXT = '[REDACTED_PATH]';
3
+ export const PRIVACY_EXCLUDED_TOOL_OUTPUT = '[Excluded tool payload by opencode-lcm privacy policy.]';
4
+ export const PRIVACY_EXCLUDED_FILE_CONTENT = '[Excluded file content by opencode-lcm privacy policy.]';
5
+ export const PRIVACY_EXCLUDED_FILE_REFERENCE = '[Excluded file reference by opencode-lcm privacy policy.]';
6
+ const EXEMPT_STRING_KEYS = new Set([
7
+ 'agent',
8
+ 'artifactID',
9
+ 'callID',
10
+ 'fieldName',
11
+ 'id',
12
+ 'messageID',
13
+ 'mime',
14
+ 'modelID',
15
+ 'name',
16
+ 'nodeID',
17
+ 'parentID',
18
+ 'parentSessionID',
19
+ 'partID',
20
+ 'projectID',
21
+ 'providerID',
22
+ 'role',
23
+ 'rootSessionID',
24
+ 'sessionID',
25
+ 'status',
26
+ 'tool',
27
+ 'type',
28
+ 'urlScheme',
29
+ ]);
30
+ function compilePattern(source) {
31
+ try {
32
+ const probe = new RegExp(source, 'u');
33
+ if (probe.test(''))
34
+ return undefined;
35
+ return new RegExp(source, 'gu');
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ function applyPatterns(value, patterns, replacement) {
42
+ let next = value;
43
+ for (const pattern of patterns) {
44
+ pattern.lastIndex = 0;
45
+ next = next.replace(pattern, replacement);
46
+ }
47
+ return next;
48
+ }
49
+ function matchesPattern(value, pattern) {
50
+ pattern.lastIndex = 0;
51
+ return pattern.test(value);
52
+ }
53
+ export function compilePrivacyOptions(options) {
54
+ return {
55
+ excludeToolPrefixes: [
56
+ ...new Set(options.excludeToolPrefixes.filter((value) => value.length > 0)),
57
+ ],
58
+ excludePathPatterns: options.excludePathPatterns
59
+ .map((source) => compilePattern(source))
60
+ .filter((pattern) => Boolean(pattern)),
61
+ redactPatterns: options.redactPatterns
62
+ .map((source) => compilePattern(source))
63
+ .filter((pattern) => Boolean(pattern)),
64
+ };
65
+ }
66
+ export function redactText(value, privacy) {
67
+ const redacted = applyPatterns(value, privacy.redactPatterns, PRIVACY_REDACTION_TEXT);
68
+ return applyPatterns(redacted, privacy.excludePathPatterns, PRIVACY_REDACTED_PATH_TEXT);
69
+ }
70
+ export function redactStructuredValue(value, privacy, currentKey) {
71
+ if (typeof value === 'string') {
72
+ return (currentKey && EXEMPT_STRING_KEYS.has(currentKey) ? value : redactText(value, privacy));
73
+ }
74
+ if (!value || typeof value !== 'object')
75
+ return value;
76
+ if (Array.isArray(value)) {
77
+ return value.map((entry) => redactStructuredValue(entry, privacy, currentKey));
78
+ }
79
+ const entries = Object.entries(value).map(([key, entry]) => [
80
+ key,
81
+ redactStructuredValue(entry, privacy, key),
82
+ ]);
83
+ return Object.fromEntries(entries);
84
+ }
85
+ export function isExcludedTool(toolName, privacy) {
86
+ return privacy.excludeToolPrefixes.some((prefix) => toolName.startsWith(prefix));
87
+ }
88
+ export function matchesExcludedPath(candidates, privacy) {
89
+ return candidates.some((candidate) => typeof candidate === 'string' &&
90
+ candidate.length > 0 &&
91
+ privacy.excludePathPatterns.some((pattern) => matchesPattern(candidate, pattern)));
92
+ }
@@ -0,0 +1,12 @@
1
+ import type { SearchResult } from './types.js';
2
+ export type SearchCandidate = {
3
+ id: string;
4
+ type: string;
5
+ sessionID?: string;
6
+ timestamp: number;
7
+ snippet: string;
8
+ content: string;
9
+ sourceKind: 'message' | 'summary' | 'artifact';
10
+ sourceOrder: number;
11
+ };
12
+ export declare function rankSearchCandidates(candidates: SearchCandidate[], query: string, limit: number): SearchResult[];
@@ -0,0 +1,98 @@
1
+ function escapeRegExp(value) {
2
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3
+ }
4
+ function tokenizeQuery(query) {
5
+ return [...new Set(query.toLowerCase().match(/[a-z0-9_]+/g) ?? [])];
6
+ }
7
+ function buildRecencyRange(candidates) {
8
+ let oldest = Number.POSITIVE_INFINITY;
9
+ let newest = Number.NEGATIVE_INFINITY;
10
+ for (const candidate of candidates) {
11
+ oldest = Math.min(oldest, candidate.timestamp);
12
+ newest = Math.max(newest, candidate.timestamp);
13
+ }
14
+ if (!Number.isFinite(oldest) || !Number.isFinite(newest)) {
15
+ return { oldest: 0, newest: 0 };
16
+ }
17
+ return { oldest, newest };
18
+ }
19
+ const MESSAGE_BASE_SCORE = 135;
20
+ const ARTIFACT_BASE_SCORE = 96;
21
+ const SUMMARY_BASE_SCORE = 78;
22
+ const USER_ROLE_BONUS = 22;
23
+ const ASSISTANT_ROLE_BONUS = 12;
24
+ const EXACT_PHRASE_BONUS = 90;
25
+ const COVERAGE_MULTIPLIER = 70;
26
+ const TOKEN_MATCH_MULTIPLIER = 12;
27
+ const TOTAL_HIT_MULTIPLIER = 2;
28
+ const BOUNDARY_HIT_MULTIPLIER = 4;
29
+ const SNIPPET_EXACT_BONUS = 24;
30
+ const SOURCE_ORDER_DECAY_BASE = 18;
31
+ const RECENCY_MULTIPLIER = 28;
32
+ function scoreSearchCandidate(candidate, query, tokens, recencyRange) {
33
+ const content = candidate.content.toLowerCase();
34
+ const snippet = candidate.snippet.toLowerCase();
35
+ const exactPhrase = content.includes(query);
36
+ const base = candidate.sourceKind === 'message'
37
+ ? MESSAGE_BASE_SCORE
38
+ : candidate.sourceKind === 'artifact'
39
+ ? ARTIFACT_BASE_SCORE
40
+ : SUMMARY_BASE_SCORE;
41
+ let matchedTokens = 0;
42
+ let totalHits = 0;
43
+ let boundaryHits = 0;
44
+ for (const token of tokens) {
45
+ const hasToken = content.includes(token);
46
+ if (hasToken)
47
+ matchedTokens += 1;
48
+ const boundaryPattern = new RegExp(`\\b${escapeRegExp(token)}\\b`, 'g');
49
+ const matches = content.match(boundaryPattern)?.length ?? 0;
50
+ boundaryHits += matches;
51
+ totalHits += matches > 0 ? matches : hasToken ? 1 : 0;
52
+ }
53
+ const coverage = tokens.length > 0 ? matchedTokens / tokens.length : 0;
54
+ let score = base;
55
+ if (candidate.sourceKind === 'message') {
56
+ score +=
57
+ candidate.type === 'user'
58
+ ? USER_ROLE_BONUS
59
+ : candidate.type === 'assistant'
60
+ ? ASSISTANT_ROLE_BONUS
61
+ : 0;
62
+ }
63
+ score += exactPhrase ? EXACT_PHRASE_BONUS : 0;
64
+ score += coverage * COVERAGE_MULTIPLIER;
65
+ score += matchedTokens * TOKEN_MATCH_MULTIPLIER;
66
+ score += Math.min(totalHits, matchedTokens + 2) * TOTAL_HIT_MULTIPLIER;
67
+ score += Math.min(boundaryHits, matchedTokens) * BOUNDARY_HIT_MULTIPLIER;
68
+ score += snippet.includes(query) ? SNIPPET_EXACT_BONUS : 0;
69
+ score += Math.max(0, SOURCE_ORDER_DECAY_BASE - candidate.sourceOrder);
70
+ if (recencyRange.newest > recencyRange.oldest) {
71
+ const recencyRatio = (candidate.timestamp - recencyRange.oldest) / (recencyRange.newest - recencyRange.oldest);
72
+ score += recencyRatio * RECENCY_MULTIPLIER;
73
+ }
74
+ return score;
75
+ }
76
+ export function rankSearchCandidates(candidates, query, limit) {
77
+ const exactQuery = query.toLowerCase();
78
+ const tokens = tokenizeQuery(query);
79
+ const deduped = new Map();
80
+ const recencyRange = buildRecencyRange(candidates);
81
+ for (const candidate of candidates) {
82
+ const score = scoreSearchCandidate(candidate, exactQuery, tokens, recencyRange);
83
+ const key = `${candidate.type}:${candidate.id}`;
84
+ const existing = deduped.get(key);
85
+ if (!existing ||
86
+ score > existing.score ||
87
+ (score === existing.score && candidate.timestamp > existing.timestamp)) {
88
+ deduped.set(key, {
89
+ ...candidate,
90
+ score,
91
+ });
92
+ }
93
+ }
94
+ return [...deduped.values()]
95
+ .sort((a, b) => b.score - a.score || b.timestamp - a.timestamp)
96
+ .slice(0, limit)
97
+ .map(({ content: _content, sourceKind: _sourceKind, sourceOrder: _sourceOrder, score: _score, ...result }) => result);
98
+ }
@@ -0,0 +1,31 @@
1
+ import type { SqlDatabaseLike, SqlStatementLike } from './store-types.js';
2
+ /**
3
+ * Validates that a SQL statement result contains the expected number of rows.
4
+ * Throws a descriptive error if the constraint is violated.
5
+ */
6
+ export declare function assertSingleRow(result: unknown, operation: string): asserts result is Record<string, unknown>;
7
+ /**
8
+ * Validates that a SQL statement affected at least one row.
9
+ * Useful for UPDATE/DELETE operations where zero rows may indicate a bug.
10
+ */
11
+ export declare function assertAffectedRows(statement: SqlStatementLike, args: unknown[], operation: string, minRows?: number): void;
12
+ /**
13
+ * Safely executes a SQL statement and returns typed results.
14
+ * Wraps the raw SQLite call with validation to catch schema mismatches early.
15
+ */
16
+ export declare function safeQuery<T extends Record<string, unknown>>(statement: SqlStatementLike, args: unknown[], operation: string): T[];
17
+ /**
18
+ * Safely executes a SQL statement that should return exactly one row.
19
+ */
20
+ export declare function safeQueryOne<T extends Record<string, unknown>>(statement: SqlStatementLike, args: unknown[], operation: string): T | undefined;
21
+ /**
22
+ * Wraps database operations in a transaction with proper error handling.
23
+ * Automatically rolls back on failure.
24
+ */
25
+ export declare function withTransaction(db: SqlDatabaseLike, operation: string, fn: () => void): void;
26
+ /**
27
+ * Lightweight runtime validator for SQL row results.
28
+ * Checks that required keys exist and have the expected types.
29
+ * Returns the row as-is if valid, throws if not.
30
+ */
31
+ export declare function validateRow<T extends Record<string, unknown>>(row: unknown, schema: Record<keyof T, 'string' | 'number' | 'boolean' | 'object' | 'nullable'>, operation: string): T;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Validates that a SQL statement result contains the expected number of rows.
3
+ * Throws a descriptive error if the constraint is violated.
4
+ */
5
+ export function assertSingleRow(result, operation) {
6
+ if (!result || typeof result !== 'object') {
7
+ throw new Error(`${operation}: expected a single row, got ${typeof result}`);
8
+ }
9
+ }
10
+ /**
11
+ * Validates that a SQL statement affected at least one row.
12
+ * Useful for UPDATE/DELETE operations where zero rows may indicate a bug.
13
+ */
14
+ export function assertAffectedRows(statement, args, operation, minRows = 1) {
15
+ const result = statement.run(...args);
16
+ const changes = result?.changes ?? 0;
17
+ if (changes < minRows) {
18
+ throw new Error(`${operation}: expected at least ${minRows} affected rows, got ${changes}`);
19
+ }
20
+ }
21
+ /**
22
+ * Safely executes a SQL statement and returns typed results.
23
+ * Wraps the raw SQLite call with validation to catch schema mismatches early.
24
+ */
25
+ export function safeQuery(statement, args, operation) {
26
+ const result = statement.all(...args);
27
+ if (!Array.isArray(result)) {
28
+ throw new Error(`${operation}: expected array result, got ${typeof result}`);
29
+ }
30
+ return result;
31
+ }
32
+ /**
33
+ * Safely executes a SQL statement that should return exactly one row.
34
+ */
35
+ export function safeQueryOne(statement, args, operation) {
36
+ const result = statement.get(...args);
37
+ if (result && typeof result !== 'object') {
38
+ throw new Error(`${operation}: expected object or undefined, got ${typeof result}`);
39
+ }
40
+ return result;
41
+ }
42
+ /**
43
+ * Wraps database operations in a transaction with proper error handling.
44
+ * Automatically rolls back on failure.
45
+ */
46
+ export function withTransaction(db, operation, fn) {
47
+ db.exec('BEGIN');
48
+ try {
49
+ fn();
50
+ db.exec('COMMIT');
51
+ }
52
+ catch (error) {
53
+ db.exec('ROLLBACK');
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ throw new Error(`${operation} transaction failed: ${message}`);
56
+ }
57
+ }
58
+ /**
59
+ * Lightweight runtime validator for SQL row results.
60
+ * Checks that required keys exist and have the expected types.
61
+ * Returns the row as-is if valid, throws if not.
62
+ */
63
+ export function validateRow(row, schema, operation) {
64
+ if (!row || typeof row !== 'object' || Array.isArray(row)) {
65
+ throw new Error(`${operation}: expected an object row, got ${typeof row}`);
66
+ }
67
+ const record = row;
68
+ for (const [key, expectedType] of Object.entries(schema)) {
69
+ const value = record[key];
70
+ if (expectedType === 'nullable')
71
+ continue;
72
+ if (value === null || value === undefined) {
73
+ throw new Error(`${operation}: missing required column "${key}"`);
74
+ }
75
+ if (typeof value !== expectedType) {
76
+ throw new Error(`${operation}: column "${key}" expected ${expectedType}, got ${typeof value}`);
77
+ }
78
+ }
79
+ return record;
80
+ }
@@ -0,0 +1,50 @@
1
+ import { type CompiledPrivacyOptions } from './privacy.js';
2
+ import type { ArtifactBlobRow, ArtifactRow } from './store-snapshot.js';
3
+ import type { SqlDatabaseLike } from './store-types.js';
4
+ import type { ConversationMessage, NormalizedSession } from './types.js';
5
+ export type ArtifactData = {
6
+ artifactID: string;
7
+ sessionID: string;
8
+ messageID: string;
9
+ partID: string;
10
+ artifactKind: string;
11
+ fieldName: string;
12
+ previewText: string;
13
+ contentText: string;
14
+ contentHash: string;
15
+ charCount: number;
16
+ createdAt: number;
17
+ metadata: Record<string, unknown>;
18
+ };
19
+ export type ExternalizedMessage = {
20
+ storedMessage: ConversationMessage;
21
+ artifacts: ArtifactData[];
22
+ };
23
+ export type ExternalizedSession = {
24
+ storedSession: NormalizedSession;
25
+ artifacts: ArtifactData[];
26
+ };
27
+ export type StoreArtifactBindings = {
28
+ workspaceDirectory: string;
29
+ options: {
30
+ artifactPreviewChars: number;
31
+ binaryPreviewProviders: string[];
32
+ largeContentThreshold: number;
33
+ previewBytePeek: number;
34
+ privacy: CompiledPrivacyOptions;
35
+ };
36
+ getDb(): SqlDatabaseLike;
37
+ readArtifactBlobSync(contentHash?: string | null): ArtifactBlobRow | undefined;
38
+ upsertSessionRowSync(session: NormalizedSession): void;
39
+ upsertMessageInfoSync(sessionID: string, message: ConversationMessage): void;
40
+ deleteMessageSync(sessionID: string, messageID: string): void;
41
+ replaceMessageSearchRowSync(sessionID: string, message: ConversationMessage): void;
42
+ replaceMessageSearchRowsSync(session: NormalizedSession): void;
43
+ };
44
+ export declare function formatArtifactMetadataLines(metadata: Record<string, unknown>): string[];
45
+ export declare function buildArtifactSearchContent(artifact: ArtifactData): string;
46
+ export declare function externalizeMessage(bindings: StoreArtifactBindings, message: ConversationMessage): Promise<ExternalizedMessage>;
47
+ export declare function externalizeSession(bindings: StoreArtifactBindings, session: NormalizedSession): Promise<ExternalizedSession>;
48
+ export declare function persistStoredSessionSync(bindings: StoreArtifactBindings, storedSession: NormalizedSession, artifacts: ArtifactData[]): void;
49
+ export declare function replaceStoredMessageSync(bindings: StoreArtifactBindings, sessionID: string, storedMessage: ConversationMessage, artifacts: ArtifactData[]): void;
50
+ export declare function materializeArtifactRow(bindings: StoreArtifactBindings, row: ArtifactRow): ArtifactData;