@siftd/connect-agent 0.2.8 → 0.2.10

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.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * File Watcher for Reactive Workflows
3
+ *
4
+ * Enables the orchestrator to:
5
+ * - Watch directories/files for changes
6
+ * - Trigger callbacks on file events
7
+ * - Support glob patterns for filtering
8
+ * - Debounce rapid changes
9
+ */
10
+ import { EventEmitter } from 'events';
11
+ export type FileEventType = 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir';
12
+ export interface FileEvent {
13
+ type: FileEventType;
14
+ path: string;
15
+ timestamp: string;
16
+ }
17
+ export interface WatchRule {
18
+ id: string;
19
+ pattern: string;
20
+ events: FileEventType[];
21
+ action: string;
22
+ debounceMs?: number;
23
+ enabled: boolean;
24
+ lastTriggered?: string;
25
+ }
26
+ export interface WatcherConfig {
27
+ rulesFile: string;
28
+ defaultDebounce: number;
29
+ }
30
+ export declare class FileWatcher extends EventEmitter {
31
+ private watchers;
32
+ private rules;
33
+ private debounceTimers;
34
+ private config;
35
+ private rulesFile;
36
+ constructor(workspaceDir: string, config?: Partial<WatcherConfig>);
37
+ /**
38
+ * Add a watch rule
39
+ */
40
+ addRule(rule: Omit<WatchRule, 'id' | 'enabled'>): string;
41
+ /**
42
+ * Remove a watch rule
43
+ */
44
+ removeRule(id: string): boolean;
45
+ /**
46
+ * Enable/disable a rule
47
+ */
48
+ toggleRule(id: string, enabled: boolean): boolean;
49
+ /**
50
+ * List all rules
51
+ */
52
+ listRules(): WatchRule[];
53
+ /**
54
+ * Get a specific rule
55
+ */
56
+ getRule(id: string): WatchRule | undefined;
57
+ /**
58
+ * Watch a specific path
59
+ */
60
+ watch(targetPath: string, callback: (event: FileEvent) => void): string;
61
+ /**
62
+ * Stop watching a specific watcher
63
+ */
64
+ unwatch(watcherId: string): void;
65
+ /**
66
+ * Stop all watchers
67
+ */
68
+ stopAll(): void;
69
+ /**
70
+ * Get watcher status
71
+ */
72
+ status(): {
73
+ activeWatchers: number;
74
+ rules: number;
75
+ enabledRules: number;
76
+ };
77
+ private watchDirectory;
78
+ private watchFile;
79
+ private determineEventType;
80
+ private startWatching;
81
+ private stopWatching;
82
+ private loadRules;
83
+ private saveRules;
84
+ }
85
+ /**
86
+ * Common watch patterns for development workflows
87
+ */
88
+ export declare const COMMON_PATTERNS: {
89
+ typescript: {
90
+ pattern: string;
91
+ events: FileEventType[];
92
+ action: string;
93
+ };
94
+ tests: {
95
+ pattern: string;
96
+ events: FileEventType[];
97
+ action: string;
98
+ };
99
+ config: {
100
+ pattern: string;
101
+ events: FileEventType[];
102
+ action: string;
103
+ };
104
+ markdown: {
105
+ pattern: string;
106
+ events: FileEventType[];
107
+ action: string;
108
+ };
109
+ };
@@ -0,0 +1,275 @@
1
+ /**
2
+ * File Watcher for Reactive Workflows
3
+ *
4
+ * Enables the orchestrator to:
5
+ * - Watch directories/files for changes
6
+ * - Trigger callbacks on file events
7
+ * - Support glob patterns for filtering
8
+ * - Debounce rapid changes
9
+ */
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import { EventEmitter } from 'events';
13
+ export class FileWatcher extends EventEmitter {
14
+ watchers = new Map();
15
+ rules = new Map();
16
+ debounceTimers = new Map();
17
+ config;
18
+ rulesFile;
19
+ constructor(workspaceDir, config) {
20
+ super();
21
+ this.rulesFile = config?.rulesFile || path.join(workspaceDir, '.watch-rules.json');
22
+ this.config = {
23
+ rulesFile: this.rulesFile,
24
+ defaultDebounce: config?.defaultDebounce || 500
25
+ };
26
+ this.loadRules();
27
+ }
28
+ /**
29
+ * Add a watch rule
30
+ */
31
+ addRule(rule) {
32
+ const id = `watch_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
33
+ const fullRule = {
34
+ ...rule,
35
+ id,
36
+ enabled: true,
37
+ debounceMs: rule.debounceMs || this.config.defaultDebounce
38
+ };
39
+ this.rules.set(id, fullRule);
40
+ this.saveRules();
41
+ this.startWatching(fullRule);
42
+ return id;
43
+ }
44
+ /**
45
+ * Remove a watch rule
46
+ */
47
+ removeRule(id) {
48
+ const rule = this.rules.get(id);
49
+ if (!rule)
50
+ return false;
51
+ this.stopWatching(rule);
52
+ this.rules.delete(id);
53
+ this.saveRules();
54
+ return true;
55
+ }
56
+ /**
57
+ * Enable/disable a rule
58
+ */
59
+ toggleRule(id, enabled) {
60
+ const rule = this.rules.get(id);
61
+ if (!rule)
62
+ return false;
63
+ rule.enabled = enabled;
64
+ if (enabled) {
65
+ this.startWatching(rule);
66
+ }
67
+ else {
68
+ this.stopWatching(rule);
69
+ }
70
+ this.saveRules();
71
+ return true;
72
+ }
73
+ /**
74
+ * List all rules
75
+ */
76
+ listRules() {
77
+ return Array.from(this.rules.values());
78
+ }
79
+ /**
80
+ * Get a specific rule
81
+ */
82
+ getRule(id) {
83
+ return this.rules.get(id);
84
+ }
85
+ /**
86
+ * Watch a specific path
87
+ */
88
+ watch(targetPath, callback) {
89
+ const id = `direct_${Date.now()}`;
90
+ try {
91
+ const resolvedPath = path.resolve(targetPath);
92
+ const stat = fs.statSync(resolvedPath);
93
+ if (stat.isDirectory()) {
94
+ this.watchDirectory(resolvedPath, callback, id);
95
+ }
96
+ else {
97
+ this.watchFile(resolvedPath, callback, id);
98
+ }
99
+ return id;
100
+ }
101
+ catch (error) {
102
+ console.error(`[WATCHER] Failed to watch ${targetPath}:`, error);
103
+ throw error;
104
+ }
105
+ }
106
+ /**
107
+ * Stop watching a specific watcher
108
+ */
109
+ unwatch(watcherId) {
110
+ const watcher = this.watchers.get(watcherId);
111
+ if (watcher) {
112
+ watcher.close();
113
+ this.watchers.delete(watcherId);
114
+ }
115
+ }
116
+ /**
117
+ * Stop all watchers
118
+ */
119
+ stopAll() {
120
+ for (const [id, watcher] of this.watchers) {
121
+ watcher.close();
122
+ }
123
+ this.watchers.clear();
124
+ for (const timer of this.debounceTimers.values()) {
125
+ clearTimeout(timer);
126
+ }
127
+ this.debounceTimers.clear();
128
+ }
129
+ /**
130
+ * Get watcher status
131
+ */
132
+ status() {
133
+ return {
134
+ activeWatchers: this.watchers.size,
135
+ rules: this.rules.size,
136
+ enabledRules: Array.from(this.rules.values()).filter(r => r.enabled).length
137
+ };
138
+ }
139
+ watchDirectory(dirPath, callback, id) {
140
+ const watcher = fs.watch(dirPath, { recursive: true }, (eventType, filename) => {
141
+ if (!filename)
142
+ return;
143
+ const fullPath = path.join(dirPath, filename);
144
+ const event = {
145
+ type: eventType === 'rename' ? this.determineEventType(fullPath) : 'change',
146
+ path: fullPath,
147
+ timestamp: new Date().toISOString()
148
+ };
149
+ callback(event);
150
+ });
151
+ this.watchers.set(id, watcher);
152
+ }
153
+ watchFile(filePath, callback, id) {
154
+ const watcher = fs.watch(filePath, (eventType) => {
155
+ const event = {
156
+ type: eventType === 'rename' ? 'unlink' : 'change',
157
+ path: filePath,
158
+ timestamp: new Date().toISOString()
159
+ };
160
+ callback(event);
161
+ });
162
+ this.watchers.set(id, watcher);
163
+ }
164
+ determineEventType(filePath) {
165
+ try {
166
+ const stat = fs.statSync(filePath);
167
+ return stat.isDirectory() ? 'addDir' : 'add';
168
+ }
169
+ catch {
170
+ return 'unlink';
171
+ }
172
+ }
173
+ startWatching(rule) {
174
+ if (!rule.enabled)
175
+ return;
176
+ try {
177
+ const targetPath = path.resolve(rule.pattern);
178
+ // Check if path exists
179
+ if (!fs.existsSync(targetPath)) {
180
+ console.log(`[WATCHER] Path does not exist yet: ${targetPath}`);
181
+ return;
182
+ }
183
+ const callback = (event) => {
184
+ if (!rule.events.includes(event.type))
185
+ return;
186
+ // Debounce
187
+ const debounceKey = `${rule.id}_${event.path}`;
188
+ const existing = this.debounceTimers.get(debounceKey);
189
+ if (existing) {
190
+ clearTimeout(existing);
191
+ }
192
+ const timer = setTimeout(() => {
193
+ this.debounceTimers.delete(debounceKey);
194
+ rule.lastTriggered = new Date().toISOString();
195
+ this.saveRules();
196
+ // Emit event for orchestrator to handle
197
+ this.emit('trigger', {
198
+ rule,
199
+ event,
200
+ action: rule.action
201
+ });
202
+ }, rule.debounceMs || this.config.defaultDebounce);
203
+ this.debounceTimers.set(debounceKey, timer);
204
+ };
205
+ this.watch(targetPath, callback);
206
+ console.log(`[WATCHER] Started watching: ${rule.pattern}`);
207
+ }
208
+ catch (error) {
209
+ console.error(`[WATCHER] Failed to start watching ${rule.pattern}:`, error);
210
+ }
211
+ }
212
+ stopWatching(rule) {
213
+ // Find and close watchers for this rule
214
+ for (const [id, watcher] of this.watchers) {
215
+ if (id.includes(rule.id)) {
216
+ watcher.close();
217
+ this.watchers.delete(id);
218
+ }
219
+ }
220
+ }
221
+ loadRules() {
222
+ try {
223
+ if (fs.existsSync(this.rulesFile)) {
224
+ const data = JSON.parse(fs.readFileSync(this.rulesFile, 'utf8'));
225
+ for (const rule of data.rules || []) {
226
+ this.rules.set(rule.id, rule);
227
+ if (rule.enabled) {
228
+ this.startWatching(rule);
229
+ }
230
+ }
231
+ console.log(`[WATCHER] Loaded ${this.rules.size} watch rules`);
232
+ }
233
+ }
234
+ catch (error) {
235
+ console.error('[WATCHER] Failed to load rules:', error);
236
+ }
237
+ }
238
+ saveRules() {
239
+ try {
240
+ const data = {
241
+ rules: Array.from(this.rules.values()),
242
+ savedAt: new Date().toISOString()
243
+ };
244
+ fs.writeFileSync(this.rulesFile, JSON.stringify(data, null, 2));
245
+ }
246
+ catch (error) {
247
+ console.error('[WATCHER] Failed to save rules:', error);
248
+ }
249
+ }
250
+ }
251
+ /**
252
+ * Common watch patterns for development workflows
253
+ */
254
+ export const COMMON_PATTERNS = {
255
+ typescript: {
256
+ pattern: '**/*.ts',
257
+ events: ['change', 'add'],
258
+ action: 'TypeScript file changed - run type check and tests'
259
+ },
260
+ tests: {
261
+ pattern: '**/*.test.ts',
262
+ events: ['change', 'add'],
263
+ action: 'Test file changed - run tests'
264
+ },
265
+ config: {
266
+ pattern: '**/package.json',
267
+ events: ['change'],
268
+ action: 'Package.json changed - check for dependency updates'
269
+ },
270
+ markdown: {
271
+ pattern: '**/*.md',
272
+ events: ['change', 'add'],
273
+ action: 'Documentation changed - validate links and formatting'
274
+ }
275
+ };
package/dist/heartbeat.js CHANGED
@@ -10,7 +10,7 @@ import { hostname } from 'os';
10
10
  import { createHash } from 'crypto';
11
11
  import { getServerUrl, getAgentToken, getUserId, isCloudMode } from './config.js';
12
12
  const HEARTBEAT_INTERVAL = 10000; // 10 seconds
13
- const VERSION = '0.2.8'; // Should match package.json
13
+ const VERSION = '0.2.10'; // Should match package.json
14
14
  const state = {
15
15
  intervalId: null,
16
16
  runnerId: null,
@@ -22,6 +22,8 @@ export declare class MasterOrchestrator {
22
22
  private bashTool;
23
23
  private webTools;
24
24
  private workerTools;
25
+ private sharedState;
26
+ private fileWatcher;
25
27
  private verboseMode;
26
28
  constructor(options: {
27
29
  apiKey: string;
@@ -78,8 +80,12 @@ export declare class MasterOrchestrator {
78
80
  */
79
81
  private delegateToWorker;
80
82
  /**
81
- * Extract memory contributions from worker output
82
- * Workers can contribute learnings using: [MEMORY] type=X | content=Y
83
+ * Extract memory and coordination contributions from worker output
84
+ * Workers can contribute using:
85
+ * - [MEMORY] type=X | content=Y
86
+ * - [SHARE] key=X | value=Y
87
+ * - [SIGNAL] name=X | data=Y
88
+ * - [MESSAGE] to=X | content=Y
83
89
  */
84
90
  private extractWorkerMemories;
85
91
  /**
@@ -114,6 +120,26 @@ export declare class MasterOrchestrator {
114
120
  * Stop a running server on a port
115
121
  */
116
122
  private executeStopServer;
123
+ /**
124
+ * Add a file watch rule
125
+ */
126
+ private executeAddWatchRule;
127
+ /**
128
+ * Remove a file watch rule
129
+ */
130
+ private executeRemoveWatchRule;
131
+ /**
132
+ * List all file watch rules
133
+ */
134
+ private executeListWatchRules;
135
+ /**
136
+ * Toggle a file watch rule
137
+ */
138
+ private executeToggleWatchRule;
139
+ /**
140
+ * Get file watcher status
141
+ */
142
+ private executeWatchStatus;
117
143
  /**
118
144
  * Format tool preview for user
119
145
  */
@@ -10,9 +10,11 @@ import { existsSync } from 'fs';
10
10
  import { AdvancedMemoryStore } from './core/memory-advanced.js';
11
11
  import { TaskScheduler } from './core/scheduler.js';
12
12
  import { SystemIndexer } from './core/system-indexer.js';
13
+ import { FileWatcher } from './core/file-watcher.js';
13
14
  import { BashTool } from './tools/bash.js';
14
15
  import { WebTools } from './tools/web.js';
15
16
  import { WorkerTools } from './tools/worker.js';
17
+ import { SharedState } from './workers/shared-state.js';
16
18
  import { getKnowledgeForPrompt } from './genesis/index.js';
17
19
  const SYSTEM_PROMPT = `You're the user's personal assistant - the friendly brain behind their Connect app. You chat with them from any browser, remember everything about them, and dispatch Claude Code workers to do things on their machine.
18
20
 
@@ -85,6 +87,8 @@ export class MasterOrchestrator {
85
87
  bashTool;
86
88
  webTools;
87
89
  workerTools;
90
+ sharedState;
91
+ fileWatcher;
88
92
  verboseMode = new Map(); // per-user verbose mode
89
93
  constructor(options) {
90
94
  this.client = new Anthropic({ apiKey: options.apiKey });
@@ -108,6 +112,15 @@ export class MasterOrchestrator {
108
112
  this.bashTool = new BashTool(this.workspaceDir);
109
113
  this.webTools = new WebTools();
110
114
  this.workerTools = new WorkerTools(this.workspaceDir);
115
+ this.sharedState = new SharedState(this.workspaceDir);
116
+ this.fileWatcher = new FileWatcher(this.workspaceDir);
117
+ // Set up file watcher trigger handler
118
+ this.fileWatcher.on('trigger', async ({ rule, event, action }) => {
119
+ console.log(`[WATCHER] Triggered: ${rule.pattern} (${event.type}: ${event.path})`);
120
+ // Auto-delegate to worker when file changes
121
+ const task = `${action}\n\nTriggered by file change:\n- File: ${event.path}\n- Event: ${event.type}\n- Rule: ${rule.pattern}`;
122
+ this.delegateToWorker(task, undefined, this.workspaceDir);
123
+ });
111
124
  }
112
125
  /**
113
126
  * Initialize the orchestrator - indexes filesystem on first call
@@ -590,6 +603,84 @@ Be specific about what you want done.`,
590
603
  },
591
604
  required: ['port']
592
605
  }
606
+ },
607
+ // File watching tools for reactive workflows
608
+ {
609
+ name: 'add_watch_rule',
610
+ description: 'Add a file watch rule to automatically trigger actions when files change. Useful for reactive workflows like auto-running tests when code changes.',
611
+ input_schema: {
612
+ type: 'object',
613
+ properties: {
614
+ pattern: {
615
+ type: 'string',
616
+ description: 'File path or directory to watch (e.g., "./src", "/path/to/file.ts")'
617
+ },
618
+ events: {
619
+ type: 'array',
620
+ items: { type: 'string', enum: ['add', 'change', 'unlink', 'addDir', 'unlinkDir'] },
621
+ description: 'Events to watch for (default: ["change", "add"])'
622
+ },
623
+ action: {
624
+ type: 'string',
625
+ description: 'Task description to execute when triggered (sent to a worker)'
626
+ },
627
+ debounce_ms: {
628
+ type: 'number',
629
+ description: 'Debounce delay in milliseconds (default: 500)'
630
+ }
631
+ },
632
+ required: ['pattern', 'action']
633
+ }
634
+ },
635
+ {
636
+ name: 'remove_watch_rule',
637
+ description: 'Remove a file watch rule by its ID.',
638
+ input_schema: {
639
+ type: 'object',
640
+ properties: {
641
+ rule_id: {
642
+ type: 'string',
643
+ description: 'The ID of the watch rule to remove'
644
+ }
645
+ },
646
+ required: ['rule_id']
647
+ }
648
+ },
649
+ {
650
+ name: 'list_watch_rules',
651
+ description: 'List all active file watch rules.',
652
+ input_schema: {
653
+ type: 'object',
654
+ properties: {},
655
+ required: []
656
+ }
657
+ },
658
+ {
659
+ name: 'toggle_watch_rule',
660
+ description: 'Enable or disable a file watch rule.',
661
+ input_schema: {
662
+ type: 'object',
663
+ properties: {
664
+ rule_id: {
665
+ type: 'string',
666
+ description: 'The ID of the watch rule to toggle'
667
+ },
668
+ enabled: {
669
+ type: 'boolean',
670
+ description: 'Whether to enable (true) or disable (false) the rule'
671
+ }
672
+ },
673
+ required: ['rule_id', 'enabled']
674
+ }
675
+ },
676
+ {
677
+ name: 'watch_status',
678
+ description: 'Get the status of the file watcher system.',
679
+ input_schema: {
680
+ type: 'object',
681
+ properties: {},
682
+ required: []
683
+ }
593
684
  }
594
685
  ];
595
686
  }
@@ -669,6 +760,22 @@ Be specific about what you want done.`,
669
760
  case 'stop_local_server':
670
761
  result = await this.executeStopServer(input.port);
671
762
  break;
763
+ // File watching tools
764
+ case 'add_watch_rule':
765
+ result = await this.executeAddWatchRule(input.pattern, input.action, input.events, input.debounce_ms);
766
+ break;
767
+ case 'remove_watch_rule':
768
+ result = await this.executeRemoveWatchRule(input.rule_id);
769
+ break;
770
+ case 'list_watch_rules':
771
+ result = await this.executeListWatchRules();
772
+ break;
773
+ case 'toggle_watch_rule':
774
+ result = await this.executeToggleWatchRule(input.rule_id, input.enabled);
775
+ break;
776
+ case 'watch_status':
777
+ result = await this.executeWatchStatus();
778
+ break;
672
779
  default:
673
780
  result = { success: false, output: `Unknown tool: ${toolUse.name}` };
674
781
  }
@@ -703,6 +810,8 @@ Be specific about what you want done.`,
703
810
  // Memory search failed, continue without it
704
811
  console.log('[ORCHESTRATOR] Memory search failed, continuing without context');
705
812
  }
813
+ // Get shared state context for worker coordination
814
+ const sharedContext = this.sharedState.getSummaryForWorker(id);
706
815
  // Build prompt for worker with memory context and checkpoint instructions
707
816
  let prompt = task;
708
817
  if (context) {
@@ -711,6 +820,9 @@ Be specific about what you want done.`,
711
820
  if (memoryContext) {
712
821
  prompt += memoryContext;
713
822
  }
823
+ if (sharedContext) {
824
+ prompt += `\n\nWORKER COORDINATION:${sharedContext}`;
825
+ }
714
826
  // Add checkpoint and logging instructions to prevent data loss
715
827
  const logFile = `/tmp/worker-${id}-log.txt`;
716
828
  prompt += `
@@ -730,7 +842,14 @@ If you discover something important (patterns, user preferences, technical insig
730
842
  end your response with a line like:
731
843
  [MEMORY] type=semantic | content=User prefers X over Y for this type of task
732
844
  [MEMORY] type=procedural | content=When doing X, always check Y first
733
- This helps me remember and improve for future tasks.`;
845
+ This helps me remember and improve for future tasks.
846
+
847
+ WORKER COORDINATION (optional):
848
+ If you need to share data with other workers or signal completion:
849
+ [SHARE] key=myData | value={"result": "something useful"}
850
+ [SIGNAL] name=step1_complete | data={"files": ["a.ts", "b.ts"]}
851
+ [MESSAGE] to=worker_xyz | content=Please review the changes I made
852
+ This enables parallel workers to coordinate.`;
734
853
  console.log(`[ORCHESTRATOR] Delegating to worker ${id}: ${task.slice(0, 80)}...`);
735
854
  return new Promise((resolve) => {
736
855
  const job = {
@@ -822,37 +941,76 @@ This helps me remember and improve for future tasks.`;
822
941
  });
823
942
  }
824
943
  /**
825
- * Extract memory contributions from worker output
826
- * Workers can contribute learnings using: [MEMORY] type=X | content=Y
944
+ * Extract memory and coordination contributions from worker output
945
+ * Workers can contribute using:
946
+ * - [MEMORY] type=X | content=Y
947
+ * - [SHARE] key=X | value=Y
948
+ * - [SIGNAL] name=X | data=Y
949
+ * - [MESSAGE] to=X | content=Y
827
950
  */
828
951
  async extractWorkerMemories(output, workerId) {
829
- // Match lines like: [MEMORY] type=semantic | content=User prefers X
952
+ let memoryCount = 0;
953
+ let coordCount = 0;
954
+ // Extract memory contributions
830
955
  const memoryPattern = /\[MEMORY\]\s*type=(\w+)\s*\|\s*content=(.+?)(?=\n|$)/g;
831
956
  let match;
832
- let count = 0;
833
957
  while ((match = memoryPattern.exec(output)) !== null) {
834
958
  const type = match[1].toLowerCase();
835
959
  const content = match[2].trim();
836
960
  if (!content)
837
961
  continue;
838
- // Validate memory type
839
962
  const validTypes = ['episodic', 'semantic', 'procedural'];
840
963
  const memType = validTypes.includes(type) ? type : 'semantic';
841
964
  try {
842
965
  await this.memory.remember(content, {
843
966
  type: memType,
844
967
  source: `worker:${workerId}`,
845
- importance: 0.7, // Worker-contributed memories are valuable
968
+ importance: 0.7,
846
969
  tags: ['worker-contributed', 'auto-learned']
847
970
  });
848
- count++;
971
+ memoryCount++;
849
972
  }
850
973
  catch (error) {
851
974
  console.log(`[ORCHESTRATOR] Failed to store worker memory: ${error}`);
852
975
  }
853
976
  }
854
- if (count > 0) {
855
- console.log(`[ORCHESTRATOR] Extracted ${count} memory contributions from worker ${workerId}`);
977
+ // Extract shared state contributions
978
+ const sharePattern = /\[SHARE\]\s*key=(\w+)\s*\|\s*value=(.+?)(?=\n|$)/g;
979
+ while ((match = sharePattern.exec(output)) !== null) {
980
+ const key = match[1];
981
+ let value = match[2].trim();
982
+ try {
983
+ value = JSON.parse(value);
984
+ }
985
+ catch { /* use as string */ }
986
+ this.sharedState.set(key, value, workerId);
987
+ coordCount++;
988
+ }
989
+ // Extract signal completions
990
+ const signalPattern = /\[SIGNAL\]\s*name=(\w+)(?:\s*\|\s*data=(.+?))?(?=\n|$)/g;
991
+ while ((match = signalPattern.exec(output)) !== null) {
992
+ const name = match[1];
993
+ let data = match[2]?.trim();
994
+ try {
995
+ data = data ? JSON.parse(data) : undefined;
996
+ }
997
+ catch { /* use as string */ }
998
+ this.sharedState.signalComplete(name, workerId, data);
999
+ coordCount++;
1000
+ }
1001
+ // Extract messages
1002
+ const messagePattern = /\[MESSAGE\]\s*to=(\w+)\s*\|\s*content=(.+?)(?=\n|$)/g;
1003
+ while ((match = messagePattern.exec(output)) !== null) {
1004
+ const to = match[1];
1005
+ const content = match[2].trim();
1006
+ this.sharedState.sendMessage(workerId, to, 'data', content);
1007
+ coordCount++;
1008
+ }
1009
+ if (memoryCount > 0) {
1010
+ console.log(`[ORCHESTRATOR] Extracted ${memoryCount} memory contributions from worker ${workerId}`);
1011
+ }
1012
+ if (coordCount > 0) {
1013
+ console.log(`[ORCHESTRATOR] Extracted ${coordCount} coordination messages from worker ${workerId}`);
856
1014
  }
857
1015
  }
858
1016
  /**
@@ -1014,6 +1172,99 @@ This helps me remember and improve for future tasks.`;
1014
1172
  return { success: false, output: `No server running on port ${port}` };
1015
1173
  }
1016
1174
  }
1175
+ /**
1176
+ * Add a file watch rule
1177
+ */
1178
+ async executeAddWatchRule(pattern, action, events, debounceMs) {
1179
+ try {
1180
+ const validEvents = ['add', 'change', 'unlink', 'addDir', 'unlinkDir'];
1181
+ const eventList = events
1182
+ ? events.filter((e) => validEvents.includes(e))
1183
+ : ['change', 'add'];
1184
+ const ruleId = this.fileWatcher.addRule({
1185
+ pattern,
1186
+ events: eventList,
1187
+ action,
1188
+ debounceMs
1189
+ });
1190
+ console.log(`[ORCHESTRATOR] Added watch rule ${ruleId}: ${pattern}`);
1191
+ return {
1192
+ success: true,
1193
+ output: `Watch rule added:\n- ID: ${ruleId}\n- Pattern: ${pattern}\n- Events: ${eventList.join(', ')}\n- Action: ${action}\n- Debounce: ${debounceMs || 500}ms`
1194
+ };
1195
+ }
1196
+ catch (error) {
1197
+ const msg = error instanceof Error ? error.message : String(error);
1198
+ return { success: false, output: `Failed to add watch rule: ${msg}` };
1199
+ }
1200
+ }
1201
+ /**
1202
+ * Remove a file watch rule
1203
+ */
1204
+ async executeRemoveWatchRule(ruleId) {
1205
+ try {
1206
+ const removed = this.fileWatcher.removeRule(ruleId);
1207
+ if (removed) {
1208
+ console.log(`[ORCHESTRATOR] Removed watch rule ${ruleId}`);
1209
+ return { success: true, output: `Watch rule ${ruleId} removed` };
1210
+ }
1211
+ return { success: false, output: `Watch rule ${ruleId} not found` };
1212
+ }
1213
+ catch (error) {
1214
+ const msg = error instanceof Error ? error.message : String(error);
1215
+ return { success: false, output: `Failed to remove watch rule: ${msg}` };
1216
+ }
1217
+ }
1218
+ /**
1219
+ * List all file watch rules
1220
+ */
1221
+ async executeListWatchRules() {
1222
+ try {
1223
+ const rules = this.fileWatcher.listRules();
1224
+ if (rules.length === 0) {
1225
+ return { success: true, output: 'No watch rules configured.' };
1226
+ }
1227
+ const output = rules.map(r => `${r.enabled ? '✓' : '⏸'} ${r.id}\n Pattern: ${r.pattern}\n Events: ${r.events.join(', ')}\n Action: ${r.action.slice(0, 60)}...${r.lastTriggered ? `\n Last triggered: ${r.lastTriggered}` : ''}`).join('\n\n');
1228
+ return { success: true, output: `Watch Rules (${rules.length}):\n\n${output}` };
1229
+ }
1230
+ catch (error) {
1231
+ const msg = error instanceof Error ? error.message : String(error);
1232
+ return { success: false, output: `Failed to list watch rules: ${msg}` };
1233
+ }
1234
+ }
1235
+ /**
1236
+ * Toggle a file watch rule
1237
+ */
1238
+ async executeToggleWatchRule(ruleId, enabled) {
1239
+ try {
1240
+ const toggled = this.fileWatcher.toggleRule(ruleId, enabled);
1241
+ if (toggled) {
1242
+ console.log(`[ORCHESTRATOR] ${enabled ? 'Enabled' : 'Disabled'} watch rule ${ruleId}`);
1243
+ return { success: true, output: `Watch rule ${ruleId} ${enabled ? 'enabled' : 'disabled'}` };
1244
+ }
1245
+ return { success: false, output: `Watch rule ${ruleId} not found` };
1246
+ }
1247
+ catch (error) {
1248
+ const msg = error instanceof Error ? error.message : String(error);
1249
+ return { success: false, output: `Failed to toggle watch rule: ${msg}` };
1250
+ }
1251
+ }
1252
+ /**
1253
+ * Get file watcher status
1254
+ */
1255
+ async executeWatchStatus() {
1256
+ try {
1257
+ const status = this.fileWatcher.status();
1258
+ return {
1259
+ success: true,
1260
+ output: `File Watcher Status:\n- Active watchers: ${status.activeWatchers}\n- Total rules: ${status.rules}\n- Enabled rules: ${status.enabledRules}`
1261
+ };
1262
+ }
1263
+ catch (error) {
1264
+ const msg = error instanceof Error ? error.message : String(error);
1265
+ return { success: false, output: `Failed to get watcher status: ${msg}` };
1266
+ }
1267
+ }
1017
1268
  /**
1018
1269
  * Format tool preview for user
1019
1270
  */
@@ -1057,6 +1308,16 @@ This helps me remember and improve for future tasks.`;
1057
1308
  return `Starting server on port ${input.port || 8080}...`;
1058
1309
  case 'stop_local_server':
1059
1310
  return `Stopping server on port ${input.port}...`;
1311
+ case 'add_watch_rule':
1312
+ return `Adding watch rule for ${input.pattern}...`;
1313
+ case 'remove_watch_rule':
1314
+ return `Removing watch rule ${input.rule_id}...`;
1315
+ case 'list_watch_rules':
1316
+ return 'Listing watch rules...';
1317
+ case 'toggle_watch_rule':
1318
+ return `${input.enabled ? 'Enabling' : 'Disabling'} watch rule ${input.rule_id}...`;
1319
+ case 'watch_status':
1320
+ return 'Getting watcher status...';
1060
1321
  default:
1061
1322
  return null;
1062
1323
  }
@@ -1077,6 +1338,7 @@ This helps me remember and improve for future tasks.`;
1077
1338
  * Shutdown cleanly
1078
1339
  */
1079
1340
  shutdown() {
1341
+ this.fileWatcher.stopAll();
1080
1342
  this.scheduler.shutdown();
1081
1343
  this.memory.close();
1082
1344
  }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Shared State for Worker Communication
3
+ *
4
+ * Enables workers to:
5
+ * - Share data via a common state store
6
+ * - Signal completion of dependencies
7
+ * - Lock resources to prevent conflicts
8
+ * - Pass messages between workers
9
+ */
10
+ export interface SharedMessage {
11
+ from: string;
12
+ to: string | '*';
13
+ type: 'data' | 'signal' | 'request';
14
+ content: string;
15
+ timestamp: string;
16
+ }
17
+ export interface ResourceLock {
18
+ resource: string;
19
+ owner: string;
20
+ acquired: string;
21
+ expires: string;
22
+ }
23
+ export declare class SharedState {
24
+ private stateDir;
25
+ private stateFile;
26
+ private messagesFile;
27
+ private locksFile;
28
+ constructor(workspaceDir: string);
29
+ private ensureStateDir;
30
+ /**
31
+ * Set a shared value
32
+ */
33
+ set(key: string, value: unknown, workerId?: string): void;
34
+ /**
35
+ * Get a shared value
36
+ */
37
+ get(key: string): unknown;
38
+ /**
39
+ * Get all shared state
40
+ */
41
+ getAll(): Record<string, unknown>;
42
+ /**
43
+ * Signal that a worker has completed a specific task/dependency
44
+ */
45
+ signalComplete(signalName: string, workerId: string, data?: unknown): void;
46
+ /**
47
+ * Wait for a signal (polling-based)
48
+ * Returns true if signal is already set, false if not
49
+ */
50
+ checkSignal(signalName: string): {
51
+ completed: boolean;
52
+ data?: unknown;
53
+ };
54
+ /**
55
+ * Send a message to another worker
56
+ */
57
+ sendMessage(from: string, to: string, type: 'data' | 'signal' | 'request', content: string): void;
58
+ /**
59
+ * Get messages for a worker
60
+ */
61
+ getMessages(workerId: string, since?: string): SharedMessage[];
62
+ /**
63
+ * Try to acquire a resource lock
64
+ */
65
+ acquireLock(resource: string, workerId: string, durationMs?: number): boolean;
66
+ /**
67
+ * Release a resource lock
68
+ */
69
+ releaseLock(resource: string, workerId: string): boolean;
70
+ /**
71
+ * Clean up expired locks
72
+ */
73
+ cleanupExpiredLocks(): void;
74
+ /**
75
+ * Get summary for injecting into worker prompts
76
+ */
77
+ getSummaryForWorker(workerId: string): string;
78
+ /**
79
+ * Clear all shared state (for cleanup between sessions)
80
+ */
81
+ clear(): void;
82
+ private loadState;
83
+ private saveState;
84
+ private loadSignals;
85
+ private saveSignals;
86
+ private loadMessages;
87
+ private saveMessages;
88
+ private loadLocks;
89
+ private saveLocks;
90
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Shared State for Worker Communication
3
+ *
4
+ * Enables workers to:
5
+ * - Share data via a common state store
6
+ * - Signal completion of dependencies
7
+ * - Lock resources to prevent conflicts
8
+ * - Pass messages between workers
9
+ */
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ export class SharedState {
13
+ stateDir;
14
+ stateFile;
15
+ messagesFile;
16
+ locksFile;
17
+ constructor(workspaceDir) {
18
+ this.stateDir = path.join(workspaceDir, '.worker-state');
19
+ this.stateFile = path.join(this.stateDir, 'state.json');
20
+ this.messagesFile = path.join(this.stateDir, 'messages.json');
21
+ this.locksFile = path.join(this.stateDir, 'locks.json');
22
+ this.ensureStateDir();
23
+ }
24
+ ensureStateDir() {
25
+ if (!fs.existsSync(this.stateDir)) {
26
+ fs.mkdirSync(this.stateDir, { recursive: true });
27
+ }
28
+ }
29
+ /**
30
+ * Set a shared value
31
+ */
32
+ set(key, value, workerId) {
33
+ const state = this.loadState();
34
+ state[key] = {
35
+ value,
36
+ setBy: workerId || 'unknown',
37
+ timestamp: new Date().toISOString()
38
+ };
39
+ this.saveState(state);
40
+ }
41
+ /**
42
+ * Get a shared value
43
+ */
44
+ get(key) {
45
+ const state = this.loadState();
46
+ return state[key]?.value;
47
+ }
48
+ /**
49
+ * Get all shared state
50
+ */
51
+ getAll() {
52
+ const state = this.loadState();
53
+ const result = {};
54
+ for (const [key, entry] of Object.entries(state)) {
55
+ result[key] = entry.value;
56
+ }
57
+ return result;
58
+ }
59
+ /**
60
+ * Signal that a worker has completed a specific task/dependency
61
+ */
62
+ signalComplete(signalName, workerId, data) {
63
+ const signals = this.loadSignals();
64
+ signals[signalName] = {
65
+ completed: true,
66
+ completedBy: workerId,
67
+ timestamp: new Date().toISOString(),
68
+ data
69
+ };
70
+ this.saveSignals(signals);
71
+ }
72
+ /**
73
+ * Wait for a signal (polling-based)
74
+ * Returns true if signal is already set, false if not
75
+ */
76
+ checkSignal(signalName) {
77
+ const signals = this.loadSignals();
78
+ const signal = signals[signalName];
79
+ if (signal?.completed) {
80
+ return { completed: true, data: signal.data };
81
+ }
82
+ return { completed: false };
83
+ }
84
+ /**
85
+ * Send a message to another worker
86
+ */
87
+ sendMessage(from, to, type, content) {
88
+ const messages = this.loadMessages();
89
+ messages.push({
90
+ from,
91
+ to,
92
+ type,
93
+ content,
94
+ timestamp: new Date().toISOString()
95
+ });
96
+ // Keep last 100 messages
97
+ if (messages.length > 100) {
98
+ messages.splice(0, messages.length - 100);
99
+ }
100
+ this.saveMessages(messages);
101
+ }
102
+ /**
103
+ * Get messages for a worker
104
+ */
105
+ getMessages(workerId, since) {
106
+ const messages = this.loadMessages();
107
+ return messages.filter(m => {
108
+ const isForMe = m.to === workerId || m.to === '*';
109
+ const isAfterSince = !since || m.timestamp > since;
110
+ return isForMe && isAfterSince;
111
+ });
112
+ }
113
+ /**
114
+ * Try to acquire a resource lock
115
+ */
116
+ acquireLock(resource, workerId, durationMs = 60000) {
117
+ const locks = this.loadLocks();
118
+ const now = new Date();
119
+ const existing = locks[resource];
120
+ // Check if existing lock is still valid
121
+ if (existing && new Date(existing.expires) > now && existing.owner !== workerId) {
122
+ return false; // Lock held by another worker
123
+ }
124
+ // Acquire lock
125
+ locks[resource] = {
126
+ resource,
127
+ owner: workerId,
128
+ acquired: now.toISOString(),
129
+ expires: new Date(now.getTime() + durationMs).toISOString()
130
+ };
131
+ this.saveLocks(locks);
132
+ return true;
133
+ }
134
+ /**
135
+ * Release a resource lock
136
+ */
137
+ releaseLock(resource, workerId) {
138
+ const locks = this.loadLocks();
139
+ if (locks[resource]?.owner === workerId) {
140
+ delete locks[resource];
141
+ this.saveLocks(locks);
142
+ return true;
143
+ }
144
+ return false;
145
+ }
146
+ /**
147
+ * Clean up expired locks
148
+ */
149
+ cleanupExpiredLocks() {
150
+ const locks = this.loadLocks();
151
+ const now = new Date();
152
+ let changed = false;
153
+ for (const [resource, lock] of Object.entries(locks)) {
154
+ if (new Date(lock.expires) < now) {
155
+ delete locks[resource];
156
+ changed = true;
157
+ }
158
+ }
159
+ if (changed) {
160
+ this.saveLocks(locks);
161
+ }
162
+ }
163
+ /**
164
+ * Get summary for injecting into worker prompts
165
+ */
166
+ getSummaryForWorker(workerId) {
167
+ const state = this.getAll();
168
+ const messages = this.getMessages(workerId);
169
+ const signals = this.loadSignals();
170
+ let summary = '';
171
+ if (Object.keys(state).length > 0) {
172
+ summary += '\n## Shared State:\n';
173
+ for (const [key, value] of Object.entries(state)) {
174
+ summary += `- ${key}: ${JSON.stringify(value)}\n`;
175
+ }
176
+ }
177
+ if (messages.length > 0) {
178
+ summary += '\n## Messages for you:\n';
179
+ for (const msg of messages.slice(-5)) { // Last 5 messages
180
+ summary += `- [${msg.from}] ${msg.content}\n`;
181
+ }
182
+ }
183
+ const completedSignals = Object.entries(signals).filter(([, s]) => s.completed);
184
+ if (completedSignals.length > 0) {
185
+ summary += '\n## Completed dependencies:\n';
186
+ for (const [name] of completedSignals) {
187
+ summary += `- ${name} ✓\n`;
188
+ }
189
+ }
190
+ return summary;
191
+ }
192
+ /**
193
+ * Clear all shared state (for cleanup between sessions)
194
+ */
195
+ clear() {
196
+ if (fs.existsSync(this.stateFile))
197
+ fs.unlinkSync(this.stateFile);
198
+ if (fs.existsSync(this.messagesFile))
199
+ fs.unlinkSync(this.messagesFile);
200
+ if (fs.existsSync(this.locksFile))
201
+ fs.unlinkSync(this.locksFile);
202
+ }
203
+ // Private helpers
204
+ loadState() {
205
+ try {
206
+ if (fs.existsSync(this.stateFile)) {
207
+ return JSON.parse(fs.readFileSync(this.stateFile, 'utf8'));
208
+ }
209
+ }
210
+ catch { /* ignore */ }
211
+ return {};
212
+ }
213
+ saveState(state) {
214
+ fs.writeFileSync(this.stateFile, JSON.stringify(state, null, 2));
215
+ }
216
+ loadSignals() {
217
+ try {
218
+ const signalsFile = path.join(this.stateDir, 'signals.json');
219
+ if (fs.existsSync(signalsFile)) {
220
+ return JSON.parse(fs.readFileSync(signalsFile, 'utf8'));
221
+ }
222
+ }
223
+ catch { /* ignore */ }
224
+ return {};
225
+ }
226
+ saveSignals(signals) {
227
+ const signalsFile = path.join(this.stateDir, 'signals.json');
228
+ fs.writeFileSync(signalsFile, JSON.stringify(signals, null, 2));
229
+ }
230
+ loadMessages() {
231
+ try {
232
+ if (fs.existsSync(this.messagesFile)) {
233
+ return JSON.parse(fs.readFileSync(this.messagesFile, 'utf8'));
234
+ }
235
+ }
236
+ catch { /* ignore */ }
237
+ return [];
238
+ }
239
+ saveMessages(messages) {
240
+ fs.writeFileSync(this.messagesFile, JSON.stringify(messages, null, 2));
241
+ }
242
+ loadLocks() {
243
+ try {
244
+ if (fs.existsSync(this.locksFile)) {
245
+ return JSON.parse(fs.readFileSync(this.locksFile, 'utf8'));
246
+ }
247
+ }
248
+ catch { /* ignore */ }
249
+ return {};
250
+ }
251
+ saveLocks(locks) {
252
+ fs.writeFileSync(this.locksFile, JSON.stringify(locks, null, 2));
253
+ }
254
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",