agent-state-machine 2.0.1 → 2.0.2

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
@@ -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(workflowName, { remoteEnabled = false, useLocalServer = false } = {}) {
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,7 +354,8 @@ 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
- await runtime.enableRemote(remoteUrl);
357
+ const sessionToken = ensureRemotePath(entry, { forceNew: forceNewRemotePath });
358
+ await runtime.enableRemote(remoteUrl, { sessionToken });
187
359
  }
188
360
 
189
361
  try {
@@ -227,15 +399,24 @@ async function main() {
227
399
  case 'run':
228
400
  if (!workflowName) {
229
401
  console.error('Error: Workflow name required');
230
- console.error(`Usage: state-machine ${command} <workflow-name> [--local]`);
402
+ console.error(`Usage: state-machine ${command} <workflow-name> [--local] [--new] [-reset] [-reset-hard]`);
231
403
  process.exit(1);
232
404
  }
233
405
  {
234
406
  // Remote is enabled by default, --local uses local server instead
235
407
  const useLocalServer = args.includes('--local') || args.includes('-l');
408
+ const forceNewRemotePath = args.includes('--new') || args.includes('-n');
409
+ const preReset = args.includes('-reset');
410
+ const preResetHard = args.includes('-reset-hard');
236
411
  const remoteEnabled = !useLocalServer; // Use Vercel if not local
237
412
  try {
238
- await runOrResume(workflowName, { remoteEnabled, useLocalServer });
413
+ await runOrResume(workflowName, {
414
+ remoteEnabled,
415
+ useLocalServer,
416
+ forceNewRemotePath,
417
+ preReset,
418
+ preResetHard
419
+ });
239
420
  } catch (err) {
240
421
  console.error('Error:', err.message || String(err));
241
422
  process.exit(1);
@@ -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;
@@ -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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-state-machine",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
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",