agent-state-machine 2.2.0 → 2.2.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/bin/cli.js +48 -0
- package/lib/remote/client.js +37 -8
- package/package.json +1 -1
- package/templates/project-builder/README.md +304 -56
- package/templates/project-builder/agents/sanity-runner.js +3 -1
- package/templates/starter/README.md +291 -42
- package/vercel-server/api/submit/[token].js +2 -2
- package/vercel-server/api/ws/cli.js +40 -2
- package/vercel-server/local-server.js +32 -3
- package/vercel-server/public/remote/assets/{index-BOKpYANC.js → index-CbgeVnKw.js} +28 -28
- package/vercel-server/public/remote/index.html +1 -1
- package/vercel-server/ui/src/App.jsx +0 -43
|
@@ -1,62 +1,100 @@
|
|
|
1
|
-
#
|
|
1
|
+
# agent-state-machine
|
|
2
2
|
|
|
3
|
-
A workflow
|
|
3
|
+
A workflow runner for building **linear, stateful agent workflows** in plain JavaScript.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
You write normal `async/await` code. The runtime handles:
|
|
6
|
+
- **Auto-persisted** `memory` (saved to disk on mutation)
|
|
7
|
+
- **Auto-tracked** `fileTree` (detects file changes made by agents via Git)
|
|
8
|
+
- **Human-in-the-loop** blocking via `askHuman()` or agent-driven interactions
|
|
9
|
+
- Local **JS agents** + **Markdown agents** (LLM-powered)
|
|
10
|
+
- **Agent retries** with history logging for failures
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
__WORKFLOW_NAME__/
|
|
9
|
-
├── workflow.js # Native JS workflow (async/await)
|
|
10
|
-
├── config.js # Model/API key configuration
|
|
11
|
-
├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
|
|
12
|
-
├── interactions/ # Human-in-the-loop inputs (created at runtime)
|
|
13
|
-
├── state/ # Runtime state (current.json, history.jsonl)
|
|
14
|
-
└── steering/ # Steering configuration
|
|
15
|
-
```
|
|
12
|
+
---
|
|
16
13
|
|
|
17
|
-
##
|
|
14
|
+
## Install
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
You need to install the package **globally** to get the CLI, and **locally** in your project so your workflow can import the library.
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
state-machine run __WORKFLOW_NAME__
|
|
24
|
-
```
|
|
18
|
+
### Global CLI
|
|
19
|
+
Provides the `state-machine` command.
|
|
25
20
|
|
|
26
|
-
Check status:
|
|
27
21
|
```bash
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
# npm
|
|
23
|
+
npm i -g agent-state-machine
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
state-machine history __WORKFLOW_NAME__
|
|
25
|
+
# pnpm
|
|
26
|
+
pnpm add -g agent-state-machine
|
|
34
27
|
```
|
|
35
28
|
|
|
36
|
-
|
|
29
|
+
### Local Library
|
|
30
|
+
Required so your `workflow.js` can `import { agent, memory, fileTree } from 'agent-state-machine'`.
|
|
31
|
+
|
|
37
32
|
```bash
|
|
38
|
-
|
|
33
|
+
# npm
|
|
34
|
+
npm i agent-state-machine
|
|
35
|
+
|
|
36
|
+
# pnpm (for monorepos/turbo, install in root)
|
|
37
|
+
pnpm add agent-state-machine -w
|
|
39
38
|
```
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
Requirements: Node.js >= 16.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## CLI
|
|
45
|
+
|
|
42
46
|
```bash
|
|
43
|
-
state-machine
|
|
47
|
+
state-machine --setup <workflow-name>
|
|
48
|
+
state-machine --setup <workflow-name> --template <template-name>
|
|
49
|
+
state-machine run <workflow-name>
|
|
50
|
+
state-machine run <workflow-name> -reset
|
|
51
|
+
state-machine run <workflow-name> -reset-hard
|
|
52
|
+
|
|
53
|
+
state-machine -reset <workflow-name>
|
|
54
|
+
state-machine -reset-hard <workflow-name>
|
|
55
|
+
|
|
56
|
+
state-machine history <workflow-name> [limit]
|
|
44
57
|
```
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
Templates live in `templates/` and `starter` is used by default.
|
|
60
|
+
|
|
61
|
+
Workflows live in:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
workflows/<name>/
|
|
65
|
+
├── workflow.js # Native JS workflow (async/await)
|
|
66
|
+
├── config.js # Model/API key configuration
|
|
67
|
+
├── package.json # Sets "type": "module" for this workflow folder
|
|
68
|
+
├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
|
|
69
|
+
├── interactions/ # Human-in-the-loop files (auto-created)
|
|
70
|
+
├── state/ # current.json, history.jsonl
|
|
71
|
+
└── steering/ # global.md + config.json
|
|
49
72
|
```
|
|
50
73
|
|
|
51
|
-
|
|
74
|
+
---
|
|
52
75
|
|
|
53
|
-
|
|
76
|
+
## Writing workflows (native JS)
|
|
77
|
+
|
|
78
|
+
Edit `config.js` to set models and API keys for the workflow.
|
|
54
79
|
|
|
55
80
|
```js
|
|
81
|
+
/**
|
|
82
|
+
/**
|
|
83
|
+
* project-builder Workflow
|
|
84
|
+
*
|
|
85
|
+
* Native JavaScript workflow - write normal async/await code!
|
|
86
|
+
*
|
|
87
|
+
* Features:
|
|
88
|
+
* - memory object auto-persists to disk (use memory guards for idempotency)
|
|
89
|
+
* - Use standard JS control flow (if, for, etc.)
|
|
90
|
+
* - Interactive prompts pause and wait for user input
|
|
91
|
+
*/
|
|
92
|
+
|
|
56
93
|
import { agent, memory, askHuman, parallel } from 'agent-state-machine';
|
|
94
|
+
import { notify } from './scripts/mac-notification.js';
|
|
57
95
|
|
|
58
96
|
export default async function() {
|
|
59
|
-
console.log('Starting
|
|
97
|
+
console.log('Starting project-builder workflow...');
|
|
60
98
|
|
|
61
99
|
// Example: Get user input (saved to memory)
|
|
62
100
|
const userLocation = await askHuman('Where do you live?');
|
|
@@ -88,31 +126,242 @@ export default async function() {
|
|
|
88
126
|
// console.log('b: ' + JSON.stringify(b))
|
|
89
127
|
// console.log('c: ' + JSON.stringify(c))
|
|
90
128
|
|
|
91
|
-
notify(['
|
|
129
|
+
notify(['project-builder', userInfo.name || userInfo + ' has been greeted!']);
|
|
92
130
|
|
|
93
131
|
console.log('Workflow completed!');
|
|
94
132
|
}
|
|
95
133
|
```
|
|
96
134
|
|
|
97
|
-
|
|
135
|
+
### Resuming workflows
|
|
136
|
+
|
|
137
|
+
`state-machine run` restarts your workflow from the top, loading the persisted state.
|
|
138
|
+
|
|
139
|
+
If the workflow needs human input, it will **block inline** in the terminal. You can answer in the terminal, edit `interactions/<slug>.md`, or respond in the browser.
|
|
140
|
+
|
|
141
|
+
If the process is interrupted, running `state-machine run <workflow-name>` again will continue execution (assuming your workflow uses `memory` to skip completed steps).
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Core API
|
|
146
|
+
|
|
147
|
+
### `agent(name, params?, options?)`
|
|
148
|
+
|
|
149
|
+
Runs `workflows/<name>/agents/<agent>.(js|mjs|cjs)` or `<agent>.md`.
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
const out = await agent('review', { file: 'src/app.js' });
|
|
153
|
+
memory.lastReview = out;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Options:
|
|
157
|
+
- `retry` (number | false): default `2` (3 total attempts). Use `false` to disable retries.
|
|
158
|
+
- `steering` (string | string[]): extra steering files to load from `workflows/<name>/steering/`.
|
|
159
|
+
|
|
160
|
+
Context is explicit: only `params` are provided to agents unless you pass additional data.
|
|
161
|
+
|
|
162
|
+
### `memory`
|
|
163
|
+
|
|
164
|
+
A persisted object for your workflow.
|
|
165
|
+
|
|
166
|
+
- Mutations auto-save to `workflows/<name>/state/current.json`.
|
|
167
|
+
- Use it as your "long-lived state" between runs.
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
memory.count = (memory.count || 0) + 1;
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `fileTree`
|
|
174
|
+
|
|
175
|
+
Auto-tracked file changes made by agents.
|
|
176
|
+
|
|
177
|
+
- Before each `await agent(...)`, the runtime captures a Git baseline
|
|
178
|
+
- After the agent completes, it detects created/modified/deleted files
|
|
179
|
+
- Changes are stored in `memory.fileTree` and persisted to `current.json`
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
// Files are auto-tracked when agents create them
|
|
183
|
+
await agent('code-writer', { task: 'Create auth module' });
|
|
184
|
+
|
|
185
|
+
// Access tracked files
|
|
186
|
+
console.log(memory.fileTree);
|
|
187
|
+
// { "src/auth.js": { status: "created", createdBy: "code-writer", ... } }
|
|
188
|
+
|
|
189
|
+
// Pass file context to other agents
|
|
190
|
+
await agent('code-reviewer', { fileTree: memory.fileTree });
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Configuration in `config.js`:
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
export const config = {
|
|
197
|
+
// ... models and apiKeys ...
|
|
198
|
+
projectRoot: process.env.PROJECT_ROOT, // defaults to ../.. from workflow
|
|
199
|
+
fileTracking: true, // enable/disable (default: true)
|
|
200
|
+
fileTrackingIgnore: ['node_modules/**', '.git/**', 'dist/**'],
|
|
201
|
+
fileTrackingKeepDeleted: false // keep deleted files in tree
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### `trackFile(path, options?)` / `untrackFile(path)`
|
|
206
|
+
|
|
207
|
+
Manual file tracking utilities:
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
import { trackFile, getFileTree, untrackFile } from 'agent-state-machine';
|
|
211
|
+
|
|
212
|
+
trackFile('README.md', { caption: 'Project docs' });
|
|
213
|
+
const tree = getFileTree();
|
|
214
|
+
untrackFile('old-file.js');
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### `askHuman(question, options?)`
|
|
218
|
+
|
|
219
|
+
Gets user input.
|
|
220
|
+
|
|
221
|
+
- In a TTY, it prompts in the terminal (or via the browser when remote follow is enabled).
|
|
222
|
+
- Otherwise it creates `interactions/<slug>.md` and blocks until you confirm in the terminal (or respond in the browser).
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
const repo = await askHuman('What repo should I work on?', { slug: 'repo' });
|
|
226
|
+
memory.repo = repo;
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### `parallel([...])` / `parallelLimit([...], limit)`
|
|
98
230
|
|
|
99
|
-
|
|
231
|
+
Run multiple `agent()` calls concurrently:
|
|
100
232
|
|
|
101
233
|
```js
|
|
234
|
+
import { agent, parallel, parallelLimit } from 'agent-state-machine';
|
|
235
|
+
|
|
236
|
+
const [a, b] = await parallel([
|
|
237
|
+
agent('review', { file: 'src/a.js' }),
|
|
238
|
+
agent('review', { file: 'src/b.js' }),
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
const results = await parallelLimit(
|
|
242
|
+
['a.js', 'b.js', 'c.js'].map(f => agent('review', { file: f })),
|
|
243
|
+
2
|
|
244
|
+
);
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Agents
|
|
250
|
+
|
|
251
|
+
Agents live in `workflows/<workflow>/agents/`.
|
|
252
|
+
|
|
253
|
+
### JavaScript agents
|
|
254
|
+
|
|
255
|
+
**ESM (`.js` / `.mjs`)**:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
// workflows/<name>/agents/example.js
|
|
102
259
|
import { llm } from 'agent-state-machine';
|
|
103
260
|
|
|
104
261
|
export default async function handler(context) {
|
|
105
|
-
|
|
106
|
-
|
|
262
|
+
// context includes:
|
|
263
|
+
// - params passed to agent(name, params)
|
|
264
|
+
// - context._steering (global + optional additional steering content)
|
|
265
|
+
// - context._config (models/apiKeys/workflowDir/projectRoot)
|
|
266
|
+
|
|
267
|
+
// Optionally return _files to annotate tracked files
|
|
268
|
+
return {
|
|
269
|
+
ok: true,
|
|
270
|
+
_files: [{ path: 'src/example.js', caption: 'Example module' }]
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**CommonJS (`.cjs`)** (only if you prefer CJS):
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
// workflows/<name>/agents/example.cjs
|
|
279
|
+
async function handler(context) {
|
|
280
|
+
return { ok: true };
|
|
107
281
|
}
|
|
282
|
+
|
|
283
|
+
module.exports = handler;
|
|
284
|
+
module.exports.handler = handler;
|
|
108
285
|
```
|
|
109
286
|
|
|
110
|
-
|
|
287
|
+
If you need to request human input from a JS agent, return an `_interaction` payload:
|
|
288
|
+
|
|
289
|
+
```js
|
|
290
|
+
return {
|
|
291
|
+
_interaction: {
|
|
292
|
+
slug: 'approval',
|
|
293
|
+
targetKey: 'approval',
|
|
294
|
+
content: 'Please approve this change (yes/no).'
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The runtime will block execution and wait for your response in the terminal.
|
|
300
|
+
|
|
301
|
+
### Markdown agents (`.md`)
|
|
302
|
+
|
|
303
|
+
Markdown agents are LLM-backed prompt templates with optional frontmatter.
|
|
304
|
+
Frontmatter can include `steering` to load additional files from `workflows/<name>/steering/`.
|
|
111
305
|
|
|
112
306
|
```md
|
|
113
307
|
---
|
|
114
|
-
model:
|
|
308
|
+
model: smart
|
|
115
309
|
output: greeting
|
|
310
|
+
steering: tone, product
|
|
311
|
+
---
|
|
312
|
+
Generate a friendly greeting for {{name}}.
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Calling it:
|
|
316
|
+
|
|
317
|
+
```js
|
|
318
|
+
const { greeting } = await agent('greeter', { name: 'Sam' });
|
|
319
|
+
memory.greeting = greeting;
|
|
320
|
+
```
|
|
321
|
+
|
|
116
322
|
---
|
|
117
|
-
|
|
323
|
+
|
|
324
|
+
## Models & LLM execution
|
|
325
|
+
|
|
326
|
+
In your workflow’s `export const config = { models: { ... } }`, each model value can be:
|
|
327
|
+
|
|
328
|
+
### CLI command
|
|
329
|
+
|
|
330
|
+
```js
|
|
331
|
+
export const config = {
|
|
332
|
+
models: {
|
|
333
|
+
smart: "claude -m claude-sonnet-4-20250514 -p"
|
|
334
|
+
}
|
|
335
|
+
};
|
|
118
336
|
```
|
|
337
|
+
|
|
338
|
+
### API target
|
|
339
|
+
|
|
340
|
+
Format: `api:<provider>:<model>`
|
|
341
|
+
|
|
342
|
+
```js
|
|
343
|
+
export const config = {
|
|
344
|
+
models: {
|
|
345
|
+
smart: "api:openai:gpt-4.1-mini"
|
|
346
|
+
},
|
|
347
|
+
apiKeys: {
|
|
348
|
+
openai: process.env.OPENAI_API_KEY
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
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. Remote follow links persist across runs (stored in `config.js`) unless you pass `-n`/`--new` to regenerate.
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## State & persistence
|
|
358
|
+
|
|
359
|
+
Native JS workflows persist to:
|
|
360
|
+
|
|
361
|
+
- `workflows/<name>/state/current.json` — status, memory (includes fileTree), pending interaction
|
|
362
|
+
- `workflows/<name>/state/history.jsonl` — event log (newest entries first, includes agent retry/failure entries)
|
|
363
|
+
- `workflows/<name>/interactions/*.md` — human input files (when paused)
|
|
364
|
+
|
|
365
|
+
## License
|
|
366
|
+
|
|
367
|
+
MIT
|
|
@@ -66,8 +66,8 @@ export default async function handler(req, res) {
|
|
|
66
66
|
response,
|
|
67
67
|
}));
|
|
68
68
|
|
|
69
|
-
// Set TTL on pending list
|
|
70
|
-
await redis.expire(pendingKey,
|
|
69
|
+
// Set TTL on pending list (24 hours - same as session, allows laptop sleep)
|
|
70
|
+
await redis.expire(pendingKey, 24 * 60 * 60);
|
|
71
71
|
|
|
72
72
|
// Log event to events list (single source of truth for UI)
|
|
73
73
|
await addEvent(token, {
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
export default async function handler(req, res) {
|
|
22
22
|
// Enable CORS
|
|
23
23
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
24
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
24
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
25
25
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
26
26
|
|
|
27
27
|
if (req.method === 'OPTIONS') {
|
|
@@ -36,6 +36,10 @@ export default async function handler(req, res) {
|
|
|
36
36
|
return handleGet(req, res);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
if (req.method === 'DELETE') {
|
|
40
|
+
return handleDelete(req, res);
|
|
41
|
+
}
|
|
42
|
+
|
|
39
43
|
return res.status(405).json({ error: 'Method not allowed' });
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -161,12 +165,22 @@ async function handleGet(req, res) {
|
|
|
161
165
|
|
|
162
166
|
// Poll every 5 seconds (10 calls per 50s timeout vs 50 calls before)
|
|
163
167
|
while (Date.now() - startTime < timeoutMs) {
|
|
164
|
-
|
|
168
|
+
// Peek at first item without removing (LINDEX 0)
|
|
169
|
+
// We only remove AFTER CLI confirms receipt via DELETE request
|
|
170
|
+
const pending = await redis.lindex(pendingKey, 0);
|
|
165
171
|
|
|
166
172
|
if (pending) {
|
|
167
173
|
const data = typeof pending === 'object' ? pending : JSON.parse(pending);
|
|
174
|
+
|
|
175
|
+
// Generate a receipt ID so CLI can confirm
|
|
176
|
+
const receiptId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
177
|
+
|
|
178
|
+
// DON'T remove yet - CLI will confirm with DELETE request
|
|
179
|
+
// This prevents data loss if response doesn't reach CLI
|
|
180
|
+
|
|
168
181
|
return res.status(200).json({
|
|
169
182
|
type: 'interaction_response',
|
|
183
|
+
receiptId,
|
|
170
184
|
...data,
|
|
171
185
|
});
|
|
172
186
|
}
|
|
@@ -182,3 +196,27 @@ async function handleGet(req, res) {
|
|
|
182
196
|
return res.status(500).json({ error: err.message });
|
|
183
197
|
}
|
|
184
198
|
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Handle DELETE requests - CLI confirms receipt of interaction
|
|
202
|
+
* This removes the interaction from the pending queue
|
|
203
|
+
*/
|
|
204
|
+
async function handleDelete(req, res) {
|
|
205
|
+
const { token } = req.query;
|
|
206
|
+
|
|
207
|
+
if (!token) {
|
|
208
|
+
return res.status(400).json({ error: 'Missing token parameter' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const channel = KEYS.interactions(token);
|
|
212
|
+
const pendingKey = `${channel}:pending`;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Remove the first item (the one we just sent)
|
|
216
|
+
await redis.lpop(pendingKey);
|
|
217
|
+
return res.status(200).json({ success: true });
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.error('Error confirming interaction receipt:', err);
|
|
220
|
+
return res.status(500).json({ error: err.message });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -101,7 +101,7 @@ function sendJson(res, status, data) {
|
|
|
101
101
|
res.writeHead(status, {
|
|
102
102
|
'Content-Type': 'application/json',
|
|
103
103
|
'Access-Control-Allow-Origin': '*',
|
|
104
|
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
104
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
105
105
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
106
106
|
});
|
|
107
107
|
res.end(JSON.stringify(data));
|
|
@@ -187,6 +187,7 @@ async function handleCliPost(req, res) {
|
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
189
|
* Handle CLI GET (long-poll for interactions)
|
|
190
|
+
* Peeks at first item without removing - CLI confirms via DELETE
|
|
190
191
|
*/
|
|
191
192
|
async function handleCliGet(req, res, query) {
|
|
192
193
|
const { token, timeout = '30000' } = query;
|
|
@@ -207,7 +208,8 @@ async function handleCliGet(req, res, query) {
|
|
|
207
208
|
const checkInterval = setInterval(() => {
|
|
208
209
|
if (session.pendingInteractions.length > 0) {
|
|
209
210
|
clearInterval(checkInterval);
|
|
210
|
-
|
|
211
|
+
// Peek at first item WITHOUT removing - CLI will confirm via DELETE
|
|
212
|
+
const interaction = session.pendingInteractions[0];
|
|
211
213
|
return sendJson(res, 200, {
|
|
212
214
|
type: 'interaction_response',
|
|
213
215
|
...interaction,
|
|
@@ -227,6 +229,30 @@ async function handleCliGet(req, res, query) {
|
|
|
227
229
|
});
|
|
228
230
|
}
|
|
229
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Handle CLI DELETE (confirm receipt of interaction)
|
|
234
|
+
* Removes the first pending interaction after CLI confirms receipt
|
|
235
|
+
*/
|
|
236
|
+
function handleCliDelete(req, res, query) {
|
|
237
|
+
const { token } = query;
|
|
238
|
+
|
|
239
|
+
if (!token) {
|
|
240
|
+
return sendJson(res, 400, { error: 'Missing token' });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const session = getSession(token);
|
|
244
|
+
if (!session) {
|
|
245
|
+
return sendJson(res, 404, { error: 'Session not found' });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Remove the first pending interaction (the one we just sent)
|
|
249
|
+
if (session.pendingInteractions.length > 0) {
|
|
250
|
+
session.pendingInteractions.shift();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return sendJson(res, 200, { success: true });
|
|
254
|
+
}
|
|
255
|
+
|
|
230
256
|
/**
|
|
231
257
|
* Handle SSE events endpoint for browsers
|
|
232
258
|
*/
|
|
@@ -446,7 +472,7 @@ async function handleRequest(req, res) {
|
|
|
446
472
|
if (req.method === 'OPTIONS') {
|
|
447
473
|
res.writeHead(200, {
|
|
448
474
|
'Access-Control-Allow-Origin': '*',
|
|
449
|
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
475
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
450
476
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
451
477
|
});
|
|
452
478
|
return res.end();
|
|
@@ -460,6 +486,9 @@ async function handleRequest(req, res) {
|
|
|
460
486
|
if (req.method === 'GET') {
|
|
461
487
|
return handleCliGet(req, res, query);
|
|
462
488
|
}
|
|
489
|
+
if (req.method === 'DELETE') {
|
|
490
|
+
return handleCliDelete(req, res, query);
|
|
491
|
+
}
|
|
463
492
|
}
|
|
464
493
|
|
|
465
494
|
// Route: Session UI
|