agent-state-machine 1.1.0 → 1.3.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 +17 -12
- package/bin/cli.js +7 -9
- package/lib/runtime/agent.js +2 -1
- package/lib/runtime/prompt.js +2 -1
- package/lib/setup.js +26 -16
- package/lib/ui/index.html +92 -27
- package/lib/ui/server.js +35 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,11 +44,11 @@ Requirements: Node.js >= 16.
|
|
|
44
44
|
```bash
|
|
45
45
|
state-machine --setup <workflow-name>
|
|
46
46
|
state-machine run <workflow-name>
|
|
47
|
-
|
|
48
|
-
state-machine
|
|
47
|
+
|
|
48
|
+
state-machine follow <workflow-name> (view prompt trace history in browser with live updates)
|
|
49
49
|
state-machine history <workflow-name> [limit]
|
|
50
|
-
state-machine
|
|
51
|
-
state-machine reset <workflow-name>
|
|
50
|
+
state-machine reset <workflow-name> (clears memory/state)
|
|
51
|
+
state-machine reset-hard <workflow-name> (clears everything: history/interactions/memory)
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Workflows live in:
|
|
@@ -120,24 +120,29 @@ export default async function() {
|
|
|
120
120
|
// await agent('yoda-greeter', userInfo);
|
|
121
121
|
|
|
122
122
|
// Example: Parallel execution
|
|
123
|
-
// const [a, b] = await parallel([
|
|
124
|
-
// agent('
|
|
125
|
-
// agent('
|
|
123
|
+
// const [a, b, c] = await parallel([
|
|
124
|
+
// agent('yoda-greeter', { name: 'the names augustus but friends call me gus' }),
|
|
125
|
+
// agent('yoda-greeter', { name: 'uriah' }),
|
|
126
|
+
// agent('yoda-greeter', { name: 'lucas' })
|
|
126
127
|
// ]);
|
|
127
128
|
|
|
129
|
+
// console.log('a: ' + JSON.stringify(a))
|
|
130
|
+
// console.log('b: ' + JSON.stringify(b))
|
|
131
|
+
// console.log('c: ' + JSON.stringify(c))
|
|
132
|
+
|
|
128
133
|
notify(['project-builder', userInfo.name || userInfo + ' has been greeted!']);
|
|
129
134
|
|
|
130
135
|
console.log('Workflow completed!');
|
|
131
136
|
}
|
|
132
137
|
```
|
|
133
138
|
|
|
134
|
-
###
|
|
139
|
+
### Resuming workflows
|
|
135
140
|
|
|
136
|
-
`
|
|
141
|
+
`state-machine run` restarts your workflow from the top, loading the persisted state.
|
|
137
142
|
|
|
138
143
|
If the workflow needs human input, it will **block inline** in the terminal. You’ll be told which `interactions/<slug>.md` file to edit; after you fill it in, press `y` in the same terminal session to continue.
|
|
139
144
|
|
|
140
|
-
If the process is interrupted, running `state-machine
|
|
145
|
+
If the process is interrupted, running `state-machine run <workflow-name>` again will continue execution (assuming your workflow uses `memory` to skip completed steps).
|
|
141
146
|
|
|
142
147
|
---
|
|
143
148
|
|
|
@@ -293,10 +298,10 @@ export const config = {
|
|
|
293
298
|
};
|
|
294
299
|
```
|
|
295
300
|
|
|
296
|
-
The runtime captures the fully-built prompt in `state/history.jsonl`, viewable via:
|
|
301
|
+
The runtime captures the fully-built prompt in `state/history.jsonl`, viewable in the browser with live updates via:
|
|
297
302
|
|
|
298
303
|
```bash
|
|
299
|
-
state-machine
|
|
304
|
+
state-machine follow <workflow-name>
|
|
300
305
|
```
|
|
301
306
|
|
|
302
307
|
---
|
package/bin/cli.js
CHANGED
|
@@ -29,13 +29,12 @@ Agent State Machine CLI (Native JS Workflows Only) v${getVersion()}
|
|
|
29
29
|
|
|
30
30
|
Usage:
|
|
31
31
|
state-machine --setup <workflow-name> Create a new workflow project
|
|
32
|
-
state-machine run <workflow-name> Run a workflow
|
|
33
|
-
state-machine
|
|
32
|
+
state-machine run <workflow-name> Run a workflow (loads existing state)
|
|
33
|
+
state-machine follow <workflow-name> View prompt trace history in browser with live updates
|
|
34
34
|
state-machine status [workflow-name] Show current state (or list all)
|
|
35
|
-
state-machine history <workflow-name> [limit] Show execution history
|
|
36
|
-
state-machine
|
|
37
|
-
state-machine reset <workflow-name>
|
|
38
|
-
state-machine reset-hard <workflow-name> Hard reset (clear history/interactions)
|
|
35
|
+
state-machine history <workflow-name> [limit] Show execution history logs
|
|
36
|
+
state-machine reset <workflow-name> Reset workflow state (clears memory/state)
|
|
37
|
+
state-machine reset-hard <workflow-name> Hard reset (clears everything: history/interactions/memory)
|
|
39
38
|
state-machine list List all workflows
|
|
40
39
|
state-machine help Show this help
|
|
41
40
|
|
|
@@ -184,7 +183,6 @@ async function main() {
|
|
|
184
183
|
|
|
185
184
|
switch (command) {
|
|
186
185
|
case 'run':
|
|
187
|
-
case 'resume':
|
|
188
186
|
if (!workflowName) {
|
|
189
187
|
console.error('Error: Workflow name required');
|
|
190
188
|
console.error(`Usage: state-machine ${command} <workflow-name>`);
|
|
@@ -224,10 +222,10 @@ async function main() {
|
|
|
224
222
|
}
|
|
225
223
|
break;
|
|
226
224
|
|
|
227
|
-
case '
|
|
225
|
+
case 'follow':
|
|
228
226
|
if (!workflowName) {
|
|
229
227
|
console.error('Error: Workflow name required');
|
|
230
|
-
console.error('Usage: state-machine
|
|
228
|
+
console.error('Usage: state-machine follow <workflow-name>');
|
|
231
229
|
process.exit(1);
|
|
232
230
|
}
|
|
233
231
|
{
|
package/lib/runtime/agent.js
CHANGED
package/lib/runtime/prompt.js
CHANGED
package/lib/setup.js
CHANGED
|
@@ -99,11 +99,16 @@ export default async function() {
|
|
|
99
99
|
// await agent('yoda-greeter', userInfo);
|
|
100
100
|
|
|
101
101
|
// Example: Parallel execution
|
|
102
|
-
// const [a, b] = await parallel([
|
|
103
|
-
// agent('
|
|
104
|
-
// agent('
|
|
102
|
+
// const [a, b, c] = await parallel([
|
|
103
|
+
// agent('yoda-greeter', { name: 'the names augustus but friends call me gus' }),
|
|
104
|
+
// agent('yoda-greeter', { name: 'uriah' }),
|
|
105
|
+
// agent('yoda-greeter', { name: 'lucas' })
|
|
105
106
|
// ]);
|
|
106
107
|
|
|
108
|
+
// console.log('a: ' + JSON.stringify(a))
|
|
109
|
+
// console.log('b: ' + JSON.stringify(b))
|
|
110
|
+
// console.log('c: ' + JSON.stringify(c))
|
|
111
|
+
|
|
107
112
|
notify(['${workflowName}', userInfo.name || userInfo + ' has been greeted!']);
|
|
108
113
|
|
|
109
114
|
console.log('Workflow completed!');
|
|
@@ -173,7 +178,7 @@ Once you have it create a yoda-greeting.md file in root dir with the greeting.
|
|
|
173
178
|
You are a fast, direct worker. Do NOT investigate the codebase or read files unless strictly necessary. Perform the requested action immediately using the provided context. Avoid "thinking" steps or creating plans if the task is simple.
|
|
174
179
|
`;
|
|
175
180
|
|
|
176
|
-
const yodaNameCollectorAgent = `---
|
|
181
|
+
const yodaNameCollectorAgent = `---
|
|
177
182
|
model: low
|
|
178
183
|
output: name
|
|
179
184
|
---
|
|
@@ -294,16 +299,11 @@ ${workflowName}/
|
|
|
294
299
|
|
|
295
300
|
## Usage
|
|
296
301
|
|
|
297
|
-
Run the workflow:
|
|
302
|
+
Run the workflow (or resume if interrupted):
|
|
298
303
|
\\\`\\\`\\\`bash
|
|
299
304
|
state-machine run ${workflowName}
|
|
300
305
|
\\\`\\\`\\\`
|
|
301
306
|
|
|
302
|
-
Resume a paused workflow:
|
|
303
|
-
\\\`\\\`\\\`bash
|
|
304
|
-
state-machine resume ${workflowName}
|
|
305
|
-
\\\`\\\`\\\`
|
|
306
|
-
|
|
307
307
|
Check status:
|
|
308
308
|
\\\`\\\`\\\`bash
|
|
309
309
|
state-machine status ${workflowName}
|
|
@@ -314,16 +314,21 @@ View history:
|
|
|
314
314
|
state-machine history ${workflowName}
|
|
315
315
|
\\\`\\\`\\\`
|
|
316
316
|
|
|
317
|
-
View trace logs in browser:
|
|
317
|
+
View trace logs in browser with live updates:
|
|
318
318
|
\\\`\\\`\\\`bash
|
|
319
|
-
state-machine
|
|
319
|
+
state-machine follow ${workflowName}
|
|
320
320
|
\\\`\\\`\\\`
|
|
321
321
|
|
|
322
|
-
Reset state:
|
|
322
|
+
Reset state (clears memory/state):
|
|
323
323
|
\\\`\\\`\\\`bash
|
|
324
324
|
state-machine reset ${workflowName}
|
|
325
325
|
\\\`\\\`\\\`
|
|
326
326
|
|
|
327
|
+
Hard reset (clears everything: history/interactions/memory):
|
|
328
|
+
\\\`\\\`\\\`bash
|
|
329
|
+
state-machine reset-hard ${workflowName}
|
|
330
|
+
\\\`\\\`\\\`
|
|
331
|
+
|
|
327
332
|
## Writing Workflows
|
|
328
333
|
|
|
329
334
|
Edit \`workflow.js\` - write normal async JavaScript:
|
|
@@ -354,11 +359,16 @@ export default async function() {
|
|
|
354
359
|
// await agent('yoda-greeter', userInfo);
|
|
355
360
|
|
|
356
361
|
// Example: Parallel execution
|
|
357
|
-
// const [a, b] = await parallel([
|
|
358
|
-
// agent('
|
|
359
|
-
// agent('
|
|
362
|
+
// const [a, b, c] = await parallel([
|
|
363
|
+
// agent('yoda-greeter', { name: 'the names augustus but friends call me gus' }),
|
|
364
|
+
// agent('yoda-greeter', { name: 'uriah' }),
|
|
365
|
+
// agent('yoda-greeter', { name: 'lucas' })
|
|
360
366
|
// ]);
|
|
361
367
|
|
|
368
|
+
// console.log('a: ' + JSON.stringify(a))
|
|
369
|
+
// console.log('b: ' + JSON.stringify(b))
|
|
370
|
+
// console.log('c: ' + JSON.stringify(c))
|
|
371
|
+
|
|
362
372
|
notify(['project-builder', userInfo.name || userInfo + ' has been greeted!']);
|
|
363
373
|
|
|
364
374
|
console.log('Workflow completed!');
|
package/lib/ui/index.html
CHANGED
|
@@ -41,20 +41,40 @@
|
|
|
41
41
|
</svg>
|
|
42
42
|
);
|
|
43
43
|
|
|
44
|
-
const
|
|
45
|
-
<svg xmlns="http://www.w3.org/2000/svg" className="h-
|
|
46
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="
|
|
44
|
+
const CopyIcon = () => (
|
|
45
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
46
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
47
47
|
</svg>
|
|
48
48
|
);
|
|
49
49
|
|
|
50
|
-
const
|
|
51
|
-
<svg xmlns="http://www.w3.org/2000/svg" className="h-
|
|
52
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="
|
|
53
|
-
{/* Simple Down Arrow for Newest Top */}
|
|
54
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
|
50
|
+
const CheckIcon = () => (
|
|
51
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
52
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
55
53
|
</svg>
|
|
56
54
|
);
|
|
57
55
|
|
|
56
|
+
function CopyButton({ text, className }) {
|
|
57
|
+
const [copied, setCopied] = useState(false);
|
|
58
|
+
|
|
59
|
+
const handleCopy = () => {
|
|
60
|
+
const content = typeof text === 'object' ? JSON.stringify(text, null, 2) : text;
|
|
61
|
+
navigator.clipboard.writeText(content);
|
|
62
|
+
setCopied(true);
|
|
63
|
+
setTimeout(() => setCopied(false), 2000);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<button
|
|
68
|
+
onClick={handleCopy}
|
|
69
|
+
className={`flex items-center space-x-1 text-[9px] uppercase tracking-wider transition-colors hover:text-blue-500 focus:outline-none ${className}`}
|
|
70
|
+
title="Copy to clipboard"
|
|
71
|
+
>
|
|
72
|
+
{copied ? <CheckIcon /> : <CopyIcon />}
|
|
73
|
+
<span>{copied ? 'Copied' : 'Copy'}</span>
|
|
74
|
+
</button>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
function App() {
|
|
59
79
|
const [history, setHistory] = useState([]);
|
|
60
80
|
const [loading, setLoading] = useState(true);
|
|
@@ -118,14 +138,9 @@
|
|
|
118
138
|
</div>
|
|
119
139
|
);
|
|
120
140
|
|
|
121
|
-
// Filter for events we want to display
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
'WORKFLOW_STARTED', 'WORKFLOW_COMPLETED', 'WORKFLOW_FAILED', 'WORKFLOW_RESET',
|
|
125
|
-
'AGENT_STARTED', 'AGENT_COMPLETED', 'AGENT_FAILED',
|
|
126
|
-
'INTERACTION_REQUESTED', 'INTERACTION_RESOLVED'
|
|
127
|
-
].includes(item.event)
|
|
128
|
-
);
|
|
141
|
+
// Filter for events we want to display - NOW INCLUDES EVERYTHING
|
|
142
|
+
// We only filter out nulls or malformed entries if any
|
|
143
|
+
let visibleEvents = history;
|
|
129
144
|
|
|
130
145
|
// Apply Sort
|
|
131
146
|
// History from API is "Newest First" (index 0 is latest)
|
|
@@ -222,19 +237,40 @@
|
|
|
222
237
|
);
|
|
223
238
|
}
|
|
224
239
|
|
|
225
|
-
// 4. Interaction Requested
|
|
226
|
-
if (item.event === 'INTERACTION_REQUESTED') {
|
|
240
|
+
// 4. Interaction / Prompt Requested
|
|
241
|
+
if (item.event === 'INTERACTION_REQUESTED' || item.event === 'PROMPT_REQUESTED') {
|
|
242
|
+
const isPrompt = item.event === 'PROMPT_REQUESTED';
|
|
227
243
|
return (
|
|
228
244
|
<div key={idx} className="flex justify-center">
|
|
229
245
|
<div className="bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 border-dashed rounded-lg px-6 py-4 text-center max-w-md w-full">
|
|
230
|
-
<div className="text-[10px] text-zinc-400 dark:text-zinc-600 uppercase font-bold tracking-widest mb-1">
|
|
231
|
-
|
|
246
|
+
<div className="text-[10px] text-zinc-400 dark:text-zinc-600 uppercase font-bold tracking-widest mb-1">
|
|
247
|
+
{isPrompt ? 'User Input Requested' : 'Human Intervention Needed'}
|
|
248
|
+
</div>
|
|
249
|
+
<div className="text-xs text-zinc-600 dark:text-zinc-400 italic">
|
|
250
|
+
{item.question ? `"${item.question}"` : `Waiting for response to "${item.slug}"...`}
|
|
251
|
+
</div>
|
|
232
252
|
</div>
|
|
233
253
|
</div>
|
|
234
254
|
);
|
|
235
255
|
}
|
|
236
256
|
|
|
237
|
-
// 5.
|
|
257
|
+
// 5. Prompt Answered
|
|
258
|
+
if (item.event === 'PROMPT_ANSWERED') {
|
|
259
|
+
return (
|
|
260
|
+
<div key={idx} className="flex justify-center">
|
|
261
|
+
<div className="bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-900/50 rounded-lg px-4 py-2 text-center max-w-md w-full">
|
|
262
|
+
<div className="text-[10px] text-green-600 dark:text-green-400 uppercase font-bold tracking-widest mb-1">
|
|
263
|
+
User Answered
|
|
264
|
+
</div>
|
|
265
|
+
<div className="text-xs text-green-700 dark:text-green-300 italic font-medium">
|
|
266
|
+
"{item.answer}"
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 6. Agent Completed / Interaction Resolved (The Bubbles)
|
|
238
274
|
if (item.event === 'AGENT_COMPLETED' || item.event === 'INTERACTION_RESOLVED') {
|
|
239
275
|
return (
|
|
240
276
|
<div key={idx} className="flex flex-col space-y-4">
|
|
@@ -250,8 +286,13 @@
|
|
|
250
286
|
{/* Output (Response) - NOW ON TOP */}
|
|
251
287
|
{(item.output || item.result) && (
|
|
252
288
|
<div className="flex justify-end w-full group">
|
|
253
|
-
<div className="max-w-[85%] bg-blue-50 dark:bg-blue-950/20 border border-blue-100 dark:border-blue-900/40 rounded-2xl rounded-tr-none shadow-sm p-6 transition-all hover:border-blue-200 dark:hover:border-blue-800">
|
|
254
|
-
<div className="
|
|
289
|
+
<div className="max-w-[85%] bg-blue-50 dark:bg-blue-950/20 border border-blue-100 dark:border-blue-900/40 rounded-2xl rounded-tr-none shadow-sm p-6 transition-all hover:border-blue-200 dark:hover:border-blue-800 relative">
|
|
290
|
+
<div className="flex justify-between items-center mb-3">
|
|
291
|
+
<div className="text-[9px] font-black text-blue-300 dark:text-blue-800/60 uppercase tracking-[0.2em] text-right w-full">Output / Response</div>
|
|
292
|
+
<div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
293
|
+
<CopyButton text={item.output || item.result} className="text-blue-400 hover:text-blue-600 dark:text-blue-600 dark:hover:text-blue-400" />
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
255
296
|
<div className="markdown-body text-gray-800 dark:text-zinc-200 text-sm overflow-x-auto leading-relaxed">
|
|
256
297
|
{typeof item.output === 'object' ? JSON.stringify(item.output, null, 2) : (item.output || item.result)}
|
|
257
298
|
</div>
|
|
@@ -262,8 +303,13 @@
|
|
|
262
303
|
{/* Prompt (Input) - NOW ON BOTTOM */}
|
|
263
304
|
{item.prompt && (
|
|
264
305
|
<div className="flex justify-start w-full group">
|
|
265
|
-
<div className="max-w-[85%] bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl rounded-tl-none shadow-sm p-6 transition-all hover:border-zinc-300 dark:hover:border-zinc-700">
|
|
266
|
-
<div className="
|
|
306
|
+
<div className="max-w-[85%] bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl rounded-tl-none shadow-sm p-6 transition-all hover:border-zinc-300 dark:hover:border-zinc-700 relative">
|
|
307
|
+
<div className="flex justify-between items-center mb-3">
|
|
308
|
+
<div className="text-[9px] font-black text-zinc-300 dark:text-zinc-700 uppercase tracking-[0.2em]">Prompt / Input</div>
|
|
309
|
+
<div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
310
|
+
<CopyButton text={item.prompt} className="text-gray-400 hover:text-gray-600 dark:text-zinc-600 dark:hover:text-zinc-400" />
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
267
313
|
<div className="markdown-body text-gray-800 dark:text-zinc-300 text-sm overflow-x-auto leading-relaxed">
|
|
268
314
|
{item.prompt}
|
|
269
315
|
</div>
|
|
@@ -274,7 +320,26 @@
|
|
|
274
320
|
);
|
|
275
321
|
}
|
|
276
322
|
|
|
277
|
-
|
|
323
|
+
// 7. CATCH-ALL for Unknown Events
|
|
324
|
+
// If we made it here, it's an event we didn't explicitly handle.
|
|
325
|
+
// Render it generically.
|
|
326
|
+
return (
|
|
327
|
+
<div key={idx} className="flex justify-center px-4">
|
|
328
|
+
<div className="bg-gray-100 dark:bg-zinc-900/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-4 py-3 text-xs w-full max-w-2xl font-mono overflow-x-auto">
|
|
329
|
+
<div className="text-[9px] text-gray-400 dark:text-zinc-600 uppercase tracking-widest mb-1 flex justify-between">
|
|
330
|
+
<span>{item.event}</span>
|
|
331
|
+
<span>{formatTime(item.timestamp)}</span>
|
|
332
|
+
</div>
|
|
333
|
+
<div className="text-gray-600 dark:text-zinc-400 whitespace-pre-wrap">
|
|
334
|
+
{JSON.stringify(item, (key, value) => {
|
|
335
|
+
// Exclude redundant keys to keep it clean
|
|
336
|
+
if (key === 'event' || key === 'timestamp') return undefined;
|
|
337
|
+
return value;
|
|
338
|
+
}, 2).replace(/^{|}$/g, '').trim()}
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
278
343
|
})}
|
|
279
344
|
</div>
|
|
280
345
|
|
|
@@ -291,4 +356,4 @@
|
|
|
291
356
|
root.render(<App />);
|
|
292
357
|
</script>
|
|
293
358
|
</body>
|
|
294
|
-
</html>
|
|
359
|
+
</html>
|
package/lib/ui/server.js
CHANGED
|
@@ -10,7 +10,7 @@ import { fileURLToPath } from 'url';
|
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
11
|
const __dirname = path.dirname(__filename);
|
|
12
12
|
|
|
13
|
-
export function startServer(workflowDir,
|
|
13
|
+
export function startServer(workflowDir, initialPort = 3000) {
|
|
14
14
|
const clients = new Set();
|
|
15
15
|
const stateDir = path.join(workflowDir, 'state');
|
|
16
16
|
|
|
@@ -45,7 +45,8 @@ export function startServer(workflowDir, port = 3000) {
|
|
|
45
45
|
console.warn('Warning: Failed to setup file watcher:', err.message);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
// Request Handler
|
|
49
|
+
const requestHandler = (req, res) => {
|
|
49
50
|
// Serve the main HTML page
|
|
50
51
|
if (req.url === '/' || req.url === '/index.html') {
|
|
51
52
|
const htmlPath = path.join(__dirname, 'index.html');
|
|
@@ -115,11 +116,35 @@ export function startServer(workflowDir, port = 3000) {
|
|
|
115
116
|
// 404
|
|
116
117
|
res.writeHead(404);
|
|
117
118
|
res.end('Not found');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Port hunting logic
|
|
122
|
+
let port = initialPort;
|
|
123
|
+
const maxPort = initialPort + 100; // Try up to 100 ports
|
|
124
|
+
|
|
125
|
+
const attemptServer = () => {
|
|
126
|
+
const server = http.createServer(requestHandler);
|
|
127
|
+
|
|
128
|
+
server.on('error', (e) => {
|
|
129
|
+
if (e.code === 'EADDRINUSE') {
|
|
130
|
+
if (port < maxPort) {
|
|
131
|
+
console.log(`Port ${port} is in use, trying ${port + 1}...`);
|
|
132
|
+
port++;
|
|
133
|
+
attemptServer();
|
|
134
|
+
} else {
|
|
135
|
+
console.error(`Error: Could not find an open port between ${initialPort} and ${maxPort}.`);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
console.error('Server error:', e);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
server.listen(port, () => {
|
|
143
|
+
console.log(`\n> Follow UI running at http://localhost:${port}`);
|
|
144
|
+
console.log(`> Viewing history for: ${workflowDir}`);
|
|
145
|
+
console.log(`> Press Ctrl+C to stop`);
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
attemptServer();
|
|
150
|
+
}
|