agent-state-machine 2.0.1 → 2.0.3
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 +192 -7
- package/lib/remote/client.js +2 -1
- package/lib/runtime/runtime.js +4 -1
- package/package.json +1 -1
- package/vercel-server/ui/index.html +677 -426
package/bin/cli.js
CHANGED
|
@@ -5,6 +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
|
+
import { generateSessionToken } from '../lib/remote/client.js';
|
|
8
9
|
|
|
9
10
|
import { startLocalServer } from '../vercel-server/local-server.js';
|
|
10
11
|
|
|
@@ -35,6 +36,9 @@ Usage:
|
|
|
35
36
|
state-machine --setup <workflow-name> Create a new workflow project
|
|
36
37
|
state-machine run <workflow-name> Run a workflow (remote follow enabled by default)
|
|
37
38
|
state-machine run <workflow-name> -l Run with local server (localhost:3000)
|
|
39
|
+
state-machine run <workflow-name> -n Generate a new remote follow path
|
|
40
|
+
state-machine run <workflow-name> -reset Reset workflow state before running
|
|
41
|
+
state-machine run <workflow-name> -reset-hard Hard reset workflow before running
|
|
38
42
|
|
|
39
43
|
state-machine status [workflow-name] Show current state (or list all)
|
|
40
44
|
state-machine history <workflow-name> [limit] Show execution history logs
|
|
@@ -46,6 +50,9 @@ Usage:
|
|
|
46
50
|
Options:
|
|
47
51
|
--setup, -s Initialize a new workflow with directory structure
|
|
48
52
|
--local, -l Use local server instead of remote (starts on localhost:3000)
|
|
53
|
+
--new, -n Generate a new remote follow path
|
|
54
|
+
-reset Reset workflow state before running
|
|
55
|
+
-reset-hard Hard reset workflow before running
|
|
49
56
|
--help, -h Show help
|
|
50
57
|
--version, -v Show version
|
|
51
58
|
|
|
@@ -80,6 +87,155 @@ function resolveWorkflowEntry(workflowDir) {
|
|
|
80
87
|
return null;
|
|
81
88
|
}
|
|
82
89
|
|
|
90
|
+
function findConfigObjectRange(source) {
|
|
91
|
+
const match = source.match(/export\s+const\s+config\s*=/);
|
|
92
|
+
if (!match) return null;
|
|
93
|
+
const startSearch = match.index + match[0].length;
|
|
94
|
+
const braceStart = source.indexOf('{', startSearch);
|
|
95
|
+
if (braceStart === -1) return null;
|
|
96
|
+
|
|
97
|
+
let depth = 0;
|
|
98
|
+
let inString = null;
|
|
99
|
+
let inLineComment = false;
|
|
100
|
+
let inBlockComment = false;
|
|
101
|
+
let escape = false;
|
|
102
|
+
|
|
103
|
+
for (let i = braceStart; i < source.length; i += 1) {
|
|
104
|
+
const ch = source[i];
|
|
105
|
+
const next = source[i + 1];
|
|
106
|
+
|
|
107
|
+
if (inLineComment) {
|
|
108
|
+
if (ch === '\n') inLineComment = false;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (inBlockComment) {
|
|
113
|
+
if (ch === '*' && next === '/') {
|
|
114
|
+
inBlockComment = false;
|
|
115
|
+
i += 1;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (inString) {
|
|
121
|
+
if (escape) {
|
|
122
|
+
escape = false;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (ch === '\\') {
|
|
126
|
+
escape = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (ch === inString) {
|
|
130
|
+
inString = null;
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (ch === '"' || ch === '\'' || ch === '`') {
|
|
136
|
+
inString = ch;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (ch === '/' && next === '/') {
|
|
141
|
+
inLineComment = true;
|
|
142
|
+
i += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (ch === '/' && next === '*') {
|
|
147
|
+
inBlockComment = true;
|
|
148
|
+
i += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (ch === '{') {
|
|
153
|
+
depth += 1;
|
|
154
|
+
} else if (ch === '}') {
|
|
155
|
+
depth -= 1;
|
|
156
|
+
if (depth === 0) {
|
|
157
|
+
return { start: braceStart, end: i };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readRemotePathFromWorkflow(workflowFile) {
|
|
166
|
+
const source = fs.readFileSync(workflowFile, 'utf-8');
|
|
167
|
+
const range = findConfigObjectRange(source);
|
|
168
|
+
if (!range) return null;
|
|
169
|
+
const configSource = source.slice(range.start, range.end + 1);
|
|
170
|
+
const match = configSource.match(/\bremotePath\s*:\s*(['"`])([^'"`]+)\1/);
|
|
171
|
+
return match ? match[2] : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function writeRemotePathToWorkflow(workflowFile, remotePath) {
|
|
175
|
+
const source = fs.readFileSync(workflowFile, 'utf-8');
|
|
176
|
+
const range = findConfigObjectRange(source);
|
|
177
|
+
const remoteLine = `remotePath: "${remotePath}"`;
|
|
178
|
+
|
|
179
|
+
if (!range) {
|
|
180
|
+
const hasConfigExport = /export\s+const\s+config\s*=/.test(source);
|
|
181
|
+
if (hasConfigExport) {
|
|
182
|
+
throw new Error('Workflow config export is not an object literal; add remotePath manually.');
|
|
183
|
+
}
|
|
184
|
+
const trimmed = source.replace(/\s*$/, '');
|
|
185
|
+
const appended = `${trimmed}\n\nexport const config = {\n ${remoteLine}\n};\n`;
|
|
186
|
+
fs.writeFileSync(workflowFile, appended);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const configSource = source.slice(range.start, range.end + 1);
|
|
191
|
+
const remoteRegex = /\bremotePath\s*:\s*(['"`])([^'"`]*?)\1/;
|
|
192
|
+
let updatedConfigSource;
|
|
193
|
+
|
|
194
|
+
if (remoteRegex.test(configSource)) {
|
|
195
|
+
updatedConfigSource = configSource.replace(remoteRegex, remoteLine);
|
|
196
|
+
} else {
|
|
197
|
+
const inner = configSource.slice(1, -1);
|
|
198
|
+
const indentMatch = inner.match(/\n([ \t]+)\S/);
|
|
199
|
+
const indent = indentMatch ? indentMatch[1] : ' ';
|
|
200
|
+
const trimmedInner = inner.replace(/\s*$/, '');
|
|
201
|
+
const hasContent = trimmedInner.trim().length > 0;
|
|
202
|
+
let updatedInner = trimmedInner;
|
|
203
|
+
|
|
204
|
+
if (hasContent) {
|
|
205
|
+
for (let i = updatedInner.length - 1; i >= 0; i -= 1) {
|
|
206
|
+
const ch = updatedInner[i];
|
|
207
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') continue;
|
|
208
|
+
if (ch !== ',') {
|
|
209
|
+
updatedInner += ',';
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const needsNewline = updatedInner && !updatedInner.endsWith('\n');
|
|
216
|
+
const insert = `${indent}${remoteLine},\n`;
|
|
217
|
+
const newInner = hasContent
|
|
218
|
+
? `${updatedInner}${needsNewline ? '\n' : ''}${insert}`
|
|
219
|
+
: `\n${insert}`;
|
|
220
|
+
updatedConfigSource = `{${newInner}}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const updatedSource =
|
|
224
|
+
source.slice(0, range.start) +
|
|
225
|
+
updatedConfigSource +
|
|
226
|
+
source.slice(range.end + 1);
|
|
227
|
+
fs.writeFileSync(workflowFile, updatedSource);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function ensureRemotePath(workflowFile, { forceNew = false } = {}) {
|
|
231
|
+
const existing = readRemotePathFromWorkflow(workflowFile);
|
|
232
|
+
if (existing && !forceNew) return existing;
|
|
233
|
+
|
|
234
|
+
const remotePath = generateSessionToken();
|
|
235
|
+
writeRemotePathToWorkflow(workflowFile, remotePath);
|
|
236
|
+
return remotePath;
|
|
237
|
+
}
|
|
238
|
+
|
|
83
239
|
function readState(workflowDir) {
|
|
84
240
|
const stateFile = path.join(workflowDir, 'state', 'current.json');
|
|
85
241
|
if (!fs.existsSync(stateFile)) return null;
|
|
@@ -145,7 +301,16 @@ function listWorkflows() {
|
|
|
145
301
|
console.log('');
|
|
146
302
|
}
|
|
147
303
|
|
|
148
|
-
async function runOrResume(
|
|
304
|
+
async function runOrResume(
|
|
305
|
+
workflowName,
|
|
306
|
+
{
|
|
307
|
+
remoteEnabled = false,
|
|
308
|
+
useLocalServer = false,
|
|
309
|
+
forceNewRemotePath = false,
|
|
310
|
+
preReset = false,
|
|
311
|
+
preResetHard = false
|
|
312
|
+
} = {}
|
|
313
|
+
) {
|
|
149
314
|
const workflowDir = resolveWorkflowDir(workflowName);
|
|
150
315
|
|
|
151
316
|
if (!fs.existsSync(workflowDir)) {
|
|
@@ -161,6 +326,12 @@ async function runOrResume(workflowName, { remoteEnabled = false, useLocalServer
|
|
|
161
326
|
}
|
|
162
327
|
|
|
163
328
|
const runtime = new WorkflowRuntime(workflowDir);
|
|
329
|
+
if (preResetHard) {
|
|
330
|
+
runtime.resetHard();
|
|
331
|
+
} else if (preReset) {
|
|
332
|
+
runtime.reset();
|
|
333
|
+
}
|
|
334
|
+
|
|
164
335
|
const workflowUrl = pathToFileURL(entry).href;
|
|
165
336
|
|
|
166
337
|
let localServer = null;
|
|
@@ -183,20 +354,25 @@ async function runOrResume(workflowName, { remoteEnabled = false, useLocalServer
|
|
|
183
354
|
|
|
184
355
|
// Enable remote follow mode if we have a URL
|
|
185
356
|
if (remoteUrl) {
|
|
186
|
-
|
|
357
|
+
const sessionToken = ensureRemotePath(entry, { forceNew: forceNewRemotePath });
|
|
358
|
+
await runtime.enableRemote(remoteUrl, { sessionToken });
|
|
187
359
|
}
|
|
188
360
|
|
|
189
361
|
try {
|
|
190
362
|
await runtime.runWorkflow(workflowUrl);
|
|
191
363
|
} finally {
|
|
192
|
-
//
|
|
193
|
-
if (remoteUrl) {
|
|
364
|
+
// Keep local server alive after run so the session remains accessible.
|
|
365
|
+
if (!useLocalServer && remoteUrl) {
|
|
194
366
|
await runtime.disableRemote();
|
|
195
367
|
}
|
|
196
|
-
if (localServer) {
|
|
368
|
+
if (!useLocalServer && localServer) {
|
|
197
369
|
localServer.close();
|
|
198
370
|
}
|
|
199
371
|
}
|
|
372
|
+
|
|
373
|
+
if (useLocalServer) {
|
|
374
|
+
console.log('Local server still running for follow session. Press Ctrl+C to stop.');
|
|
375
|
+
}
|
|
200
376
|
}
|
|
201
377
|
|
|
202
378
|
async function main() {
|
|
@@ -227,15 +403,24 @@ async function main() {
|
|
|
227
403
|
case 'run':
|
|
228
404
|
if (!workflowName) {
|
|
229
405
|
console.error('Error: Workflow name required');
|
|
230
|
-
console.error(`Usage: state-machine ${command} <workflow-name> [--local]`);
|
|
406
|
+
console.error(`Usage: state-machine ${command} <workflow-name> [--local] [--new] [-reset] [-reset-hard]`);
|
|
231
407
|
process.exit(1);
|
|
232
408
|
}
|
|
233
409
|
{
|
|
234
410
|
// Remote is enabled by default, --local uses local server instead
|
|
235
411
|
const useLocalServer = args.includes('--local') || args.includes('-l');
|
|
412
|
+
const forceNewRemotePath = args.includes('--new') || args.includes('-n');
|
|
413
|
+
const preReset = args.includes('-reset');
|
|
414
|
+
const preResetHard = args.includes('-reset-hard');
|
|
236
415
|
const remoteEnabled = !useLocalServer; // Use Vercel if not local
|
|
237
416
|
try {
|
|
238
|
-
await runOrResume(workflowName, {
|
|
417
|
+
await runOrResume(workflowName, {
|
|
418
|
+
remoteEnabled,
|
|
419
|
+
useLocalServer,
|
|
420
|
+
forceNewRemotePath,
|
|
421
|
+
preReset,
|
|
422
|
+
preResetHard
|
|
423
|
+
});
|
|
239
424
|
} catch (err) {
|
|
240
425
|
console.error('Error:', err.message || String(err));
|
|
241
426
|
process.exit(1);
|
package/lib/remote/client.js
CHANGED
|
@@ -80,6 +80,7 @@ export class RemoteClient {
|
|
|
80
80
|
* @param {string} options.workflowName - Name of the workflow
|
|
81
81
|
* @param {function} options.onInteractionResponse - Callback when interaction response received
|
|
82
82
|
* @param {function} [options.onStatusChange] - Callback when connection status changes
|
|
83
|
+
* @param {string} [options.sessionToken] - Optional session token to reuse
|
|
83
84
|
*/
|
|
84
85
|
constructor(options) {
|
|
85
86
|
this.serverUrl = options.serverUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
@@ -87,7 +88,7 @@ export class RemoteClient {
|
|
|
87
88
|
this.onInteractionResponse = options.onInteractionResponse;
|
|
88
89
|
this.onStatusChange = options.onStatusChange || (() => {});
|
|
89
90
|
|
|
90
|
-
this.sessionToken = generateSessionToken();
|
|
91
|
+
this.sessionToken = options.sessionToken || generateSessionToken();
|
|
91
92
|
this.connected = false;
|
|
92
93
|
this.polling = false;
|
|
93
94
|
this.pollAbortController = null;
|
package/lib/runtime/runtime.js
CHANGED
|
@@ -507,12 +507,15 @@ export class WorkflowRuntime {
|
|
|
507
507
|
/**
|
|
508
508
|
* Enable remote follow mode
|
|
509
509
|
* @param {string} serverUrl - Base URL of the remote server
|
|
510
|
+
* @param {object} [options]
|
|
511
|
+
* @param {string} [options.sessionToken] - Optional session token to reuse
|
|
510
512
|
* @returns {Promise<string>} The remote URL for browser access
|
|
511
513
|
*/
|
|
512
|
-
async enableRemote(serverUrl) {
|
|
514
|
+
async enableRemote(serverUrl, options = {}) {
|
|
513
515
|
this.remoteClient = new RemoteClient({
|
|
514
516
|
serverUrl,
|
|
515
517
|
workflowName: this.workflowName,
|
|
518
|
+
sessionToken: options.sessionToken,
|
|
516
519
|
onInteractionResponse: (slug, targetKey, response) => {
|
|
517
520
|
this.handleRemoteInteraction(slug, targetKey, response);
|
|
518
521
|
},
|