@siftd/connect-agent 0.2.30 → 0.2.31
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 +182 -0
- package/dist/core/assets.d.ts +176 -0
- package/dist/core/assets.js +192 -0
- package/dist/core/preview-worker.d.ts +98 -0
- package/dist/core/preview-worker.js +395 -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 +1 -1
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 } 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: AssetManifest[];
|
|
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: AssetManifest;
|
|
37
|
+
preview: AssetPreview | null;
|
|
38
|
+
}
|
|
39
|
+
export interface AssetGroupResponse {
|
|
40
|
+
type: 'asset.group';
|
|
41
|
+
requestId: string;
|
|
42
|
+
groupId: string;
|
|
43
|
+
assets: AssetManifest[];
|
|
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
|
+
* Compatible with existing gallery_workers message
|
|
83
|
+
*/
|
|
84
|
+
export declare function galleryStateToMessage(state: GalleryState): {
|
|
85
|
+
type: 'gallery_state';
|
|
86
|
+
assets: AssetManifest[];
|
|
87
|
+
previews: {
|
|
88
|
+
[key: string]: AssetPreview;
|
|
89
|
+
};
|
|
90
|
+
groups: {
|
|
91
|
+
[key: string]: string[];
|
|
92
|
+
};
|
|
93
|
+
views: ViewSpec[];
|
|
94
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
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 { getAsset, getPreview, getAllAssets, getAssetsByGroup, getAssetRegistry, } from './preview-worker.js';
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// VIEW REGISTRY
|
|
15
|
+
// ============================================================================
|
|
16
|
+
const viewRegistry = new Map();
|
|
17
|
+
export function registerView(view) {
|
|
18
|
+
viewRegistry.set(view.id, view);
|
|
19
|
+
}
|
|
20
|
+
export function getView(viewId) {
|
|
21
|
+
return viewRegistry.get(viewId);
|
|
22
|
+
}
|
|
23
|
+
export function getAllViews() {
|
|
24
|
+
return Array.from(viewRegistry.values());
|
|
25
|
+
}
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// REQUEST HANDLER
|
|
28
|
+
// ============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Handle asset request from WebSocket
|
|
31
|
+
*/
|
|
32
|
+
export async function handleAssetRequest(request) {
|
|
33
|
+
try {
|
|
34
|
+
switch (request.type) {
|
|
35
|
+
case 'asset.list':
|
|
36
|
+
return handleAssetList(request);
|
|
37
|
+
case 'asset.get':
|
|
38
|
+
return handleAssetGet(request);
|
|
39
|
+
case 'asset.preview':
|
|
40
|
+
return handleAssetPreview(request);
|
|
41
|
+
case 'asset.group':
|
|
42
|
+
return handleAssetGroup(request);
|
|
43
|
+
case 'view.get':
|
|
44
|
+
return handleViewGet(request);
|
|
45
|
+
case 'view.list':
|
|
46
|
+
return handleViewList(request);
|
|
47
|
+
default:
|
|
48
|
+
return {
|
|
49
|
+
type: 'asset.error',
|
|
50
|
+
requestId: request.requestId,
|
|
51
|
+
error: `Unknown request type: ${request.type}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return {
|
|
57
|
+
type: 'asset.error',
|
|
58
|
+
requestId: request.requestId,
|
|
59
|
+
error: error instanceof Error ? error.message : String(error),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function handleAssetList(request) {
|
|
64
|
+
return {
|
|
65
|
+
type: 'asset.list',
|
|
66
|
+
requestId: request.requestId,
|
|
67
|
+
assets: getAllAssets(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function handleAssetGet(request) {
|
|
71
|
+
const { assetId, requestId } = request;
|
|
72
|
+
if (!assetId) {
|
|
73
|
+
return { type: 'asset.error', requestId, error: 'Missing assetId' };
|
|
74
|
+
}
|
|
75
|
+
const manifest = getAsset(assetId);
|
|
76
|
+
if (!manifest) {
|
|
77
|
+
return { type: 'asset.error', requestId, error: `Asset not found: ${assetId}` };
|
|
78
|
+
}
|
|
79
|
+
if (!existsSync(manifest.path)) {
|
|
80
|
+
return { type: 'asset.error', requestId, error: `File not found: ${manifest.path}` };
|
|
81
|
+
}
|
|
82
|
+
// Read file and encode as base64
|
|
83
|
+
const data = readFileSync(manifest.path);
|
|
84
|
+
const stats = statSync(manifest.path);
|
|
85
|
+
return {
|
|
86
|
+
type: 'asset.get',
|
|
87
|
+
requestId,
|
|
88
|
+
assetId,
|
|
89
|
+
data: data.toString('base64'),
|
|
90
|
+
mime: manifest.mime,
|
|
91
|
+
size: stats.size,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function handleAssetPreview(request) {
|
|
95
|
+
const { assetId, requestId } = request;
|
|
96
|
+
if (!assetId) {
|
|
97
|
+
return { type: 'asset.error', requestId, error: 'Missing assetId' };
|
|
98
|
+
}
|
|
99
|
+
const manifest = getAsset(assetId);
|
|
100
|
+
if (!manifest) {
|
|
101
|
+
return { type: 'asset.error', requestId, error: `Asset not found: ${assetId}` };
|
|
102
|
+
}
|
|
103
|
+
const preview = getPreview(assetId) || null;
|
|
104
|
+
return {
|
|
105
|
+
type: 'asset.preview',
|
|
106
|
+
requestId,
|
|
107
|
+
assetId,
|
|
108
|
+
manifest,
|
|
109
|
+
preview,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function handleAssetGroup(request) {
|
|
113
|
+
const { groupId, requestId } = request;
|
|
114
|
+
if (!groupId) {
|
|
115
|
+
return { type: 'asset.error', requestId, error: 'Missing groupId' };
|
|
116
|
+
}
|
|
117
|
+
const assets = getAssetsByGroup(groupId);
|
|
118
|
+
const previews = assets.map(a => getPreview(a.id) || null);
|
|
119
|
+
return {
|
|
120
|
+
type: 'asset.group',
|
|
121
|
+
requestId,
|
|
122
|
+
groupId,
|
|
123
|
+
assets,
|
|
124
|
+
previews,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function handleViewGet(request) {
|
|
128
|
+
const { viewId, requestId } = request;
|
|
129
|
+
if (!viewId) {
|
|
130
|
+
return { type: 'asset.error', requestId, error: 'Missing viewId' };
|
|
131
|
+
}
|
|
132
|
+
const view = getView(viewId) || null;
|
|
133
|
+
return {
|
|
134
|
+
type: 'view.get',
|
|
135
|
+
requestId,
|
|
136
|
+
view,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function handleViewList(request) {
|
|
140
|
+
return {
|
|
141
|
+
type: 'view.list',
|
|
142
|
+
requestId: request.requestId,
|
|
143
|
+
views: getAllViews(),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export function buildGalleryState() {
|
|
147
|
+
const registry = getAssetRegistry();
|
|
148
|
+
const groups = new Map();
|
|
149
|
+
for (const [groupId, assetIds] of registry.byGroup) {
|
|
150
|
+
const assets = Array.from(assetIds)
|
|
151
|
+
.map(id => registry.assets.get(id))
|
|
152
|
+
.filter(Boolean);
|
|
153
|
+
groups.set(groupId, assets);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
assets: getAllAssets(),
|
|
157
|
+
previews: registry.previews,
|
|
158
|
+
groups,
|
|
159
|
+
views: getAllViews(),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Convert gallery state to WebSocket message format
|
|
164
|
+
* Compatible with existing gallery_workers message
|
|
165
|
+
*/
|
|
166
|
+
export function galleryStateToMessage(state) {
|
|
167
|
+
const previews = {};
|
|
168
|
+
for (const [id, preview] of state.previews) {
|
|
169
|
+
previews[id] = preview;
|
|
170
|
+
}
|
|
171
|
+
const groups = {};
|
|
172
|
+
for (const [groupId, assets] of state.groups) {
|
|
173
|
+
groups[groupId] = assets.map(a => a.id);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
type: 'gallery_state',
|
|
177
|
+
assets: state.assets,
|
|
178
|
+
previews,
|
|
179
|
+
groups,
|
|
180
|
+
views: state.views,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
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 */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Absolute path to the file */
|
|
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
|
+
export interface AssetPreview {
|
|
32
|
+
/** Reference to the asset */
|
|
33
|
+
assetId: string;
|
|
34
|
+
/** Thumbnail path (for images/PDFs) */
|
|
35
|
+
thumbnailPath?: string;
|
|
36
|
+
/** Thumbnail as base64 data URL (for inline display) */
|
|
37
|
+
thumbnailData?: string;
|
|
38
|
+
/** Extracted text snippet (first ~500 chars) */
|
|
39
|
+
textSnippet?: string;
|
|
40
|
+
/** Image dimensions */
|
|
41
|
+
dimensions?: {
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
};
|
|
45
|
+
/** PDF page count */
|
|
46
|
+
pageCount?: number;
|
|
47
|
+
/** Additional metadata */
|
|
48
|
+
metadata?: Record<string, unknown>;
|
|
49
|
+
/** When preview was generated */
|
|
50
|
+
generatedAt: number;
|
|
51
|
+
/** Cache key: sha256 + transform params */
|
|
52
|
+
cacheKey: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ViewSpec {
|
|
55
|
+
/** View ID */
|
|
56
|
+
id: string;
|
|
57
|
+
/** Display title */
|
|
58
|
+
title: string;
|
|
59
|
+
/** Layout type */
|
|
60
|
+
layout: 'single' | 'grid' | 'twoColumn' | 'compare' | 'stack' | 'tabs';
|
|
61
|
+
/** Items in the view */
|
|
62
|
+
items?: ViewItem[];
|
|
63
|
+
/** For twoColumn layout */
|
|
64
|
+
left?: ViewItem[];
|
|
65
|
+
right?: ViewItem[];
|
|
66
|
+
/** For compare layout */
|
|
67
|
+
before?: ViewItem;
|
|
68
|
+
after?: ViewItem;
|
|
69
|
+
/** Optional description */
|
|
70
|
+
description?: string;
|
|
71
|
+
/** Created timestamp */
|
|
72
|
+
createdAt: number;
|
|
73
|
+
}
|
|
74
|
+
export interface ViewItem {
|
|
75
|
+
/** Item type */
|
|
76
|
+
type: 'asset' | 'markdown' | 'text' | 'html';
|
|
77
|
+
/** For type=asset, the asset ID */
|
|
78
|
+
assetId?: string;
|
|
79
|
+
/** For type=markdown/text/html, the content */
|
|
80
|
+
content?: string;
|
|
81
|
+
/** Viewer to use */
|
|
82
|
+
viewer?: 'image' | 'pdf' | 'code' | 'markdown' | 'text' | 'video' | 'audio';
|
|
83
|
+
/** Viewer params (overlays, annotations, etc.) */
|
|
84
|
+
params?: ViewerParams;
|
|
85
|
+
/** Optional label */
|
|
86
|
+
label?: string;
|
|
87
|
+
}
|
|
88
|
+
export interface ViewerParams {
|
|
89
|
+
/** Scale bar overlay for microscopy images */
|
|
90
|
+
overlay?: 'scaleBar' | 'grid' | 'ruler';
|
|
91
|
+
/** Brightness adjustment (-100 to 100) */
|
|
92
|
+
brightness?: number;
|
|
93
|
+
/** Contrast adjustment (-100 to 100) */
|
|
94
|
+
contrast?: number;
|
|
95
|
+
/** Crop region */
|
|
96
|
+
crop?: {
|
|
97
|
+
x: number;
|
|
98
|
+
y: number;
|
|
99
|
+
width: number;
|
|
100
|
+
height: number;
|
|
101
|
+
};
|
|
102
|
+
/** Zoom level */
|
|
103
|
+
zoom?: number;
|
|
104
|
+
/** Highlight lines (for code) */
|
|
105
|
+
highlightLines?: number[];
|
|
106
|
+
/** Annotations */
|
|
107
|
+
annotations?: Annotation[];
|
|
108
|
+
}
|
|
109
|
+
export interface Annotation {
|
|
110
|
+
type: 'box' | 'circle' | 'arrow' | 'text';
|
|
111
|
+
x: number;
|
|
112
|
+
y: number;
|
|
113
|
+
width?: number;
|
|
114
|
+
height?: number;
|
|
115
|
+
label?: string;
|
|
116
|
+
color?: string;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Generate SHA256 hash of file contents
|
|
120
|
+
*/
|
|
121
|
+
export declare function hashFile(filePath: string): string;
|
|
122
|
+
/**
|
|
123
|
+
* Get MIME type from file extension
|
|
124
|
+
*/
|
|
125
|
+
export declare function getMimeType(filePath: string): string;
|
|
126
|
+
/**
|
|
127
|
+
* Generate unique asset ID
|
|
128
|
+
*/
|
|
129
|
+
export declare function generateAssetId(): string;
|
|
130
|
+
/**
|
|
131
|
+
* Create asset manifest for a file
|
|
132
|
+
*/
|
|
133
|
+
export declare function createAssetManifest(filePath: string, options?: {
|
|
134
|
+
groupId?: string;
|
|
135
|
+
label?: AssetManifest['label'];
|
|
136
|
+
}): AssetManifest;
|
|
137
|
+
/**
|
|
138
|
+
* Write asset manifest alongside the file
|
|
139
|
+
*/
|
|
140
|
+
export declare function writeAssetManifest(manifest: AssetManifest): void;
|
|
141
|
+
/**
|
|
142
|
+
* Read asset manifest for a file
|
|
143
|
+
*/
|
|
144
|
+
export declare function readAssetManifest(filePath: string): AssetManifest | null;
|
|
145
|
+
/**
|
|
146
|
+
* Get preview cache directory
|
|
147
|
+
*/
|
|
148
|
+
export declare function getPreviewCacheDir(): string;
|
|
149
|
+
/**
|
|
150
|
+
* Generate cache key for preview
|
|
151
|
+
*/
|
|
152
|
+
export declare function getPreviewCacheKey(sha256: string, params?: ViewerParams): string;
|
|
153
|
+
/**
|
|
154
|
+
* Check if preview exists in cache
|
|
155
|
+
*/
|
|
156
|
+
export declare function getPreviewFromCache(cacheKey: string): AssetPreview | null;
|
|
157
|
+
/**
|
|
158
|
+
* Save preview to cache
|
|
159
|
+
*/
|
|
160
|
+
export declare function savePreviewToCache(preview: AssetPreview): void;
|
|
161
|
+
/**
|
|
162
|
+
* Create a ViewSpec for comparing two assets
|
|
163
|
+
*/
|
|
164
|
+
export declare function createCompareView(beforeAssetId: string, afterAssetId: string, title?: string): ViewSpec;
|
|
165
|
+
/**
|
|
166
|
+
* Create a ViewSpec for a grid of assets
|
|
167
|
+
*/
|
|
168
|
+
export declare function createGridView(assetIds: string[], title?: string): ViewSpec;
|
|
169
|
+
/**
|
|
170
|
+
* Create a ViewSpec for a report with markdown and assets
|
|
171
|
+
*/
|
|
172
|
+
export declare function createReportView(title: string, sections: Array<{
|
|
173
|
+
markdown?: string;
|
|
174
|
+
assetId?: string;
|
|
175
|
+
label?: string;
|
|
176
|
+
}>): ViewSpec;
|
|
@@ -0,0 +1,192 @@
|
|
|
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 } from 'fs';
|
|
9
|
+
import { basename, extname, join } from 'path';
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// UTILITY FUNCTIONS
|
|
12
|
+
// ============================================================================
|
|
13
|
+
/**
|
|
14
|
+
* Generate SHA256 hash of file contents
|
|
15
|
+
*/
|
|
16
|
+
export function hashFile(filePath) {
|
|
17
|
+
const content = readFileSync(filePath);
|
|
18
|
+
return createHash('sha256').update(content).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get MIME type from file extension
|
|
22
|
+
*/
|
|
23
|
+
export function getMimeType(filePath) {
|
|
24
|
+
const ext = extname(filePath).toLowerCase().slice(1);
|
|
25
|
+
const mimeMap = {
|
|
26
|
+
// Images
|
|
27
|
+
png: 'image/png',
|
|
28
|
+
jpg: 'image/jpeg',
|
|
29
|
+
jpeg: 'image/jpeg',
|
|
30
|
+
gif: 'image/gif',
|
|
31
|
+
svg: 'image/svg+xml',
|
|
32
|
+
webp: 'image/webp',
|
|
33
|
+
ico: 'image/x-icon',
|
|
34
|
+
// Documents
|
|
35
|
+
pdf: 'application/pdf',
|
|
36
|
+
doc: 'application/msword',
|
|
37
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
38
|
+
// Code/Text
|
|
39
|
+
js: 'text/javascript',
|
|
40
|
+
ts: 'text/typescript',
|
|
41
|
+
jsx: 'text/javascript',
|
|
42
|
+
tsx: 'text/typescript',
|
|
43
|
+
json: 'application/json',
|
|
44
|
+
html: 'text/html',
|
|
45
|
+
css: 'text/css',
|
|
46
|
+
md: 'text/markdown',
|
|
47
|
+
txt: 'text/plain',
|
|
48
|
+
csv: 'text/csv',
|
|
49
|
+
xml: 'text/xml',
|
|
50
|
+
yaml: 'text/yaml',
|
|
51
|
+
yml: 'text/yaml',
|
|
52
|
+
sh: 'text/x-shellscript',
|
|
53
|
+
py: 'text/x-python',
|
|
54
|
+
// Data
|
|
55
|
+
sqlite: 'application/x-sqlite3',
|
|
56
|
+
db: 'application/x-sqlite3',
|
|
57
|
+
// Video/Audio
|
|
58
|
+
mp4: 'video/mp4',
|
|
59
|
+
webm: 'video/webm',
|
|
60
|
+
mp3: 'audio/mpeg',
|
|
61
|
+
wav: 'audio/wav',
|
|
62
|
+
};
|
|
63
|
+
return mimeMap[ext] || 'application/octet-stream';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Generate unique asset ID
|
|
67
|
+
*/
|
|
68
|
+
export function generateAssetId() {
|
|
69
|
+
const timestamp = Date.now().toString(36);
|
|
70
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
71
|
+
return `asset_${timestamp}_${random}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create asset manifest for a file
|
|
75
|
+
*/
|
|
76
|
+
export function createAssetManifest(filePath, options = {}) {
|
|
77
|
+
const stats = statSync(filePath);
|
|
78
|
+
return {
|
|
79
|
+
id: generateAssetId(),
|
|
80
|
+
path: filePath,
|
|
81
|
+
name: basename(filePath),
|
|
82
|
+
mime: getMimeType(filePath),
|
|
83
|
+
sha256: hashFile(filePath),
|
|
84
|
+
size: stats.size,
|
|
85
|
+
createdAt: Date.now(),
|
|
86
|
+
groupId: options.groupId,
|
|
87
|
+
label: options.label,
|
|
88
|
+
kind: 'file',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Write asset manifest alongside the file
|
|
93
|
+
*/
|
|
94
|
+
export function writeAssetManifest(manifest) {
|
|
95
|
+
const manifestPath = manifest.path + '.asset.json';
|
|
96
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Read asset manifest for a file
|
|
100
|
+
*/
|
|
101
|
+
export function readAssetManifest(filePath) {
|
|
102
|
+
const manifestPath = filePath + '.asset.json';
|
|
103
|
+
if (!existsSync(manifestPath))
|
|
104
|
+
return null;
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get preview cache directory
|
|
114
|
+
*/
|
|
115
|
+
export function getPreviewCacheDir() {
|
|
116
|
+
const home = process.env.HOME || '/tmp';
|
|
117
|
+
const cacheDir = join(home, '.connect-hub', 'preview-cache');
|
|
118
|
+
if (!existsSync(cacheDir)) {
|
|
119
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
return cacheDir;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Generate cache key for preview
|
|
125
|
+
*/
|
|
126
|
+
export function getPreviewCacheKey(sha256, params) {
|
|
127
|
+
const paramsStr = params ? JSON.stringify(params) : '';
|
|
128
|
+
return createHash('sha256').update(sha256 + paramsStr).digest('hex').substring(0, 16);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Check if preview exists in cache
|
|
132
|
+
*/
|
|
133
|
+
export function getPreviewFromCache(cacheKey) {
|
|
134
|
+
const cachePath = join(getPreviewCacheDir(), `${cacheKey}.json`);
|
|
135
|
+
if (!existsSync(cachePath))
|
|
136
|
+
return null;
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(readFileSync(cachePath, 'utf-8'));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Save preview to cache
|
|
146
|
+
*/
|
|
147
|
+
export function savePreviewToCache(preview) {
|
|
148
|
+
const cachePath = join(getPreviewCacheDir(), `${preview.cacheKey}.json`);
|
|
149
|
+
writeFileSync(cachePath, JSON.stringify(preview, null, 2));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create a ViewSpec for comparing two assets
|
|
153
|
+
*/
|
|
154
|
+
export function createCompareView(beforeAssetId, afterAssetId, title = 'Comparison') {
|
|
155
|
+
return {
|
|
156
|
+
id: `view_${Date.now().toString(36)}`,
|
|
157
|
+
title,
|
|
158
|
+
layout: 'compare',
|
|
159
|
+
before: { type: 'asset', assetId: beforeAssetId, label: 'Before' },
|
|
160
|
+
after: { type: 'asset', assetId: afterAssetId, label: 'After' },
|
|
161
|
+
createdAt: Date.now(),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Create a ViewSpec for a grid of assets
|
|
166
|
+
*/
|
|
167
|
+
export function createGridView(assetIds, title = 'Gallery') {
|
|
168
|
+
return {
|
|
169
|
+
id: `view_${Date.now().toString(36)}`,
|
|
170
|
+
title,
|
|
171
|
+
layout: 'grid',
|
|
172
|
+
items: assetIds.map(id => ({ type: 'asset', assetId: id })),
|
|
173
|
+
createdAt: Date.now(),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Create a ViewSpec for a report with markdown and assets
|
|
178
|
+
*/
|
|
179
|
+
export function createReportView(title, sections) {
|
|
180
|
+
return {
|
|
181
|
+
id: `view_${Date.now().toString(36)}`,
|
|
182
|
+
title,
|
|
183
|
+
layout: 'stack',
|
|
184
|
+
items: sections.map(section => {
|
|
185
|
+
if (section.markdown) {
|
|
186
|
+
return { type: 'markdown', content: section.markdown, label: section.label };
|
|
187
|
+
}
|
|
188
|
+
return { type: 'asset', assetId: section.assetId, label: section.label };
|
|
189
|
+
}),
|
|
190
|
+
createdAt: Date.now(),
|
|
191
|
+
};
|
|
192
|
+
}
|