@siftd/connect-agent 0.2.30 → 0.2.32

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.
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Asset System - Fast-path preview architecture
3
+ *
4
+ * Assets are files produced by workers with metadata for instant preview.
5
+ * The preview-worker generates thumbnails/metadata WITHOUT using the LLM.
6
+ */
7
+ import { createHash } from 'crypto';
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, realpathSync } from 'fs';
9
+ import { basename, extname, join, relative, sep } from 'path';
10
+ /**
11
+ * Convert internal manifest to client-safe version (strips path)
12
+ */
13
+ export function toClientManifest(manifest) {
14
+ const { path, ...clientSafe } = manifest;
15
+ return clientSafe;
16
+ }
17
+ /**
18
+ * Validate that a resolved path is within an allowed root (prevents traversal/symlink escapes)
19
+ */
20
+ export function isPathWithinRoot(filePath, rootDir) {
21
+ try {
22
+ const resolvedPath = realpathSync(filePath);
23
+ const resolvedRoot = realpathSync(rootDir);
24
+ return resolvedPath.startsWith(resolvedRoot + sep);
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ // ============================================================================
31
+ // UTILITY FUNCTIONS
32
+ // ============================================================================
33
+ /**
34
+ * Generate SHA256 hash of file contents
35
+ */
36
+ export function hashFile(filePath) {
37
+ const content = readFileSync(filePath);
38
+ return createHash('sha256').update(content).digest('hex');
39
+ }
40
+ /**
41
+ * Get MIME type from file extension
42
+ */
43
+ export function getMimeType(filePath) {
44
+ const ext = extname(filePath).toLowerCase().slice(1);
45
+ const mimeMap = {
46
+ // Images
47
+ png: 'image/png',
48
+ jpg: 'image/jpeg',
49
+ jpeg: 'image/jpeg',
50
+ gif: 'image/gif',
51
+ svg: 'image/svg+xml',
52
+ webp: 'image/webp',
53
+ ico: 'image/x-icon',
54
+ // Documents
55
+ pdf: 'application/pdf',
56
+ doc: 'application/msword',
57
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
58
+ // Code/Text
59
+ js: 'text/javascript',
60
+ ts: 'text/typescript',
61
+ jsx: 'text/javascript',
62
+ tsx: 'text/typescript',
63
+ json: 'application/json',
64
+ html: 'text/html',
65
+ css: 'text/css',
66
+ md: 'text/markdown',
67
+ txt: 'text/plain',
68
+ csv: 'text/csv',
69
+ xml: 'text/xml',
70
+ yaml: 'text/yaml',
71
+ yml: 'text/yaml',
72
+ sh: 'text/x-shellscript',
73
+ py: 'text/x-python',
74
+ // Data
75
+ sqlite: 'application/x-sqlite3',
76
+ db: 'application/x-sqlite3',
77
+ // Video/Audio
78
+ mp4: 'video/mp4',
79
+ webm: 'video/webm',
80
+ mp3: 'audio/mpeg',
81
+ wav: 'audio/wav',
82
+ };
83
+ return mimeMap[ext] || 'application/octet-stream';
84
+ }
85
+ /**
86
+ * Generate deterministic asset ID from content hash + relative path
87
+ * Content-addressed: same file always gets same ID
88
+ */
89
+ export function generateAssetId(sha256, relativePath) {
90
+ // Use first 12 chars of sha256 + path hash for uniqueness
91
+ const prefix = sha256.substring(0, 12);
92
+ if (relativePath) {
93
+ const pathHash = createHash('sha256').update(relativePath).digest('hex').substring(0, 6);
94
+ return `asset_${prefix}_${pathHash}`;
95
+ }
96
+ return `asset_${prefix}`;
97
+ }
98
+ /**
99
+ * Create asset manifest for a file
100
+ * @param rootDir - Root directory for computing relative path (for ID stability)
101
+ */
102
+ export function createAssetManifest(filePath, options = {}) {
103
+ const stats = statSync(filePath);
104
+ const sha256 = hashFile(filePath);
105
+ // Compute relative path using path.relative() for stable IDs
106
+ const relativePath = options.rootDir
107
+ ? relative(options.rootDir, filePath)
108
+ : basename(filePath);
109
+ return {
110
+ id: generateAssetId(sha256, relativePath),
111
+ path: filePath,
112
+ name: basename(filePath),
113
+ mime: getMimeType(filePath),
114
+ sha256,
115
+ size: stats.size,
116
+ createdAt: Date.now(),
117
+ groupId: options.groupId,
118
+ label: options.label,
119
+ kind: 'file',
120
+ };
121
+ }
122
+ /**
123
+ * Write asset manifest alongside the file
124
+ */
125
+ export function writeAssetManifest(manifest) {
126
+ const manifestPath = manifest.path + '.asset.json';
127
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
128
+ }
129
+ /**
130
+ * Read asset manifest for a file
131
+ */
132
+ export function readAssetManifest(filePath) {
133
+ const manifestPath = filePath + '.asset.json';
134
+ if (!existsSync(manifestPath))
135
+ return null;
136
+ try {
137
+ return JSON.parse(readFileSync(manifestPath, 'utf-8'));
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ /**
144
+ * Get preview cache directory
145
+ */
146
+ export function getPreviewCacheDir() {
147
+ const home = process.env.HOME || '/tmp';
148
+ const cacheDir = join(home, '.connect-hub', 'preview-cache');
149
+ if (!existsSync(cacheDir)) {
150
+ mkdirSync(cacheDir, { recursive: true });
151
+ }
152
+ return cacheDir;
153
+ }
154
+ /**
155
+ * Canonical JSON stringify - sorts keys recursively for stable hashing
156
+ */
157
+ function canonicalStringify(obj) {
158
+ if (obj === null || typeof obj !== 'object') {
159
+ return JSON.stringify(obj);
160
+ }
161
+ if (Array.isArray(obj)) {
162
+ return '[' + obj.map(canonicalStringify).join(',') + ']';
163
+ }
164
+ const sortedKeys = Object.keys(obj).sort();
165
+ const pairs = sortedKeys.map(key => JSON.stringify(key) + ':' + canonicalStringify(obj[key]));
166
+ return '{' + pairs.join(',') + '}';
167
+ }
168
+ /**
169
+ * Generate cache key for preview
170
+ * Uses canonical stringify for stable key generation regardless of key order
171
+ */
172
+ export function getPreviewCacheKey(sha256, params) {
173
+ const paramsStr = params ? canonicalStringify(params) : '';
174
+ return createHash('sha256').update(sha256 + paramsStr).digest('hex').substring(0, 16);
175
+ }
176
+ /**
177
+ * Check if preview exists in cache
178
+ */
179
+ export function getPreviewFromCache(cacheKey) {
180
+ const cachePath = join(getPreviewCacheDir(), `${cacheKey}.json`);
181
+ if (!existsSync(cachePath))
182
+ return null;
183
+ try {
184
+ return JSON.parse(readFileSync(cachePath, 'utf-8'));
185
+ }
186
+ catch {
187
+ return null;
188
+ }
189
+ }
190
+ /**
191
+ * Save preview to cache
192
+ */
193
+ export function savePreviewToCache(preview) {
194
+ const cachePath = join(getPreviewCacheDir(), `${preview.cacheKey}.json`);
195
+ writeFileSync(cachePath, JSON.stringify(preview, null, 2));
196
+ }
197
+ /**
198
+ * Create a ViewSpec for comparing two assets
199
+ */
200
+ export function createCompareView(beforeAssetId, afterAssetId, title = 'Comparison') {
201
+ return {
202
+ id: `view_${Date.now().toString(36)}`,
203
+ title,
204
+ layout: 'compare',
205
+ before: { type: 'asset', assetId: beforeAssetId, label: 'Before' },
206
+ after: { type: 'asset', assetId: afterAssetId, label: 'After' },
207
+ createdAt: Date.now(),
208
+ };
209
+ }
210
+ /**
211
+ * Create a ViewSpec for a grid of assets
212
+ */
213
+ export function createGridView(assetIds, title = 'Gallery') {
214
+ return {
215
+ id: `view_${Date.now().toString(36)}`,
216
+ title,
217
+ layout: 'grid',
218
+ items: assetIds.map(id => ({ type: 'asset', assetId: id })),
219
+ createdAt: Date.now(),
220
+ };
221
+ }
222
+ /**
223
+ * Create a ViewSpec for a report with markdown and assets
224
+ */
225
+ export function createReportView(title, sections) {
226
+ return {
227
+ id: `view_${Date.now().toString(36)}`,
228
+ title,
229
+ layout: 'stack',
230
+ items: sections.map(section => {
231
+ if (section.markdown) {
232
+ return { type: 'markdown', content: section.markdown, label: section.label };
233
+ }
234
+ return { type: 'asset', assetId: section.assetId, label: section.label };
235
+ }),
236
+ createdAt: Date.now(),
237
+ };
238
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Preview Worker - Fast-path local preview generation
3
+ *
4
+ * Watches ~/Lia-Hub/shared/outputs/ and generates:
5
+ * - Thumbnails (for images)
6
+ * - Metadata extraction (dimensions, page count, text snippets)
7
+ * - Preview JSON for instant gallery display
8
+ *
9
+ * NO LLM involvement - pure local, deterministic processing.
10
+ * Targets sub-second response for most assets.
11
+ *
12
+ * Uses chokidar for reliable cross-platform file watching with:
13
+ * - awaitWriteFinish to handle half-written files
14
+ * - Ignore patterns to avoid loops (*.asset.json, thumbs, etc.)
15
+ */
16
+ import { AssetManifest, AssetPreview } from './assets.js';
17
+ export interface PreviewWorkerOptions {
18
+ /** Directories to watch */
19
+ watchDirs?: string[];
20
+ /** Max concurrent preview jobs */
21
+ concurrency?: number;
22
+ /** Callback when asset is discovered */
23
+ onAsset?: (manifest: AssetManifest) => void;
24
+ /** Callback when preview is ready */
25
+ onPreview?: (preview: AssetPreview) => void;
26
+ /** Enable verbose logging */
27
+ verbose?: boolean;
28
+ }
29
+ export declare class PreviewWorker {
30
+ private watcher;
31
+ private queue;
32
+ private processing;
33
+ private debounceTimers;
34
+ private concurrency;
35
+ private options;
36
+ private running;
37
+ private rootDir;
38
+ constructor(options?: PreviewWorkerOptions);
39
+ /**
40
+ * Start watching directories
41
+ */
42
+ start(): void;
43
+ /**
44
+ * Handle file add/change with debouncing
45
+ */
46
+ private handleFile;
47
+ /**
48
+ * Stop watching
49
+ */
50
+ stop(): void;
51
+ /**
52
+ * Add file to processing queue
53
+ */
54
+ private enqueue;
55
+ /**
56
+ * Process queue with concurrency limit
57
+ */
58
+ private processQueue;
59
+ /**
60
+ * Process a single file - generate manifest and preview
61
+ */
62
+ private processFile;
63
+ /**
64
+ * Generate preview for an asset
65
+ */
66
+ private generatePreview;
67
+ /**
68
+ * Get image dimensions using sips (macOS) or file command
69
+ */
70
+ private getImageInfo;
71
+ /**
72
+ * Generate thumbnail as base64 data URL
73
+ * Uses sips on macOS for fast resizing
74
+ */
75
+ private generateImageThumbnail;
76
+ /**
77
+ * Get PDF info using pdfinfo or mdls
78
+ */
79
+ private getPdfInfo;
80
+ /**
81
+ * Get text snippet from file
82
+ */
83
+ private getTextSnippet;
84
+ /**
85
+ * Extract group ID from file path
86
+ * e.g., ~/Lia-Hub/shared/outputs/run_abc123/file.png -> run_abc123
87
+ */
88
+ private extractGroupId;
89
+ private log;
90
+ }
91
+ export interface AssetRegistry {
92
+ assets: Map<string, AssetManifest>;
93
+ previews: Map<string, AssetPreview>;
94
+ byGroup: Map<string, Set<string>>;
95
+ }
96
+ export declare function getAssetRegistry(): AssetRegistry;
97
+ export declare function registerAsset(manifest: AssetManifest): void;
98
+ export declare function registerPreview(preview: AssetPreview): void;
99
+ export declare function getAsset(assetId: string): AssetManifest | undefined;
100
+ export declare function getPreview(assetId: string): AssetPreview | undefined;
101
+ export declare function getAssetsByGroup(groupId: string): AssetManifest[];
102
+ export declare function getAllAssets(): AssetManifest[];
103
+ export declare function startPreviewWorker(options?: PreviewWorkerOptions): PreviewWorker;
104
+ export declare function stopPreviewWorker(): void;