@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 CHANGED
@@ -5,6 +5,7 @@ import { MasterOrchestrator } from './orchestrator.js';
5
5
  import { AgentWebSocket } from './websocket.js';
6
6
  import { startHeartbeat, stopHeartbeat, getHeartbeatState } from './heartbeat.js';
7
7
  import { loadHubContext, readScratchpad } from './core/hub.js';
8
+ import { startPreviewWorker, stopPreviewWorker } from './core/preview-worker.js';
8
9
  // Strip ANSI escape codes for clean output
9
10
  function stripAnsi(str) {
10
11
  return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
@@ -246,6 +247,21 @@ export async function runAgent(pollInterval = 2000) {
246
247
  console.log(`[AGENT] Scratchpad: ${scratchpad.length} chars of pending notes`);
247
248
  }
248
249
  }
250
+ // Start preview worker for fast-path asset previews (no LLM)
251
+ const previewWorker = startPreviewWorker({
252
+ verbose: true,
253
+ onAsset: (manifest) => {
254
+ console.log(`[PREVIEW] New asset: ${manifest.name} (${manifest.id})`);
255
+ // Send gallery state update via WebSocket
256
+ if (wsClient?.connected()) {
257
+ wsClient.sendGalleryState();
258
+ }
259
+ },
260
+ onPreview: (preview) => {
261
+ console.log(`[PREVIEW] Preview ready: ${preview.assetId}`);
262
+ },
263
+ });
264
+ console.log('[AGENT] Preview worker started (fast-path asset system)');
249
265
  // Start heartbeat to maintain presence in runner registry
250
266
  const runnerType = isCloudMode() ? 'vm' : 'local';
251
267
  startHeartbeat({
@@ -262,6 +278,7 @@ export async function runAgent(pollInterval = 2000) {
262
278
  process.on('SIGINT', async () => {
263
279
  console.log('\n[AGENT] Shutting down...');
264
280
  stopHeartbeat();
281
+ stopPreviewWorker();
265
282
  if (wsClient) {
266
283
  wsClient.close();
267
284
  }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Asset API - WebSocket-based asset serving
3
+ *
4
+ * Handles asset requests from the Connect web UI:
5
+ * - asset.list - List all assets
6
+ * - asset.get - Get asset bytes (streamed)
7
+ * - asset.preview - Get preview metadata + thumbnail
8
+ * - asset.group - Get assets by group
9
+ * - view.get - Get ViewSpec
10
+ */
11
+ import { AssetManifest, AssetPreview, ViewSpec, ClientAssetManifest } from './assets.js';
12
+ export interface AssetRequest {
13
+ type: 'asset.list' | 'asset.get' | 'asset.preview' | 'asset.group' | 'view.get' | 'view.list';
14
+ requestId: string;
15
+ assetId?: string;
16
+ groupId?: string;
17
+ viewId?: string;
18
+ }
19
+ export interface AssetListResponse {
20
+ type: 'asset.list';
21
+ requestId: string;
22
+ assets: ClientAssetManifest[];
23
+ }
24
+ export interface AssetGetResponse {
25
+ type: 'asset.get';
26
+ requestId: string;
27
+ assetId: string;
28
+ data: string;
29
+ mime: string;
30
+ size: number;
31
+ }
32
+ export interface AssetPreviewResponse {
33
+ type: 'asset.preview';
34
+ requestId: string;
35
+ assetId: string;
36
+ manifest: ClientAssetManifest;
37
+ preview: AssetPreview | null;
38
+ }
39
+ export interface AssetGroupResponse {
40
+ type: 'asset.group';
41
+ requestId: string;
42
+ groupId: string;
43
+ assets: ClientAssetManifest[];
44
+ previews: (AssetPreview | null)[];
45
+ }
46
+ export interface ViewGetResponse {
47
+ type: 'view.get';
48
+ requestId: string;
49
+ view: ViewSpec | null;
50
+ }
51
+ export interface ViewListResponse {
52
+ type: 'view.list';
53
+ requestId: string;
54
+ views: ViewSpec[];
55
+ }
56
+ export interface AssetErrorResponse {
57
+ type: 'asset.error';
58
+ requestId: string;
59
+ error: string;
60
+ }
61
+ export type AssetResponse = AssetListResponse | AssetGetResponse | AssetPreviewResponse | AssetGroupResponse | ViewGetResponse | ViewListResponse | AssetErrorResponse;
62
+ export declare function registerView(view: ViewSpec): void;
63
+ export declare function getView(viewId: string): ViewSpec | undefined;
64
+ export declare function getAllViews(): ViewSpec[];
65
+ /**
66
+ * Handle asset request from WebSocket
67
+ */
68
+ export declare function handleAssetRequest(request: AssetRequest): Promise<AssetResponse>;
69
+ /**
70
+ * Build gallery state for WebSocket broadcast
71
+ * This replaces the old extractFilesFromOutput approach
72
+ */
73
+ export interface GalleryState {
74
+ assets: AssetManifest[];
75
+ previews: Map<string, AssetPreview>;
76
+ groups: Map<string, AssetManifest[]>;
77
+ views: ViewSpec[];
78
+ }
79
+ export declare function buildGalleryState(): GalleryState;
80
+ /**
81
+ * Convert gallery state to WebSocket message format
82
+ * Strips absolute paths from assets for client safety
83
+ */
84
+ export declare function galleryStateToMessage(state: GalleryState): {
85
+ type: 'gallery_state';
86
+ assets: ClientAssetManifest[];
87
+ previews: {
88
+ [key: string]: AssetPreview;
89
+ };
90
+ groups: {
91
+ [key: string]: string[];
92
+ };
93
+ views: ViewSpec[];
94
+ };
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Asset API - WebSocket-based asset serving
3
+ *
4
+ * Handles asset requests from the Connect web UI:
5
+ * - asset.list - List all assets
6
+ * - asset.get - Get asset bytes (streamed)
7
+ * - asset.preview - Get preview metadata + thumbnail
8
+ * - asset.group - Get assets by group
9
+ * - view.get - Get ViewSpec
10
+ */
11
+ import { readFileSync, existsSync, statSync } from 'fs';
12
+ import { toClientManifest, isPathWithinRoot, } from './assets.js';
13
+ import { getAsset, getPreview, getAllAssets, getAssetsByGroup, getAssetRegistry, } from './preview-worker.js';
14
+ import { getSharedOutputPath } from './hub.js';
15
+ // Allowed roots for asset access (security)
16
+ function getAllowedRoots() {
17
+ const home = process.env.HOME || '/tmp';
18
+ return [
19
+ getSharedOutputPath(),
20
+ `${home}/Lia-Hub`,
21
+ '/tmp',
22
+ ];
23
+ }
24
+ /**
25
+ * Validate that a path is within allowed roots (uses realpath to prevent traversal/symlink escapes)
26
+ */
27
+ function isPathAllowed(filePath) {
28
+ const allowedRoots = getAllowedRoots();
29
+ return allowedRoots.some(root => isPathWithinRoot(filePath, root));
30
+ }
31
+ // ============================================================================
32
+ // VIEW REGISTRY
33
+ // ============================================================================
34
+ const viewRegistry = new Map();
35
+ export function registerView(view) {
36
+ viewRegistry.set(view.id, view);
37
+ }
38
+ export function getView(viewId) {
39
+ return viewRegistry.get(viewId);
40
+ }
41
+ export function getAllViews() {
42
+ return Array.from(viewRegistry.values());
43
+ }
44
+ // ============================================================================
45
+ // REQUEST HANDLER
46
+ // ============================================================================
47
+ /**
48
+ * Handle asset request from WebSocket
49
+ */
50
+ export async function handleAssetRequest(request) {
51
+ try {
52
+ switch (request.type) {
53
+ case 'asset.list':
54
+ return handleAssetList(request);
55
+ case 'asset.get':
56
+ return handleAssetGet(request);
57
+ case 'asset.preview':
58
+ return handleAssetPreview(request);
59
+ case 'asset.group':
60
+ return handleAssetGroup(request);
61
+ case 'view.get':
62
+ return handleViewGet(request);
63
+ case 'view.list':
64
+ return handleViewList(request);
65
+ default:
66
+ return {
67
+ type: 'asset.error',
68
+ requestId: request.requestId,
69
+ error: `Unknown request type: ${request.type}`,
70
+ };
71
+ }
72
+ }
73
+ catch (error) {
74
+ return {
75
+ type: 'asset.error',
76
+ requestId: request.requestId,
77
+ error: error instanceof Error ? error.message : String(error),
78
+ };
79
+ }
80
+ }
81
+ function handleAssetList(request) {
82
+ return {
83
+ type: 'asset.list',
84
+ requestId: request.requestId,
85
+ assets: getAllAssets().map(toClientManifest),
86
+ };
87
+ }
88
+ function handleAssetGet(request) {
89
+ const { assetId, requestId } = request;
90
+ if (!assetId) {
91
+ return { type: 'asset.error', requestId, error: 'Missing assetId' };
92
+ }
93
+ const manifest = getAsset(assetId);
94
+ if (!manifest) {
95
+ return { type: 'asset.error', requestId, error: `Asset not found: ${assetId}` };
96
+ }
97
+ // Security: validate path is within allowed roots
98
+ if (!isPathAllowed(manifest.path)) {
99
+ return { type: 'asset.error', requestId, error: 'Access denied' };
100
+ }
101
+ if (!existsSync(manifest.path)) {
102
+ return { type: 'asset.error', requestId, error: 'File not found' };
103
+ }
104
+ // Read file and encode as base64
105
+ const data = readFileSync(manifest.path);
106
+ const stats = statSync(manifest.path);
107
+ return {
108
+ type: 'asset.get',
109
+ requestId,
110
+ assetId,
111
+ data: data.toString('base64'),
112
+ mime: manifest.mime,
113
+ size: stats.size,
114
+ };
115
+ }
116
+ function handleAssetPreview(request) {
117
+ const { assetId, requestId } = request;
118
+ if (!assetId) {
119
+ return { type: 'asset.error', requestId, error: 'Missing assetId' };
120
+ }
121
+ const manifest = getAsset(assetId);
122
+ if (!manifest) {
123
+ return { type: 'asset.error', requestId, error: `Asset not found: ${assetId}` };
124
+ }
125
+ const preview = getPreview(assetId) || null;
126
+ return {
127
+ type: 'asset.preview',
128
+ requestId,
129
+ assetId,
130
+ manifest: toClientManifest(manifest),
131
+ preview,
132
+ };
133
+ }
134
+ function handleAssetGroup(request) {
135
+ const { groupId, requestId } = request;
136
+ if (!groupId) {
137
+ return { type: 'asset.error', requestId, error: 'Missing groupId' };
138
+ }
139
+ const assets = getAssetsByGroup(groupId);
140
+ const previews = assets.map(a => getPreview(a.id) || null);
141
+ return {
142
+ type: 'asset.group',
143
+ requestId,
144
+ groupId,
145
+ assets: assets.map(toClientManifest),
146
+ previews,
147
+ };
148
+ }
149
+ function handleViewGet(request) {
150
+ const { viewId, requestId } = request;
151
+ if (!viewId) {
152
+ return { type: 'asset.error', requestId, error: 'Missing viewId' };
153
+ }
154
+ const view = getView(viewId) || null;
155
+ return {
156
+ type: 'view.get',
157
+ requestId,
158
+ view,
159
+ };
160
+ }
161
+ function handleViewList(request) {
162
+ return {
163
+ type: 'view.list',
164
+ requestId: request.requestId,
165
+ views: getAllViews(),
166
+ };
167
+ }
168
+ export function buildGalleryState() {
169
+ const registry = getAssetRegistry();
170
+ const groups = new Map();
171
+ for (const [groupId, assetIds] of registry.byGroup) {
172
+ const assets = Array.from(assetIds)
173
+ .map(id => registry.assets.get(id))
174
+ .filter(Boolean);
175
+ groups.set(groupId, assets);
176
+ }
177
+ return {
178
+ assets: getAllAssets(),
179
+ previews: registry.previews,
180
+ groups,
181
+ views: getAllViews(),
182
+ };
183
+ }
184
+ /**
185
+ * Convert gallery state to WebSocket message format
186
+ * Strips absolute paths from assets for client safety
187
+ */
188
+ export function galleryStateToMessage(state) {
189
+ const previews = {};
190
+ for (const [id, preview] of state.previews) {
191
+ previews[id] = preview;
192
+ }
193
+ const groups = {};
194
+ for (const [groupId, assets] of state.groups) {
195
+ groups[groupId] = assets.map(a => a.id);
196
+ }
197
+ return {
198
+ type: 'gallery_state',
199
+ assets: state.assets.map(toClientManifest),
200
+ previews,
201
+ groups,
202
+ views: state.views,
203
+ };
204
+ }
@@ -0,0 +1,203 @@
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
+ export interface AssetManifest {
8
+ /** Unique asset ID (content-addressed) */
9
+ id: string;
10
+ /** Absolute path to the file (INTERNAL ONLY - never send to client) */
11
+ path: string;
12
+ /** Filename */
13
+ name: string;
14
+ /** MIME type */
15
+ mime: string;
16
+ /** SHA256 content hash for caching */
17
+ sha256: string;
18
+ /** File size in bytes */
19
+ size: number;
20
+ /** Creation timestamp */
21
+ createdAt: number;
22
+ /** Worker/run group ID for grouping related assets */
23
+ groupId?: string;
24
+ /** Label for the asset (old, new, diff, etc.) */
25
+ label?: 'old' | 'new' | 'diff' | 'output' | 'source';
26
+ /** Asset kind */
27
+ kind: 'file' | 'view';
28
+ /** For kind=view, the ViewSpec */
29
+ viewSpec?: ViewSpec;
30
+ }
31
+ /**
32
+ * Client-safe asset manifest - no absolute paths exposed
33
+ */
34
+ export interface ClientAssetManifest {
35
+ id: string;
36
+ name: string;
37
+ mime: string;
38
+ sha256: string;
39
+ size: number;
40
+ createdAt: number;
41
+ groupId?: string;
42
+ label?: 'old' | 'new' | 'diff' | 'output' | 'source';
43
+ kind: 'file' | 'view';
44
+ viewSpec?: ViewSpec;
45
+ }
46
+ /**
47
+ * Convert internal manifest to client-safe version (strips path)
48
+ */
49
+ export declare function toClientManifest(manifest: AssetManifest): ClientAssetManifest;
50
+ /**
51
+ * Validate that a resolved path is within an allowed root (prevents traversal/symlink escapes)
52
+ */
53
+ export declare function isPathWithinRoot(filePath: string, rootDir: string): boolean;
54
+ export interface AssetPreview {
55
+ /** Reference to the asset */
56
+ assetId: string;
57
+ /** Thumbnail path (for images/PDFs) */
58
+ thumbnailPath?: string;
59
+ /** Thumbnail as base64 data URL (for inline display) */
60
+ thumbnailData?: string;
61
+ /** Extracted text snippet (first ~500 chars) */
62
+ textSnippet?: string;
63
+ /** Image dimensions */
64
+ dimensions?: {
65
+ width: number;
66
+ height: number;
67
+ };
68
+ /** PDF page count */
69
+ pageCount?: number;
70
+ /** Additional metadata */
71
+ metadata?: Record<string, unknown>;
72
+ /** When preview was generated */
73
+ generatedAt: number;
74
+ /** Cache key: sha256 + transform params */
75
+ cacheKey: string;
76
+ }
77
+ export interface ViewSpec {
78
+ /** View ID */
79
+ id: string;
80
+ /** Display title */
81
+ title: string;
82
+ /** Layout type */
83
+ layout: 'single' | 'grid' | 'twoColumn' | 'compare' | 'stack' | 'tabs';
84
+ /** Items in the view */
85
+ items?: ViewItem[];
86
+ /** For twoColumn layout */
87
+ left?: ViewItem[];
88
+ right?: ViewItem[];
89
+ /** For compare layout */
90
+ before?: ViewItem;
91
+ after?: ViewItem;
92
+ /** Optional description */
93
+ description?: string;
94
+ /** Created timestamp */
95
+ createdAt: number;
96
+ }
97
+ export interface ViewItem {
98
+ /** Item type */
99
+ type: 'asset' | 'markdown' | 'text' | 'html';
100
+ /** For type=asset, the asset ID */
101
+ assetId?: string;
102
+ /** For type=markdown/text/html, the content */
103
+ content?: string;
104
+ /** Viewer to use */
105
+ viewer?: 'image' | 'pdf' | 'code' | 'markdown' | 'text' | 'video' | 'audio';
106
+ /** Viewer params (overlays, annotations, etc.) */
107
+ params?: ViewerParams;
108
+ /** Optional label */
109
+ label?: string;
110
+ }
111
+ export interface ViewerParams {
112
+ /** Scale bar overlay for microscopy images */
113
+ overlay?: 'scaleBar' | 'grid' | 'ruler';
114
+ /** Brightness adjustment (-100 to 100) */
115
+ brightness?: number;
116
+ /** Contrast adjustment (-100 to 100) */
117
+ contrast?: number;
118
+ /** Crop region */
119
+ crop?: {
120
+ x: number;
121
+ y: number;
122
+ width: number;
123
+ height: number;
124
+ };
125
+ /** Zoom level */
126
+ zoom?: number;
127
+ /** Highlight lines (for code) */
128
+ highlightLines?: number[];
129
+ /** Annotations */
130
+ annotations?: Annotation[];
131
+ }
132
+ export interface Annotation {
133
+ type: 'box' | 'circle' | 'arrow' | 'text';
134
+ x: number;
135
+ y: number;
136
+ width?: number;
137
+ height?: number;
138
+ label?: string;
139
+ color?: string;
140
+ }
141
+ /**
142
+ * Generate SHA256 hash of file contents
143
+ */
144
+ export declare function hashFile(filePath: string): string;
145
+ /**
146
+ * Get MIME type from file extension
147
+ */
148
+ export declare function getMimeType(filePath: string): string;
149
+ /**
150
+ * Generate deterministic asset ID from content hash + relative path
151
+ * Content-addressed: same file always gets same ID
152
+ */
153
+ export declare function generateAssetId(sha256: string, relativePath?: string): string;
154
+ /**
155
+ * Create asset manifest for a file
156
+ * @param rootDir - Root directory for computing relative path (for ID stability)
157
+ */
158
+ export declare function createAssetManifest(filePath: string, options?: {
159
+ groupId?: string;
160
+ label?: AssetManifest['label'];
161
+ rootDir?: string;
162
+ }): AssetManifest;
163
+ /**
164
+ * Write asset manifest alongside the file
165
+ */
166
+ export declare function writeAssetManifest(manifest: AssetManifest): void;
167
+ /**
168
+ * Read asset manifest for a file
169
+ */
170
+ export declare function readAssetManifest(filePath: string): AssetManifest | null;
171
+ /**
172
+ * Get preview cache directory
173
+ */
174
+ export declare function getPreviewCacheDir(): string;
175
+ /**
176
+ * Generate cache key for preview
177
+ * Uses canonical stringify for stable key generation regardless of key order
178
+ */
179
+ export declare function getPreviewCacheKey(sha256: string, params?: ViewerParams): string;
180
+ /**
181
+ * Check if preview exists in cache
182
+ */
183
+ export declare function getPreviewFromCache(cacheKey: string): AssetPreview | null;
184
+ /**
185
+ * Save preview to cache
186
+ */
187
+ export declare function savePreviewToCache(preview: AssetPreview): void;
188
+ /**
189
+ * Create a ViewSpec for comparing two assets
190
+ */
191
+ export declare function createCompareView(beforeAssetId: string, afterAssetId: string, title?: string): ViewSpec;
192
+ /**
193
+ * Create a ViewSpec for a grid of assets
194
+ */
195
+ export declare function createGridView(assetIds: string[], title?: string): ViewSpec;
196
+ /**
197
+ * Create a ViewSpec for a report with markdown and assets
198
+ */
199
+ export declare function createReportView(title: string, sections: Array<{
200
+ markdown?: string;
201
+ assetId?: string;
202
+ label?: string;
203
+ }>): ViewSpec;