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/src/options.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AutomaticRetrievalOptions,
|
|
3
|
+
AutomaticRetrievalScopeBudgets,
|
|
4
|
+
AutomaticRetrievalStopOptions,
|
|
5
|
+
InteropOptions,
|
|
6
|
+
OpencodeLcmOptions,
|
|
7
|
+
PrivacyOptions,
|
|
8
|
+
RetentionPolicyOptions,
|
|
9
|
+
ScopeDefaults,
|
|
10
|
+
ScopeName,
|
|
11
|
+
ScopeProfile,
|
|
12
|
+
} from './types.js';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_INTEROP: InteropOptions = {
|
|
15
|
+
contextMode: true,
|
|
16
|
+
neverOverrideCompactionPrompt: true,
|
|
17
|
+
ignoreToolPrefixes: ['ctx_'],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SCOPE_DEFAULTS: ScopeDefaults = {
|
|
21
|
+
grep: 'session',
|
|
22
|
+
describe: 'session',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const DEFAULT_RETENTION: RetentionPolicyOptions = {
|
|
26
|
+
staleSessionDays: undefined,
|
|
27
|
+
deletedSessionDays: 30,
|
|
28
|
+
orphanBlobDays: 14,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const DEFAULT_PRIVACY: PrivacyOptions = {
|
|
32
|
+
excludeToolPrefixes: [],
|
|
33
|
+
excludePathPatterns: [],
|
|
34
|
+
redactPatterns: [],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const DEFAULT_AUTOMATIC_RETRIEVAL: AutomaticRetrievalOptions = {
|
|
38
|
+
enabled: true,
|
|
39
|
+
maxChars: 900,
|
|
40
|
+
minTokens: 2,
|
|
41
|
+
maxMessageHits: 2,
|
|
42
|
+
maxSummaryHits: 1,
|
|
43
|
+
maxArtifactHits: 1,
|
|
44
|
+
scopeOrder: ['session', 'root', 'worktree'],
|
|
45
|
+
scopeBudgets: {
|
|
46
|
+
session: 16,
|
|
47
|
+
root: 12,
|
|
48
|
+
worktree: 8,
|
|
49
|
+
all: 6,
|
|
50
|
+
},
|
|
51
|
+
stop: {
|
|
52
|
+
targetHits: 3,
|
|
53
|
+
stopOnFirstScopeWithHits: false,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const DEFAULT_OPTIONS: OpencodeLcmOptions = {
|
|
58
|
+
interop: DEFAULT_INTEROP,
|
|
59
|
+
scopeDefaults: DEFAULT_SCOPE_DEFAULTS,
|
|
60
|
+
scopeProfiles: [],
|
|
61
|
+
retention: DEFAULT_RETENTION,
|
|
62
|
+
privacy: DEFAULT_PRIVACY,
|
|
63
|
+
automaticRetrieval: DEFAULT_AUTOMATIC_RETRIEVAL,
|
|
64
|
+
compactContextLimit: 1200,
|
|
65
|
+
systemHint: true,
|
|
66
|
+
freshTailMessages: 10,
|
|
67
|
+
minMessagesForTransform: 16,
|
|
68
|
+
summaryCharBudget: 1500,
|
|
69
|
+
partCharBudget: 160,
|
|
70
|
+
largeContentThreshold: 1200,
|
|
71
|
+
artifactPreviewChars: 220,
|
|
72
|
+
artifactViewChars: 4000,
|
|
73
|
+
binaryPreviewProviders: [
|
|
74
|
+
'fingerprint',
|
|
75
|
+
'byte-peek',
|
|
76
|
+
'image-dimensions',
|
|
77
|
+
'pdf-metadata',
|
|
78
|
+
'zip-metadata',
|
|
79
|
+
],
|
|
80
|
+
previewBytePeek: 16,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
84
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
85
|
+
return value as Record<string, unknown>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function asBoolean(value: unknown, fallback: boolean): boolean {
|
|
89
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function asNumber(value: unknown, fallback: number): number {
|
|
93
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function asNonNegativeNumber(value: unknown, fallback: number): number {
|
|
97
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function asOptionalNumber(value: unknown): number | undefined {
|
|
101
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function asStringArray(value: unknown, fallback: string[]): string[] {
|
|
105
|
+
if (!Array.isArray(value)) return fallback;
|
|
106
|
+
const next = value.filter((item): item is string => typeof item === 'string' && item.length > 0);
|
|
107
|
+
return next.length > 0 ? next : fallback;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function asScopeName(value: unknown, fallback: ScopeName): ScopeName {
|
|
111
|
+
return value === 'session' || value === 'root' || value === 'worktree' || value === 'all'
|
|
112
|
+
? value
|
|
113
|
+
: fallback;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function asScopeNameArray(value: unknown, fallback: ScopeName[]): ScopeName[] {
|
|
117
|
+
if (!Array.isArray(value)) return fallback;
|
|
118
|
+
const result: ScopeName[] = [];
|
|
119
|
+
|
|
120
|
+
for (const item of value) {
|
|
121
|
+
if (item !== 'session' && item !== 'root' && item !== 'worktree' && item !== 'all') continue;
|
|
122
|
+
if (result.includes(item)) continue;
|
|
123
|
+
result.push(item);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result.length > 0 ? result : fallback;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function asScopeDefaults(value: unknown, fallback: ScopeDefaults): ScopeDefaults {
|
|
130
|
+
const record = asRecord(value);
|
|
131
|
+
return {
|
|
132
|
+
grep: asScopeName(record?.grep, fallback.grep),
|
|
133
|
+
describe: asScopeName(record?.describe, fallback.describe),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function asScopeProfiles(value: unknown): ScopeProfile[] {
|
|
138
|
+
if (!Array.isArray(value)) return [];
|
|
139
|
+
|
|
140
|
+
const result: ScopeProfile[] = [];
|
|
141
|
+
|
|
142
|
+
for (const item of value) {
|
|
143
|
+
const record = asRecord(item);
|
|
144
|
+
const worktree =
|
|
145
|
+
typeof record?.worktree === 'string' && record.worktree.length > 0
|
|
146
|
+
? record.worktree
|
|
147
|
+
: undefined;
|
|
148
|
+
if (!worktree) continue;
|
|
149
|
+
|
|
150
|
+
result.push({
|
|
151
|
+
worktree,
|
|
152
|
+
grep:
|
|
153
|
+
record?.grep === undefined
|
|
154
|
+
? undefined
|
|
155
|
+
: asScopeName(record.grep, DEFAULT_SCOPE_DEFAULTS.grep),
|
|
156
|
+
describe:
|
|
157
|
+
record?.describe === undefined
|
|
158
|
+
? undefined
|
|
159
|
+
: asScopeName(record.describe, DEFAULT_SCOPE_DEFAULTS.describe),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function asRetentionOptions(
|
|
167
|
+
value: unknown,
|
|
168
|
+
fallback: RetentionPolicyOptions,
|
|
169
|
+
): RetentionPolicyOptions {
|
|
170
|
+
const record = asRecord(value);
|
|
171
|
+
return {
|
|
172
|
+
staleSessionDays:
|
|
173
|
+
record?.staleSessionDays === undefined
|
|
174
|
+
? fallback.staleSessionDays
|
|
175
|
+
: asOptionalNumber(record.staleSessionDays),
|
|
176
|
+
deletedSessionDays:
|
|
177
|
+
record?.deletedSessionDays === undefined
|
|
178
|
+
? fallback.deletedSessionDays
|
|
179
|
+
: asOptionalNumber(record.deletedSessionDays),
|
|
180
|
+
orphanBlobDays:
|
|
181
|
+
record?.orphanBlobDays === undefined
|
|
182
|
+
? fallback.orphanBlobDays
|
|
183
|
+
: asOptionalNumber(record.orphanBlobDays),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function asPrivacyOptions(value: unknown, fallback: PrivacyOptions): PrivacyOptions {
|
|
188
|
+
const record = asRecord(value);
|
|
189
|
+
return {
|
|
190
|
+
excludeToolPrefixes: asStringArray(record?.excludeToolPrefixes, fallback.excludeToolPrefixes),
|
|
191
|
+
excludePathPatterns: asStringArray(record?.excludePathPatterns, fallback.excludePathPatterns),
|
|
192
|
+
redactPatterns: asStringArray(record?.redactPatterns, fallback.redactPatterns),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function asAutomaticRetrievalOptions(
|
|
197
|
+
value: unknown,
|
|
198
|
+
fallback: AutomaticRetrievalOptions,
|
|
199
|
+
): AutomaticRetrievalOptions {
|
|
200
|
+
const record = asRecord(value);
|
|
201
|
+
return {
|
|
202
|
+
enabled: asBoolean(record?.enabled, fallback.enabled),
|
|
203
|
+
maxChars: asNumber(record?.maxChars, fallback.maxChars),
|
|
204
|
+
minTokens: asNumber(record?.minTokens, fallback.minTokens),
|
|
205
|
+
maxMessageHits: asNumber(record?.maxMessageHits, fallback.maxMessageHits),
|
|
206
|
+
maxSummaryHits: asNumber(record?.maxSummaryHits, fallback.maxSummaryHits),
|
|
207
|
+
maxArtifactHits: asNumber(record?.maxArtifactHits, fallback.maxArtifactHits),
|
|
208
|
+
scopeOrder: asScopeNameArray(record?.scopeOrder, fallback.scopeOrder),
|
|
209
|
+
scopeBudgets: asAutomaticRetrievalScopeBudgets(record?.scopeBudgets, fallback.scopeBudgets),
|
|
210
|
+
stop: asAutomaticRetrievalStopOptions(record?.stop, fallback.stop),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function asAutomaticRetrievalScopeBudgets(
|
|
215
|
+
value: unknown,
|
|
216
|
+
fallback: AutomaticRetrievalScopeBudgets,
|
|
217
|
+
): AutomaticRetrievalScopeBudgets {
|
|
218
|
+
const record = asRecord(value);
|
|
219
|
+
return {
|
|
220
|
+
session: asNonNegativeNumber(record?.session, fallback.session),
|
|
221
|
+
root: asNonNegativeNumber(record?.root, fallback.root),
|
|
222
|
+
worktree: asNonNegativeNumber(record?.worktree, fallback.worktree),
|
|
223
|
+
all: asNonNegativeNumber(record?.all, fallback.all),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function asAutomaticRetrievalStopOptions(
|
|
228
|
+
value: unknown,
|
|
229
|
+
fallback: AutomaticRetrievalStopOptions,
|
|
230
|
+
): AutomaticRetrievalStopOptions {
|
|
231
|
+
const record = asRecord(value);
|
|
232
|
+
return {
|
|
233
|
+
targetHits: asNonNegativeNumber(record?.targetHits, fallback.targetHits),
|
|
234
|
+
stopOnFirstScopeWithHits: asBoolean(
|
|
235
|
+
record?.stopOnFirstScopeWithHits,
|
|
236
|
+
fallback.stopOnFirstScopeWithHits,
|
|
237
|
+
),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function resolveOptions(raw: unknown): OpencodeLcmOptions {
|
|
242
|
+
const options = asRecord(raw);
|
|
243
|
+
const interop = asRecord(options?.interop);
|
|
244
|
+
const scopeDefaults = asScopeDefaults(options?.scopeDefaults, DEFAULT_SCOPE_DEFAULTS);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
interop: {
|
|
248
|
+
contextMode: asBoolean(interop?.contextMode, DEFAULT_INTEROP.contextMode),
|
|
249
|
+
neverOverrideCompactionPrompt: asBoolean(
|
|
250
|
+
interop?.neverOverrideCompactionPrompt,
|
|
251
|
+
DEFAULT_INTEROP.neverOverrideCompactionPrompt,
|
|
252
|
+
),
|
|
253
|
+
ignoreToolPrefixes: asStringArray(
|
|
254
|
+
interop?.ignoreToolPrefixes,
|
|
255
|
+
DEFAULT_INTEROP.ignoreToolPrefixes,
|
|
256
|
+
),
|
|
257
|
+
},
|
|
258
|
+
scopeDefaults,
|
|
259
|
+
scopeProfiles: asScopeProfiles(options?.scopeProfiles),
|
|
260
|
+
retention: asRetentionOptions(options?.retention, DEFAULT_RETENTION),
|
|
261
|
+
privacy: asPrivacyOptions(options?.privacy, DEFAULT_PRIVACY),
|
|
262
|
+
automaticRetrieval: asAutomaticRetrievalOptions(
|
|
263
|
+
options?.automaticRetrieval,
|
|
264
|
+
DEFAULT_AUTOMATIC_RETRIEVAL,
|
|
265
|
+
),
|
|
266
|
+
compactContextLimit: asNumber(
|
|
267
|
+
options?.compactContextLimit,
|
|
268
|
+
DEFAULT_OPTIONS.compactContextLimit,
|
|
269
|
+
),
|
|
270
|
+
systemHint: asBoolean(options?.systemHint, DEFAULT_OPTIONS.systemHint),
|
|
271
|
+
storeDir:
|
|
272
|
+
typeof options?.storeDir === 'string' && options.storeDir.length > 0
|
|
273
|
+
? options.storeDir
|
|
274
|
+
: undefined,
|
|
275
|
+
freshTailMessages: asNumber(options?.freshTailMessages, DEFAULT_OPTIONS.freshTailMessages),
|
|
276
|
+
minMessagesForTransform: asNumber(
|
|
277
|
+
options?.minMessagesForTransform,
|
|
278
|
+
DEFAULT_OPTIONS.minMessagesForTransform,
|
|
279
|
+
),
|
|
280
|
+
summaryCharBudget: asNumber(options?.summaryCharBudget, DEFAULT_OPTIONS.summaryCharBudget),
|
|
281
|
+
partCharBudget: asNumber(options?.partCharBudget, DEFAULT_OPTIONS.partCharBudget),
|
|
282
|
+
largeContentThreshold: asNumber(
|
|
283
|
+
options?.largeContentThreshold,
|
|
284
|
+
DEFAULT_OPTIONS.largeContentThreshold,
|
|
285
|
+
),
|
|
286
|
+
artifactPreviewChars: asNumber(
|
|
287
|
+
options?.artifactPreviewChars,
|
|
288
|
+
DEFAULT_OPTIONS.artifactPreviewChars,
|
|
289
|
+
),
|
|
290
|
+
artifactViewChars: asNumber(options?.artifactViewChars, DEFAULT_OPTIONS.artifactViewChars),
|
|
291
|
+
binaryPreviewProviders: asStringArray(
|
|
292
|
+
options?.binaryPreviewProviders,
|
|
293
|
+
DEFAULT_OPTIONS.binaryPreviewProviders,
|
|
294
|
+
),
|
|
295
|
+
previewBytePeek: asNumber(options?.previewBytePeek, DEFAULT_OPTIONS.previewBytePeek),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import type { Part } from '@opencode-ai/sdk';
|
|
5
|
+
|
|
6
|
+
import { getLogger } from './logging.js';
|
|
7
|
+
import { resolveWorkspacePath } from './workspace-path.js';
|
|
8
|
+
|
|
9
|
+
type FilePart = Extract<Part, { type: 'file' }>;
|
|
10
|
+
|
|
11
|
+
type PreviewContext = {
|
|
12
|
+
workspaceDirectory: string;
|
|
13
|
+
file: FilePart;
|
|
14
|
+
category: string;
|
|
15
|
+
extension?: string;
|
|
16
|
+
mime?: string;
|
|
17
|
+
enabledProviders: string[];
|
|
18
|
+
bytePeek: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type PreviewOutput = {
|
|
22
|
+
metadata: Record<string, unknown>;
|
|
23
|
+
lines: string[];
|
|
24
|
+
summaryBits: string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ProviderName =
|
|
28
|
+
| 'fingerprint'
|
|
29
|
+
| 'byte-peek'
|
|
30
|
+
| 'image-dimensions'
|
|
31
|
+
| 'pdf-metadata'
|
|
32
|
+
| 'zip-metadata';
|
|
33
|
+
|
|
34
|
+
type Provider = {
|
|
35
|
+
name: ProviderName;
|
|
36
|
+
apply(context: PreviewContext, helpers: ProviderHelpers): PreviewOutput;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type ProviderHelpers = {
|
|
40
|
+
resolvePath(): string | undefined;
|
|
41
|
+
readBytes(): Buffer | undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function inferLocalPath(workspaceDirectory: string, file: FilePart): string | undefined {
|
|
45
|
+
const sourcePath = file.source && 'path' in file.source ? file.source.path : undefined;
|
|
46
|
+
if (!sourcePath) return undefined;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
return resolveWorkspacePath(workspaceDirectory, sourcePath);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
getLogger().debug('Failed to resolve workspace path', { sourcePath, error });
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toHexPreview(buffer: Buffer, bytes: number): string | undefined {
|
|
57
|
+
if (buffer.length === 0) return undefined;
|
|
58
|
+
return [...buffer.subarray(0, Math.max(1, bytes))]
|
|
59
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
60
|
+
.join(' ');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parsePngDimensions(buffer: Buffer): { width: number; height: number } | undefined {
|
|
64
|
+
if (buffer.length < 24) return undefined;
|
|
65
|
+
if (!buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
|
|
66
|
+
return undefined;
|
|
67
|
+
return {
|
|
68
|
+
width: buffer.readUInt32BE(16),
|
|
69
|
+
height: buffer.readUInt32BE(20),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseGifDimensions(buffer: Buffer): { width: number; height: number } | undefined {
|
|
74
|
+
if (buffer.length < 10) return undefined;
|
|
75
|
+
const header = buffer.subarray(0, 6).toString('ascii');
|
|
76
|
+
if (header !== 'GIF87a' && header !== 'GIF89a') return undefined;
|
|
77
|
+
return {
|
|
78
|
+
width: buffer.readUInt16LE(6),
|
|
79
|
+
height: buffer.readUInt16LE(8),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseJpegDimensions(buffer: Buffer): { width: number; height: number } | undefined {
|
|
84
|
+
if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8) return undefined;
|
|
85
|
+
let offset = 2;
|
|
86
|
+
while (offset + 9 < buffer.length) {
|
|
87
|
+
if (buffer[offset] !== 0xff) {
|
|
88
|
+
offset += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const marker = buffer[offset + 1];
|
|
92
|
+
if (marker === 0xd9 || marker === 0xda) break;
|
|
93
|
+
const size = buffer.readUInt16BE(offset + 2);
|
|
94
|
+
if (size < 2 || offset + 2 + size > buffer.length) break;
|
|
95
|
+
if (
|
|
96
|
+
(marker >= 0xc0 && marker <= 0xc3) ||
|
|
97
|
+
(marker >= 0xc5 && marker <= 0xc7) ||
|
|
98
|
+
(marker >= 0xc9 && marker <= 0xcb) ||
|
|
99
|
+
(marker >= 0xcd && marker <= 0xcf)
|
|
100
|
+
) {
|
|
101
|
+
return {
|
|
102
|
+
height: buffer.readUInt16BE(offset + 5),
|
|
103
|
+
width: buffer.readUInt16BE(offset + 7),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
offset += 2 + size;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function estimatePdfPages(buffer: Buffer): number | undefined {
|
|
112
|
+
const text = buffer.toString('latin1');
|
|
113
|
+
const matches = text.match(/\/Type\s*\/Page([^s]|$)/g);
|
|
114
|
+
return matches && matches.length > 0 ? matches.length : undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function estimateZipEntries(buffer: Buffer): number | undefined {
|
|
118
|
+
if (buffer.length < 4) return undefined;
|
|
119
|
+
|
|
120
|
+
const localFileHeaderSignature = 0x04034b50;
|
|
121
|
+
const endOfCentralDirectorySignature = 0x06054b50;
|
|
122
|
+
const firstSignature = buffer.readUInt32LE(0);
|
|
123
|
+
if (
|
|
124
|
+
firstSignature !== localFileHeaderSignature &&
|
|
125
|
+
firstSignature !== endOfCentralDirectorySignature
|
|
126
|
+
) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const searchStart = Math.max(0, buffer.length - 65557);
|
|
131
|
+
for (let offset = buffer.length - 22; offset >= searchStart; offset -= 1) {
|
|
132
|
+
if (buffer.readUInt32LE(offset) !== endOfCentralDirectorySignature) continue;
|
|
133
|
+
return buffer.readUInt16LE(offset + 10);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let entries = 0;
|
|
137
|
+
for (let offset = 0; offset <= buffer.length - 4; offset += 1) {
|
|
138
|
+
if (buffer.readUInt32LE(offset) === localFileHeaderSignature) entries += 1;
|
|
139
|
+
}
|
|
140
|
+
return entries > 0 ? entries : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const fingerprintProvider: Provider = {
|
|
144
|
+
name: 'fingerprint',
|
|
145
|
+
apply(_context, helpers) {
|
|
146
|
+
const filePath = helpers.resolvePath();
|
|
147
|
+
const bytes = helpers.readBytes();
|
|
148
|
+
if (!filePath || !bytes) return { metadata: {}, lines: [], summaryBits: [] };
|
|
149
|
+
|
|
150
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
151
|
+
const sizeBytes = bytes.length;
|
|
152
|
+
return {
|
|
153
|
+
metadata: {
|
|
154
|
+
previewLocalPath: filePath,
|
|
155
|
+
previewSha256: sha256,
|
|
156
|
+
previewSizeBytes: sizeBytes,
|
|
157
|
+
},
|
|
158
|
+
lines: [`Fingerprint: sha256 ${sha256} (${sizeBytes} bytes)`],
|
|
159
|
+
summaryBits: [`sha256 ${sha256.slice(0, 12)}`, `${sizeBytes} bytes`],
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const bytePeekProvider: Provider = {
|
|
165
|
+
name: 'byte-peek',
|
|
166
|
+
apply(context, helpers) {
|
|
167
|
+
const bytes = helpers.readBytes();
|
|
168
|
+
const preview = bytes ? toHexPreview(bytes, context.bytePeek) : undefined;
|
|
169
|
+
if (!preview) return { metadata: {}, lines: [], summaryBits: [] };
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
metadata: {
|
|
173
|
+
previewBytePeekHex: preview,
|
|
174
|
+
},
|
|
175
|
+
lines: [`Byte peek: ${preview}`],
|
|
176
|
+
summaryBits: [`peek ${preview}`],
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const imageDimensionsProvider: Provider = {
|
|
182
|
+
name: 'image-dimensions',
|
|
183
|
+
apply(context, helpers) {
|
|
184
|
+
if (context.category !== 'image') return { metadata: {}, lines: [], summaryBits: [] };
|
|
185
|
+
const bytes = helpers.readBytes();
|
|
186
|
+
if (!bytes) return { metadata: {}, lines: [], summaryBits: [] };
|
|
187
|
+
|
|
188
|
+
const dimensions =
|
|
189
|
+
parsePngDimensions(bytes) ?? parseGifDimensions(bytes) ?? parseJpegDimensions(bytes);
|
|
190
|
+
if (!dimensions) return { metadata: {}, lines: [], summaryBits: [] };
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
metadata: {
|
|
194
|
+
previewImageWidth: dimensions.width,
|
|
195
|
+
previewImageHeight: dimensions.height,
|
|
196
|
+
},
|
|
197
|
+
lines: [`Image dimensions: ${dimensions.width}x${dimensions.height}`],
|
|
198
|
+
summaryBits: [`${dimensions.width}x${dimensions.height}`],
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const pdfMetadataProvider: Provider = {
|
|
204
|
+
name: 'pdf-metadata',
|
|
205
|
+
apply(context, helpers) {
|
|
206
|
+
if (context.category !== 'pdf') return { metadata: {}, lines: [], summaryBits: [] };
|
|
207
|
+
const bytes = helpers.readBytes();
|
|
208
|
+
if (!bytes) return { metadata: {}, lines: [], summaryBits: [] };
|
|
209
|
+
const pageEstimate = estimatePdfPages(bytes);
|
|
210
|
+
if (!pageEstimate) return { metadata: {}, lines: [], summaryBits: [] };
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
metadata: {
|
|
214
|
+
previewPdfPageEstimate: pageEstimate,
|
|
215
|
+
},
|
|
216
|
+
lines: [`PDF page estimate: ${pageEstimate}`],
|
|
217
|
+
summaryBits: [`${pageEstimate} page${pageEstimate === 1 ? '' : 's'}`],
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const zipMetadataProvider: Provider = {
|
|
223
|
+
name: 'zip-metadata',
|
|
224
|
+
apply(_context, helpers) {
|
|
225
|
+
const bytes = helpers.readBytes();
|
|
226
|
+
if (!bytes) return { metadata: {}, lines: [], summaryBits: [] };
|
|
227
|
+
const entryCount = estimateZipEntries(bytes);
|
|
228
|
+
if (entryCount === undefined) return { metadata: {}, lines: [], summaryBits: [] };
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
metadata: {
|
|
232
|
+
previewZipEntryCount: entryCount,
|
|
233
|
+
},
|
|
234
|
+
lines: [`ZIP entries: ${entryCount}`],
|
|
235
|
+
summaryBits: [`${entryCount} entr${entryCount === 1 ? 'y' : 'ies'}`],
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const PROVIDERS: Provider[] = [
|
|
241
|
+
fingerprintProvider,
|
|
242
|
+
bytePeekProvider,
|
|
243
|
+
imageDimensionsProvider,
|
|
244
|
+
pdfMetadataProvider,
|
|
245
|
+
zipMetadataProvider,
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
export async function runBinaryPreviewProviders(context: PreviewContext): Promise<PreviewOutput> {
|
|
249
|
+
const localPath = inferLocalPath(context.workspaceDirectory, context.file);
|
|
250
|
+
let resolvedPath: string | undefined;
|
|
251
|
+
let resolvedBytes: Buffer | undefined;
|
|
252
|
+
|
|
253
|
+
if (localPath) {
|
|
254
|
+
try {
|
|
255
|
+
resolvedBytes = await readFile(localPath);
|
|
256
|
+
resolvedPath = localPath;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
getLogger().debug('Failed to read file bytes for preview', { filePath: localPath, error });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const helpers: ProviderHelpers = {
|
|
263
|
+
resolvePath() {
|
|
264
|
+
return resolvedPath;
|
|
265
|
+
},
|
|
266
|
+
readBytes() {
|
|
267
|
+
return resolvedBytes;
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const enabled = new Set(context.enabledProviders);
|
|
272
|
+
const outputs = PROVIDERS.filter((provider) => enabled.has(provider.name)).map((provider) => ({
|
|
273
|
+
name: provider.name,
|
|
274
|
+
output: provider.apply(context, helpers),
|
|
275
|
+
}));
|
|
276
|
+
|
|
277
|
+
const metadata: Record<string, unknown> = {
|
|
278
|
+
previewProviders: outputs
|
|
279
|
+
.filter(
|
|
280
|
+
(entry) => Object.keys(entry.output.metadata).length > 0 || entry.output.lines.length > 0,
|
|
281
|
+
)
|
|
282
|
+
.map((entry) => entry.name),
|
|
283
|
+
};
|
|
284
|
+
const lines: string[] = [];
|
|
285
|
+
const summaryBits: string[] = [];
|
|
286
|
+
|
|
287
|
+
for (const entry of outputs) {
|
|
288
|
+
Object.assign(metadata, entry.output.metadata);
|
|
289
|
+
lines.push(...entry.output.lines);
|
|
290
|
+
summaryBits.push(...entry.output.summaryBits);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
metadata,
|
|
295
|
+
lines,
|
|
296
|
+
summaryBits,
|
|
297
|
+
};
|
|
298
|
+
}
|
package/src/privacy.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { PrivacyOptions } from './types.js';
|
|
2
|
+
|
|
3
|
+
export const PRIVACY_REDACTION_TEXT = '[REDACTED]';
|
|
4
|
+
export const PRIVACY_REDACTED_PATH_TEXT = '[REDACTED_PATH]';
|
|
5
|
+
export const PRIVACY_EXCLUDED_TOOL_OUTPUT =
|
|
6
|
+
'[Excluded tool payload by opencode-lcm privacy policy.]';
|
|
7
|
+
export const PRIVACY_EXCLUDED_FILE_CONTENT =
|
|
8
|
+
'[Excluded file content by opencode-lcm privacy policy.]';
|
|
9
|
+
export const PRIVACY_EXCLUDED_FILE_REFERENCE =
|
|
10
|
+
'[Excluded file reference by opencode-lcm privacy policy.]';
|
|
11
|
+
|
|
12
|
+
export type CompiledPrivacyOptions = {
|
|
13
|
+
excludeToolPrefixes: string[];
|
|
14
|
+
excludePathPatterns: RegExp[];
|
|
15
|
+
redactPatterns: RegExp[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const EXEMPT_STRING_KEYS = new Set([
|
|
19
|
+
'agent',
|
|
20
|
+
'artifactID',
|
|
21
|
+
'callID',
|
|
22
|
+
'fieldName',
|
|
23
|
+
'id',
|
|
24
|
+
'messageID',
|
|
25
|
+
'mime',
|
|
26
|
+
'modelID',
|
|
27
|
+
'name',
|
|
28
|
+
'nodeID',
|
|
29
|
+
'parentID',
|
|
30
|
+
'parentSessionID',
|
|
31
|
+
'partID',
|
|
32
|
+
'projectID',
|
|
33
|
+
'providerID',
|
|
34
|
+
'role',
|
|
35
|
+
'rootSessionID',
|
|
36
|
+
'sessionID',
|
|
37
|
+
'status',
|
|
38
|
+
'tool',
|
|
39
|
+
'type',
|
|
40
|
+
'urlScheme',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
function compilePattern(source: string): RegExp | undefined {
|
|
44
|
+
try {
|
|
45
|
+
const probe = new RegExp(source, 'u');
|
|
46
|
+
if (probe.test('')) return undefined;
|
|
47
|
+
return new RegExp(source, 'gu');
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function applyPatterns(value: string, patterns: RegExp[], replacement: string): string {
|
|
54
|
+
let next = value;
|
|
55
|
+
for (const pattern of patterns) {
|
|
56
|
+
pattern.lastIndex = 0;
|
|
57
|
+
next = next.replace(pattern, replacement);
|
|
58
|
+
}
|
|
59
|
+
return next;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function matchesPattern(value: string, pattern: RegExp): boolean {
|
|
63
|
+
pattern.lastIndex = 0;
|
|
64
|
+
return pattern.test(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function compilePrivacyOptions(options: PrivacyOptions): CompiledPrivacyOptions {
|
|
68
|
+
return {
|
|
69
|
+
excludeToolPrefixes: [
|
|
70
|
+
...new Set(options.excludeToolPrefixes.filter((value) => value.length > 0)),
|
|
71
|
+
],
|
|
72
|
+
excludePathPatterns: options.excludePathPatterns
|
|
73
|
+
.map((source) => compilePattern(source))
|
|
74
|
+
.filter((pattern): pattern is RegExp => Boolean(pattern)),
|
|
75
|
+
redactPatterns: options.redactPatterns
|
|
76
|
+
.map((source) => compilePattern(source))
|
|
77
|
+
.filter((pattern): pattern is RegExp => Boolean(pattern)),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function redactText(value: string, privacy: CompiledPrivacyOptions): string {
|
|
82
|
+
const redacted = applyPatterns(value, privacy.redactPatterns, PRIVACY_REDACTION_TEXT);
|
|
83
|
+
return applyPatterns(redacted, privacy.excludePathPatterns, PRIVACY_REDACTED_PATH_TEXT);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function redactStructuredValue<T>(
|
|
87
|
+
value: T,
|
|
88
|
+
privacy: CompiledPrivacyOptions,
|
|
89
|
+
currentKey?: string,
|
|
90
|
+
): T {
|
|
91
|
+
if (typeof value === 'string') {
|
|
92
|
+
return (
|
|
93
|
+
currentKey && EXEMPT_STRING_KEYS.has(currentKey) ? value : redactText(value, privacy)
|
|
94
|
+
) as T;
|
|
95
|
+
}
|
|
96
|
+
if (!value || typeof value !== 'object') return value;
|
|
97
|
+
if (Array.isArray(value)) {
|
|
98
|
+
return value.map((entry) => redactStructuredValue(entry, privacy, currentKey)) as T;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const entries = Object.entries(value).map(([key, entry]) => [
|
|
102
|
+
key,
|
|
103
|
+
redactStructuredValue(entry, privacy, key),
|
|
104
|
+
]);
|
|
105
|
+
return Object.fromEntries(entries) as T;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function isExcludedTool(toolName: string, privacy: CompiledPrivacyOptions): boolean {
|
|
109
|
+
return privacy.excludeToolPrefixes.some((prefix) => toolName.startsWith(prefix));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function matchesExcludedPath(
|
|
113
|
+
candidates: Array<string | undefined>,
|
|
114
|
+
privacy: CompiledPrivacyOptions,
|
|
115
|
+
): boolean {
|
|
116
|
+
return candidates.some(
|
|
117
|
+
(candidate) =>
|
|
118
|
+
typeof candidate === 'string' &&
|
|
119
|
+
candidate.length > 0 &&
|
|
120
|
+
privacy.excludePathPatterns.some((pattern) => matchesPattern(candidate, pattern)),
|
|
121
|
+
);
|
|
122
|
+
}
|