agent-state-machine 2.4.0 → 2.5.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/bin/cli.js CHANGED
@@ -338,13 +338,7 @@ async function runOrResume(
338
338
  remoteUrl = process.env.STATE_MACHINE_REMOTE_URL || DEFAULT_REMOTE_URL;
339
339
  }
340
340
 
341
- // Enable remote follow mode if we have a URL
342
- if (remoteUrl) {
343
- const sessionToken = ensureRemotePath(configFile, { forceNew: forceNewRemotePath });
344
- await runtime.enableRemote(remoteUrl, { sessionToken, uiBaseUrl: useLocalServer });
345
- }
346
-
347
- // Set full-auto mode from CLI flag (will be merged with config.js during runWorkflow)
341
+ // Set full-auto mode from CLI flag BEFORE enabling remote (so session_init includes correct config)
348
342
  if (fullAuto) {
349
343
  runtime.workflowConfig.fullAuto = true;
350
344
  if (autoSelectDelay !== null) {
@@ -354,6 +348,12 @@ async function runOrResume(
354
348
  console.log(`\n\x1b[36m\x1b[1m⚡ Full-auto mode enabled\x1b[0m - Agent will auto-select recommended options after ${delay}s countdown`);
355
349
  }
356
350
 
351
+ // Enable remote follow mode if we have a URL
352
+ if (remoteUrl) {
353
+ const sessionToken = ensureRemotePath(configFile, { forceNew: forceNewRemotePath });
354
+ await runtime.enableRemote(remoteUrl, { sessionToken, uiBaseUrl: useLocalServer });
355
+ }
356
+
357
357
  // Set non-verbose mode from CLI flag
358
358
  if (nonVerbose) {
359
359
  runtime.workflowConfig.nonVerbose = true;
@@ -89,6 +89,7 @@ export class RemoteClient {
89
89
  * @param {string} options.serverUrl - Base URL of remote server (e.g., https://example.vercel.app)
90
90
  * @param {string} options.workflowName - Name of the workflow
91
91
  * @param {function} options.onInteractionResponse - Callback when interaction response received
92
+ * @param {function} [options.onConfigUpdate] - Callback when config update received from browser
92
93
  * @param {function} [options.onStatusChange] - Callback when connection status changes
93
94
  * @param {string} [options.sessionToken] - Optional session token to reuse
94
95
  * @param {boolean} [options.uiBaseUrl] - If true, return base URL for UI instead of /s/{token}
@@ -97,6 +98,7 @@ export class RemoteClient {
97
98
  this.serverUrl = options.serverUrl.replace(/\/$/, ''); // Remove trailing slash
98
99
  this.workflowName = options.workflowName;
99
100
  this.onInteractionResponse = options.onInteractionResponse;
101
+ this.onConfigUpdate = options.onConfigUpdate || (() => {});
100
102
  this.onStatusChange = options.onStatusChange || (() => {});
101
103
  this.uiBaseUrl = Boolean(options.uiBaseUrl);
102
104
 
@@ -166,16 +168,18 @@ export class RemoteClient {
166
168
  }
167
169
 
168
170
  /**
169
- * Send initial session info with history
171
+ * Send initial session info with history and config
170
172
  * @param {Array} history - Array of history entries
173
+ * @param {object} [config] - Optional workflow config (fullAuto, autoSelectDelay)
171
174
  */
172
- async sendSessionInit(history = []) {
175
+ async sendSessionInit(history = [], config = null) {
173
176
  this.initialHistorySent = true;
174
177
  await this.send({
175
178
  type: 'session_init',
176
179
  sessionToken: this.sessionToken,
177
180
  workflowName: this.workflowName,
178
181
  history,
182
+ config,
179
183
  });
180
184
  }
181
185
 
@@ -231,7 +235,7 @@ export class RemoteClient {
231
235
  }
232
236
 
233
237
  /**
234
- * Poll for interaction responses
238
+ * Poll for interaction responses and config updates
235
239
  * Uses 35s timeout to stay under Vercel's 50s limit with buffer
236
240
  */
237
241
  async poll() {
@@ -246,20 +250,29 @@ export class RemoteClient {
246
250
  consecutiveErrors = 0; // Reset on success
247
251
 
248
252
  if (response.status === 200 && response.data) {
249
- const { type, slug, targetKey, response: interactionResponse } = response.data;
253
+ const { type, slug, targetKey, response: interactionResponse, fullAuto, autoSelectDelay, stop } = response.data;
250
254
 
251
255
  if (type === 'interaction_response' && this.onInteractionResponse) {
252
256
  // Confirm receipt BEFORE processing - removes from Redis pending queue
253
- // This ensures we don't lose the interaction if processing fails
254
257
  try {
255
258
  const confirmUrl = `${this.serverUrl}/api/ws/cli?token=${this.sessionToken}`;
256
259
  await makeRequest(confirmUrl, { method: 'DELETE' }, null, 10000);
257
260
  } catch (err) {
258
- // Non-fatal - interaction will be re-delivered on next poll
259
261
  console.error(`${C.dim}Remote: Failed to confirm receipt: ${err.message}${C.reset}`);
260
262
  }
261
263
 
262
264
  this.onInteractionResponse(slug, targetKey, interactionResponse);
265
+ } else if (type === 'config_update') {
266
+ // Confirm receipt of config update
267
+ try {
268
+ const confirmUrl = `${this.serverUrl}/api/ws/cli?token=${this.sessionToken}&type=config`;
269
+ await makeRequest(confirmUrl, { method: 'DELETE' }, null, 10000);
270
+ } catch (err) {
271
+ console.error(`${C.dim}Remote: Failed to confirm config receipt: ${err.message}${C.reset}`);
272
+ }
273
+
274
+ // Call config update callback
275
+ this.onConfigUpdate({ fullAuto, autoSelectDelay, stop });
263
276
  }
264
277
  }
265
278
 
@@ -13,6 +13,8 @@ import { pathToFileURL } from 'url';
13
13
  import { getCurrentRuntime } from './runtime.js';
14
14
  import { formatInteractionPrompt } from './interaction.js';
15
15
  import { withChangeTracking } from './track-changes.js';
16
+ import { resolveUnknownModel } from './model-resolution.js';
17
+ import { detectAvailableCLIs } from '../llm.js';
16
18
 
17
19
  const require = createRequire(import.meta.url);
18
20
 
@@ -374,6 +376,23 @@ async function executeMDAgent(runtime, agentPath, name, params, options = {}) {
374
376
 
375
377
  const model = config.model || 'fast';
376
378
 
379
+ // Resolve model alias to actual model config for display
380
+ let resolvedModel = baseConfig.models?.[model];
381
+ if (!resolvedModel) {
382
+ // Auto-resolve unknown model (same logic as llm.js)
383
+ try {
384
+ resolvedModel = await resolveUnknownModel(model, baseConfig, runtime.workflowDir, {
385
+ availableCLIs: detectAvailableCLIs()
386
+ });
387
+ // Cache it for future use
388
+ if (!baseConfig.models) baseConfig.models = {};
389
+ baseConfig.models[model] = resolvedModel;
390
+ runtime.workflowConfig.models[model] = resolvedModel;
391
+ } catch {
392
+ resolvedModel = model; // Fallback to alias if resolution fails
393
+ }
394
+ }
395
+
377
396
  const fullPrompt = buildPrompt(context, {
378
397
  model,
379
398
  prompt: interpolatedPrompt,
@@ -381,7 +400,7 @@ async function executeMDAgent(runtime, agentPath, name, params, options = {}) {
381
400
  responseType: config.response
382
401
  });
383
402
 
384
- await logAgentStart(runtime, name, fullPrompt);
403
+ await logAgentStart(runtime, name, fullPrompt, resolvedModel, model);
385
404
 
386
405
  console.log(` Using model: ${model}`);
387
406
 
@@ -647,7 +666,7 @@ ${content}
647
666
  return response;
648
667
  }
649
668
 
650
- async function logAgentStart(runtime, name, prompt) {
669
+ async function logAgentStart(runtime, name, prompt, model = null, modelAlias = null) {
651
670
  if (runtime._agentResumeFlags?.has(name)) {
652
671
  runtime._agentResumeFlags.delete(name);
653
672
  await runtime.prependHistory({
@@ -666,5 +685,13 @@ async function logAgentStart(runtime, name, prompt) {
666
685
  entry.prompt = prompt;
667
686
  }
668
687
 
688
+ if (model) {
689
+ entry.model = model;
690
+ }
691
+
692
+ if (modelAlias && modelAlias !== model) {
693
+ entry.modelAlias = modelAlias;
694
+ }
695
+
669
696
  await runtime.prependHistory(entry);
670
697
  }
@@ -585,6 +585,31 @@ export class WorkflowRuntime {
585
585
  }
586
586
  }
587
587
 
588
+ /**
589
+ * Handle config update from remote browser UI
590
+ * Called by RemoteClient when it receives a config_update message
591
+ */
592
+ handleRemoteConfigUpdate(config) {
593
+ if (config.fullAuto !== undefined) {
594
+ const wasFullAuto = this.workflowConfig.fullAuto;
595
+ this.workflowConfig.fullAuto = config.fullAuto;
596
+ if (wasFullAuto !== config.fullAuto) {
597
+ console.log(`${C.cyan}Remote: Full-auto mode ${config.fullAuto ? 'enabled' : 'disabled'}${C.reset}`);
598
+ }
599
+ }
600
+
601
+ if (config.autoSelectDelay !== undefined) {
602
+ this.workflowConfig.autoSelectDelay = config.autoSelectDelay;
603
+ console.log(`${C.dim}Remote: Auto-select delay set to ${config.autoSelectDelay}s${C.reset}`);
604
+ }
605
+
606
+ if (config.stop) {
607
+ console.log(`\n${C.yellow}${C.bold}Remote: Stop requested${C.reset}`);
608
+ // Trigger graceful shutdown
609
+ process.emit('SIGINT');
610
+ }
611
+ }
612
+
588
613
  /**
589
614
  * Read the user's response from an interaction file
590
615
  */
@@ -779,6 +804,9 @@ export class WorkflowRuntime {
779
804
  onInteractionResponse: (slug, targetKey, response) => {
780
805
  this.handleRemoteInteraction(slug, targetKey, response);
781
806
  },
807
+ onConfigUpdate: (config) => {
808
+ this.handleRemoteConfigUpdate(config);
809
+ },
782
810
  onStatusChange: (status) => {
783
811
  if (status === 'disconnected') {
784
812
  console.log(`${C.yellow}Remote: Connection lost, attempting to reconnect...${C.reset}`);
@@ -790,10 +818,14 @@ export class WorkflowRuntime {
790
818
 
791
819
  await this.remoteClient.connect();
792
820
 
793
- // Send existing history if connected
821
+ // Send existing history if connected, including current config
794
822
  if (this.remoteClient.connected) {
795
823
  const history = this.loadHistory();
796
- await this.remoteClient.sendSessionInit(history);
824
+ const config = {
825
+ fullAuto: this.workflowConfig.fullAuto || false,
826
+ autoSelectDelay: this.workflowConfig.autoSelectDelay ?? 20,
827
+ };
828
+ await this.remoteClient.sendSessionInit(history, config);
797
829
  }
798
830
 
799
831
  this.remoteEnabled = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-state-machine",
3
- "version": "2.4.0",
3
+ "version": "2.5.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",
@@ -0,0 +1,76 @@
1
+ /**
2
+ * File: /vercel-server/api/config/[token].js
3
+ *
4
+ * POST endpoint for browser config updates (fullAuto, autoSelectDelay, stop)
5
+ */
6
+
7
+ import {
8
+ getSession,
9
+ updateSession,
10
+ pushConfigUpdate,
11
+ } from '../../lib/redis.js';
12
+
13
+ export default async function handler(req, res) {
14
+ // Enable CORS
15
+ res.setHeader('Access-Control-Allow-Origin', '*');
16
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
17
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
18
+
19
+ if (req.method === 'OPTIONS') {
20
+ return res.status(200).end();
21
+ }
22
+
23
+ if (req.method !== 'POST') {
24
+ return res.status(405).json({ error: 'Method not allowed' });
25
+ }
26
+
27
+ const { token } = req.query;
28
+
29
+ if (!token) {
30
+ return res.status(400).json({ error: 'Missing token parameter' });
31
+ }
32
+
33
+ try {
34
+ // Validate session
35
+ const session = await getSession(token);
36
+ if (!session) {
37
+ return res.status(404).json({ error: 'Session not found or expired' });
38
+ }
39
+
40
+ // Check if CLI is connected
41
+ if (!session.cliConnected) {
42
+ return res.status(503).json({ error: 'CLI is disconnected. Cannot update config.' });
43
+ }
44
+
45
+ // Parse body
46
+ const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
47
+ const { fullAuto, autoSelectDelay, stop } = body;
48
+
49
+ // Build config update object with only provided fields
50
+ const configUpdate = {};
51
+ if (fullAuto !== undefined) configUpdate.fullAuto = fullAuto;
52
+ if (autoSelectDelay !== undefined) configUpdate.autoSelectDelay = autoSelectDelay;
53
+ if (stop !== undefined) configUpdate.stop = stop;
54
+
55
+ if (Object.keys(configUpdate).length === 0) {
56
+ return res.status(400).json({ error: 'No config fields provided' });
57
+ }
58
+
59
+ // Push config update to pending queue for CLI to poll
60
+ await pushConfigUpdate(token, configUpdate);
61
+
62
+ // Update session metadata with new config (except stop which is transient)
63
+ if (!stop) {
64
+ const currentConfig = session.config || { fullAuto: false, autoSelectDelay: 20 };
65
+ const newConfig = { ...currentConfig };
66
+ if (fullAuto !== undefined) newConfig.fullAuto = fullAuto;
67
+ if (autoSelectDelay !== undefined) newConfig.autoSelectDelay = autoSelectDelay;
68
+ await updateSession(token, { config: newConfig });
69
+ }
70
+
71
+ return res.status(200).json({ success: true });
72
+ } catch (err) {
73
+ console.error('Error updating config:', err);
74
+ return res.status(500).json({ error: err.message });
75
+ }
76
+ }
@@ -42,6 +42,7 @@ export default async function handler(req, res) {
42
42
  return res.status(200).json({
43
43
  workflowName: session.workflowName,
44
44
  cliConnected: session.cliConnected,
45
+ config: session.config || { fullAuto: false, autoSelectDelay: 20 },
45
46
  entries,
46
47
  });
47
48
  } catch (err) {
@@ -14,6 +14,8 @@ import {
14
14
  setCLIConnected,
15
15
  addEvent,
16
16
  setEvents,
17
+ peekConfigUpdate,
18
+ popConfigUpdate,
17
19
  redis,
18
20
  KEYS,
19
21
  } from '../../lib/redis.js';
@@ -59,10 +61,14 @@ async function handlePost(req, res) {
59
61
  try {
60
62
  switch (action) {
61
63
  case 'session_init': {
62
- const { workflowName, history } = body;
64
+ const { workflowName, history, config } = body;
63
65
 
64
- // Create session
65
- await createSession(sessionToken, { workflowName, cliConnected: true });
66
+ // Create session with initial config
67
+ await createSession(sessionToken, {
68
+ workflowName,
69
+ cliConnected: true,
70
+ config: config || null,
71
+ });
66
72
 
67
73
  // Replace events with the provided history snapshot (single source of truth)
68
74
  await setEvents(sessionToken, history || []);
@@ -140,7 +146,7 @@ async function handlePost(req, res) {
140
146
  }
141
147
 
142
148
  /**
143
- * Handle GET requests - long-poll for interaction responses
149
+ * Handle GET requests - long-poll for interaction responses and config updates
144
150
  * Uses efficient polling with 5-second intervals (Upstash doesn't support BLPOP)
145
151
  */
146
152
  async function handleGet(req, res) {
@@ -165,8 +171,7 @@ async function handleGet(req, res) {
165
171
 
166
172
  // Poll every 5 seconds (10 calls per 50s timeout vs 50 calls before)
167
173
  while (Date.now() - startTime < timeoutMs) {
168
- // Peek at first item without removing (LINDEX 0)
169
- // We only remove AFTER CLI confirms receipt via DELETE request
174
+ // Check for pending interactions first (higher priority)
170
175
  const pending = await redis.lindex(pendingKey, 0);
171
176
 
172
177
  if (pending) {
@@ -175,9 +180,6 @@ async function handleGet(req, res) {
175
180
  // Generate a receipt ID so CLI can confirm
176
181
  const receiptId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
177
182
 
178
- // DON'T remove yet - CLI will confirm with DELETE request
179
- // This prevents data loss if response doesn't reach CLI
180
-
181
183
  return res.status(200).json({
182
184
  type: 'interaction_response',
183
185
  receiptId,
@@ -185,11 +187,24 @@ async function handleGet(req, res) {
185
187
  });
186
188
  }
187
189
 
188
- // Wait 5 seconds before checking again (was 1 second)
190
+ // Check for pending config updates
191
+ const configUpdate = await peekConfigUpdate(token);
192
+
193
+ if (configUpdate) {
194
+ const receiptId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
195
+
196
+ return res.status(200).json({
197
+ type: 'config_update',
198
+ receiptId,
199
+ ...configUpdate,
200
+ });
201
+ }
202
+
203
+ // Wait 5 seconds before checking again
189
204
  await new Promise((resolve) => setTimeout(resolve, 5000));
190
205
  }
191
206
 
192
- // Timeout - no interaction received
207
+ // Timeout - no interaction or config update received
193
208
  return res.status(204).end();
194
209
  } catch (err) {
195
210
  console.error('Error polling for interactions:', err);
@@ -198,25 +213,29 @@ async function handleGet(req, res) {
198
213
  }
199
214
 
200
215
  /**
201
- * Handle DELETE requests - CLI confirms receipt of interaction
202
- * This removes the interaction from the pending queue
216
+ * Handle DELETE requests - CLI confirms receipt of interaction or config update
217
+ * This removes the item from the pending queue
203
218
  */
204
219
  async function handleDelete(req, res) {
205
- const { token } = req.query;
220
+ const { token, type = 'interaction' } = req.query;
206
221
 
207
222
  if (!token) {
208
223
  return res.status(400).json({ error: 'Missing token parameter' });
209
224
  }
210
225
 
211
- const channel = KEYS.interactions(token);
212
- const pendingKey = `${channel}:pending`;
213
-
214
226
  try {
215
- // Remove the first item (the one we just sent)
216
- await redis.lpop(pendingKey);
227
+ if (type === 'config') {
228
+ // Remove pending config update
229
+ await popConfigUpdate(token);
230
+ } else {
231
+ // Remove pending interaction (default)
232
+ const channel = KEYS.interactions(token);
233
+ const pendingKey = `${channel}:pending`;
234
+ await redis.lpop(pendingKey);
235
+ }
217
236
  return res.status(200).json({ success: true });
218
237
  } catch (err) {
219
- console.error('Error confirming interaction receipt:', err);
238
+ console.error('Error confirming receipt:', err);
220
239
  return res.status(500).json({ error: err.message });
221
240
  }
222
241
  }
@@ -42,6 +42,8 @@ function createSession(token, data) {
42
42
  cliConnected: true,
43
43
  history: data.history || [],
44
44
  pendingInteractions: [],
45
+ pendingConfigUpdates: [],
46
+ config: data.config || { fullAuto: false, autoSelectDelay: 20 },
45
47
  createdAt: Date.now(),
46
48
  };
47
49
  sessions.set(token, session);
@@ -66,6 +68,23 @@ function broadcastToSession(token, event) {
66
68
  }
67
69
  }
68
70
 
71
+ /**
72
+ * Broadcast "update" to trigger browser refetch
73
+ * The browser SSE handler listens for this string to call fetchData()
74
+ */
75
+ function broadcastUpdate(token) {
76
+ const clients = sseClients.get(token);
77
+ if (!clients) return;
78
+
79
+ for (const client of clients) {
80
+ try {
81
+ client.write('data: update\n\n');
82
+ } catch (e) {
83
+ clients.delete(client);
84
+ }
85
+ }
86
+ }
87
+
69
88
  /**
70
89
  * Parse request body
71
90
  */
@@ -122,8 +141,8 @@ async function handleCliPost(req, res) {
122
141
 
123
142
  switch (action) {
124
143
  case 'session_init': {
125
- const { workflowName, history } = body;
126
- createSession(sessionToken, { workflowName, history });
144
+ const { workflowName, history, config } = body;
145
+ createSession(sessionToken, { workflowName, history, config });
127
146
 
128
147
  broadcastToSession(sessionToken, {
129
148
  type: 'cli_connected',
@@ -138,6 +157,9 @@ async function handleCliPost(req, res) {
138
157
  });
139
158
  }
140
159
 
160
+ // Trigger browser refetch to pick up config and latest state
161
+ broadcastUpdate(sessionToken);
162
+
141
163
  return sendJson(res, 200, { success: true });
142
164
  }
143
165
 
@@ -186,7 +208,7 @@ async function handleCliPost(req, res) {
186
208
  }
187
209
 
188
210
  /**
189
- * Handle CLI GET (long-poll for interactions)
211
+ * Handle CLI GET (long-poll for interactions and config updates)
190
212
  * Peeks at first item without removing - CLI confirms via DELETE
191
213
  */
192
214
  async function handleCliGet(req, res, query) {
@@ -204,11 +226,11 @@ async function handleCliGet(req, res, query) {
204
226
  const timeoutMs = Math.min(parseInt(timeout, 10), 55000);
205
227
  const startTime = Date.now();
206
228
 
207
- // Poll for pending interactions
229
+ // Poll for pending interactions and config updates
208
230
  const checkInterval = setInterval(() => {
231
+ // Check interactions first (higher priority)
209
232
  if (session.pendingInteractions.length > 0) {
210
233
  clearInterval(checkInterval);
211
- // Peek at first item WITHOUT removing - CLI will confirm via DELETE
212
234
  const interaction = session.pendingInteractions[0];
213
235
  return sendJson(res, 200, {
214
236
  type: 'interaction_response',
@@ -216,6 +238,16 @@ async function handleCliGet(req, res, query) {
216
238
  });
217
239
  }
218
240
 
241
+ // Check config updates
242
+ if (session.pendingConfigUpdates && session.pendingConfigUpdates.length > 0) {
243
+ clearInterval(checkInterval);
244
+ const configUpdate = session.pendingConfigUpdates[0];
245
+ return sendJson(res, 200, {
246
+ type: 'config_update',
247
+ ...configUpdate,
248
+ });
249
+ }
250
+
219
251
  if (Date.now() - startTime >= timeoutMs) {
220
252
  clearInterval(checkInterval);
221
253
  res.writeHead(204);
@@ -230,11 +262,11 @@ async function handleCliGet(req, res, query) {
230
262
  }
231
263
 
232
264
  /**
233
- * Handle CLI DELETE (confirm receipt of interaction)
234
- * Removes the first pending interaction after CLI confirms receipt
265
+ * Handle CLI DELETE (confirm receipt of interaction or config update)
266
+ * Removes the first pending item after CLI confirms receipt
235
267
  */
236
268
  function handleCliDelete(req, res, query) {
237
- const { token } = query;
269
+ const { token, type = 'interaction' } = query;
238
270
 
239
271
  if (!token) {
240
272
  return sendJson(res, 400, { error: 'Missing token' });
@@ -245,9 +277,16 @@ function handleCliDelete(req, res, query) {
245
277
  return sendJson(res, 404, { error: 'Session not found' });
246
278
  }
247
279
 
248
- // Remove the first pending interaction (the one we just sent)
249
- if (session.pendingInteractions.length > 0) {
250
- session.pendingInteractions.shift();
280
+ if (type === 'config') {
281
+ // Remove the first pending config update
282
+ if (session.pendingConfigUpdates && session.pendingConfigUpdates.length > 0) {
283
+ session.pendingConfigUpdates.shift();
284
+ }
285
+ } else {
286
+ // Remove the first pending interaction (default)
287
+ if (session.pendingInteractions.length > 0) {
288
+ session.pendingInteractions.shift();
289
+ }
251
290
  }
252
291
 
253
292
  return sendJson(res, 200, { success: true });
@@ -318,6 +357,7 @@ function handleHistoryGet(res, token) {
318
357
  return sendJson(res, 200, {
319
358
  workflowName: session.workflowName,
320
359
  cliConnected: session.cliConnected,
360
+ config: session.config || { fullAuto: false, autoSelectDelay: 20 },
321
361
  entries: session.history,
322
362
  });
323
363
  }
@@ -352,6 +392,47 @@ async function handleSubmitPost(req, res, token) {
352
392
  return sendJson(res, 200, { success: true });
353
393
  }
354
394
 
395
+ /**
396
+ * Handle config update POST from browser
397
+ */
398
+ async function handleConfigPost(req, res, token) {
399
+ const session = getSession(token);
400
+ if (!session) {
401
+ return sendJson(res, 404, { error: 'Session not found' });
402
+ }
403
+
404
+ if (!session.cliConnected) {
405
+ return sendJson(res, 503, { error: 'CLI is disconnected' });
406
+ }
407
+
408
+ const body = await parseBody(req);
409
+ const { fullAuto, autoSelectDelay, stop } = body;
410
+
411
+ // Build config update object
412
+ const configUpdate = {};
413
+ if (fullAuto !== undefined) configUpdate.fullAuto = fullAuto;
414
+ if (autoSelectDelay !== undefined) configUpdate.autoSelectDelay = autoSelectDelay;
415
+ if (stop !== undefined) configUpdate.stop = stop;
416
+
417
+ if (Object.keys(configUpdate).length === 0) {
418
+ return sendJson(res, 400, { error: 'No config fields provided' });
419
+ }
420
+
421
+ // Add to pending config updates for CLI to pick up
422
+ if (!session.pendingConfigUpdates) {
423
+ session.pendingConfigUpdates = [];
424
+ }
425
+ session.pendingConfigUpdates.push(configUpdate);
426
+
427
+ // Update session config (except stop which is transient)
428
+ if (!stop) {
429
+ if (fullAuto !== undefined) session.config.fullAuto = fullAuto;
430
+ if (autoSelectDelay !== undefined) session.config.autoSelectDelay = autoSelectDelay;
431
+ }
432
+
433
+ return sendJson(res, 200, { success: true });
434
+ }
435
+
355
436
  /**
356
437
  * Serve session UI
357
438
  */
@@ -496,6 +577,12 @@ async function handleRequest(req, res) {
496
577
  return handleSubmitPost(req, res, submitMatch[1]);
497
578
  }
498
579
 
580
+ // Route: Config
581
+ const configMatch = pathname.match(/^\/api\/config\/([^/]+)$/);
582
+ if (configMatch && req.method === 'POST') {
583
+ return handleConfigPost(req, res, configMatch[1]);
584
+ }
585
+
499
586
  // Route: Static files
500
587
  if (pathname === '/') {
501
588
  const defaultToken = getDefaultSessionToken();