@siftd/connect-agent 0.2.22 → 0.2.24
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 +8 -0
- package/dist/core/file-tracker.d.ts +36 -0
- package/dist/core/file-tracker.js +253 -0
- package/dist/orchestrator.d.ts +21 -0
- package/dist/orchestrator.js +83 -1
- package/dist/tools/worker.d.ts +10 -0
- package/dist/tools/worker.js +12 -0
- package/dist/workers/manager.d.ts +22 -1
- package/dist/workers/manager.js +64 -0
- package/dist/workers/types.d.ts +12 -0
- package/package.json +1 -1
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
|
+
}
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/orchestrator.js
CHANGED
|
@@ -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,61 @@ 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
|
+
// Also set on workerTools for spawn_worker jobs
|
|
178
|
+
this.workerTools.setGalleryCallback(callback ? (workers) => {
|
|
179
|
+
// Merge with delegate_to_worker jobs and broadcast
|
|
180
|
+
this.broadcastGalleryUpdate();
|
|
181
|
+
} : null);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get gallery workers with their assets
|
|
185
|
+
*/
|
|
186
|
+
getGalleryWorkers() {
|
|
187
|
+
const workers = [];
|
|
188
|
+
const seenIds = new Set();
|
|
189
|
+
// Include jobs from delegateToWorker (this.jobs)
|
|
190
|
+
for (const [id, job] of this.jobs) {
|
|
191
|
+
// Only include jobs with assets or that are running
|
|
192
|
+
if (job.assets || job.status === 'running') {
|
|
193
|
+
workers.push({
|
|
194
|
+
id,
|
|
195
|
+
task: job.task,
|
|
196
|
+
status: job.status === 'timeout' ? 'failed' : job.status,
|
|
197
|
+
assets: job.assets || []
|
|
198
|
+
});
|
|
199
|
+
seenIds.add(id);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Include jobs from workerTools (spawn_worker)
|
|
203
|
+
try {
|
|
204
|
+
const spawnedWorkers = this.workerTools.getGalleryWorkers();
|
|
205
|
+
for (const worker of spawnedWorkers) {
|
|
206
|
+
if (!seenIds.has(worker.id)) {
|
|
207
|
+
workers.push(worker);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// WorkerTools not available
|
|
213
|
+
}
|
|
214
|
+
return workers;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Broadcast gallery update to callback
|
|
218
|
+
*/
|
|
219
|
+
broadcastGalleryUpdate() {
|
|
220
|
+
if (!this.galleryCallback)
|
|
221
|
+
return;
|
|
222
|
+
const workers = this.getGalleryWorkers();
|
|
223
|
+
if (workers.length > 0) {
|
|
224
|
+
this.galleryCallback(workers);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
170
227
|
/**
|
|
171
228
|
* Get current status of all workers (from both delegateToWorker and spawn_worker)
|
|
172
229
|
*/
|
|
@@ -866,13 +923,24 @@ Be specific about what you want done.`,
|
|
|
866
923
|
console.log(`[ORCHESTRATOR] Worker ${id} starting: ${task.slice(0, 80)}...`);
|
|
867
924
|
// Estimate task duration
|
|
868
925
|
const estimatedTime = this.estimateTaskDuration(task);
|
|
926
|
+
// Take snapshot of files before worker starts (for asset tracking)
|
|
927
|
+
let beforeSnapshot;
|
|
928
|
+
try {
|
|
929
|
+
beforeSnapshot = takeSnapshot(cwd);
|
|
930
|
+
console.log(`[ORCHESTRATOR] Snapshot: ${beforeSnapshot.files.size} files in ${cwd}`);
|
|
931
|
+
}
|
|
932
|
+
catch (err) {
|
|
933
|
+
console.log(`[ORCHESTRATOR] Could not take snapshot: ${err}`);
|
|
934
|
+
}
|
|
869
935
|
const job = {
|
|
870
936
|
id,
|
|
871
937
|
task: task.slice(0, 200),
|
|
872
938
|
status: 'running',
|
|
873
939
|
startTime: Date.now(),
|
|
874
940
|
output: '',
|
|
875
|
-
estimatedTime
|
|
941
|
+
estimatedTime,
|
|
942
|
+
workingDir: cwd,
|
|
943
|
+
beforeSnapshot
|
|
876
944
|
};
|
|
877
945
|
// Escape single quotes in prompt for shell safety
|
|
878
946
|
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
@@ -914,6 +982,20 @@ Be specific about what you want done.`,
|
|
|
914
982
|
job.endTime = Date.now();
|
|
915
983
|
const duration = Math.round((job.endTime - job.startTime) / 1000);
|
|
916
984
|
console.log(`[ORCHESTRATOR] Worker ${id} done in ${duration}s`);
|
|
985
|
+
// Track file changes (for gallery)
|
|
986
|
+
if (job.beforeSnapshot) {
|
|
987
|
+
try {
|
|
988
|
+
const afterSnapshot = takeSnapshot(job.workingDir);
|
|
989
|
+
const assets = compareSnapshots(job.beforeSnapshot, afterSnapshot);
|
|
990
|
+
job.assets = assets;
|
|
991
|
+
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)`);
|
|
992
|
+
// Broadcast gallery update
|
|
993
|
+
this.broadcastGalleryUpdate();
|
|
994
|
+
}
|
|
995
|
+
catch (err) {
|
|
996
|
+
console.log(`[ORCHESTRATOR] Could not track assets: ${err}`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
917
999
|
const result = job.output.trim() || '(No output)';
|
|
918
1000
|
// Notify via callback (sends to user via WebSocket)
|
|
919
1001
|
if (this.workerResultCallback) {
|
package/dist/tools/worker.d.ts
CHANGED
|
@@ -2,10 +2,20 @@
|
|
|
2
2
|
* Worker Tools
|
|
3
3
|
* Tools for spawning and managing Claude Code workers
|
|
4
4
|
*/
|
|
5
|
+
import { GalleryCallback, GalleryWorker } from '../workers/manager.js';
|
|
5
6
|
import type { ToolResult } from './bash.js';
|
|
7
|
+
export { GalleryCallback, GalleryWorker };
|
|
6
8
|
export declare class WorkerTools {
|
|
7
9
|
private manager;
|
|
8
10
|
constructor(workspaceDir: string);
|
|
11
|
+
/**
|
|
12
|
+
* Set callback for gallery updates (worker assets for UI)
|
|
13
|
+
*/
|
|
14
|
+
setGalleryCallback(callback: GalleryCallback | null): void;
|
|
15
|
+
/**
|
|
16
|
+
* Get gallery workers with assets
|
|
17
|
+
*/
|
|
18
|
+
getGalleryWorkers(): GalleryWorker[];
|
|
9
19
|
/**
|
|
10
20
|
* Spawn a new Claude Code worker
|
|
11
21
|
*/
|
package/dist/tools/worker.js
CHANGED
|
@@ -8,6 +8,18 @@ export class WorkerTools {
|
|
|
8
8
|
constructor(workspaceDir) {
|
|
9
9
|
this.manager = new WorkerManager(workspaceDir);
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Set callback for gallery updates (worker assets for UI)
|
|
13
|
+
*/
|
|
14
|
+
setGalleryCallback(callback) {
|
|
15
|
+
this.manager.setGalleryCallback(callback);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get gallery workers with assets
|
|
19
|
+
*/
|
|
20
|
+
getGalleryWorkers() {
|
|
21
|
+
return this.manager.getGalleryWorkers();
|
|
22
|
+
}
|
|
11
23
|
/**
|
|
12
24
|
* Spawn a new Claude Code worker
|
|
13
25
|
*/
|
|
@@ -2,11 +2,32 @@
|
|
|
2
2
|
* Claude Code Worker Manager
|
|
3
3
|
* Spawns and manages Claude Code CLI instances for parallel task execution
|
|
4
4
|
*/
|
|
5
|
-
import { WorkerJob, SpawnOptions, WorkerConfig } from './types.js';
|
|
5
|
+
import { WorkerJob, SpawnOptions, WorkerConfig, WorkerAsset } from './types.js';
|
|
6
|
+
export interface GalleryWorker {
|
|
7
|
+
id: string;
|
|
8
|
+
task: string;
|
|
9
|
+
status: 'running' | 'completed' | 'failed';
|
|
10
|
+
assets: WorkerAsset[];
|
|
11
|
+
}
|
|
12
|
+
export type GalleryCallback = (workers: GalleryWorker[]) => void;
|
|
6
13
|
export declare class WorkerManager {
|
|
7
14
|
private config;
|
|
8
15
|
private activeWorkers;
|
|
16
|
+
private fileSnapshots;
|
|
17
|
+
private galleryCallback;
|
|
9
18
|
constructor(workspaceDir: string, configOverrides?: Partial<WorkerConfig>);
|
|
19
|
+
/**
|
|
20
|
+
* Set callback for gallery updates (worker assets for UI)
|
|
21
|
+
*/
|
|
22
|
+
setGalleryCallback(callback: GalleryCallback | null): void;
|
|
23
|
+
/**
|
|
24
|
+
* Get gallery workers with assets for UI
|
|
25
|
+
*/
|
|
26
|
+
getGalleryWorkers(): GalleryWorker[];
|
|
27
|
+
/**
|
|
28
|
+
* Broadcast gallery update
|
|
29
|
+
*/
|
|
30
|
+
private broadcastGalleryUpdate;
|
|
10
31
|
/**
|
|
11
32
|
* Generate a unique job ID
|
|
12
33
|
*/
|
package/dist/workers/manager.js
CHANGED
|
@@ -6,9 +6,12 @@ import { spawn } from 'child_process';
|
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { DEFAULT_WORKER_CONFIG } from './types.js';
|
|
9
|
+
import { takeSnapshot, compareSnapshots } from '../core/file-tracker.js';
|
|
9
10
|
export class WorkerManager {
|
|
10
11
|
config;
|
|
11
12
|
activeWorkers = new Map();
|
|
13
|
+
fileSnapshots = new Map(); // Before-snapshots per job
|
|
14
|
+
galleryCallback = null;
|
|
12
15
|
constructor(workspaceDir, configOverrides) {
|
|
13
16
|
this.config = {
|
|
14
17
|
...DEFAULT_WORKER_CONFIG,
|
|
@@ -20,6 +23,39 @@ export class WorkerManager {
|
|
|
20
23
|
fs.mkdirSync(this.config.jobsDir, { recursive: true });
|
|
21
24
|
}
|
|
22
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Set callback for gallery updates (worker assets for UI)
|
|
28
|
+
*/
|
|
29
|
+
setGalleryCallback(callback) {
|
|
30
|
+
this.galleryCallback = callback;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get gallery workers with assets for UI
|
|
34
|
+
*/
|
|
35
|
+
getGalleryWorkers() {
|
|
36
|
+
const jobs = this.list();
|
|
37
|
+
return jobs
|
|
38
|
+
.filter(job => job.assets && job.assets.length > 0 || job.status === 'running')
|
|
39
|
+
.slice(0, 20) // Limit for UI
|
|
40
|
+
.map(job => ({
|
|
41
|
+
id: job.id,
|
|
42
|
+
task: job.task.slice(0, 60),
|
|
43
|
+
status: (job.status === 'timeout' || job.status === 'cancelled') ? 'failed' :
|
|
44
|
+
(job.status === 'pending' ? 'running' : job.status),
|
|
45
|
+
assets: job.assets || []
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Broadcast gallery update
|
|
50
|
+
*/
|
|
51
|
+
broadcastGalleryUpdate() {
|
|
52
|
+
if (!this.galleryCallback)
|
|
53
|
+
return;
|
|
54
|
+
const workers = this.getGalleryWorkers();
|
|
55
|
+
if (workers.length > 0) {
|
|
56
|
+
this.galleryCallback(workers);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
23
59
|
/**
|
|
24
60
|
* Generate a unique job ID
|
|
25
61
|
*/
|
|
@@ -58,6 +94,15 @@ export class WorkerManager {
|
|
|
58
94
|
// Validate timeout
|
|
59
95
|
const effectiveTimeout = Math.min(timeout, this.config.maxTimeout);
|
|
60
96
|
const jobId = this.generateJobId();
|
|
97
|
+
// Take snapshot before spawning (for asset tracking)
|
|
98
|
+
try {
|
|
99
|
+
const beforeSnapshot = takeSnapshot(workspace);
|
|
100
|
+
this.fileSnapshots.set(jobId, beforeSnapshot);
|
|
101
|
+
console.log(`[WORKER] Snapshot for ${jobId}: ${beforeSnapshot.files.size} files`);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
console.log(`[WORKER] Could not take snapshot for ${jobId}: ${err}`);
|
|
105
|
+
}
|
|
61
106
|
const job = {
|
|
62
107
|
id: jobId,
|
|
63
108
|
task,
|
|
@@ -155,6 +200,25 @@ This ensures nothing is lost even if your output gets truncated.`;
|
|
|
155
200
|
if (stderr && code !== 0) {
|
|
156
201
|
currentJob.error = stderr.trim();
|
|
157
202
|
}
|
|
203
|
+
// Track file changes (for gallery)
|
|
204
|
+
const beforeSnapshot = this.fileSnapshots.get(jobId);
|
|
205
|
+
if (beforeSnapshot) {
|
|
206
|
+
try {
|
|
207
|
+
const afterSnapshot = takeSnapshot(currentJob.workspace);
|
|
208
|
+
const assets = compareSnapshots(beforeSnapshot, afterSnapshot);
|
|
209
|
+
currentJob.assets = assets;
|
|
210
|
+
const newCount = assets.filter(a => a.type === 'new').length;
|
|
211
|
+
const modCount = assets.filter(a => a.type === 'modified').length;
|
|
212
|
+
console.log(`[WORKER] ${jobId} assets: ${assets.length} files (${newCount} new, ${modCount} modified)`);
|
|
213
|
+
// Clean up snapshot
|
|
214
|
+
this.fileSnapshots.delete(jobId);
|
|
215
|
+
// Broadcast gallery update
|
|
216
|
+
this.broadcastGalleryUpdate();
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
console.log(`[WORKER] Could not track assets for ${jobId}: ${err}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
158
222
|
this.saveJob(currentJob);
|
|
159
223
|
}
|
|
160
224
|
});
|
package/dist/workers/types.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Worker System Types
|
|
3
3
|
*/
|
|
4
|
+
export interface WorkerAsset {
|
|
5
|
+
path: string;
|
|
6
|
+
name: string;
|
|
7
|
+
type: 'new' | 'modified' | 'unchanged';
|
|
8
|
+
fileType: 'code' | 'image' | 'pdf' | 'text' | 'other';
|
|
9
|
+
preview?: string;
|
|
10
|
+
diff?: Array<{
|
|
11
|
+
type: 'context' | 'add' | 'remove';
|
|
12
|
+
content: string;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
4
15
|
export interface WorkerJob {
|
|
5
16
|
id: string;
|
|
6
17
|
task: string;
|
|
@@ -15,6 +26,7 @@ export interface WorkerJob {
|
|
|
15
26
|
pid?: number;
|
|
16
27
|
timeout: number;
|
|
17
28
|
exitCode?: number;
|
|
29
|
+
assets?: WorkerAsset[];
|
|
18
30
|
}
|
|
19
31
|
export interface SpawnOptions {
|
|
20
32
|
workspace?: string;
|