agent-state-machine 1.4.1 → 2.0.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/README.md +2 -6
- package/bin/cli.js +39 -30
- package/lib/runtime/prompt.js +1 -0
- package/lib/ui/index.html +339 -114
- package/package.json +4 -2
- package/vercel-server/local-server.js +547 -0
- package/vercel-server/public/index.html +57 -0
- package/lib/ui/server.js +0 -150
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-state-machine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"bin",
|
|
30
|
-
"lib"
|
|
30
|
+
"lib",
|
|
31
|
+
"vercel-server/local-server.js",
|
|
32
|
+
"vercel-server/public"
|
|
31
33
|
]
|
|
32
34
|
}
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Local development server for testing remote follow
|
|
5
|
+
* Uses in-memory storage instead of Redis
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node vercel-server/local-server.js
|
|
9
|
+
*
|
|
10
|
+
* Or import and start programmatically:
|
|
11
|
+
* import { startLocalServer } from './vercel-server/local-server.js';
|
|
12
|
+
* const { port, url } = await startLocalServer();
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import http from 'http';
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = path.dirname(__filename);
|
|
22
|
+
|
|
23
|
+
const PORT = process.env.PORT || 3001;
|
|
24
|
+
|
|
25
|
+
// In-memory session storage
|
|
26
|
+
const sessions = new Map();
|
|
27
|
+
|
|
28
|
+
// SSE clients per session
|
|
29
|
+
const sseClients = new Map(); // token -> Set<res>
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get or create a session
|
|
33
|
+
*/
|
|
34
|
+
function getSession(token) {
|
|
35
|
+
return sessions.get(token);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createSession(token, data) {
|
|
39
|
+
const session = {
|
|
40
|
+
workflowName: data.workflowName,
|
|
41
|
+
cliConnected: true,
|
|
42
|
+
history: data.history || [],
|
|
43
|
+
pendingInteractions: [],
|
|
44
|
+
createdAt: Date.now(),
|
|
45
|
+
};
|
|
46
|
+
sessions.set(token, session);
|
|
47
|
+
return session;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Broadcast event to all SSE clients for a session
|
|
52
|
+
*/
|
|
53
|
+
function broadcastToSession(token, event) {
|
|
54
|
+
const clients = sseClients.get(token);
|
|
55
|
+
if (!clients) return;
|
|
56
|
+
|
|
57
|
+
const data = JSON.stringify(event);
|
|
58
|
+
for (const client of clients) {
|
|
59
|
+
try {
|
|
60
|
+
client.write(`data: ${data}\n\n`);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
clients.delete(client);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse request body
|
|
69
|
+
*/
|
|
70
|
+
async function parseBody(req) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
let body = '';
|
|
73
|
+
req.on('data', chunk => body += chunk);
|
|
74
|
+
req.on('end', () => {
|
|
75
|
+
try {
|
|
76
|
+
resolve(body ? JSON.parse(body) : {});
|
|
77
|
+
} catch {
|
|
78
|
+
resolve({});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse URL and query params
|
|
86
|
+
*/
|
|
87
|
+
function parseUrl(req) {
|
|
88
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
89
|
+
return {
|
|
90
|
+
pathname: url.pathname,
|
|
91
|
+
query: Object.fromEntries(url.searchParams),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Send JSON response
|
|
97
|
+
*/
|
|
98
|
+
function sendJson(res, status, data) {
|
|
99
|
+
res.writeHead(status, {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
'Access-Control-Allow-Origin': '*',
|
|
102
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
103
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
104
|
+
});
|
|
105
|
+
res.end(JSON.stringify(data));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Handle CLI POST requests
|
|
110
|
+
*/
|
|
111
|
+
async function handleCliPost(req, res) {
|
|
112
|
+
const body = await parseBody(req);
|
|
113
|
+
const { type, sessionToken } = body;
|
|
114
|
+
|
|
115
|
+
if (!sessionToken) {
|
|
116
|
+
return sendJson(res, 400, { error: 'Missing sessionToken' });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
switch (type) {
|
|
120
|
+
case 'session_init': {
|
|
121
|
+
const { workflowName, history } = body;
|
|
122
|
+
createSession(sessionToken, { workflowName, history });
|
|
123
|
+
|
|
124
|
+
broadcastToSession(sessionToken, {
|
|
125
|
+
type: 'cli_connected',
|
|
126
|
+
workflowName,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Send history to any connected browsers
|
|
130
|
+
if (history && history.length > 0) {
|
|
131
|
+
broadcastToSession(sessionToken, {
|
|
132
|
+
type: 'history',
|
|
133
|
+
entries: history,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return sendJson(res, 200, { success: true });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case 'event': {
|
|
141
|
+
const session = getSession(sessionToken);
|
|
142
|
+
if (!session) {
|
|
143
|
+
return sendJson(res, 404, { error: 'Session not found' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const { timestamp, event, ...eventData } = body;
|
|
147
|
+
const historyEvent = {
|
|
148
|
+
timestamp: timestamp || new Date().toISOString(),
|
|
149
|
+
event,
|
|
150
|
+
...eventData,
|
|
151
|
+
};
|
|
152
|
+
delete historyEvent.sessionToken;
|
|
153
|
+
delete historyEvent.type;
|
|
154
|
+
|
|
155
|
+
// Add to history
|
|
156
|
+
session.history.unshift(historyEvent);
|
|
157
|
+
|
|
158
|
+
// Broadcast to browsers
|
|
159
|
+
broadcastToSession(sessionToken, {
|
|
160
|
+
type: 'event',
|
|
161
|
+
...historyEvent,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return sendJson(res, 200, { success: true });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case 'session_end': {
|
|
168
|
+
const session = getSession(sessionToken);
|
|
169
|
+
if (session) {
|
|
170
|
+
session.cliConnected = false;
|
|
171
|
+
broadcastToSession(sessionToken, {
|
|
172
|
+
type: 'cli_disconnected',
|
|
173
|
+
reason: body.reason,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return sendJson(res, 200, { success: true });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
default:
|
|
180
|
+
return sendJson(res, 400, { error: `Unknown type: ${type}` });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Handle CLI GET (long-poll for interactions)
|
|
186
|
+
*/
|
|
187
|
+
async function handleCliGet(req, res, query) {
|
|
188
|
+
const { token, timeout = '30000' } = query;
|
|
189
|
+
|
|
190
|
+
if (!token) {
|
|
191
|
+
return sendJson(res, 400, { error: 'Missing token' });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const session = getSession(token);
|
|
195
|
+
if (!session) {
|
|
196
|
+
return sendJson(res, 404, { error: 'Session not found' });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const timeoutMs = Math.min(parseInt(timeout, 10), 55000);
|
|
200
|
+
const startTime = Date.now();
|
|
201
|
+
|
|
202
|
+
// Poll for pending interactions
|
|
203
|
+
const checkInterval = setInterval(() => {
|
|
204
|
+
if (session.pendingInteractions.length > 0) {
|
|
205
|
+
clearInterval(checkInterval);
|
|
206
|
+
const interaction = session.pendingInteractions.shift();
|
|
207
|
+
return sendJson(res, 200, {
|
|
208
|
+
type: 'interaction_response',
|
|
209
|
+
...interaction,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
214
|
+
clearInterval(checkInterval);
|
|
215
|
+
res.writeHead(204);
|
|
216
|
+
res.end();
|
|
217
|
+
}
|
|
218
|
+
}, 500);
|
|
219
|
+
|
|
220
|
+
// Clean up on client disconnect
|
|
221
|
+
req.on('close', () => {
|
|
222
|
+
clearInterval(checkInterval);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Handle SSE events endpoint for browsers
|
|
228
|
+
*/
|
|
229
|
+
function handleEventsSSE(req, res, token) {
|
|
230
|
+
const session = getSession(token);
|
|
231
|
+
if (!session) {
|
|
232
|
+
return sendJson(res, 404, { error: 'Session not found' });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Set up SSE
|
|
236
|
+
res.writeHead(200, {
|
|
237
|
+
'Content-Type': 'text/event-stream',
|
|
238
|
+
'Cache-Control': 'no-cache',
|
|
239
|
+
'Connection': 'keep-alive',
|
|
240
|
+
'Access-Control-Allow-Origin': '*',
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
res.write('retry: 3000\n\n');
|
|
244
|
+
|
|
245
|
+
// Send initial status
|
|
246
|
+
res.write(`data: ${JSON.stringify({
|
|
247
|
+
type: 'status',
|
|
248
|
+
cliConnected: session.cliConnected,
|
|
249
|
+
workflowName: session.workflowName,
|
|
250
|
+
})}\n\n`);
|
|
251
|
+
|
|
252
|
+
// Send existing history
|
|
253
|
+
res.write(`data: ${JSON.stringify({
|
|
254
|
+
type: 'history',
|
|
255
|
+
entries: session.history,
|
|
256
|
+
})}\n\n`);
|
|
257
|
+
|
|
258
|
+
// Add to SSE clients
|
|
259
|
+
if (!sseClients.has(token)) {
|
|
260
|
+
sseClients.set(token, new Set());
|
|
261
|
+
}
|
|
262
|
+
sseClients.get(token).add(res);
|
|
263
|
+
|
|
264
|
+
// Keepalive
|
|
265
|
+
const keepalive = setInterval(() => {
|
|
266
|
+
res.write(': keepalive\n\n');
|
|
267
|
+
}, 15000);
|
|
268
|
+
|
|
269
|
+
// Clean up on disconnect
|
|
270
|
+
req.on('close', () => {
|
|
271
|
+
clearInterval(keepalive);
|
|
272
|
+
const clients = sseClients.get(token);
|
|
273
|
+
if (clients) {
|
|
274
|
+
clients.delete(res);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Handle history GET
|
|
281
|
+
*/
|
|
282
|
+
function handleHistoryGet(res, token) {
|
|
283
|
+
const session = getSession(token);
|
|
284
|
+
if (!session) {
|
|
285
|
+
return sendJson(res, 404, { error: 'Session not found' });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return sendJson(res, 200, {
|
|
289
|
+
workflowName: session.workflowName,
|
|
290
|
+
cliConnected: session.cliConnected,
|
|
291
|
+
entries: session.history,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Handle interaction submit POST
|
|
297
|
+
*/
|
|
298
|
+
async function handleSubmitPost(req, res, token) {
|
|
299
|
+
const session = getSession(token);
|
|
300
|
+
if (!session) {
|
|
301
|
+
return sendJson(res, 404, { error: 'Session not found' });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!session.cliConnected) {
|
|
305
|
+
return sendJson(res, 503, { error: 'CLI is disconnected' });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const body = await parseBody(req);
|
|
309
|
+
const { slug, targetKey, response } = body;
|
|
310
|
+
|
|
311
|
+
if (!slug || !response) {
|
|
312
|
+
return sendJson(res, 400, { error: 'Missing slug or response' });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Add to pending interactions for CLI to pick up
|
|
316
|
+
session.pendingInteractions.push({
|
|
317
|
+
slug,
|
|
318
|
+
targetKey: targetKey || `_interaction_${slug}`,
|
|
319
|
+
response,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Log to history (include answer preview)
|
|
323
|
+
const event = {
|
|
324
|
+
timestamp: new Date().toISOString(),
|
|
325
|
+
event: 'INTERACTION_SUBMITTED',
|
|
326
|
+
slug,
|
|
327
|
+
targetKey: targetKey || `_interaction_${slug}`,
|
|
328
|
+
answer: response.substring(0, 200) + (response.length > 200 ? '...' : ''),
|
|
329
|
+
source: 'remote',
|
|
330
|
+
};
|
|
331
|
+
session.history.unshift(event);
|
|
332
|
+
|
|
333
|
+
// Broadcast to browsers
|
|
334
|
+
broadcastToSession(token, {
|
|
335
|
+
type: 'event',
|
|
336
|
+
...event,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return sendJson(res, 200, { success: true });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Serve session UI
|
|
344
|
+
*/
|
|
345
|
+
const MASTER_TEMPLATE_PATH = path.join(__dirname, '..', 'lib', 'ui', 'index.html');
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Get session HTML by reading the master template from lib/ui/index.html
|
|
349
|
+
*/
|
|
350
|
+
function getSessionHTML(token, workflowName) {
|
|
351
|
+
try {
|
|
352
|
+
const template = fs.readFileSync(MASTER_TEMPLATE_PATH, 'utf8');
|
|
353
|
+
return template
|
|
354
|
+
.replace(/\{\{SESSION_TOKEN\}\}/g, token)
|
|
355
|
+
.replace(/\{\{WORKFLOW_NAME\}\}/g, workflowName || 'Workflow');
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.error('Error loading master template:', err);
|
|
358
|
+
return `
|
|
359
|
+
<!DOCTYPE html>
|
|
360
|
+
<html>
|
|
361
|
+
<head><title>Error</title></head>
|
|
362
|
+
<body style="font-family: system-ui; max-width: 600px; margin: 100px auto; text-align: center;">
|
|
363
|
+
<h1>Error loading UI template</h1>
|
|
364
|
+
<p>${err.message}</p>
|
|
365
|
+
<p>Make sure <code>lib/ui/index.html</code> exists.</p>
|
|
366
|
+
</body>
|
|
367
|
+
</html>
|
|
368
|
+
`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
function serveSessionUI(res, token) {
|
|
375
|
+
const session = getSession(token);
|
|
376
|
+
|
|
377
|
+
if (!session) {
|
|
378
|
+
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
379
|
+
return res.end(`
|
|
380
|
+
<!DOCTYPE html>
|
|
381
|
+
<html>
|
|
382
|
+
<head><title>Session Not Found</title></head>
|
|
383
|
+
<body style="font-family: system-ui; max-width: 600px; margin: 100px auto; text-align: center;">
|
|
384
|
+
<h1>Session Not Found</h1>
|
|
385
|
+
<p>This session has expired or does not exist.</p>
|
|
386
|
+
</body>
|
|
387
|
+
</html>
|
|
388
|
+
`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const html = getSessionHTML(token, session.workflowName);
|
|
392
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
393
|
+
res.end(html);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// getSessionHTML was moved up and updated to read from MASTER_TEMPLATE_PATH
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Serve static files
|
|
400
|
+
*/
|
|
401
|
+
function serveStatic(res, filepath) {
|
|
402
|
+
const fullPath = path.join(__dirname, 'public', filepath);
|
|
403
|
+
|
|
404
|
+
if (!fs.existsSync(fullPath)) {
|
|
405
|
+
res.writeHead(404);
|
|
406
|
+
return res.end('Not found');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const ext = path.extname(fullPath);
|
|
410
|
+
const contentTypes = {
|
|
411
|
+
'.html': 'text/html',
|
|
412
|
+
'.js': 'application/javascript',
|
|
413
|
+
'.css': 'text/css',
|
|
414
|
+
'.json': 'application/json',
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const content = fs.readFileSync(fullPath);
|
|
418
|
+
res.writeHead(200, { 'Content-Type': contentTypes[ext] || 'text/plain' });
|
|
419
|
+
res.end(content);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Main request handler
|
|
424
|
+
*/
|
|
425
|
+
async function handleRequest(req, res) {
|
|
426
|
+
const { pathname, query } = parseUrl(req);
|
|
427
|
+
|
|
428
|
+
// Handle CORS preflight
|
|
429
|
+
if (req.method === 'OPTIONS') {
|
|
430
|
+
res.writeHead(200, {
|
|
431
|
+
'Access-Control-Allow-Origin': '*',
|
|
432
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
433
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
434
|
+
});
|
|
435
|
+
return res.end();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Route: CLI endpoint
|
|
439
|
+
if (pathname === '/api/ws/cli') {
|
|
440
|
+
if (req.method === 'POST') {
|
|
441
|
+
return handleCliPost(req, res);
|
|
442
|
+
}
|
|
443
|
+
if (req.method === 'GET') {
|
|
444
|
+
return handleCliGet(req, res, query);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Route: Session UI
|
|
449
|
+
const sessionMatch = pathname.match(/^\/s\/([^/]+)$/);
|
|
450
|
+
if (sessionMatch) {
|
|
451
|
+
return serveSessionUI(res, sessionMatch[1]);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Route: Events SSE
|
|
455
|
+
const eventsMatch = pathname.match(/^\/api\/events\/([^/]+)$/);
|
|
456
|
+
if (eventsMatch && req.method === 'GET') {
|
|
457
|
+
return handleEventsSSE(req, res, eventsMatch[1]);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Route: History
|
|
461
|
+
const historyMatch = pathname.match(/^\/api\/history\/([^/]+)$/);
|
|
462
|
+
if (historyMatch && req.method === 'GET') {
|
|
463
|
+
return handleHistoryGet(res, historyMatch[1]);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Route: Submit
|
|
467
|
+
const submitMatch = pathname.match(/^\/api\/submit\/([^/]+)$/);
|
|
468
|
+
if (submitMatch && req.method === 'POST') {
|
|
469
|
+
return handleSubmitPost(req, res, submitMatch[1]);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Route: Static files
|
|
473
|
+
if (pathname === '/' || pathname === '/index.html') {
|
|
474
|
+
return serveStatic(res, 'index.html');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// 404
|
|
478
|
+
res.writeHead(404);
|
|
479
|
+
res.end('Not found');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Start the local server programmatically
|
|
484
|
+
* @param {number} initialPort - Starting port to try (default 3000)
|
|
485
|
+
* @param {boolean} silent - Suppress console output
|
|
486
|
+
* @returns {Promise<{port: number, url: string, server: http.Server}>}
|
|
487
|
+
*/
|
|
488
|
+
export function startLocalServer(initialPort = 3000, silent = false) {
|
|
489
|
+
return new Promise((resolve, reject) => {
|
|
490
|
+
let port = initialPort;
|
|
491
|
+
const maxPort = initialPort + 100;
|
|
492
|
+
|
|
493
|
+
const tryPort = () => {
|
|
494
|
+
const server = http.createServer(handleRequest);
|
|
495
|
+
|
|
496
|
+
server.on('error', (e) => {
|
|
497
|
+
if (e.code === 'EADDRINUSE') {
|
|
498
|
+
if (port < maxPort) {
|
|
499
|
+
port++;
|
|
500
|
+
tryPort();
|
|
501
|
+
} else {
|
|
502
|
+
reject(new Error(`Could not find open port between ${initialPort} and ${maxPort}`));
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
reject(e);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
server.listen(port, () => {
|
|
510
|
+
const url = `http://localhost:${port}`;
|
|
511
|
+
if (!silent) {
|
|
512
|
+
console.log(`Local server running at ${url}`);
|
|
513
|
+
}
|
|
514
|
+
resolve({ port, url, server });
|
|
515
|
+
});
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
tryPort();
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Run standalone if executed directly
|
|
523
|
+
const isMainModule = process.argv[1] && (
|
|
524
|
+
process.argv[1].endsWith('local-server.js') ||
|
|
525
|
+
process.argv[1].endsWith('local-server')
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
if (isMainModule) {
|
|
529
|
+
const PORT = process.env.PORT || 3000;
|
|
530
|
+
startLocalServer(parseInt(PORT, 10)).then(({ port, url }) => {
|
|
531
|
+
console.log(`
|
|
532
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
533
|
+
│ Agent State Machine - Local Remote Follow Server │
|
|
534
|
+
├─────────────────────────────────────────────────────────────┤
|
|
535
|
+
│ Server running at: ${url.padEnd(37)}│
|
|
536
|
+
│ │
|
|
537
|
+
│ To test remote follow, run your workflow with: │
|
|
538
|
+
│ state-machine run <workflow-name> --local │
|
|
539
|
+
│ │
|
|
540
|
+
│ Press Ctrl+C to stop │
|
|
541
|
+
└─────────────────────────────────────────────────────────────┘
|
|
542
|
+
`);
|
|
543
|
+
}).catch(err => {
|
|
544
|
+
console.error('Failed to start server:', err.message);
|
|
545
|
+
process.exit(1);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Agent State Machine - Remote Follow</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body class="bg-zinc-950 text-zinc-100 min-h-screen flex items-center justify-center">
|
|
10
|
+
<div class="max-w-lg text-center p-8">
|
|
11
|
+
<h1 class="text-3xl font-bold mb-4">Agent State Machine</h1>
|
|
12
|
+
<p class="text-zinc-400 mb-8">Remote Follow Server</p>
|
|
13
|
+
|
|
14
|
+
<div class="bg-zinc-900 rounded-lg p-6 text-left">
|
|
15
|
+
<h2 class="text-lg font-semibold mb-4">How to use</h2>
|
|
16
|
+
|
|
17
|
+
<ol class="space-y-4 text-zinc-300">
|
|
18
|
+
<li class="flex gap-3">
|
|
19
|
+
<span class="text-zinc-500 font-mono">1.</span>
|
|
20
|
+
<span>Install package:
|
|
21
|
+
<br><code class="bg-zinc-800 px-2 py-0.5 rounded">npm i -D agent-state-machine</code></span>
|
|
22
|
+
</li>
|
|
23
|
+
|
|
24
|
+
<li class="flex gap-3">
|
|
25
|
+
<span class="text-zinc-500 font-mono">2.</span>
|
|
26
|
+
<span>Initialize your workflow:
|
|
27
|
+
<br><code class="bg-zinc-800 px-2 py-0.5 rounded">npx state-machine --setup my-workflow</code></span>
|
|
28
|
+
</li>
|
|
29
|
+
|
|
30
|
+
<li class="flex gap-3">
|
|
31
|
+
<span class="text-zinc-500 font-mono">3.</span>
|
|
32
|
+
<span>Run your workflow:</span>
|
|
33
|
+
</li>
|
|
34
|
+
<li class="pl-6">
|
|
35
|
+
<code class="bg-zinc-800 px-3 py-2 rounded block text-sm">
|
|
36
|
+
npx state-machine run my-workflow
|
|
37
|
+
</code>
|
|
38
|
+
</li>
|
|
39
|
+
|
|
40
|
+
<li class="flex gap-3 mt-4">
|
|
41
|
+
<span class="text-zinc-500 font-mono">4.</span>
|
|
42
|
+
<span>The CLI will print a unique URL. Share it with anyone who needs to follow along or interact with the workflow.</span>
|
|
43
|
+
</li>
|
|
44
|
+
|
|
45
|
+
<li class="flex gap-3 mt-4">
|
|
46
|
+
<span class="text-zinc-500 font-mono">5.</span>
|
|
47
|
+
<span>Open the URL in a browser to see live workflow events and submit interaction responses.</span>
|
|
48
|
+
</li>
|
|
49
|
+
</ol>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<p class="text-zinc-500 text-sm mt-8">
|
|
53
|
+
Learn more at <a href="https://github.com/superbasicman/state-machine" target="_blank" class="text-cyan-400 hover:underline">GitHub</a>
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|