@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.
- package/dist/agent.js +17 -0
- package/dist/core/asset-api.d.ts +94 -0
- package/dist/core/asset-api.js +204 -0
- package/dist/core/assets.d.ts +203 -0
- package/dist/core/assets.js +238 -0
- package/dist/core/preview-worker.d.ts +104 -0
- package/dist/core/preview-worker.js +408 -0
- package/dist/genesis/tool-patterns.json +9 -4
- package/dist/orchestrator.js +24 -5
- package/dist/prompts/worker-system.d.ts +1 -1
- package/dist/prompts/worker-system.js +34 -7
- package/dist/websocket.d.ts +19 -2
- package/dist/websocket.js +40 -2
- package/package.json +3 -1
|
@@ -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;
|