agent-state-machine 1.3.3 → 1.4.0

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/bin/cli.js CHANGED
@@ -23,13 +23,16 @@ function getVersion() {
23
23
  }
24
24
  }
25
25
 
26
+ // Default remote server URL (can be overridden with STATE_MACHINE_REMOTE_URL env var)
27
+ const DEFAULT_REMOTE_URL = process.env.STATE_MACHINE_REMOTE_URL || 'http://localhost:3001';
28
+
26
29
  function printHelp() {
27
30
  console.log(`
28
31
  Agent State Machine CLI (Native JS Workflows Only) v${getVersion()}
29
32
 
30
33
  Usage:
31
34
  state-machine --setup <workflow-name> Create a new workflow project
32
- state-machine run <workflow-name> Run a workflow (loads existing state)
35
+ state-machine run <workflow-name> [--remote] Run a workflow (--remote enables remote follow)
33
36
  state-machine follow <workflow-name> View prompt trace history in browser with live updates
34
37
  state-machine status [workflow-name] Show current state (or list all)
35
38
  state-machine history <workflow-name> [limit] Show execution history logs
@@ -40,9 +43,13 @@ Usage:
40
43
 
41
44
  Options:
42
45
  --setup, -s Initialize a new workflow with directory structure
46
+ --remote, -r Enable remote follow (generates shareable URL for browser access)
43
47
  --help, -h Show help
44
48
  --version, -v Show version
45
49
 
50
+ Environment Variables:
51
+ STATE_MACHINE_REMOTE_URL Override the default remote server URL
52
+
46
53
  Workflow Structure:
47
54
  workflows/<name>/
48
55
  ├── workflow.js # Native JS workflow (async/await)
@@ -136,7 +143,7 @@ function listWorkflows() {
136
143
  console.log('');
137
144
  }
138
145
 
139
- async function runOrResume(workflowName) {
146
+ async function runOrResume(workflowName, { remoteEnabled = false } = {}) {
140
147
  const workflowDir = resolveWorkflowDir(workflowName);
141
148
 
142
149
  if (!fs.existsSync(workflowDir)) {
@@ -154,7 +161,20 @@ async function runOrResume(workflowName) {
154
161
  const runtime = new WorkflowRuntime(workflowDir);
155
162
  const workflowUrl = pathToFileURL(entry).href;
156
163
 
157
- await runtime.runWorkflow(workflowUrl);
164
+ // Enable remote follow mode if requested
165
+ if (remoteEnabled) {
166
+ const remoteUrl = process.env.STATE_MACHINE_REMOTE_URL || DEFAULT_REMOTE_URL;
167
+ await runtime.enableRemote(remoteUrl);
168
+ }
169
+
170
+ try {
171
+ await runtime.runWorkflow(workflowUrl);
172
+ } finally {
173
+ // Always disable remote on completion
174
+ if (remoteEnabled) {
175
+ await runtime.disableRemote();
176
+ }
177
+ }
158
178
  }
159
179
 
160
180
  async function main() {
@@ -185,14 +205,17 @@ async function main() {
185
205
  case 'run':
186
206
  if (!workflowName) {
187
207
  console.error('Error: Workflow name required');
188
- console.error(`Usage: state-machine ${command} <workflow-name>`);
208
+ console.error(`Usage: state-machine ${command} <workflow-name> [--remote]`);
189
209
  process.exit(1);
190
210
  }
191
- try {
192
- await runOrResume(workflowName);
193
- } catch (err) {
194
- console.error('Error:', err.message || String(err));
195
- process.exit(1);
211
+ {
212
+ const remoteEnabled = args.includes('--remote') || args.includes('-r');
213
+ try {
214
+ await runOrResume(workflowName, { remoteEnabled });
215
+ } catch (err) {
216
+ console.error('Error:', err.message || String(err));
217
+ process.exit(1);
218
+ }
196
219
  }
197
220
  break;
198
221
 
@@ -0,0 +1,252 @@
1
+ /**
2
+ * File: /lib/remote/client.js
3
+ *
4
+ * RemoteClient - HTTP client for connecting to the remote follow server
5
+ * Uses HTTP POST for sending events and long-polling for receiving interactions
6
+ */
7
+
8
+ import crypto from 'crypto';
9
+ import http from 'http';
10
+ import https from 'https';
11
+
12
+ // ANSI Colors
13
+ const C = {
14
+ reset: '\x1b[0m',
15
+ bold: '\x1b[1m',
16
+ dim: '\x1b[2m',
17
+ red: '\x1b[31m',
18
+ green: '\x1b[32m',
19
+ yellow: '\x1b[33m',
20
+ cyan: '\x1b[36m'
21
+ };
22
+
23
+ /**
24
+ * Generate a cryptographically secure session token
25
+ * @returns {string} 32-byte base64url-encoded token
26
+ */
27
+ export function generateSessionToken() {
28
+ return crypto.randomBytes(32).toString('base64url');
29
+ }
30
+
31
+ /**
32
+ * Make an HTTP/HTTPS request
33
+ */
34
+ function makeRequest(url, options, body = null) {
35
+ return new Promise((resolve, reject) => {
36
+ const parsedUrl = new URL(url);
37
+ const client = parsedUrl.protocol === 'https:' ? https : http;
38
+
39
+ const req = client.request(parsedUrl, options, (res) => {
40
+ let data = '';
41
+
42
+ res.on('data', (chunk) => {
43
+ data += chunk;
44
+ });
45
+
46
+ res.on('end', () => {
47
+ let parsedData = null;
48
+ if (data) {
49
+ try {
50
+ parsedData = JSON.parse(data);
51
+ } catch {
52
+ // Response is not JSON (could be HTML error page)
53
+ parsedData = { error: data.substring(0, 200) };
54
+ }
55
+ }
56
+ resolve({
57
+ status: res.statusCode,
58
+ data: parsedData,
59
+ });
60
+ });
61
+ });
62
+
63
+ req.on('error', reject);
64
+
65
+ if (body) {
66
+ req.write(JSON.stringify(body));
67
+ }
68
+
69
+ req.end();
70
+ });
71
+ }
72
+
73
+ /**
74
+ * RemoteClient class - manages HTTP connection to remote server
75
+ */
76
+ export class RemoteClient {
77
+ /**
78
+ * @param {object} options
79
+ * @param {string} options.serverUrl - Base URL of remote server (e.g., https://example.vercel.app)
80
+ * @param {string} options.workflowName - Name of the workflow
81
+ * @param {function} options.onInteractionResponse - Callback when interaction response received
82
+ * @param {function} [options.onStatusChange] - Callback when connection status changes
83
+ */
84
+ constructor(options) {
85
+ this.serverUrl = options.serverUrl.replace(/\/$/, ''); // Remove trailing slash
86
+ this.workflowName = options.workflowName;
87
+ this.onInteractionResponse = options.onInteractionResponse;
88
+ this.onStatusChange = options.onStatusChange || (() => {});
89
+
90
+ this.sessionToken = generateSessionToken();
91
+ this.connected = false;
92
+ this.polling = false;
93
+ this.pollAbortController = null;
94
+ this.initialHistorySent = false;
95
+ }
96
+
97
+ /**
98
+ * Get the full remote URL for browser access
99
+ */
100
+ getRemoteUrl() {
101
+ return `${this.serverUrl}/s/${this.sessionToken}`;
102
+ }
103
+
104
+ /**
105
+ * Connect to the remote server
106
+ * @returns {Promise<void>}
107
+ */
108
+ async connect() {
109
+ try {
110
+ // Test connection by making a simple request
111
+ const testUrl = `${this.serverUrl}/api/history/${this.sessionToken}`;
112
+ const response = await makeRequest(testUrl, { method: 'GET' });
113
+
114
+ // 404 is expected for new sessions
115
+ if (response.status !== 404 && response.status !== 200) {
116
+ throw new Error(`Server returned ${response.status}`);
117
+ }
118
+
119
+ this.connected = true;
120
+ this.onStatusChange('connected');
121
+
122
+ // Start polling for interactions
123
+ this.startPolling();
124
+
125
+ } catch (err) {
126
+ console.log(`${C.yellow}Warning: Could not connect to remote server: ${err.message}${C.reset}`);
127
+ // Don't fail - remote is optional
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Send a message to the server via HTTP POST
133
+ */
134
+ async send(msg) {
135
+ if (!this.connected) {
136
+ return;
137
+ }
138
+
139
+ try {
140
+ const url = `${this.serverUrl}/api/ws/cli`;
141
+ await makeRequest(url, {
142
+ method: 'POST',
143
+ headers: {
144
+ 'Content-Type': 'application/json',
145
+ },
146
+ }, msg);
147
+ } catch (err) {
148
+ console.error(`${C.dim}Remote: Failed to send message: ${err.message}${C.reset}`);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Send initial session info with history
154
+ * @param {Array} history - Array of history entries
155
+ */
156
+ async sendSessionInit(history = []) {
157
+ this.initialHistorySent = true;
158
+ await this.send({
159
+ type: 'session_init',
160
+ sessionToken: this.sessionToken,
161
+ workflowName: this.workflowName,
162
+ history,
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Send an event to the server
168
+ * @param {object} event - Event object with timestamp, event type, etc.
169
+ */
170
+ async sendEvent(event) {
171
+ // Only send events after initial history has been sent
172
+ if (!this.initialHistorySent) {
173
+ return;
174
+ }
175
+
176
+ await this.send({
177
+ type: 'event',
178
+ sessionToken: this.sessionToken,
179
+ ...event,
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Send session end notification
185
+ * @param {string} reason - Reason for ending (completed, failed, user_quit)
186
+ */
187
+ async sendSessionEnd(reason = 'completed') {
188
+ await this.send({
189
+ type: 'session_end',
190
+ sessionToken: this.sessionToken,
191
+ reason,
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Start polling for interaction responses
197
+ */
198
+ startPolling() {
199
+ if (this.polling) return;
200
+ this.polling = true;
201
+
202
+ this.poll();
203
+ }
204
+
205
+ /**
206
+ * Poll for interaction responses
207
+ */
208
+ async poll() {
209
+ while (this.polling && this.connected) {
210
+ try {
211
+ const url = `${this.serverUrl}/api/ws/cli?token=${this.sessionToken}&timeout=30000`;
212
+ const response = await makeRequest(url, { method: 'GET' });
213
+
214
+ if (response.status === 200 && response.data) {
215
+ const { type, slug, targetKey, response: interactionResponse } = response.data;
216
+
217
+ if (type === 'interaction_response' && this.onInteractionResponse) {
218
+ this.onInteractionResponse(slug, targetKey, interactionResponse);
219
+ }
220
+ }
221
+
222
+ // If 204 (no content), just continue polling
223
+ // Small delay between polls
224
+ await new Promise(resolve => setTimeout(resolve, 500));
225
+
226
+ } catch (err) {
227
+ // Connection error - wait and retry
228
+ await new Promise(resolve => setTimeout(resolve, 5000));
229
+ }
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Stop polling
235
+ */
236
+ stopPolling() {
237
+ this.polling = false;
238
+ }
239
+
240
+ /**
241
+ * Disconnect from the server
242
+ */
243
+ async disconnect() {
244
+ this.stopPolling();
245
+
246
+ if (this.connected) {
247
+ await this.sendSessionEnd('completed');
248
+ }
249
+
250
+ this.connected = false;
251
+ }
252
+ }
@@ -11,6 +11,15 @@ import path from 'path';
11
11
  import readline from 'readline';
12
12
  import { getCurrentRuntime } from './runtime.js';
13
13
 
14
+ const C = {
15
+ reset: '\x1b[0m',
16
+ bold: '\x1b[1m',
17
+ cyan: '\x1b[36m',
18
+ yellow: '\x1b[33m',
19
+ green: '\x1b[32m',
20
+ dim: '\x1b[2m'
21
+ };
22
+
14
23
  /**
15
24
  * Request user input with memory-based resume support
16
25
  * @param {string} question - Question to ask the user
@@ -35,9 +44,8 @@ export async function initialPrompt(question, options = {}) {
35
44
 
36
45
  // Check if we're in TTY mode (interactive terminal)
37
46
  if (process.stdin.isTTY && process.stdout.isTTY) {
38
- // Interactive mode - prompt directly
39
- console.log('');
40
- const answer = await askQuestion(question);
47
+ // Interactive mode - prompt directly, with remote support
48
+ const answer = await askQuestionWithRemote(runtime, question, slug, memoryKey);
41
49
  console.log('');
42
50
 
43
51
  // Save the response to memory
@@ -88,7 +96,57 @@ ${question}
88
96
  }
89
97
 
90
98
  /**
91
- * Interactive terminal question
99
+ * Interactive terminal question with remote support
100
+ * Allows both local TTY input and remote browser responses
101
+ */
102
+ function askQuestionWithRemote(runtime, question, slug, memoryKey) {
103
+ return new Promise((resolve) => {
104
+ let resolved = false;
105
+
106
+ const cleanup = () => {
107
+ resolved = true;
108
+ runtime.pendingRemoteInteraction = null;
109
+ };
110
+
111
+ // Set up remote interaction listener if remote is enabled
112
+ if (runtime.remoteEnabled) {
113
+ runtime.pendingRemoteInteraction = {
114
+ slug,
115
+ targetKey: memoryKey,
116
+ resolve: (response) => {
117
+ if (resolved) return;
118
+ cleanup();
119
+ rl.close();
120
+ console.log(`\n${C.green}✓ Answered via remote${C.reset}`);
121
+ resolve(response);
122
+ },
123
+ reject: () => {} // Prompts don't reject
124
+ };
125
+ }
126
+
127
+ const rl = readline.createInterface({
128
+ input: process.stdin,
129
+ output: process.stdout
130
+ });
131
+
132
+ // Show remote URL if available
133
+ let prompt = `\n${C.cyan}${C.bold}${question}${C.reset}`;
134
+ if (runtime.remoteEnabled && runtime.remoteUrl) {
135
+ prompt += `\n${C.dim}(Remote: ${runtime.remoteUrl})${C.reset}`;
136
+ }
137
+ prompt += `\n${C.yellow}> ${C.reset}`;
138
+
139
+ rl.question(prompt, (answer) => {
140
+ if (resolved) return;
141
+ cleanup();
142
+ rl.close();
143
+ resolve(answer.trim());
144
+ });
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Interactive terminal question (simple, no remote support)
92
150
  */
93
151
  function askQuestion(question) {
94
152
  return new Promise((resolve) => {
@@ -97,7 +155,8 @@ function askQuestion(question) {
97
155
  output: process.stdout
98
156
  });
99
157
 
100
- rl.question(`${question}\n> `, (answer) => {
158
+ // Colorize the question and the input marker
159
+ rl.question(`\n${C.cyan}${C.bold}${question}${C.reset}\n${C.yellow}> ${C.reset}`, (answer) => {
101
160
  rl.close();
102
161
  resolve(answer.trim());
103
162
  });
@@ -113,4 +172,4 @@ function generateSlug(question) {
113
172
  .replace(/[^a-z0-9]+/g, '-')
114
173
  .replace(/^-+|-+$/g, '')
115
174
  .substring(0, 30);
116
- }
175
+ }
@@ -10,11 +10,26 @@ import fs from 'fs';
10
10
  import path from 'path';
11
11
  import readline from 'readline';
12
12
  import { createMemoryProxy } from './memory.js';
13
+ import { RemoteClient } from '../remote/client.js';
13
14
 
14
15
  // Global runtime reference for agent() and memory access
15
16
  // stored on globalThis to ensure singleton access across different module instances (CLI vs local)
16
17
  const RUNTIME_KEY = Symbol.for('agent-state-machine.runtime');
17
18
 
19
+ // ANSI Colors
20
+ const C = {
21
+ reset: '\x1b[0m',
22
+ bold: '\x1b[1m',
23
+ dim: '\x1b[2m',
24
+ red: '\x1b[31m',
25
+ green: '\x1b[32m',
26
+ yellow: '\x1b[33m',
27
+ blue: '\x1b[34m',
28
+ magenta: '\x1b[35m',
29
+ cyan: '\x1b[36m',
30
+ gray: '\x1b[90m'
31
+ };
32
+
18
33
  export function getCurrentRuntime() {
19
34
  return globalThis[RUNTIME_KEY];
20
35
  }
@@ -63,6 +78,12 @@ export class WorkflowRuntime {
63
78
 
64
79
  // Load steering
65
80
  this.steering = this.loadSteering();
81
+
82
+ // Remote follow state
83
+ this.remoteClient = null;
84
+ this.remoteEnabled = false;
85
+ this.remoteUrl = null;
86
+ this.pendingRemoteInteraction = null; // { slug, targetKey, resolve, reject }
66
87
  }
67
88
 
68
89
  ensureDirectories() {
@@ -82,7 +103,7 @@ export class WorkflowRuntime {
82
103
  try {
83
104
  return JSON.parse(fs.readFileSync(this.stateFile, 'utf-8'));
84
105
  } catch (err) {
85
- console.warn(`Warning: Failed to load state: ${err.message}`);
106
+ console.warn(`${C.yellow}Warning: Failed to load state: ${err.message}${C.reset}`);
86
107
  }
87
108
  }
88
109
  return {};
@@ -105,7 +126,7 @@ export class WorkflowRuntime {
105
126
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
106
127
  steering.enabled = config.enabled !== false;
107
128
  } catch (err) {
108
- console.warn(`Warning: Failed to load steering config: ${err.message}`);
129
+ console.warn(`${C.yellow}Warning: Failed to load steering config: ${err.message}${C.reset}`);
109
130
  }
110
131
  }
111
132
 
@@ -149,6 +170,39 @@ export class WorkflowRuntime {
149
170
  existing = fs.readFileSync(this.historyFile, 'utf-8');
150
171
  }
151
172
  fs.writeFileSync(this.historyFile, line + existing);
173
+
174
+ // Forward to remote if connected
175
+ if (this.remoteClient && this.remoteEnabled) {
176
+ this.remoteClient.sendEvent(entry);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Load all history entries from history.jsonl
182
+ * @returns {Array} Array of history entry objects
183
+ */
184
+ loadHistory() {
185
+ if (!fs.existsSync(this.historyFile)) {
186
+ return [];
187
+ }
188
+
189
+ try {
190
+ const content = fs.readFileSync(this.historyFile, 'utf-8');
191
+ return content
192
+ .trim()
193
+ .split('\n')
194
+ .filter(Boolean)
195
+ .map(line => {
196
+ try {
197
+ return JSON.parse(line);
198
+ } catch {
199
+ return null;
200
+ }
201
+ })
202
+ .filter(Boolean);
203
+ } catch {
204
+ return [];
205
+ }
152
206
  }
153
207
 
154
208
  /**
@@ -188,7 +242,7 @@ export class WorkflowRuntime {
188
242
  this.persist();
189
243
  this.prependHistory({ event: 'WORKFLOW_COMPLETED' });
190
244
 
191
- console.log(`\n✓ Workflow '${this.workflowName}' completed successfully!`);
245
+ console.log(`\n${C.green}✓ Workflow '${this.workflowName}' completed successfully!${C.reset}`);
192
246
  } catch (err) {
193
247
  this.status = 'FAILED';
194
248
  this._error = err.message;
@@ -199,7 +253,7 @@ export class WorkflowRuntime {
199
253
  error: err.message
200
254
  });
201
255
 
202
- console.error(`\n✗ Workflow '${this.workflowName}' failed: ${err.message}`);
256
+ console.error(`\n${C.red}✗ Workflow '${this.workflowName}' failed: ${err.message}${C.reset}`);
203
257
  throw err;
204
258
  } finally {
205
259
  clearCurrentRuntime();
@@ -208,10 +262,17 @@ export class WorkflowRuntime {
208
262
 
209
263
  /**
210
264
  * Wait for user to confirm interaction is complete, then return the response
265
+ * Supports both local TTY input and remote browser responses
211
266
  */
212
267
  async waitForInteraction(filePath, slug, targetKey) {
213
- console.log(`\n⏸ Interaction required. Edit: ${filePath}`);
214
- console.log(` Enter 'y' to proceed or 'q' to quit.\n`);
268
+ console.log(`\n${C.yellow}${C.bold} Interaction required.${C.reset}`);
269
+
270
+ if (this.remoteEnabled && this.remoteUrl) {
271
+ console.log(` ${C.cyan}Remote:${C.reset} ${this.remoteUrl}`);
272
+ }
273
+
274
+ console.log(` ${C.dim}Edit: ${filePath}${C.reset}`);
275
+ console.log(` Enter ${C.bold}y${C.reset} to proceed or ${C.bold}q${C.reset} to quit.\n`);
215
276
 
216
277
  const rl = readline.createInterface({
217
278
  input: process.stdin,
@@ -219,12 +280,57 @@ export class WorkflowRuntime {
219
280
  });
220
281
 
221
282
  return new Promise((resolve, reject) => {
283
+ // Track if we've already resolved (to prevent double-resolution)
284
+ let resolved = false;
285
+
286
+ const cleanup = () => {
287
+ resolved = true;
288
+ rl.close();
289
+ this.pendingRemoteInteraction = null;
290
+ };
291
+
292
+ // Set up remote interaction listener
293
+ if (this.remoteEnabled) {
294
+ this.pendingRemoteInteraction = {
295
+ slug,
296
+ targetKey,
297
+ resolve: (response) => {
298
+ if (resolved) return;
299
+ cleanup();
300
+
301
+ // Store in memory
302
+ this._rawMemory[targetKey] = response;
303
+ this.persist();
304
+
305
+ this.prependHistory({
306
+ event: 'INTERACTION_RESOLVED',
307
+ slug,
308
+ targetKey,
309
+ source: 'remote'
310
+ });
311
+
312
+ console.log(`\n${C.green}✓ Interaction resolved (remote): ${slug}${C.reset}`);
313
+ resolve(response);
314
+ },
315
+ reject: (err) => {
316
+ if (resolved) return;
317
+ cleanup();
318
+ reject(err);
319
+ }
320
+ };
321
+ }
322
+
323
+ // Local TTY input loop
222
324
  const ask = () => {
223
- rl.question('> ', (answer) => {
325
+ if (resolved) return;
326
+
327
+ rl.question(`${C.dim}> ${C.reset}`, (answer) => {
328
+ if (resolved) return;
329
+
224
330
  const a = answer.trim().toLowerCase();
225
331
  if (a === 'y') {
226
- rl.close();
227
- // Read and return the response
332
+ cleanup();
333
+ // Read and return the response from file
228
334
  try {
229
335
  const response = this.readInteractionResponse(filePath, slug, targetKey);
230
336
  resolve(response);
@@ -232,7 +338,7 @@ export class WorkflowRuntime {
232
338
  reject(err);
233
339
  }
234
340
  } else if (a === 'q') {
235
- rl.close();
341
+ cleanup();
236
342
  reject(new Error('User quit workflow'));
237
343
  } else {
238
344
  ask();
@@ -243,6 +349,17 @@ export class WorkflowRuntime {
243
349
  });
244
350
  }
245
351
 
352
+ /**
353
+ * Handle interaction response received from remote browser
354
+ * Called by RemoteClient when it receives an interaction_response message
355
+ */
356
+ handleRemoteInteraction(slug, targetKey, response) {
357
+ if (this.pendingRemoteInteraction &&
358
+ this.pendingRemoteInteraction.slug === slug) {
359
+ this.pendingRemoteInteraction.resolve(response);
360
+ }
361
+ }
362
+
246
363
  /**
247
364
  * Read the user's response from an interaction file
248
365
  */
@@ -268,7 +385,7 @@ export class WorkflowRuntime {
268
385
  targetKey
269
386
  });
270
387
 
271
- console.log(`\n✓ Interaction resolved: ${slug}`);
388
+ console.log(`\n${C.green}✓ Interaction resolved: ${slug}${C.reset}`);
272
389
  return response;
273
390
  }
274
391
 
@@ -276,20 +393,27 @@ export class WorkflowRuntime {
276
393
  * Show workflow status
277
394
  */
278
395
  showStatus() {
279
- console.log(`\nWorkflow: ${this.workflowName}`);
280
- console.log('─'.repeat(40));
281
- console.log(`Status: ${this.status}`);
396
+ console.log(`\n${C.bold}Workflow: ${C.cyan}${this.workflowName}${C.reset}`);
397
+ console.log(`${C.dim}${'─'.repeat(40)}${C.reset}`);
398
+
399
+ let statusColor = C.reset;
400
+ if (this.status === 'COMPLETED') statusColor = C.green;
401
+ if (this.status === 'FAILED') statusColor = C.red;
402
+ if (this.status === 'RUNNING') statusColor = C.blue;
403
+ if (this.status === 'IDLE') statusColor = C.gray;
404
+
405
+ console.log(`Status: ${statusColor}${this.status}${C.reset}`);
282
406
 
283
407
  if (this.startedAt) {
284
408
  console.log(`Started: ${this.startedAt}`);
285
409
  }
286
410
 
287
411
  if (this._error) {
288
- console.log(`Error: ${this._error}`);
412
+ console.log(`Error: ${C.red}${this._error}${C.reset}`);
289
413
  }
290
414
 
291
415
  const memoryKeys = Object.keys(this._rawMemory).filter((k) => !k.startsWith('_'));
292
- console.log(`\nMemory Keys: ${memoryKeys.length}`);
416
+ console.log(`\nMemory Keys: ${C.yellow}${memoryKeys.length}${C.reset}`);
293
417
  if (memoryKeys.length > 0) {
294
418
  console.log(` ${memoryKeys.slice(0, 10).join(', ')}${memoryKeys.length > 10 ? '...' : ''}`);
295
419
  }
@@ -299,8 +423,8 @@ export class WorkflowRuntime {
299
423
  * Show execution history
300
424
  */
301
425
  showHistory(limit = 20) {
302
- console.log(`\nHistory: ${this.workflowName}`);
303
- console.log('─'.repeat(40));
426
+ console.log(`\n${C.bold}History: ${C.cyan}${this.workflowName}${C.reset}`);
427
+ console.log(`${C.dim}${'─'.repeat(40)}${C.reset}`);
304
428
 
305
429
  if (!fs.existsSync(this.historyFile)) {
306
430
  console.log('No history found.');
@@ -318,10 +442,17 @@ export class WorkflowRuntime {
318
442
  try {
319
443
  const entry = JSON.parse(line);
320
444
  const time = entry.timestamp ? entry.timestamp.split('T')[1]?.split('.')[0] : '';
321
- console.log(`[${time}] ${entry.event}`);
322
- if (entry.agent) console.log(` Agent: ${entry.agent}`);
323
- if (entry.slug) console.log(` Slug: ${entry.slug}`);
324
- if (entry.error) console.log(` Error: ${entry.error}`);
445
+
446
+ let eventColor = C.reset;
447
+ if (entry.event.endsWith('_STARTED')) eventColor = C.blue;
448
+ if (entry.event.endsWith('_COMPLETED')) eventColor = C.green;
449
+ if (entry.event.endsWith('_FAILED')) eventColor = C.red;
450
+ if (entry.event.includes('INTERACTION')) eventColor = C.yellow;
451
+
452
+ console.log(`${C.dim}[${time}]${C.reset} ${eventColor}${entry.event}${C.reset}`);
453
+ if (entry.agent) console.log(` ${C.dim}Agent:${C.reset} ${entry.agent}`);
454
+ if (entry.slug) console.log(` ${C.dim}Slug:${C.reset} ${entry.slug}`);
455
+ if (entry.error) console.log(` ${C.red}Error: ${entry.error}${C.reset}`);
325
456
  } catch {}
326
457
  }
327
458
  }
@@ -341,7 +472,7 @@ export class WorkflowRuntime {
341
472
  this.persist();
342
473
  this.prependHistory({ event: 'WORKFLOW_RESET' });
343
474
 
344
- console.log(`\n✓ Workflow '${this.workflowName}' reset`);
475
+ console.log(`\n${C.yellow}✓ Workflow '${this.workflowName}' reset${C.reset}`);
345
476
  }
346
477
 
347
478
  /**
@@ -370,10 +501,62 @@ export class WorkflowRuntime {
370
501
 
371
502
  this.persist();
372
503
 
373
- console.log(`\n✓ Workflow '${this.workflowName}' hard reset (history and interactions cleared)`);
504
+ console.log(`\n${C.red}✓ Workflow '${this.workflowName}' hard reset (history and interactions cleared)${C.reset}`);
505
+ }
506
+
507
+ /**
508
+ * Enable remote follow mode
509
+ * @param {string} serverUrl - Base URL of the remote server
510
+ * @returns {Promise<string>} The remote URL for browser access
511
+ */
512
+ async enableRemote(serverUrl) {
513
+ this.remoteClient = new RemoteClient({
514
+ serverUrl,
515
+ workflowName: this.workflowName,
516
+ onInteractionResponse: (slug, targetKey, response) => {
517
+ this.handleRemoteInteraction(slug, targetKey, response);
518
+ },
519
+ onStatusChange: (status) => {
520
+ if (status === 'disconnected') {
521
+ console.log(`${C.yellow}Remote: Connection lost, attempting to reconnect...${C.reset}`);
522
+ } else if (status === 'connected' && this.remoteEnabled) {
523
+ console.log(`${C.green}Remote: Reconnected${C.reset}`);
524
+ }
525
+ }
526
+ });
527
+
528
+ await this.remoteClient.connect();
529
+
530
+ // Send existing history if connected
531
+ if (this.remoteClient.connected) {
532
+ const history = this.loadHistory();
533
+ this.remoteClient.sendSessionInit(history);
534
+ }
535
+
536
+ this.remoteEnabled = true;
537
+ this.remoteUrl = this.remoteClient.getRemoteUrl();
538
+
539
+ console.log(`\n${C.cyan}${C.bold}Remote follow enabled${C.reset}`);
540
+ console.log(` ${C.dim}URL:${C.reset} ${this.remoteUrl}\n`);
541
+
542
+ return this.remoteUrl;
543
+ }
544
+
545
+ /**
546
+ * Disable remote follow mode and disconnect
547
+ */
548
+ async disableRemote() {
549
+ if (this.remoteClient) {
550
+ await this.remoteClient.disconnect();
551
+ this.remoteClient = null;
552
+ }
553
+
554
+ this.remoteEnabled = false;
555
+ this.remoteUrl = null;
556
+ this.pendingRemoteInteraction = null;
374
557
  }
375
558
  }
376
559
 
377
560
  function escapeRegExp(string) {
378
561
  return string.replace(/[.*+?^${}()|[\\]/g, '\\$&'); // $& means the whole matched string
379
- }
562
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-state-machine",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "description": "A workflow orchestrator for running agents and scripts in sequence with state management",
6
6
  "main": "lib/index.js",