@siftd/connect-agent 0.2.22 → 0.2.23

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
@@ -257,6 +257,14 @@ export async function runAgent(pollInterval = 2000) {
257
257
  console.log(`[WORKERS] ${running.length} running`);
258
258
  }
259
259
  });
260
+ // Gallery updates - send worker assets for UI gallery view
261
+ orchestrator.setGalleryCallback((galleryWorkers) => {
262
+ if (wsClient?.connected()) {
263
+ wsClient.sendGalleryWorkers(galleryWorkers);
264
+ const totalAssets = galleryWorkers.reduce((sum, w) => sum + w.assets.length, 0);
265
+ console.log(`[GALLERY] ${galleryWorkers.length} workers, ${totalAssets} assets`);
266
+ }
267
+ });
260
268
  // Worker results - send to user when workers complete
261
269
  orchestrator.setWorkerResultCallback((workerId, result) => {
262
270
  console.log(`[WORKER DONE] ${workerId}: ${result.slice(0, 100)}...`);
@@ -0,0 +1,36 @@
1
+ /**
2
+ * File Tracker - Detects new/modified files in a directory
3
+ *
4
+ * Used to track what workers create or modify for the gallery view.
5
+ */
6
+ export interface TrackedFile {
7
+ path: string;
8
+ name: string;
9
+ mtime: number;
10
+ size: number;
11
+ hash?: string;
12
+ }
13
+ export interface FileSnapshot {
14
+ workingDir: string;
15
+ files: Map<string, TrackedFile>;
16
+ timestamp: number;
17
+ }
18
+ export interface WorkerAsset {
19
+ path: string;
20
+ name: string;
21
+ type: 'new' | 'modified' | 'unchanged';
22
+ fileType: 'code' | 'image' | 'pdf' | 'text' | 'other';
23
+ preview?: string;
24
+ diff?: Array<{
25
+ type: 'context' | 'add' | 'remove';
26
+ content: string;
27
+ }>;
28
+ }
29
+ /**
30
+ * Take a snapshot of files in a directory
31
+ */
32
+ export declare function takeSnapshot(workingDir: string): FileSnapshot;
33
+ /**
34
+ * Compare two snapshots to find new/modified files
35
+ */
36
+ export declare function compareSnapshots(before: FileSnapshot, after: FileSnapshot): WorkerAsset[];
@@ -0,0 +1,253 @@
1
+ /**
2
+ * File Tracker - Detects new/modified files in a directory
3
+ *
4
+ * Used to track what workers create or modify for the gallery view.
5
+ */
6
+ import { readdirSync, statSync, readFileSync, existsSync } from 'fs';
7
+ import { join, relative, extname } from 'path';
8
+ import { execSync } from 'child_process';
9
+ // File extensions to track
10
+ const CODE_EXTENSIONS = new Set([
11
+ '.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.go', '.rs',
12
+ '.java', '.c', '.cpp', '.h', '.cs', '.php', '.swift', '.kt',
13
+ '.json', '.yaml', '.yml', '.toml', '.xml', '.html', '.css',
14
+ '.scss', '.less', '.vue', '.svelte', '.sh', '.bash', '.zsh',
15
+ '.sql', '.graphql', '.md', '.mdx'
16
+ ]);
17
+ const IMAGE_EXTENSIONS = new Set([
18
+ '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.bmp'
19
+ ]);
20
+ const TEXT_EXTENSIONS = new Set([
21
+ '.txt', '.log', '.env', '.gitignore', '.dockerignore', '.editorconfig'
22
+ ]);
23
+ // Directories to skip
24
+ const SKIP_DIRS = new Set([
25
+ 'node_modules', '.git', '.next', 'dist', 'build', '__pycache__',
26
+ '.cache', 'coverage', '.nyc_output', 'vendor', 'target'
27
+ ]);
28
+ // Max files to track (performance limit)
29
+ const MAX_FILES = 500;
30
+ // Max depth to traverse
31
+ const MAX_DEPTH = 5;
32
+ /**
33
+ * Take a snapshot of files in a directory
34
+ */
35
+ export function takeSnapshot(workingDir) {
36
+ const files = new Map();
37
+ function walk(dir, depth) {
38
+ if (depth > MAX_DEPTH || files.size >= MAX_FILES)
39
+ return;
40
+ try {
41
+ const entries = readdirSync(dir, { withFileTypes: true });
42
+ for (const entry of entries) {
43
+ if (files.size >= MAX_FILES)
44
+ break;
45
+ const fullPath = join(dir, entry.name);
46
+ const relativePath = relative(workingDir, fullPath);
47
+ if (entry.isDirectory()) {
48
+ // Skip ignored directories
49
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.'))
50
+ continue;
51
+ walk(fullPath, depth + 1);
52
+ }
53
+ else if (entry.isFile()) {
54
+ // Only track interesting files
55
+ const ext = extname(entry.name).toLowerCase();
56
+ if (!isInterestingFile(entry.name, ext))
57
+ continue;
58
+ try {
59
+ const stats = statSync(fullPath);
60
+ files.set(relativePath, {
61
+ path: relativePath,
62
+ name: entry.name,
63
+ mtime: stats.mtimeMs,
64
+ size: stats.size
65
+ });
66
+ }
67
+ catch {
68
+ // Skip files we can't stat
69
+ }
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ // Skip directories we can't read
75
+ }
76
+ }
77
+ walk(workingDir, 0);
78
+ return {
79
+ workingDir,
80
+ files,
81
+ timestamp: Date.now()
82
+ };
83
+ }
84
+ /**
85
+ * Compare two snapshots to find new/modified files
86
+ */
87
+ export function compareSnapshots(before, after) {
88
+ const assets = [];
89
+ // Find new and modified files
90
+ for (const [path, afterFile] of after.files) {
91
+ const beforeFile = before.files.get(path);
92
+ if (!beforeFile) {
93
+ // New file
94
+ assets.push(createAsset(after.workingDir, afterFile, 'new'));
95
+ }
96
+ else if (afterFile.mtime > beforeFile.mtime || afterFile.size !== beforeFile.size) {
97
+ // Modified file
98
+ const asset = createAsset(after.workingDir, afterFile, 'modified');
99
+ // Generate diff for code files
100
+ if (asset.fileType === 'code') {
101
+ asset.diff = generateSimpleDiff(after.workingDir, path);
102
+ }
103
+ assets.push(asset);
104
+ }
105
+ }
106
+ // Sort: new files first, then by path
107
+ assets.sort((a, b) => {
108
+ if (a.type !== b.type)
109
+ return a.type === 'new' ? -1 : 1;
110
+ return a.path.localeCompare(b.path);
111
+ });
112
+ // Limit to most relevant files
113
+ return assets.slice(0, 20);
114
+ }
115
+ /**
116
+ * Create a WorkerAsset from a tracked file
117
+ */
118
+ function createAsset(workingDir, file, type) {
119
+ const ext = extname(file.name).toLowerCase();
120
+ const fileType = getFileType(ext);
121
+ const fullPath = join(workingDir, file.path);
122
+ let preview;
123
+ // Generate preview for small files
124
+ if (file.size < 10000) {
125
+ try {
126
+ if (fileType === 'code' || fileType === 'text') {
127
+ const content = readFileSync(fullPath, 'utf8');
128
+ preview = content.slice(0, 1000);
129
+ if (content.length > 1000)
130
+ preview += '\n... (truncated)';
131
+ }
132
+ else if (fileType === 'image') {
133
+ // For images, we could generate a base64 thumbnail
134
+ // For now, just indicate it's an image
135
+ preview = `[Image: ${file.name}]`;
136
+ }
137
+ }
138
+ catch {
139
+ // Skip preview on error
140
+ }
141
+ }
142
+ return {
143
+ path: file.path,
144
+ name: file.name,
145
+ type,
146
+ fileType,
147
+ preview
148
+ };
149
+ }
150
+ /**
151
+ * Get file type category from extension
152
+ */
153
+ function getFileType(ext) {
154
+ if (CODE_EXTENSIONS.has(ext))
155
+ return 'code';
156
+ if (IMAGE_EXTENSIONS.has(ext))
157
+ return 'image';
158
+ if (TEXT_EXTENSIONS.has(ext))
159
+ return 'text';
160
+ if (ext === '.pdf')
161
+ return 'pdf';
162
+ return 'other';
163
+ }
164
+ /**
165
+ * Check if a file is worth tracking
166
+ */
167
+ function isInterestingFile(name, ext) {
168
+ // Skip hidden files (except some config files)
169
+ if (name.startsWith('.') && !TEXT_EXTENSIONS.has(ext))
170
+ return false;
171
+ // Skip lock files and generated files
172
+ if (name.includes('.lock') || name.includes('-lock.'))
173
+ return false;
174
+ if (name === 'package-lock.json' || name === 'yarn.lock')
175
+ return false;
176
+ // Track code, images, text, pdf
177
+ return (CODE_EXTENSIONS.has(ext) ||
178
+ IMAGE_EXTENSIONS.has(ext) ||
179
+ TEXT_EXTENSIONS.has(ext) ||
180
+ ext === '.pdf');
181
+ }
182
+ /**
183
+ * Generate a simple diff for a file using git
184
+ */
185
+ function generateSimpleDiff(workingDir, relativePath) {
186
+ try {
187
+ // Check if we're in a git repo
188
+ const isGit = existsSync(join(workingDir, '.git'));
189
+ if (!isGit)
190
+ return undefined;
191
+ // Get git diff
192
+ const diff = execSync(`git diff --no-color -- "${relativePath}"`, {
193
+ cwd: workingDir,
194
+ encoding: 'utf8',
195
+ maxBuffer: 100 * 1024 // 100KB max
196
+ });
197
+ if (!diff.trim()) {
198
+ // No staged changes, try unstaged
199
+ const unstagedDiff = execSync(`git diff HEAD --no-color -- "${relativePath}"`, {
200
+ cwd: workingDir,
201
+ encoding: 'utf8',
202
+ maxBuffer: 100 * 1024
203
+ });
204
+ if (!unstagedDiff.trim())
205
+ return undefined;
206
+ return parseDiff(unstagedDiff);
207
+ }
208
+ return parseDiff(diff);
209
+ }
210
+ catch {
211
+ return undefined;
212
+ }
213
+ }
214
+ /**
215
+ * Parse git diff output into structured format
216
+ */
217
+ function parseDiff(diffOutput) {
218
+ const lines = diffOutput.split('\n');
219
+ const result = [];
220
+ let inHunk = false;
221
+ for (const line of lines) {
222
+ // Skip diff header lines
223
+ if (line.startsWith('diff --git') ||
224
+ line.startsWith('index ') ||
225
+ line.startsWith('---') ||
226
+ line.startsWith('+++')) {
227
+ continue;
228
+ }
229
+ // Detect hunk header
230
+ if (line.startsWith('@@')) {
231
+ inHunk = true;
232
+ continue;
233
+ }
234
+ if (!inHunk)
235
+ continue;
236
+ // Parse diff lines
237
+ if (line.startsWith('+')) {
238
+ result.push({ type: 'add', content: line.slice(1) });
239
+ }
240
+ else if (line.startsWith('-')) {
241
+ result.push({ type: 'remove', content: line.slice(1) });
242
+ }
243
+ else if (line.startsWith(' ')) {
244
+ result.push({ type: 'context', content: line.slice(1) });
245
+ }
246
+ // Limit diff size
247
+ if (result.length >= 100) {
248
+ result.push({ type: 'context', content: '... (diff truncated)' });
249
+ break;
250
+ }
251
+ }
252
+ return result;
253
+ }
@@ -5,6 +5,7 @@
5
5
  * It does NOT do the work itself. Claude Code CLI workers do the work.
6
6
  */
7
7
  import type { MessageParam } from '@anthropic-ai/sdk/resources/messages';
8
+ import { WorkerAsset } from './core/file-tracker.js';
8
9
  export type MessageSender = (message: string) => Promise<void>;
9
10
  export interface WorkerStatus {
10
11
  id: string;
@@ -15,6 +16,13 @@ export interface WorkerStatus {
15
16
  estimated: number;
16
17
  }
17
18
  export type WorkerStatusCallback = (workers: WorkerStatus[]) => void;
19
+ export interface GalleryWorker {
20
+ id: string;
21
+ task: string;
22
+ status: 'running' | 'completed' | 'failed';
23
+ assets: WorkerAsset[];
24
+ }
25
+ export type GalleryCallback = (workers: GalleryWorker[]) => void;
18
26
  export declare class MasterOrchestrator {
19
27
  private client;
20
28
  private model;
@@ -26,6 +34,7 @@ export declare class MasterOrchestrator {
26
34
  private jobCounter;
27
35
  private workerStatusCallback;
28
36
  private workerStatusInterval;
37
+ private galleryCallback;
29
38
  private userId;
30
39
  private workspaceDir;
31
40
  private claudePath;
@@ -51,6 +60,18 @@ export declare class MasterOrchestrator {
51
60
  * Set callback for worker status updates (for UI progress bars)
52
61
  */
53
62
  setWorkerStatusCallback(callback: WorkerStatusCallback | null): void;
63
+ /**
64
+ * Set callback for gallery updates (worker assets for UI gallery view)
65
+ */
66
+ setGalleryCallback(callback: GalleryCallback | null): void;
67
+ /**
68
+ * Get gallery workers with their assets
69
+ */
70
+ getGalleryWorkers(): GalleryWorker[];
71
+ /**
72
+ * Broadcast gallery update to callback
73
+ */
74
+ private broadcastGalleryUpdate;
54
75
  /**
55
76
  * Get current status of all workers (from both delegateToWorker and spawn_worker)
56
77
  */
@@ -17,6 +17,7 @@ import { WorkerTools } from './tools/worker.js';
17
17
  import { SharedState } from './workers/shared-state.js';
18
18
  import { getKnowledgeForPrompt } from './genesis/index.js';
19
19
  import { loadHubContext, formatHubContext, logAction } from './core/hub.js';
20
+ import { takeSnapshot, compareSnapshots } from './core/file-tracker.js';
20
21
  const SYSTEM_PROMPT = `You are a MASTER ORCHESTRATOR - NOT a worker. You delegate ALL file/code work to Claude Code CLI workers.
21
22
 
22
23
  CRITICAL IDENTITY:
@@ -85,6 +86,7 @@ export class MasterOrchestrator {
85
86
  jobCounter = 0;
86
87
  workerStatusCallback = null;
87
88
  workerStatusInterval = null;
89
+ galleryCallback = null;
88
90
  userId;
89
91
  workspaceDir;
90
92
  claudePath;
@@ -167,6 +169,41 @@ export class MasterOrchestrator {
167
169
  }
168
170
  }
169
171
  }
172
+ /**
173
+ * Set callback for gallery updates (worker assets for UI gallery view)
174
+ */
175
+ setGalleryCallback(callback) {
176
+ this.galleryCallback = callback;
177
+ }
178
+ /**
179
+ * Get gallery workers with their assets
180
+ */
181
+ getGalleryWorkers() {
182
+ const workers = [];
183
+ for (const [id, job] of this.jobs) {
184
+ // Only include jobs with assets or that are running
185
+ if (job.assets || job.status === 'running') {
186
+ workers.push({
187
+ id,
188
+ task: job.task,
189
+ status: job.status === 'timeout' ? 'failed' : job.status,
190
+ assets: job.assets || []
191
+ });
192
+ }
193
+ }
194
+ return workers;
195
+ }
196
+ /**
197
+ * Broadcast gallery update to callback
198
+ */
199
+ broadcastGalleryUpdate() {
200
+ if (!this.galleryCallback)
201
+ return;
202
+ const workers = this.getGalleryWorkers();
203
+ if (workers.length > 0) {
204
+ this.galleryCallback(workers);
205
+ }
206
+ }
170
207
  /**
171
208
  * Get current status of all workers (from both delegateToWorker and spawn_worker)
172
209
  */
@@ -866,13 +903,24 @@ Be specific about what you want done.`,
866
903
  console.log(`[ORCHESTRATOR] Worker ${id} starting: ${task.slice(0, 80)}...`);
867
904
  // Estimate task duration
868
905
  const estimatedTime = this.estimateTaskDuration(task);
906
+ // Take snapshot of files before worker starts (for asset tracking)
907
+ let beforeSnapshot;
908
+ try {
909
+ beforeSnapshot = takeSnapshot(cwd);
910
+ console.log(`[ORCHESTRATOR] Snapshot: ${beforeSnapshot.files.size} files in ${cwd}`);
911
+ }
912
+ catch (err) {
913
+ console.log(`[ORCHESTRATOR] Could not take snapshot: ${err}`);
914
+ }
869
915
  const job = {
870
916
  id,
871
917
  task: task.slice(0, 200),
872
918
  status: 'running',
873
919
  startTime: Date.now(),
874
920
  output: '',
875
- estimatedTime
921
+ estimatedTime,
922
+ workingDir: cwd,
923
+ beforeSnapshot
876
924
  };
877
925
  // Escape single quotes in prompt for shell safety
878
926
  const escapedPrompt = prompt.replace(/'/g, "'\\''");
@@ -914,6 +962,20 @@ Be specific about what you want done.`,
914
962
  job.endTime = Date.now();
915
963
  const duration = Math.round((job.endTime - job.startTime) / 1000);
916
964
  console.log(`[ORCHESTRATOR] Worker ${id} done in ${duration}s`);
965
+ // Track file changes (for gallery)
966
+ if (job.beforeSnapshot) {
967
+ try {
968
+ const afterSnapshot = takeSnapshot(job.workingDir);
969
+ const assets = compareSnapshots(job.beforeSnapshot, afterSnapshot);
970
+ job.assets = assets;
971
+ console.log(`[ORCHESTRATOR] Worker ${id} assets: ${assets.length} files (${assets.filter(a => a.type === 'new').length} new, ${assets.filter(a => a.type === 'modified').length} modified)`);
972
+ // Broadcast gallery update
973
+ this.broadcastGalleryUpdate();
974
+ }
975
+ catch (err) {
976
+ console.log(`[ORCHESTRATOR] Could not track assets: ${err}`);
977
+ }
978
+ }
917
979
  const result = job.output.trim() || '(No output)';
918
980
  // Notify via callback (sends to user via WebSocket)
919
981
  if (this.workerResultCallback) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.22",
3
+ "version": "0.2.23",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",