agent-state-machine 1.4.2 → 2.0.1
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 +4 -19
- package/package.json +4 -2
- package/vercel-server/api/events/[token].js +119 -0
- package/vercel-server/api/history/[token].js +51 -0
- package/vercel-server/api/session/[token].js +49 -0
- package/vercel-server/api/submit/[token].js +94 -0
- package/vercel-server/api/ws/cli.js +186 -0
- package/vercel-server/local-server.js +30 -329
- package/vercel-server/public/index.html +17 -5
- package/{lib → vercel-server}/ui/index.html +339 -114
- package/lib/ui/server.js +0 -150
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ Requirements: Node.js >= 16.
|
|
|
45
45
|
state-machine --setup <workflow-name>
|
|
46
46
|
state-machine run <workflow-name>
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
state-machine history <workflow-name> [limit]
|
|
50
50
|
state-machine reset <workflow-name> (clears memory/state)
|
|
51
51
|
state-machine reset-hard <workflow-name> (clears everything: history/interactions/memory)
|
|
@@ -298,11 +298,7 @@ export const config = {
|
|
|
298
298
|
};
|
|
299
299
|
```
|
|
300
300
|
|
|
301
|
-
The runtime captures the fully-built prompt in `state/history.jsonl`, viewable in the browser with live updates via
|
|
302
|
-
|
|
303
|
-
```bash
|
|
304
|
-
state-machine follow <workflow-name>
|
|
305
|
-
```
|
|
301
|
+
The runtime captures the fully-built prompt in `state/history.jsonl`, viewable in the browser with live updates when running with the `--local` flag or via the remote URL.
|
|
306
302
|
|
|
307
303
|
---
|
|
308
304
|
|
package/bin/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import fs from 'fs';
|
|
|
5
5
|
import { pathToFileURL, fileURLToPath } from 'url';
|
|
6
6
|
import { WorkflowRuntime } from '../lib/index.js';
|
|
7
7
|
import { setup } from '../lib/setup.js';
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
import { startLocalServer } from '../vercel-server/local-server.js';
|
|
10
10
|
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -34,8 +34,8 @@ Agent State Machine CLI (Native JS Workflows Only) v${getVersion()}
|
|
|
34
34
|
Usage:
|
|
35
35
|
state-machine --setup <workflow-name> Create a new workflow project
|
|
36
36
|
state-machine run <workflow-name> Run a workflow (remote follow enabled by default)
|
|
37
|
-
state-machine run <workflow-name>
|
|
38
|
-
|
|
37
|
+
state-machine run <workflow-name> -l Run with local server (localhost:3000)
|
|
38
|
+
|
|
39
39
|
state-machine status [workflow-name] Show current state (or list all)
|
|
40
40
|
state-machine history <workflow-name> [limit] Show execution history logs
|
|
41
41
|
state-machine reset <workflow-name> Reset workflow state (clears memory/state)
|
|
@@ -269,22 +269,7 @@ async function main() {
|
|
|
269
269
|
}
|
|
270
270
|
break;
|
|
271
271
|
|
|
272
|
-
|
|
273
|
-
if (!workflowName) {
|
|
274
|
-
console.error('Error: Workflow name required');
|
|
275
|
-
console.error('Usage: state-machine follow <workflow-name>');
|
|
276
|
-
process.exit(1);
|
|
277
|
-
}
|
|
278
|
-
{
|
|
279
|
-
const workflowDir = resolveWorkflowDir(workflowName);
|
|
280
|
-
if (!fs.existsSync(workflowDir)) {
|
|
281
|
-
console.error(`Error: Workflow '${workflowName}' not found`);
|
|
282
|
-
process.exit(1);
|
|
283
|
-
}
|
|
284
|
-
startServer(workflowDir);
|
|
285
|
-
// Do not exit, server needs to stay alive
|
|
286
|
-
}
|
|
287
|
-
break;
|
|
272
|
+
|
|
288
273
|
|
|
289
274
|
case 'reset':
|
|
290
275
|
if (!workflowName) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-state-machine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
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",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"bin",
|
|
30
30
|
"lib",
|
|
31
31
|
"vercel-server/local-server.js",
|
|
32
|
-
"vercel-server/public"
|
|
32
|
+
"vercel-server/public",
|
|
33
|
+
"vercel-server/ui",
|
|
34
|
+
"vercel-server/api"
|
|
33
35
|
]
|
|
34
36
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /vercel-server/api/events/[token].js
|
|
3
|
+
*
|
|
4
|
+
* SSE endpoint for browser connections
|
|
5
|
+
* Streams history and real-time events to connected browsers
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getSession,
|
|
10
|
+
getHistory,
|
|
11
|
+
redis,
|
|
12
|
+
KEYS,
|
|
13
|
+
refreshSession,
|
|
14
|
+
} from '../../lib/redis.js';
|
|
15
|
+
|
|
16
|
+
export const config = {
|
|
17
|
+
maxDuration: 60, // Maximum 60 seconds for SSE
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default async function handler(req, res) {
|
|
21
|
+
const { token } = req.query;
|
|
22
|
+
|
|
23
|
+
if (!token) {
|
|
24
|
+
return res.status(400).json({ error: 'Missing token parameter' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Validate session
|
|
28
|
+
const session = await getSession(token);
|
|
29
|
+
if (!session) {
|
|
30
|
+
return res.status(404).json({ error: 'Session not found or expired' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Set up SSE headers
|
|
34
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
35
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
36
|
+
res.setHeader('Connection', 'keep-alive');
|
|
37
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
38
|
+
|
|
39
|
+
// Send retry interval
|
|
40
|
+
res.write('retry: 3000\n\n');
|
|
41
|
+
|
|
42
|
+
// Send initial data
|
|
43
|
+
try {
|
|
44
|
+
// Send connection status
|
|
45
|
+
res.write(`data: ${JSON.stringify({
|
|
46
|
+
type: 'status',
|
|
47
|
+
cliConnected: session.cliConnected,
|
|
48
|
+
workflowName: session.workflowName,
|
|
49
|
+
})}\n\n`);
|
|
50
|
+
|
|
51
|
+
// Send existing history
|
|
52
|
+
const history = await getHistory(token);
|
|
53
|
+
res.write(`data: ${JSON.stringify({
|
|
54
|
+
type: 'history',
|
|
55
|
+
entries: history,
|
|
56
|
+
})}\n\n`);
|
|
57
|
+
|
|
58
|
+
// Poll for new events
|
|
59
|
+
const eventsListKey = `${KEYS.events(token)}:list`;
|
|
60
|
+
let lastEventIndex = 0;
|
|
61
|
+
|
|
62
|
+
// Get current list length to start from
|
|
63
|
+
const currentLength = await redis.llen(eventsListKey);
|
|
64
|
+
lastEventIndex = currentLength;
|
|
65
|
+
|
|
66
|
+
const pollInterval = setInterval(async () => {
|
|
67
|
+
try {
|
|
68
|
+
// Refresh session TTL
|
|
69
|
+
await refreshSession(token);
|
|
70
|
+
|
|
71
|
+
// Check for new events
|
|
72
|
+
const newLength = await redis.llen(eventsListKey);
|
|
73
|
+
|
|
74
|
+
if (newLength > lastEventIndex) {
|
|
75
|
+
// Get new events (newest first)
|
|
76
|
+
const newEvents = await redis.lrange(eventsListKey, 0, newLength - lastEventIndex - 1);
|
|
77
|
+
|
|
78
|
+
for (const event of newEvents.reverse()) {
|
|
79
|
+
const eventData = typeof event === 'object' ? event : JSON.parse(event);
|
|
80
|
+
res.write(`data: ${JSON.stringify(eventData)}\n\n`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
lastEventIndex = newLength;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check CLI status
|
|
87
|
+
const updatedSession = await getSession(token);
|
|
88
|
+
if (updatedSession && updatedSession.cliConnected !== session.cliConnected) {
|
|
89
|
+
session.cliConnected = updatedSession.cliConnected;
|
|
90
|
+
res.write(`data: ${JSON.stringify({
|
|
91
|
+
type: updatedSession.cliConnected ? 'cli_reconnected' : 'cli_disconnected',
|
|
92
|
+
})}\n\n`);
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error('Error polling events:', err);
|
|
96
|
+
}
|
|
97
|
+
}, 2000);
|
|
98
|
+
|
|
99
|
+
// Clean up on client disconnect
|
|
100
|
+
req.on('close', () => {
|
|
101
|
+
clearInterval(pollInterval);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// For Vercel, we need to keep the connection alive but also respect function timeout
|
|
105
|
+
// Send keepalive pings
|
|
106
|
+
const keepaliveInterval = setInterval(() => {
|
|
107
|
+
res.write(': keepalive\n\n');
|
|
108
|
+
}, 15000);
|
|
109
|
+
|
|
110
|
+
req.on('close', () => {
|
|
111
|
+
clearInterval(keepaliveInterval);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error('SSE error:', err);
|
|
116
|
+
res.write(`data: ${JSON.stringify({ type: 'error', message: err.message })}\n\n`);
|
|
117
|
+
res.end();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /vercel-server/api/history/[token].js
|
|
3
|
+
*
|
|
4
|
+
* REST endpoint to get session history
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getSession, getHistory, refreshSession } from '../../lib/redis.js';
|
|
8
|
+
|
|
9
|
+
export default async function handler(req, res) {
|
|
10
|
+
// Enable CORS
|
|
11
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
12
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
13
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
14
|
+
|
|
15
|
+
if (req.method === 'OPTIONS') {
|
|
16
|
+
return res.status(200).end();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (req.method !== 'GET') {
|
|
20
|
+
return res.status(405).json({ error: 'Method not allowed' });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { token } = req.query;
|
|
24
|
+
|
|
25
|
+
if (!token) {
|
|
26
|
+
return res.status(400).json({ error: 'Missing token parameter' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Validate session
|
|
31
|
+
const session = await getSession(token);
|
|
32
|
+
if (!session) {
|
|
33
|
+
return res.status(404).json({ error: 'Session not found or expired' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Refresh session TTL
|
|
37
|
+
await refreshSession(token);
|
|
38
|
+
|
|
39
|
+
// Get history
|
|
40
|
+
const entries = await getHistory(token);
|
|
41
|
+
|
|
42
|
+
return res.status(200).json({
|
|
43
|
+
workflowName: session.workflowName,
|
|
44
|
+
cliConnected: session.cliConnected,
|
|
45
|
+
entries,
|
|
46
|
+
});
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error('Error getting history:', err);
|
|
49
|
+
return res.status(500).json({ error: err.message });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// /vercel-server/api/session/[token].js
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { readFile } from 'fs/promises';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
import { getSession } from '../../lib/redis.js';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
let cachedTemplate = null;
|
|
12
|
+
async function getTemplate() {
|
|
13
|
+
if (cachedTemplate) return cachedTemplate;
|
|
14
|
+
// Point to the unified template in vercel-server/ui/index.html
|
|
15
|
+
const templatePath = path.join(__dirname, '..', '..', 'ui', 'index.html');
|
|
16
|
+
cachedTemplate = await readFile(templatePath, 'utf8');
|
|
17
|
+
return cachedTemplate;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default async function handler(req, res) {
|
|
21
|
+
const { token } = req.query;
|
|
22
|
+
|
|
23
|
+
if (!token) return res.status(400).send('Missing session token');
|
|
24
|
+
|
|
25
|
+
const session = await getSession(token);
|
|
26
|
+
if (!session) {
|
|
27
|
+
return res.status(404).send(`<!DOCTYPE html>
|
|
28
|
+
<html>
|
|
29
|
+
<head>
|
|
30
|
+
<title>Session Not Found</title>
|
|
31
|
+
<style>body { font-family: system-ui; max-width: 600px; margin: 100px auto; text-align: center; }</style>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<h1>Session Not Found</h1>
|
|
35
|
+
<p>This session has expired or does not exist.</p>
|
|
36
|
+
<p>Sessions expire 30 minutes after the last activity.</p>
|
|
37
|
+
</body>
|
|
38
|
+
</html>`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const template = await getTemplate();
|
|
42
|
+
|
|
43
|
+
const html = template
|
|
44
|
+
.replace(/\{\{SESSION_TOKEN\}\}/g, token)
|
|
45
|
+
.replace(/\{\{WORKFLOW_NAME\}\}/g, session.workflowName || 'Workflow');
|
|
46
|
+
|
|
47
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
48
|
+
res.send(html);
|
|
49
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /vercel-server/api/submit/[token].js
|
|
3
|
+
*
|
|
4
|
+
* POST endpoint for browser interaction submissions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getSession,
|
|
9
|
+
addHistoryEvent,
|
|
10
|
+
publishEvent,
|
|
11
|
+
redis,
|
|
12
|
+
KEYS,
|
|
13
|
+
} from '../../lib/redis.js';
|
|
14
|
+
|
|
15
|
+
export default async function handler(req, res) {
|
|
16
|
+
// Enable CORS
|
|
17
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
18
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
19
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
20
|
+
|
|
21
|
+
if (req.method === 'OPTIONS') {
|
|
22
|
+
return res.status(200).end();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (req.method !== 'POST') {
|
|
26
|
+
return res.status(405).json({ error: 'Method not allowed' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { token } = req.query;
|
|
30
|
+
|
|
31
|
+
if (!token) {
|
|
32
|
+
return res.status(400).json({ error: 'Missing token parameter' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Validate session
|
|
37
|
+
const session = await getSession(token);
|
|
38
|
+
if (!session) {
|
|
39
|
+
return res.status(404).json({ error: 'Session not found or expired' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if CLI is connected
|
|
43
|
+
if (!session.cliConnected) {
|
|
44
|
+
return res.status(503).json({ error: 'CLI is disconnected. Cannot submit interaction.' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Parse body
|
|
48
|
+
const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
|
|
49
|
+
const { slug, targetKey, response } = body;
|
|
50
|
+
|
|
51
|
+
if (!slug || !response) {
|
|
52
|
+
return res.status(400).json({ error: 'Missing required fields: slug, response' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validate response size (max 1MB)
|
|
56
|
+
if (response.length > 1024 * 1024) {
|
|
57
|
+
return res.status(413).json({ error: 'Response too large (max 1MB)' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Push to pending interactions list (for CLI to poll)
|
|
61
|
+
const pendingKey = `${KEYS.interactions(token)}:pending`;
|
|
62
|
+
await redis.rpush(pendingKey, JSON.stringify({
|
|
63
|
+
slug,
|
|
64
|
+
targetKey: targetKey || `_interaction_${slug}`,
|
|
65
|
+
response,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
// Set TTL on pending list
|
|
69
|
+
await redis.expire(pendingKey, 300); // 5 minutes
|
|
70
|
+
|
|
71
|
+
// Log event to history (include answer preview)
|
|
72
|
+
const event = {
|
|
73
|
+
timestamp: new Date().toISOString(),
|
|
74
|
+
event: 'INTERACTION_SUBMITTED',
|
|
75
|
+
slug,
|
|
76
|
+
targetKey: targetKey || `_interaction_${slug}`,
|
|
77
|
+
answer: response.substring(0, 200) + (response.length > 200 ? '...' : ''),
|
|
78
|
+
source: 'remote',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
await addHistoryEvent(token, event);
|
|
82
|
+
|
|
83
|
+
// Notify other browsers
|
|
84
|
+
await publishEvent(token, {
|
|
85
|
+
type: 'event',
|
|
86
|
+
...event,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return res.status(200).json({ success: true });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error('Error submitting interaction:', err);
|
|
92
|
+
return res.status(500).json({ error: err.message });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /vercel-server/api/ws/cli.js
|
|
3
|
+
*
|
|
4
|
+
* HTTP-based endpoint for CLI communication (replaces WebSocket for serverless)
|
|
5
|
+
*
|
|
6
|
+
* POST: Receive messages from CLI (session_init, event, session_end)
|
|
7
|
+
* GET: Long-poll for interaction responses
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createSession,
|
|
12
|
+
getSession,
|
|
13
|
+
updateSession,
|
|
14
|
+
setCLIConnected,
|
|
15
|
+
addHistoryEvent,
|
|
16
|
+
addHistoryEvents,
|
|
17
|
+
publishEvent,
|
|
18
|
+
redis,
|
|
19
|
+
KEYS,
|
|
20
|
+
} from '../../lib/redis.js';
|
|
21
|
+
|
|
22
|
+
export default async function handler(req, res) {
|
|
23
|
+
// Enable CORS
|
|
24
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
25
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
26
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
27
|
+
|
|
28
|
+
if (req.method === 'OPTIONS') {
|
|
29
|
+
return res.status(200).end();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (req.method === 'POST') {
|
|
33
|
+
return handlePost(req, res);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (req.method === 'GET') {
|
|
37
|
+
return handleGet(req, res);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return res.status(405).json({ error: 'Method not allowed' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Handle POST requests from CLI
|
|
45
|
+
*/
|
|
46
|
+
async function handlePost(req, res) {
|
|
47
|
+
const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
|
|
48
|
+
const { type, sessionToken } = body;
|
|
49
|
+
|
|
50
|
+
if (!sessionToken) {
|
|
51
|
+
return res.status(400).json({ error: 'Missing sessionToken' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
switch (type) {
|
|
56
|
+
case 'session_init': {
|
|
57
|
+
const { workflowName, history } = body;
|
|
58
|
+
|
|
59
|
+
// Create session
|
|
60
|
+
await createSession(sessionToken, { workflowName, cliConnected: true });
|
|
61
|
+
|
|
62
|
+
// Store initial history
|
|
63
|
+
if (history && history.length > 0) {
|
|
64
|
+
await addHistoryEvents(sessionToken, history);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Notify browsers
|
|
68
|
+
await publishEvent(sessionToken, {
|
|
69
|
+
type: 'cli_connected',
|
|
70
|
+
workflowName,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return res.status(200).json({ success: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case 'session_reconnect': {
|
|
77
|
+
const { workflowName } = body;
|
|
78
|
+
|
|
79
|
+
// Update session as connected
|
|
80
|
+
await setCLIConnected(sessionToken, true);
|
|
81
|
+
|
|
82
|
+
// Notify browsers
|
|
83
|
+
await publishEvent(sessionToken, {
|
|
84
|
+
type: 'cli_reconnected',
|
|
85
|
+
workflowName,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return res.status(200).json({ success: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case 'event': {
|
|
92
|
+
const { timestamp, event, ...eventData } = body;
|
|
93
|
+
|
|
94
|
+
const historyEvent = {
|
|
95
|
+
timestamp: timestamp || new Date().toISOString(),
|
|
96
|
+
event,
|
|
97
|
+
...eventData,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Remove sessionToken and type from event data
|
|
101
|
+
delete historyEvent.sessionToken;
|
|
102
|
+
delete historyEvent.type;
|
|
103
|
+
|
|
104
|
+
// Add to history
|
|
105
|
+
await addHistoryEvent(sessionToken, historyEvent);
|
|
106
|
+
|
|
107
|
+
// Notify browsers
|
|
108
|
+
await publishEvent(sessionToken, {
|
|
109
|
+
type: 'event',
|
|
110
|
+
...historyEvent,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return res.status(200).json({ success: true });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case 'session_end': {
|
|
117
|
+
const { reason } = body;
|
|
118
|
+
|
|
119
|
+
// Mark CLI as disconnected
|
|
120
|
+
await setCLIConnected(sessionToken, false);
|
|
121
|
+
|
|
122
|
+
// Notify browsers
|
|
123
|
+
await publishEvent(sessionToken, {
|
|
124
|
+
type: 'cli_disconnected',
|
|
125
|
+
reason,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return res.status(200).json({ success: true });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
default:
|
|
132
|
+
return res.status(400).json({ error: `Unknown message type: ${type}` });
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error('Error handling CLI message:', err);
|
|
136
|
+
return res.status(500).json({ error: err.message });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Handle GET requests - long-poll for interaction responses
|
|
142
|
+
*/
|
|
143
|
+
async function handleGet(req, res) {
|
|
144
|
+
const { token, timeout = '30000' } = req.query;
|
|
145
|
+
|
|
146
|
+
if (!token) {
|
|
147
|
+
return res.status(400).json({ error: 'Missing token parameter' });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const session = await getSession(token);
|
|
151
|
+
if (!session) {
|
|
152
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const timeoutMs = Math.min(parseInt(timeout, 10), 55000); // Max 55s for Vercel
|
|
156
|
+
const channel = KEYS.interactions(token);
|
|
157
|
+
|
|
158
|
+
// Check for pending interactions using a list
|
|
159
|
+
const pendingKey = `${channel}:pending`;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Try to get a pending interaction
|
|
163
|
+
const startTime = Date.now();
|
|
164
|
+
|
|
165
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
166
|
+
const pending = await redis.lpop(pendingKey);
|
|
167
|
+
|
|
168
|
+
if (pending) {
|
|
169
|
+
const data = typeof pending === 'object' ? pending : JSON.parse(pending);
|
|
170
|
+
return res.status(200).json({
|
|
171
|
+
type: 'interaction_response',
|
|
172
|
+
...data,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Wait before checking again
|
|
177
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Timeout - no interaction received
|
|
181
|
+
return res.status(204).end();
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error('Error polling for interactions:', err);
|
|
184
|
+
return res.status(500).json({ error: err.message });
|
|
185
|
+
}
|
|
186
|
+
}
|