@siftd/connect-agent 0.2.31 → 0.2.33
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/core/asset-api.d.ts +6 -6
- package/dist/core/asset-api.js +28 -6
- package/dist/core/assets.d.ts +31 -4
- package/dist/core/assets.js +56 -10
- package/dist/core/preview-worker.d.ts +11 -5
- package/dist/core/preview-worker.js +63 -50
- package/package.json +3 -1
package/dist/core/asset-api.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - asset.group - Get assets by group
|
|
9
9
|
* - view.get - Get ViewSpec
|
|
10
10
|
*/
|
|
11
|
-
import { AssetManifest, AssetPreview, ViewSpec } from './assets.js';
|
|
11
|
+
import { AssetManifest, AssetPreview, ViewSpec, ClientAssetManifest } from './assets.js';
|
|
12
12
|
export interface AssetRequest {
|
|
13
13
|
type: 'asset.list' | 'asset.get' | 'asset.preview' | 'asset.group' | 'view.get' | 'view.list';
|
|
14
14
|
requestId: string;
|
|
@@ -19,7 +19,7 @@ export interface AssetRequest {
|
|
|
19
19
|
export interface AssetListResponse {
|
|
20
20
|
type: 'asset.list';
|
|
21
21
|
requestId: string;
|
|
22
|
-
assets:
|
|
22
|
+
assets: ClientAssetManifest[];
|
|
23
23
|
}
|
|
24
24
|
export interface AssetGetResponse {
|
|
25
25
|
type: 'asset.get';
|
|
@@ -33,14 +33,14 @@ export interface AssetPreviewResponse {
|
|
|
33
33
|
type: 'asset.preview';
|
|
34
34
|
requestId: string;
|
|
35
35
|
assetId: string;
|
|
36
|
-
manifest:
|
|
36
|
+
manifest: ClientAssetManifest;
|
|
37
37
|
preview: AssetPreview | null;
|
|
38
38
|
}
|
|
39
39
|
export interface AssetGroupResponse {
|
|
40
40
|
type: 'asset.group';
|
|
41
41
|
requestId: string;
|
|
42
42
|
groupId: string;
|
|
43
|
-
assets:
|
|
43
|
+
assets: ClientAssetManifest[];
|
|
44
44
|
previews: (AssetPreview | null)[];
|
|
45
45
|
}
|
|
46
46
|
export interface ViewGetResponse {
|
|
@@ -79,11 +79,11 @@ export interface GalleryState {
|
|
|
79
79
|
export declare function buildGalleryState(): GalleryState;
|
|
80
80
|
/**
|
|
81
81
|
* Convert gallery state to WebSocket message format
|
|
82
|
-
*
|
|
82
|
+
* Strips absolute paths from assets for client safety
|
|
83
83
|
*/
|
|
84
84
|
export declare function galleryStateToMessage(state: GalleryState): {
|
|
85
85
|
type: 'gallery_state';
|
|
86
|
-
assets:
|
|
86
|
+
assets: ClientAssetManifest[];
|
|
87
87
|
previews: {
|
|
88
88
|
[key: string]: AssetPreview;
|
|
89
89
|
};
|
package/dist/core/asset-api.js
CHANGED
|
@@ -9,7 +9,25 @@
|
|
|
9
9
|
* - view.get - Get ViewSpec
|
|
10
10
|
*/
|
|
11
11
|
import { readFileSync, existsSync, statSync } from 'fs';
|
|
12
|
+
import { toClientManifest, isPathWithinRoot, } from './assets.js';
|
|
12
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
|
+
}
|
|
13
31
|
// ============================================================================
|
|
14
32
|
// VIEW REGISTRY
|
|
15
33
|
// ============================================================================
|
|
@@ -64,7 +82,7 @@ function handleAssetList(request) {
|
|
|
64
82
|
return {
|
|
65
83
|
type: 'asset.list',
|
|
66
84
|
requestId: request.requestId,
|
|
67
|
-
assets: getAllAssets(),
|
|
85
|
+
assets: getAllAssets().map(toClientManifest),
|
|
68
86
|
};
|
|
69
87
|
}
|
|
70
88
|
function handleAssetGet(request) {
|
|
@@ -76,8 +94,12 @@ function handleAssetGet(request) {
|
|
|
76
94
|
if (!manifest) {
|
|
77
95
|
return { type: 'asset.error', requestId, error: `Asset not found: ${assetId}` };
|
|
78
96
|
}
|
|
97
|
+
// Security: validate path is within allowed roots
|
|
98
|
+
if (!isPathAllowed(manifest.path)) {
|
|
99
|
+
return { type: 'asset.error', requestId, error: 'Access denied' };
|
|
100
|
+
}
|
|
79
101
|
if (!existsSync(manifest.path)) {
|
|
80
|
-
return { type: 'asset.error', requestId, error:
|
|
102
|
+
return { type: 'asset.error', requestId, error: 'File not found' };
|
|
81
103
|
}
|
|
82
104
|
// Read file and encode as base64
|
|
83
105
|
const data = readFileSync(manifest.path);
|
|
@@ -105,7 +127,7 @@ function handleAssetPreview(request) {
|
|
|
105
127
|
type: 'asset.preview',
|
|
106
128
|
requestId,
|
|
107
129
|
assetId,
|
|
108
|
-
manifest,
|
|
130
|
+
manifest: toClientManifest(manifest),
|
|
109
131
|
preview,
|
|
110
132
|
};
|
|
111
133
|
}
|
|
@@ -120,7 +142,7 @@ function handleAssetGroup(request) {
|
|
|
120
142
|
type: 'asset.group',
|
|
121
143
|
requestId,
|
|
122
144
|
groupId,
|
|
123
|
-
assets,
|
|
145
|
+
assets: assets.map(toClientManifest),
|
|
124
146
|
previews,
|
|
125
147
|
};
|
|
126
148
|
}
|
|
@@ -161,7 +183,7 @@ export function buildGalleryState() {
|
|
|
161
183
|
}
|
|
162
184
|
/**
|
|
163
185
|
* Convert gallery state to WebSocket message format
|
|
164
|
-
*
|
|
186
|
+
* Strips absolute paths from assets for client safety
|
|
165
187
|
*/
|
|
166
188
|
export function galleryStateToMessage(state) {
|
|
167
189
|
const previews = {};
|
|
@@ -174,7 +196,7 @@ export function galleryStateToMessage(state) {
|
|
|
174
196
|
}
|
|
175
197
|
return {
|
|
176
198
|
type: 'gallery_state',
|
|
177
|
-
assets: state.assets,
|
|
199
|
+
assets: state.assets.map(toClientManifest),
|
|
178
200
|
previews,
|
|
179
201
|
groups,
|
|
180
202
|
views: state.views,
|
package/dist/core/assets.d.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* The preview-worker generates thumbnails/metadata WITHOUT using the LLM.
|
|
6
6
|
*/
|
|
7
7
|
export interface AssetManifest {
|
|
8
|
-
/** Unique asset ID */
|
|
8
|
+
/** Unique asset ID (content-addressed) */
|
|
9
9
|
id: string;
|
|
10
|
-
/** Absolute path to the file */
|
|
10
|
+
/** Absolute path to the file (INTERNAL ONLY - never send to client) */
|
|
11
11
|
path: string;
|
|
12
12
|
/** Filename */
|
|
13
13
|
name: string;
|
|
@@ -28,6 +28,29 @@ export interface AssetManifest {
|
|
|
28
28
|
/** For kind=view, the ViewSpec */
|
|
29
29
|
viewSpec?: ViewSpec;
|
|
30
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;
|
|
31
54
|
export interface AssetPreview {
|
|
32
55
|
/** Reference to the asset */
|
|
33
56
|
assetId: string;
|
|
@@ -124,15 +147,18 @@ export declare function hashFile(filePath: string): string;
|
|
|
124
147
|
*/
|
|
125
148
|
export declare function getMimeType(filePath: string): string;
|
|
126
149
|
/**
|
|
127
|
-
* Generate
|
|
150
|
+
* Generate deterministic asset ID from content hash + relative path
|
|
151
|
+
* Content-addressed: same file always gets same ID
|
|
128
152
|
*/
|
|
129
|
-
export declare function generateAssetId(): string;
|
|
153
|
+
export declare function generateAssetId(sha256: string, relativePath?: string): string;
|
|
130
154
|
/**
|
|
131
155
|
* Create asset manifest for a file
|
|
156
|
+
* @param rootDir - Root directory for computing relative path (for ID stability)
|
|
132
157
|
*/
|
|
133
158
|
export declare function createAssetManifest(filePath: string, options?: {
|
|
134
159
|
groupId?: string;
|
|
135
160
|
label?: AssetManifest['label'];
|
|
161
|
+
rootDir?: string;
|
|
136
162
|
}): AssetManifest;
|
|
137
163
|
/**
|
|
138
164
|
* Write asset manifest alongside the file
|
|
@@ -148,6 +174,7 @@ export declare function readAssetManifest(filePath: string): AssetManifest | nul
|
|
|
148
174
|
export declare function getPreviewCacheDir(): string;
|
|
149
175
|
/**
|
|
150
176
|
* Generate cache key for preview
|
|
177
|
+
* Uses canonical stringify for stable key generation regardless of key order
|
|
151
178
|
*/
|
|
152
179
|
export declare function getPreviewCacheKey(sha256: string, params?: ViewerParams): string;
|
|
153
180
|
/**
|
package/dist/core/assets.js
CHANGED
|
@@ -5,8 +5,28 @@
|
|
|
5
5
|
* The preview-worker generates thumbnails/metadata WITHOUT using the LLM.
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from 'crypto';
|
|
8
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'fs';
|
|
9
|
-
import { basename, extname, join } from 'path';
|
|
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
|
+
}
|
|
10
30
|
// ============================================================================
|
|
11
31
|
// UTILITY FUNCTIONS
|
|
12
32
|
// ============================================================================
|
|
@@ -63,24 +83,35 @@ export function getMimeType(filePath) {
|
|
|
63
83
|
return mimeMap[ext] || 'application/octet-stream';
|
|
64
84
|
}
|
|
65
85
|
/**
|
|
66
|
-
* Generate
|
|
86
|
+
* Generate deterministic asset ID from content hash + relative path
|
|
87
|
+
* Content-addressed: same file always gets same ID
|
|
67
88
|
*/
|
|
68
|
-
export function generateAssetId() {
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
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}`;
|
|
72
97
|
}
|
|
73
98
|
/**
|
|
74
99
|
* Create asset manifest for a file
|
|
100
|
+
* @param rootDir - Root directory for computing relative path (for ID stability)
|
|
75
101
|
*/
|
|
76
102
|
export function createAssetManifest(filePath, options = {}) {
|
|
77
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);
|
|
78
109
|
return {
|
|
79
|
-
id: generateAssetId(),
|
|
110
|
+
id: generateAssetId(sha256, relativePath),
|
|
80
111
|
path: filePath,
|
|
81
112
|
name: basename(filePath),
|
|
82
113
|
mime: getMimeType(filePath),
|
|
83
|
-
sha256
|
|
114
|
+
sha256,
|
|
84
115
|
size: stats.size,
|
|
85
116
|
createdAt: Date.now(),
|
|
86
117
|
groupId: options.groupId,
|
|
@@ -120,11 +151,26 @@ export function getPreviewCacheDir() {
|
|
|
120
151
|
}
|
|
121
152
|
return cacheDir;
|
|
122
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
|
+
}
|
|
123
168
|
/**
|
|
124
169
|
* Generate cache key for preview
|
|
170
|
+
* Uses canonical stringify for stable key generation regardless of key order
|
|
125
171
|
*/
|
|
126
172
|
export function getPreviewCacheKey(sha256, params) {
|
|
127
|
-
const paramsStr = params ?
|
|
173
|
+
const paramsStr = params ? canonicalStringify(params) : '';
|
|
128
174
|
return createHash('sha256').update(sha256 + paramsStr).digest('hex').substring(0, 16);
|
|
129
175
|
}
|
|
130
176
|
/**
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* NO LLM involvement - pure local, deterministic processing.
|
|
10
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.)
|
|
11
15
|
*/
|
|
12
16
|
import { AssetManifest, AssetPreview } from './assets.js';
|
|
13
17
|
export interface PreviewWorkerOptions {
|
|
@@ -23,25 +27,27 @@ export interface PreviewWorkerOptions {
|
|
|
23
27
|
verbose?: boolean;
|
|
24
28
|
}
|
|
25
29
|
export declare class PreviewWorker {
|
|
26
|
-
private
|
|
30
|
+
private watcher;
|
|
27
31
|
private queue;
|
|
28
32
|
private processing;
|
|
33
|
+
private debounceTimers;
|
|
29
34
|
private concurrency;
|
|
30
35
|
private options;
|
|
31
36
|
private running;
|
|
37
|
+
private rootDir;
|
|
32
38
|
constructor(options?: PreviewWorkerOptions);
|
|
33
39
|
/**
|
|
34
40
|
* Start watching directories
|
|
35
41
|
*/
|
|
36
42
|
start(): void;
|
|
37
43
|
/**
|
|
38
|
-
*
|
|
44
|
+
* Handle file add/change with debouncing
|
|
39
45
|
*/
|
|
40
|
-
|
|
46
|
+
private handleFile;
|
|
41
47
|
/**
|
|
42
|
-
*
|
|
48
|
+
* Stop watching
|
|
43
49
|
*/
|
|
44
|
-
|
|
50
|
+
stop(): void;
|
|
45
51
|
/**
|
|
46
52
|
* Add file to processing queue
|
|
47
53
|
*/
|
|
@@ -8,23 +8,41 @@
|
|
|
8
8
|
*
|
|
9
9
|
* NO LLM involvement - pure local, deterministic processing.
|
|
10
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.)
|
|
11
15
|
*/
|
|
12
|
-
import
|
|
13
|
-
import { readFileSync, existsSync,
|
|
16
|
+
import chokidar from 'chokidar';
|
|
17
|
+
import { readFileSync, existsSync, mkdirSync } from 'fs';
|
|
14
18
|
import { join, basename } from 'path';
|
|
15
19
|
import { execSync } from 'child_process';
|
|
16
20
|
import { createAssetManifest, writeAssetManifest, readAssetManifest, getPreviewCacheDir, getPreviewCacheKey, getPreviewFromCache, savePreviewToCache, } from './assets.js';
|
|
17
21
|
import { getSharedOutputPath } from './hub.js';
|
|
22
|
+
// Files to ignore when watching (prevents watcher loops)
|
|
23
|
+
const IGNORE_PATTERNS = [
|
|
24
|
+
'**/*.asset.json',
|
|
25
|
+
'**/*.preview.json',
|
|
26
|
+
'**/.thumbs/**',
|
|
27
|
+
'**/thumbs/**',
|
|
28
|
+
'**/*.tmp',
|
|
29
|
+
'**/.DS_Store',
|
|
30
|
+
'**/node_modules/**',
|
|
31
|
+
'**/.git/**',
|
|
32
|
+
];
|
|
18
33
|
export class PreviewWorker {
|
|
19
|
-
|
|
34
|
+
watcher = null;
|
|
20
35
|
queue = [];
|
|
21
36
|
processing = new Set();
|
|
37
|
+
debounceTimers = new Map();
|
|
22
38
|
concurrency;
|
|
23
39
|
options;
|
|
24
40
|
running = false;
|
|
41
|
+
rootDir;
|
|
25
42
|
constructor(options = {}) {
|
|
26
43
|
this.options = options;
|
|
27
44
|
this.concurrency = options.concurrency || 4;
|
|
45
|
+
this.rootDir = getSharedOutputPath();
|
|
28
46
|
}
|
|
29
47
|
/**
|
|
30
48
|
* Start watching directories
|
|
@@ -33,65 +51,59 @@ export class PreviewWorker {
|
|
|
33
51
|
if (this.running)
|
|
34
52
|
return;
|
|
35
53
|
this.running = true;
|
|
36
|
-
const dirs = this.options.watchDirs || [
|
|
54
|
+
const dirs = this.options.watchDirs || [this.rootDir];
|
|
37
55
|
for (const dir of dirs) {
|
|
38
56
|
if (!existsSync(dir)) {
|
|
39
57
|
mkdirSync(dir, { recursive: true });
|
|
40
58
|
}
|
|
41
|
-
this.log(`Watching: ${dir}`);
|
|
42
|
-
// Initial scan
|
|
43
|
-
this.scanDirectory(dir);
|
|
44
|
-
// Watch for changes
|
|
45
|
-
const watcher = watch(dir, { recursive: true }, (event, filename) => {
|
|
46
|
-
if (!filename)
|
|
47
|
-
return;
|
|
48
|
-
const filePath = join(dir, filename);
|
|
49
|
-
// Skip manifest files and hidden files
|
|
50
|
-
if (filename.endsWith('.asset.json') || filename.startsWith('.'))
|
|
51
|
-
return;
|
|
52
|
-
// Skip directories
|
|
53
|
-
try {
|
|
54
|
-
if (statSync(filePath).isDirectory())
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return; // File may have been deleted
|
|
59
|
-
}
|
|
60
|
-
this.enqueue(filePath);
|
|
61
|
-
});
|
|
62
|
-
this.watchers.push(watcher);
|
|
63
59
|
}
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
this.log(`Watching: ${dirs.join(', ')}`);
|
|
61
|
+
// Use chokidar for reliable watching with:
|
|
62
|
+
// - awaitWriteFinish: wait until file size is stable (handles half-written files)
|
|
63
|
+
// - ignored: prevent watcher loops
|
|
64
|
+
this.watcher = chokidar.watch(dirs, {
|
|
65
|
+
ignoreInitial: false, // Process existing files on startup
|
|
66
|
+
persistent: true,
|
|
67
|
+
awaitWriteFinish: {
|
|
68
|
+
stabilityThreshold: 400, // Wait 400ms for file size to stabilize
|
|
69
|
+
pollInterval: 100,
|
|
70
|
+
},
|
|
71
|
+
ignored: IGNORE_PATTERNS,
|
|
72
|
+
depth: 10,
|
|
73
|
+
});
|
|
74
|
+
this.watcher.on('add', (filePath) => this.handleFile(filePath));
|
|
75
|
+
this.watcher.on('change', (filePath) => this.handleFile(filePath));
|
|
76
|
+
this.watcher.on('error', (error) => this.log(`Watcher error: ${error}`));
|
|
66
77
|
}
|
|
67
78
|
/**
|
|
68
|
-
*
|
|
79
|
+
* Handle file add/change with debouncing
|
|
69
80
|
*/
|
|
70
|
-
|
|
71
|
-
this
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
handleFile(filePath) {
|
|
82
|
+
// Clear existing debounce timer for this file
|
|
83
|
+
const existingTimer = this.debounceTimers.get(filePath);
|
|
84
|
+
if (existingTimer) {
|
|
85
|
+
clearTimeout(existingTimer);
|
|
74
86
|
}
|
|
75
|
-
|
|
87
|
+
// Debounce: wait 100ms before processing to batch rapid changes
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
this.debounceTimers.delete(filePath);
|
|
90
|
+
this.enqueue(filePath);
|
|
91
|
+
}, 100);
|
|
92
|
+
this.debounceTimers.set(filePath, timer);
|
|
76
93
|
}
|
|
77
94
|
/**
|
|
78
|
-
*
|
|
95
|
+
* Stop watching
|
|
79
96
|
*/
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (entry.isDirectory()) {
|
|
86
|
-
this.scanDirectory(fullPath);
|
|
87
|
-
}
|
|
88
|
-
else if (!entry.name.endsWith('.asset.json') && !entry.name.startsWith('.')) {
|
|
89
|
-
this.enqueue(fullPath);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
97
|
+
stop() {
|
|
98
|
+
this.running = false;
|
|
99
|
+
// Clear all debounce timers
|
|
100
|
+
for (const timer of this.debounceTimers.values()) {
|
|
101
|
+
clearTimeout(timer);
|
|
92
102
|
}
|
|
93
|
-
|
|
94
|
-
|
|
103
|
+
this.debounceTimers.clear();
|
|
104
|
+
if (this.watcher) {
|
|
105
|
+
this.watcher.close();
|
|
106
|
+
this.watcher = null;
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
/**
|
|
@@ -132,9 +144,10 @@ export class PreviewWorker {
|
|
|
132
144
|
// Check for existing manifest
|
|
133
145
|
let manifest = readAssetManifest(filePath);
|
|
134
146
|
if (!manifest) {
|
|
135
|
-
// Create new manifest
|
|
147
|
+
// Create new manifest with rootDir for stable IDs
|
|
136
148
|
manifest = createAssetManifest(filePath, {
|
|
137
149
|
groupId: this.extractGroupId(filePath),
|
|
150
|
+
rootDir: this.rootDir,
|
|
138
151
|
});
|
|
139
152
|
writeAssetManifest(manifest);
|
|
140
153
|
this.log(`Created manifest: ${manifest.name} (${manifest.id})`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siftd/connect-agent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.33",
|
|
4
4
|
"description": "Master orchestrator agent - control Claude Code remotely via web",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
41
41
|
"better-sqlite3": "^11.7.0",
|
|
42
|
+
"chokidar": "^5.0.0",
|
|
42
43
|
"commander": "^12.1.0",
|
|
43
44
|
"conf": "^13.0.1",
|
|
44
45
|
"node-cron": "^3.0.3",
|
|
@@ -49,6 +50,7 @@
|
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@types/better-sqlite3": "^7.6.12",
|
|
53
|
+
"@types/chokidar": "^1.7.5",
|
|
52
54
|
"@types/node": "^22.10.2",
|
|
53
55
|
"@types/node-cron": "^3.0.11",
|
|
54
56
|
"@types/pg": "^8.11.10",
|