@siftd/connect-agent 0.2.0 → 0.2.2

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
@@ -1,7 +1,9 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { pollMessages, sendResponse } from './api.js';
3
- import { getUserId, getAnthropicApiKey } from './config.js';
3
+ import { getUserId, getAnthropicApiKey, isCloudMode, getDeploymentInfo } from './config.js';
4
4
  import { MasterOrchestrator } from './orchestrator.js';
5
+ import { AgentWebSocket } from './websocket.js';
6
+ import { startHeartbeat, stopHeartbeat, getHeartbeatState } from './heartbeat.js';
5
7
  // Strip ANSI escape codes for clean output
6
8
  function stripAnsi(str) {
7
9
  return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
@@ -9,6 +11,7 @@ function stripAnsi(str) {
9
11
  // Conversation history for orchestrator mode
10
12
  let conversationHistory = [];
11
13
  let orchestrator = null;
14
+ let wsClient = null;
12
15
  /**
13
16
  * Initialize the orchestrator if API key is available
14
17
  */
@@ -82,26 +85,48 @@ async function sendToClaudeSimple(input) {
82
85
  }
83
86
  /**
84
87
  * Orchestrator mode - uses the master orchestrator with memory
88
+ * Supports WebSocket streaming for real-time progress updates
89
+ * @param apiKey Optional per-request API key from user
85
90
  */
86
- async function sendToOrchestrator(input, orch) {
91
+ async function sendToOrchestrator(input, orch, messageId, apiKey) {
87
92
  console.log(`\n[ORCHESTRATOR] Processing: ${input.substring(0, 80)}...`);
93
+ // Send typing indicator via WebSocket
94
+ if (wsClient?.connected()) {
95
+ wsClient.sendTyping(true);
96
+ }
88
97
  try {
89
98
  const response = await orch.processMessage(input, conversationHistory, async (msg) => {
90
- // Progress callback - log to console
99
+ // Progress callback - log to console and send via WebSocket
91
100
  console.log(`[ORCHESTRATOR] ${msg}`);
92
- });
93
- // Update conversation history
94
- conversationHistory.push({ role: 'user', content: input });
95
- conversationHistory.push({ role: 'assistant', content: response });
96
- // Keep history manageable (last 20 exchanges)
97
- if (conversationHistory.length > 40) {
98
- conversationHistory = conversationHistory.slice(-40);
101
+ if (wsClient?.connected() && messageId) {
102
+ wsClient.sendProgress(messageId, msg);
103
+ }
104
+ }, apiKey // Pass per-request API key
105
+ );
106
+ // Update conversation history (only if response is non-empty)
107
+ if (response && response.trim()) {
108
+ conversationHistory.push({ role: 'user', content: input });
109
+ conversationHistory.push({ role: 'assistant', content: response });
110
+ // Keep history manageable (last 20 exchanges)
111
+ if (conversationHistory.length > 40) {
112
+ conversationHistory = conversationHistory.slice(-40);
113
+ }
114
+ }
115
+ else {
116
+ console.log('[ORCHESTRATOR] Empty response - not adding to conversation history');
99
117
  }
100
118
  console.log(`\n[ORCHESTRATOR] Response: ${response.substring(0, 80)}...`);
119
+ // Stop typing indicator
120
+ if (wsClient?.connected()) {
121
+ wsClient.sendTyping(false);
122
+ }
101
123
  return response;
102
124
  }
103
125
  catch (error) {
104
126
  console.error('[ORCHESTRATOR] Error:', error);
127
+ if (wsClient?.connected()) {
128
+ wsClient.sendTyping(false);
129
+ }
105
130
  return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
106
131
  }
107
132
  }
@@ -114,18 +139,22 @@ export async function processMessage(message) {
114
139
  return 'Conversation history cleared.';
115
140
  }
116
141
  if (content === '/mode') {
142
+ const wsStatus = wsClient?.connected() ? 'WebSocket' : 'Polling';
143
+ const hbState = getHeartbeatState();
144
+ const runnerInfo = hbState.runnerId ? ` [${hbState.runnerId}]` : '';
117
145
  return orchestrator
118
- ? 'Running in ORCHESTRATOR mode (with memory and delegation)'
119
- : 'Running in SIMPLE mode (direct Claude Code relay)';
146
+ ? `Running in ORCHESTRATOR mode (${wsStatus})${runnerInfo} with memory and delegation`
147
+ : `Running in SIMPLE mode (${wsStatus})${runnerInfo} - direct Claude Code relay`;
120
148
  }
121
149
  if (content === '/memory' && orchestrator) {
122
150
  // Trigger memory stats
123
- const response = await orchestrator.processMessage('Show me my memory stats', [], undefined);
151
+ const response = await orchestrator.processMessage('Show me my memory stats', [], undefined, message.apiKey // Use per-message API key if provided
152
+ );
124
153
  return response;
125
154
  }
126
155
  try {
127
156
  if (orchestrator) {
128
- return await sendToOrchestrator(message.content, orchestrator);
157
+ return await sendToOrchestrator(message.content, orchestrator, message.id, message.apiKey);
129
158
  }
130
159
  else {
131
160
  return await sendToClaudeSimple(message.content);
@@ -140,36 +169,101 @@ export async function runAgent(pollInterval = 2000) {
140
169
  // Initialize orchestrator
141
170
  orchestrator = initOrchestrator();
142
171
  const mode = orchestrator ? 'ORCHESTRATOR' : 'SIMPLE RELAY';
172
+ const deployment = getDeploymentInfo();
143
173
  console.log('╔══════════════════════════════════════════════════╗');
144
174
  console.log('║ Connect Agent - Master Orchestrator ║');
145
175
  console.log(`║ Mode: ${mode.padEnd(41)}║`);
176
+ console.log(`║ Deployment: ${deployment.mode.toUpperCase().padEnd(35)}║`);
146
177
  console.log('╚══════════════════════════════════════════════════╝\n');
178
+ if (isCloudMode()) {
179
+ console.log(`[AGENT] Running in CLOUD mode`);
180
+ console.log(`[AGENT] Server: ${deployment.server}`);
181
+ }
147
182
  if (orchestrator) {
148
183
  console.log('[AGENT] Features: Memory, Scheduling, Worker Delegation');
184
+ // Initialize orchestrator (indexes filesystem into memory)
185
+ await orchestrator.initialize();
149
186
  }
150
- console.log('[AGENT] Polling for web messages...\n');
187
+ // Start heartbeat to maintain presence in runner registry
188
+ const runnerType = isCloudMode() ? 'vm' : 'local';
189
+ startHeartbeat({
190
+ runnerType,
191
+ capabilities: orchestrator ? ['orchestrator', 'memory'] : [],
192
+ onError: (error) => {
193
+ // Log only significant errors
194
+ if (error.message.includes('401') || error.message.includes('403')) {
195
+ console.error('[AGENT] Heartbeat auth failed - may need to re-pair');
196
+ }
197
+ },
198
+ });
151
199
  // Graceful shutdown
152
200
  process.on('SIGINT', () => {
153
201
  console.log('\n[AGENT] Shutting down...');
202
+ stopHeartbeat();
203
+ if (wsClient) {
204
+ wsClient.close();
205
+ }
154
206
  if (orchestrator) {
155
207
  orchestrator.shutdown();
156
208
  }
157
209
  process.exit(0);
158
210
  });
159
- while (true) {
160
- try {
161
- const { messages } = await pollMessages();
162
- for (const msg of messages) {
163
- const response = await processMessage(msg);
164
- await sendResponse(msg.id, response);
165
- console.log(`[AGENT] Response sent (${response.length} chars)`);
211
+ // Try WebSocket first
212
+ wsClient = new AgentWebSocket();
213
+ const wsConnected = await wsClient.connect();
214
+ if (wsConnected) {
215
+ console.log('[AGENT] Using WebSocket for real-time communication\n');
216
+ // Handle messages via WebSocket
217
+ wsClient.onMessage(async (wsMsg) => {
218
+ if (wsMsg.type === 'message' && wsMsg.content && wsMsg.id) {
219
+ const message = {
220
+ id: wsMsg.id,
221
+ content: wsMsg.content,
222
+ timestamp: wsMsg.timestamp || Date.now(),
223
+ apiKey: wsMsg.apiKey // Pass through per-request API key
224
+ };
225
+ const response = await processMessage(message);
226
+ // Send response via WebSocket
227
+ wsClient.sendResponse(wsMsg.id, response);
228
+ console.log(`[AGENT] Response sent via WebSocket (${response.length} chars)`);
166
229
  }
167
- }
168
- catch (error) {
169
- if (error instanceof Error && !error.message.includes('ECONNREFUSED')) {
230
+ });
231
+ // Keep process alive - WebSocket handles messages
232
+ console.log('[AGENT] Listening for WebSocket messages...\n');
233
+ // Always poll for messages (socket-store.ts stores messages that WebSocket doesn't see)
234
+ while (true) {
235
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
236
+ try {
237
+ const { messages } = await pollMessages();
238
+ for (const msg of messages) {
239
+ const response = await processMessage(msg);
240
+ await sendResponse(msg.id, response);
241
+ console.log(`[AGENT] Response sent (${response.length} chars)`);
242
+ }
243
+ }
244
+ catch (error) {
170
245
  console.error('[AGENT] Poll error:', error.message);
171
246
  }
172
247
  }
173
- await new Promise(resolve => setTimeout(resolve, pollInterval));
248
+ }
249
+ else {
250
+ // Fall back to polling
251
+ console.log('[AGENT] WebSocket unavailable, using HTTP polling\n');
252
+ while (true) {
253
+ try {
254
+ const { messages } = await pollMessages();
255
+ for (const msg of messages) {
256
+ const response = await processMessage(msg);
257
+ await sendResponse(msg.id, response);
258
+ console.log(`[AGENT] Response sent (${response.length} chars)`);
259
+ }
260
+ }
261
+ catch (error) {
262
+ if (error instanceof Error && !error.message.includes('ECONNREFUSED')) {
263
+ console.error('[AGENT] Poll error:', error.message);
264
+ }
265
+ }
266
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
267
+ }
174
268
  }
175
269
  }
package/dist/api.d.ts CHANGED
@@ -2,6 +2,7 @@ export interface Message {
2
2
  id: string;
3
3
  content: string;
4
4
  timestamp: number;
5
+ apiKey?: string;
5
6
  }
6
7
  export interface ConnectResponse {
7
8
  success: boolean;
package/dist/cli.js CHANGED
@@ -36,7 +36,9 @@ program
36
36
  if (options.apiKey) {
37
37
  console.log('Orchestrator mode enabled (API key saved)');
38
38
  }
39
- console.log('Run "connect-agent start" to begin processing messages.');
39
+ console.log('\nTo start the agent, run one of:');
40
+ console.log(' npx @siftd/connect-agent start');
41
+ console.log(' (or install globally: npm install -g @siftd/connect-agent)');
40
42
  }
41
43
  catch (error) {
42
44
  spinner.fail(`Connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
package/dist/config.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Check if running in cloud mode (environment variables take precedence)
3
+ */
4
+ export declare function isCloudMode(): boolean;
1
5
  export declare function getServerUrl(): string;
2
6
  export declare function setServerUrl(url: string): void;
3
7
  export declare function getAgentToken(): string | null;
@@ -9,3 +13,11 @@ export declare function setAnthropicApiKey(key: string | null): void;
9
13
  export declare function isConfigured(): boolean;
10
14
  export declare function clearConfig(): void;
11
15
  export declare function getConfigPath(): string;
16
+ /**
17
+ * Get deployment mode info for logging
18
+ */
19
+ export declare function getDeploymentInfo(): {
20
+ mode: string;
21
+ server: string;
22
+ userId: string | null;
23
+ };
package/dist/config.js CHANGED
@@ -5,14 +5,22 @@ const config = new Conf({
5
5
  serverUrl: 'https://connect.siftd.app',
6
6
  },
7
7
  });
8
+ /**
9
+ * Check if running in cloud mode (environment variables take precedence)
10
+ */
11
+ export function isCloudMode() {
12
+ return !!(process.env.CONNECT_SERVER_URL && process.env.CONNECT_AGENT_TOKEN && process.env.CONNECT_USER_ID);
13
+ }
8
14
  export function getServerUrl() {
9
- return config.get('serverUrl');
15
+ // Environment variable takes precedence (cloud mode)
16
+ return process.env.CONNECT_SERVER_URL || config.get('serverUrl');
10
17
  }
11
18
  export function setServerUrl(url) {
12
19
  config.set('serverUrl', url);
13
20
  }
14
21
  export function getAgentToken() {
15
- return config.get('agentToken') ?? null;
22
+ // Environment variable takes precedence (cloud mode)
23
+ return process.env.CONNECT_AGENT_TOKEN || config.get('agentToken') || null;
16
24
  }
17
25
  export function setAgentToken(token) {
18
26
  if (token === null) {
@@ -23,7 +31,8 @@ export function setAgentToken(token) {
23
31
  }
24
32
  }
25
33
  export function getUserId() {
26
- return config.get('userId') ?? null;
34
+ // Environment variable takes precedence (cloud mode)
35
+ return process.env.CONNECT_USER_ID || config.get('userId') || null;
27
36
  }
28
37
  export function setUserId(id) {
29
38
  if (id === null) {
@@ -34,7 +43,9 @@ export function setUserId(id) {
34
43
  }
35
44
  }
36
45
  export function getAnthropicApiKey() {
37
- return config.get('anthropicApiKey') ?? null;
46
+ // Environment variable takes precedence (cloud mode)
47
+ // Note: ANTHROPIC_API_KEY is also checked in orchestrator.ts
48
+ return process.env.ANTHROPIC_API_KEY || config.get('anthropicApiKey') || null;
38
49
  }
39
50
  export function setAnthropicApiKey(key) {
40
51
  if (key === null) {
@@ -55,3 +66,13 @@ export function clearConfig() {
55
66
  export function getConfigPath() {
56
67
  return config.path;
57
68
  }
69
+ /**
70
+ * Get deployment mode info for logging
71
+ */
72
+ export function getDeploymentInfo() {
73
+ return {
74
+ mode: isCloudMode() ? 'cloud' : 'local',
75
+ server: getServerUrl(),
76
+ userId: getUserId(),
77
+ };
78
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * System Indexer
3
+ *
4
+ * Indexes the user's filesystem INTO the memory system.
5
+ * When user says "that project" or "save in downloads",
6
+ * semantic search finds the relevant paths.
7
+ *
8
+ * No MCP. No new protocols. Just smart memory use.
9
+ */
10
+ import { AdvancedMemoryStore } from './memory-advanced.js';
11
+ export declare class SystemIndexer {
12
+ private memory;
13
+ private home;
14
+ private indexed;
15
+ constructor(memory: AdvancedMemoryStore);
16
+ /**
17
+ * Index the user's home directory structure into memory
18
+ */
19
+ indexHome(): Promise<{
20
+ projects: number;
21
+ directories: number;
22
+ total: number;
23
+ }>;
24
+ /**
25
+ * Index a directory and its subdirectories
26
+ */
27
+ private indexDirectory;
28
+ /**
29
+ * Find and index git repositories as projects
30
+ */
31
+ private indexGitRepos;
32
+ /**
33
+ * Get detailed info about a git project
34
+ */
35
+ private getProjectInfo;
36
+ /**
37
+ * Detect project type by looking at files
38
+ */
39
+ private detectProjectType;
40
+ /**
41
+ * Remember a key directory (Downloads, Documents, Desktop, etc.)
42
+ * These get high importance and multiple aliases for better search
43
+ */
44
+ private rememberKeyDirectory;
45
+ /**
46
+ * Store a project in memory
47
+ */
48
+ private rememberProject;
49
+ /**
50
+ * Generate a description for a directory
51
+ */
52
+ private describeDirectory;
53
+ /**
54
+ * Extract relevant tags from directory contents
55
+ */
56
+ private extractTags;
57
+ /**
58
+ * Re-index (clear old filesystem memories and re-scan)
59
+ */
60
+ reindex(): Promise<{
61
+ projects: number;
62
+ directories: number;
63
+ total: number;
64
+ }>;
65
+ }