agentclick 0.1.1 → 0.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/README.md CHANGED
@@ -1,154 +1,98 @@
1
1
  # AgentClick
2
2
 
3
- **Rich web UI for AI agent interactions click to edit, human-in-the-loop, preference learning.**
3
+ AI agents fail silently and take irreversible actions. AgentClick puts a human review step between your agent and the world.
4
4
 
5
- [![GitHub stars](https://img.shields.io/github/stars/agentlayer-io/AgentClick?style=flat-square)](https://github.com/agentlayer-io/AgentClick/stargazers)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE)
7
- [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/agentlayer-io/AgentClick/pulls)
5
+ [![npm version](https://img.shields.io/npm/v/agentclick)](https://www.npmjs.com/package/agentclick)
6
+ [![license](https://img.shields.io/npm/l/agentclick)](LICENSE)
7
+ [![npm downloads](https://img.shields.io/npm/dm/agentclick)](https://www.npmjs.com/package/agentclick)
8
8
 
9
9
  ---
10
10
 
11
- ## The Problem
11
+ ## Why AgentClick
12
12
 
13
- Every OpenClaw user interacts with their agent through text chat (WhatsApp / Telegram). Text is a degraded interface:
14
-
15
- - You can't click a paragraph and say "rewrite this"
16
- - You can't drag steps to reorder them
17
- - Every correction requires typing out instructions again
18
- - The agent never remembers your preferences
19
-
20
- ## The Solution
21
-
22
- When your agent finishes a task that needs your input, it opens a browser page — a purpose-built interaction UI. You click, choose, drag. No typing.
23
-
24
- ```
25
- Agent finishes email draft
26
- → Browser opens automatically
27
- → You click paragraph → choose: Delete / Rewrite / Keep
28
- → You confirm
29
- → Agent continues, remembers your choices for next time
30
- ```
31
-
32
- Every interaction teaches the agent your preferences. The more you use it, the less you need to explain.
33
-
34
- ---
35
-
36
- ## Current Status
37
-
38
- This project is already in a working prototype stage and supports multiple review flows end-to-end.
39
-
40
- Implemented:
41
-
42
- - Email review UI (legacy single-column + v2 inbox + draft layout)
43
- - Action approval UI (approve/reject + note)
44
- - Code/shell command review UI
45
- - Session history homepage with recent sessions
46
- - SQLite session persistence (`~/.openclaw/clawui-sessions.db`)
47
- - Long-poll wait endpoint for agent integration (`/api/sessions/:id/wait`)
48
- - Preference learning from paragraph deletions (writes rules to `MEMORY.md`)
49
- - Keyboard shortcuts (`Cmd/Ctrl+Enter` submit, `Escape` handling)
50
- - Browser auto-open on session creation
13
+ - **Not just approve/deny** -- edit the email subject, change the command, modify the payload before it sends.
14
+ - **Preference learning** -- delete a paragraph and tell AgentClick why. It writes the rule to disk so your agent never makes the same mistake again.
15
+ - **Framework-agnostic** -- works with OpenAI, Anthropic, LangChain, or any HTTP-capable agent. Just POST and long-poll.
51
16
 
52
17
  ---
53
18
 
54
19
  ## Quick Start
55
20
 
56
- ```bash
57
- git clone https://github.com/agentlayer-io/AgentClick.git
58
- cd AgentClick
59
- npm install
60
- npm run dev # dev mode: API on 3001, Vite UI on 5173
61
- ```
62
-
63
- ## Install (Global CLI)
64
-
65
21
  ```bash
66
22
  npm install -g agentclick
67
23
  agentclick
68
24
  ```
69
25
 
70
- This starts the AgentClick server on `http://localhost:3001` and serves the built web UI on the same port.
71
-
72
- CLI options:
26
+ Then test it with a mock session:
73
27
 
74
28
  ```bash
75
- agentclick --help
76
- PORT=3002 agentclick
29
+ curl -X POST http://localhost:3001/api/review \
30
+ -H "Content-Type: application/json" \
31
+ -d '{"type":"code_review","sessionKey":"test","payload":{"command":"rm -rf /tmp/old-cache","cwd":"/home/user","explanation":"Clean up stale cache directory","risk":"medium"}}'
77
32
  ```
78
33
 
79
- By default, `agentclick` starts on port `3001`. If that port is in use, it automatically tries `3002`, `3003`, and so on.
34
+ A browser tab opens automatically. Review, approve or reject, and close the tab.
80
35
 
81
- Optional server config (defaults shown in `.env.example`):
82
-
83
- ```bash
84
- PORT=3001
85
- OPENCLAW_WEBHOOK=http://localhost:18789/hooks/agent
86
- ```
87
-
88
- Create a local `.env` in the project root to override these values during development (server auto-loads it via `dotenv`).
36
+ ---
89
37
 
90
- Production-style local run (single port after build):
38
+ ## How It Works
91
39
 
92
- ```bash
93
- npm run build
94
- npm start # serves API + built web UI on localhost:3001
95
- ```
40
+ 1. **Agent POSTs structured data** to `http://localhost:3001/api/review` with a session key.
41
+ 2. **User reviews, edits, and approves** in the browser -- paragraph-level delete/rewrite for emails, approve/reject for commands and actions.
42
+ 3. **Agent receives the result** via long-poll (`GET /api/sessions/:id/wait`) and continues execution.
96
43
 
97
- Deployment notes (reverse proxy, Docker/OpenClaw host mapping, env vars):
44
+ No WebSockets. No framework plugins. One HTTP endpoint in, one HTTP endpoint out.
98
45
 
99
- - See `docs/deployment.md`
46
+ ---
100
47
 
101
- Copy the skill to your OpenClaw workspace:
48
+ ## Comparison
102
49
 
103
- ```bash
104
- cp -r skills/clawui-email ~/.openclaw/skills/
105
- ```
106
-
107
- Restart OpenClaw. Ask it to write an email the review page will open automatically.
50
+ | Feature | AgentClick | AgentGate | LangGraph interrupt() | Vercel AI SDK |
51
+ |---|---|---|---|---|
52
+ | Pre-built review UI | Yes | No | No | No |
53
+ | Edit before approve | Yes | No | No | No |
54
+ | Preference learning | Yes | No | No | No |
55
+ | Framework-agnostic | Yes | Yes | LangGraph only | Vercel only |
56
+ | Self-hosted | Yes | Yes | Yes | Cloud |
108
57
 
109
58
  ---
110
59
 
111
- ## Project Structure
60
+ ## Session Types
112
61
 
113
- ```
114
- AgentClick/
115
- ├── packages/
116
- │ ├── server/ # Node.js + Express — receives agent data, handles callbacks
117
- │ └── web/ # React + Vite + Tailwind — the interaction UI
118
- ├── skills/
119
- │ └── clawui-email/
120
- │ └── SKILL.md # OpenClaw skill definition
121
- └── docs/
122
- └── research.md # Market & technical research notes
123
- ```
62
+ - **email_review** -- two-column inbox and draft editor. Users can delete paragraphs with reasons, request rewrites, toggle intent suggestions, and confirm or regenerate.
63
+ - **code_review** -- displays the shell command, working directory, affected files as a collapsible tree, and risk level. Approve or reject with an optional note.
64
+ - **action_approval** -- generic high-risk action gate. Shows action description, detail, and risk badge. Approve or reject with an optional note.
124
65
 
125
66
  ---
126
67
 
127
- ## Roadmap
68
+ ## API
128
69
 
129
- - [x] **M0** Email draft review (click to delete/rewrite paragraphs)
130
- - [x] **M1** — Preference learning (auto-save rules to MEMORY.md)
131
- - [x] **M2 (partial)** Agent integration loop (`/review` create session + `/wait` long-poll + callback)
132
- - [x] **Next** Unified serving (single port; no separate Vite port in production)
133
- - [x] **Next** Production serve polish (deployment docs / environment examples)
134
- - [ ] **Next** npm global package (`agentclick`)
135
- - [ ] **Next** — Remote mode UX + link delivery polish
136
- - [ ] **Later** — Agent task visualization (Mission Control view)
137
- - [ ] **Later** — Multi-framework support (beyond OpenClaw)
70
+ | Method | Endpoint | Description |
71
+ |---|---|---|
72
+ | POST | `/api/review` | Agent creates a review session. Returns `{ sessionId, url }`. Browser opens automatically. |
73
+ | GET | `/api/sessions/:id` | Fetch session data (payload, status, result). |
74
+ | GET | `/api/sessions/:id/wait` | Long-poll. Blocks up to 5 minutes until the user completes the review. |
75
+ | POST | `/api/sessions/:id/complete` | UI submits the user's decision. Triggers preference learning and agent callback. |
138
76
 
139
77
  ---
140
78
 
141
- ## Why Not ClawX?
142
-
143
- [ClawX](https://github.com/ValueCell-ai/ClawX) is a desktop app for *managing* OpenClaw (installing skills, configuring channels, running the gateway). AgentClick is for *working with* your agent — reviewing its output, making decisions, teaching it your preferences. They're complementary.
79
+ ## Development
144
80
 
145
- ---
81
+ ```bash
82
+ git clone https://github.com/agentlayer-io/AgentClick.git
83
+ cd AgentClick
84
+ npm install
85
+ npm run dev
86
+ ```
146
87
 
147
- ## Contributing
88
+ Server runs on `http://localhost:3001`, UI on `http://localhost:5173`.
148
89
 
149
- This is an early-stage open source project. All contributions welcome — UI components, new interaction patterns, OpenClaw integration improvements, documentation.
90
+ For production-style single-port serving:
150
91
 
151
- Open an issue to discuss before submitting large PRs.
92
+ ```bash
93
+ npm run build
94
+ npm start
95
+ ```
152
96
 
153
97
  ---
154
98
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { existsSync } from 'node:fs'
3
+ import { existsSync, readFileSync } from 'node:fs'
4
4
  import { dirname, join } from 'node:path'
5
5
  import { fileURLToPath } from 'node:url'
6
6
  import { spawnSync } from 'node:child_process'
@@ -10,8 +10,18 @@ const __filename = fileURLToPath(import.meta.url)
10
10
  const rootDir = dirname(dirname(__filename))
11
11
  const webDistIndex = join(rootDir, 'packages', 'web', 'dist', 'index.html')
12
12
  const serverDistEntry = join(rootDir, 'packages', 'server', 'dist', 'index.js')
13
+ const packageJsonPath = join(rootDir, 'package.json')
13
14
  const args = process.argv.slice(2)
14
15
 
16
+ function readVersion() {
17
+ try {
18
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
19
+ return typeof pkg.version === 'string' ? pkg.version : 'unknown'
20
+ } catch {
21
+ return 'unknown'
22
+ }
23
+ }
24
+
15
25
  function printHelp() {
16
26
  console.log(`AgentClick CLI
17
27
 
@@ -20,7 +30,16 @@ Usage:
20
30
  agentclick --help
21
31
 
22
32
  Options:
33
+ --version, -v Show version number
23
34
  --help, -h Show this help message
35
+
36
+ Examples:
37
+ agentclick Start the server (auto-detects port)
38
+ PORT=4000 agentclick Start on a specific port
39
+
40
+ Environment:
41
+ PORT Server port (default: 3001, auto-increments if busy)
42
+ OPENCLAW_WEBHOOK Webhook URL for agent callbacks
24
43
  `)
25
44
  }
26
45
 
@@ -31,6 +50,10 @@ function parseArgs(argv) {
31
50
  printHelp()
32
51
  process.exit(0)
33
52
  }
53
+ if (arg === '--version' || arg === '-v') {
54
+ console.log(readVersion())
55
+ process.exit(0)
56
+ }
34
57
  console.error(`[agentclick] Unknown argument: ${arg}`)
35
58
  console.error('[agentclick] Run "agentclick --help" for usage.')
36
59
  process.exit(1)
package/package.json CHANGED
@@ -1,6 +1,25 @@
1
1
  {
2
2
  "name": "agentclick",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
+ "description": "Human-in-the-loop approval UI for AI agents. Review, edit, and approve agent actions in your browser before they execute.",
5
+ "keywords": [
6
+ "ai-agent",
7
+ "human-in-the-loop",
8
+ "approval",
9
+ "hitl",
10
+ "llm",
11
+ "langchain",
12
+ "openai",
13
+ "agent-ui",
14
+ "agentic",
15
+ "ai-safety",
16
+ "human-oversight"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/agentlayer-io/AgentClick.git"
21
+ },
22
+ "homepage": "https://github.com/agentlayer-io/AgentClick",
4
23
  "bin": {
5
24
  "agentclick": "./bin/agentclick.mjs"
6
25
  },
@@ -11,8 +30,7 @@
11
30
  "packages/server/package.json",
12
31
  "packages/web/dist/",
13
32
  "packages/web/package.json",
14
- "README.md",
15
- "AGENTS.md"
33
+ "README.md"
16
34
  ],
17
35
  "workspaces": [
18
36
  "packages/*"
@@ -6,7 +6,7 @@ import { existsSync } from 'fs';
6
6
  import { dirname, join } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { learnFromDeletions } from './preference.js';
9
- import { createSession, getSession, listSessions, completeSession } from './store.js';
9
+ import { createSession, getSession, listSessions, completeSession, setSessionRewriting, updateSessionPayload } from './store.js';
10
10
  const app = express();
11
11
  const PORT = Number(process.env.PORT || 3001);
12
12
  const OPENCLAW_WEBHOOK = process.env.OPENCLAW_WEBHOOK || 'http://localhost:18789/hooks/agent';
@@ -23,18 +23,23 @@ app.post('/api/review', async (req, res) => {
23
23
  if (!sessionKey) {
24
24
  console.warn('[agentclick] Warning: sessionKey missing — callback will be skipped');
25
25
  }
26
- const id = `session_${Date.now()}`;
26
+ const now = Date.now();
27
+ const id = `session_${now}`;
27
28
  createSession({
28
29
  id,
29
30
  type: type || 'email_review',
30
31
  payload,
31
32
  sessionKey,
32
33
  status: 'pending',
33
- createdAt: Date.now(),
34
+ createdAt: now,
35
+ updatedAt: now,
36
+ revision: 0,
34
37
  });
35
38
  const routeMap = {
36
39
  action_approval: 'approval',
37
40
  code_review: 'code-review',
41
+ form_review: 'form-review',
42
+ selection_review: 'selection',
38
43
  };
39
44
  const path = routeMap[type] ?? 'review';
40
45
  const url = `${WEB_ORIGIN}/${path}/${id}`;
@@ -112,17 +117,39 @@ app.get('/api/sessions/:id/wait', async (req, res) => {
112
117
  const session = getSession(req.params.id);
113
118
  if (!session)
114
119
  return res.status(404).json({ error: 'Session not found' });
115
- if (session.status === 'completed')
120
+ if (session.status === 'completed' || session.status === 'rewriting') {
121
+ console.log(`[agentclick] /wait returning ${session.status} for ${session.id} (revision=${session.revision})`);
116
122
  return res.json(session);
123
+ }
117
124
  await new Promise(r => setTimeout(r, POLL_MS));
118
125
  }
119
126
  res.status(408).json({ error: 'timeout', message: 'User did not complete review within 5 minutes' });
120
127
  });
128
+ // Agent updates session payload after rewriting
129
+ app.put('/api/sessions/:id/payload', (req, res) => {
130
+ const session = getSession(req.params.id);
131
+ if (!session)
132
+ return res.status(404).json({ error: 'Session not found' });
133
+ if (session.status !== 'rewriting')
134
+ return res.status(400).json({ error: 'Session is not in rewriting state' });
135
+ console.log(`[agentclick] Payload update requested for ${session.id} (status=${session.status}, revision=${session.revision})`);
136
+ updateSessionPayload(req.params.id, req.body.payload);
137
+ const updated = getSession(req.params.id);
138
+ console.log(`[agentclick] Session ${session.id} payload updated, back to pending (revision=${updated?.revision ?? 'unknown'})`);
139
+ res.json({ ok: true });
140
+ });
121
141
  // Web UI submits user actions
122
142
  app.post('/api/sessions/:id/complete', async (req, res) => {
123
143
  const session = getSession(req.params.id);
124
144
  if (!session)
125
145
  return res.status(404).json({ error: 'Session not found' });
146
+ // If user requested regeneration, set to rewriting (not completed) so agent can update
147
+ if (req.body.regenerate) {
148
+ setSessionRewriting(req.params.id, req.body);
149
+ console.log(`[agentclick] Session ${session.id} → rewriting:`, JSON.stringify(req.body, null, 2));
150
+ res.json({ ok: true, rewriting: true });
151
+ return;
152
+ }
126
153
  completeSession(req.params.id, req.body);
127
154
  console.log(`[agentclick] Session ${session.id} completed:`, JSON.stringify(req.body, null, 2));
128
155
  // Learn from delete actions and persist rules to MEMORY.md
@@ -183,6 +210,10 @@ function buildActionSummary(result) {
183
210
  if (result.regenerate) {
184
211
  lines.push('- User requested full regeneration.');
185
212
  }
213
+ const intents = (result.selectedIntents ?? []);
214
+ if (intents.length > 0) {
215
+ lines.push(`- Intent decisions: ${intents.map(i => `${i.id} → ${i.accepted ? 'accepted' : 'rejected'}`).join(', ')}`);
216
+ }
186
217
  return lines.join('\n');
187
218
  }
188
219
  if (SHOULD_SERVE_BUILT_WEB) {
@@ -15,16 +15,29 @@ db.exec(`
15
15
  status TEXT NOT NULL DEFAULT 'pending',
16
16
  result TEXT,
17
17
  sessionKey TEXT,
18
- createdAt INTEGER NOT NULL
18
+ createdAt INTEGER NOT NULL,
19
+ updatedAt INTEGER NOT NULL DEFAULT 0,
20
+ revision INTEGER NOT NULL DEFAULT 0
19
21
  )
20
22
  `);
23
+ // Migrate existing tables: add updatedAt and revision if missing
24
+ try {
25
+ db.exec(`ALTER TABLE sessions ADD COLUMN updatedAt INTEGER NOT NULL DEFAULT 0`);
26
+ }
27
+ catch { /* column already exists */ }
28
+ try {
29
+ db.exec(`ALTER TABLE sessions ADD COLUMN revision INTEGER NOT NULL DEFAULT 0`);
30
+ }
31
+ catch { /* column already exists */ }
21
32
  export function createSession(session) {
22
33
  db.prepare(`
23
- INSERT INTO sessions (id, type, payload, status, sessionKey, createdAt)
24
- VALUES (@id, @type, @payload, @status, @sessionKey, @createdAt)
34
+ INSERT INTO sessions (id, type, payload, status, sessionKey, createdAt, updatedAt, revision)
35
+ VALUES (@id, @type, @payload, @status, @sessionKey, @createdAt, @updatedAt, @revision)
25
36
  `).run({
26
37
  ...session,
27
38
  payload: JSON.stringify(session.payload),
39
+ updatedAt: session.updatedAt || Date.now(),
40
+ revision: session.revision || 0,
28
41
  });
29
42
  }
30
43
  export function getSession(id) {
@@ -39,8 +52,18 @@ export function listSessions(limit = 20) {
39
52
  }
40
53
  export function completeSession(id, result) {
41
54
  db.prepare(`
42
- UPDATE sessions SET status = 'completed', result = ? WHERE id = ?
43
- `).run(JSON.stringify(result), id);
55
+ UPDATE sessions SET status = 'completed', result = ?, updatedAt = ? WHERE id = ?
56
+ `).run(JSON.stringify(result), Date.now(), id);
57
+ }
58
+ export function setSessionRewriting(id, result) {
59
+ db.prepare(`
60
+ UPDATE sessions SET status = 'rewriting', result = ?, updatedAt = ? WHERE id = ?
61
+ `).run(JSON.stringify(result), Date.now(), id);
62
+ }
63
+ export function updateSessionPayload(id, payload) {
64
+ db.prepare(`
65
+ UPDATE sessions SET payload = ?, status = 'pending', result = NULL, updatedAt = ?, revision = revision + 1 WHERE id = ?
66
+ `).run(JSON.stringify(payload), Date.now(), id);
44
67
  }
45
68
  function deserialize(row) {
46
69
  return {
@@ -51,5 +74,7 @@ function deserialize(row) {
51
74
  result: row.result ? JSON.parse(row.result) : undefined,
52
75
  sessionKey: row.sessionKey,
53
76
  createdAt: row.createdAt,
77
+ updatedAt: row.updatedAt || 0,
78
+ revision: row.revision || 0,
54
79
  };
55
80
  }
@@ -6,7 +6,7 @@ import { existsSync } from 'fs'
6
6
  import { dirname, join } from 'path'
7
7
  import { fileURLToPath } from 'url'
8
8
  import { learnFromDeletions } from './preference.js'
9
- import { createSession, getSession, listSessions, completeSession } from './store.js'
9
+ import { createSession, getSession, listSessions, completeSession, setSessionRewriting, updateSessionPayload } from './store.js'
10
10
 
11
11
  const app = express()
12
12
  const PORT = Number(process.env.PORT || 3001)
@@ -28,19 +28,24 @@ app.post('/api/review', async (req, res) => {
28
28
  console.warn('[agentclick] Warning: sessionKey missing — callback will be skipped')
29
29
  }
30
30
 
31
- const id = `session_${Date.now()}`
31
+ const now = Date.now()
32
+ const id = `session_${now}`
32
33
  createSession({
33
34
  id,
34
35
  type: type || 'email_review',
35
36
  payload,
36
37
  sessionKey,
37
38
  status: 'pending',
38
- createdAt: Date.now(),
39
+ createdAt: now,
40
+ updatedAt: now,
41
+ revision: 0,
39
42
  })
40
43
 
41
44
  const routeMap: Record<string, string> = {
42
45
  action_approval: 'approval',
43
46
  code_review: 'code-review',
47
+ form_review: 'form-review',
48
+ selection_review: 'selection',
44
49
  }
45
50
  const path = routeMap[type] ?? 'review'
46
51
  const url = `${WEB_ORIGIN}/${path}/${id}`
@@ -126,18 +131,42 @@ app.get('/api/sessions/:id/wait', async (req, res) => {
126
131
  while (Date.now() - start < TIMEOUT_MS) {
127
132
  const session = getSession(req.params.id)
128
133
  if (!session) return res.status(404).json({ error: 'Session not found' })
129
- if (session.status === 'completed') return res.json(session)
134
+ if (session.status === 'completed' || session.status === 'rewriting') {
135
+ console.log(`[agentclick] /wait returning ${session.status} for ${session.id} (revision=${session.revision})`)
136
+ return res.json(session)
137
+ }
130
138
  await new Promise(r => setTimeout(r, POLL_MS))
131
139
  }
132
140
 
133
141
  res.status(408).json({ error: 'timeout', message: 'User did not complete review within 5 minutes' })
134
142
  })
135
143
 
144
+ // Agent updates session payload after rewriting
145
+ app.put('/api/sessions/:id/payload', (req, res) => {
146
+ const session = getSession(req.params.id)
147
+ if (!session) return res.status(404).json({ error: 'Session not found' })
148
+ if (session.status !== 'rewriting') return res.status(400).json({ error: 'Session is not in rewriting state' })
149
+
150
+ console.log(`[agentclick] Payload update requested for ${session.id} (status=${session.status}, revision=${session.revision})`)
151
+ updateSessionPayload(req.params.id, req.body.payload)
152
+ const updated = getSession(req.params.id)
153
+ console.log(`[agentclick] Session ${session.id} payload updated, back to pending (revision=${updated?.revision ?? 'unknown'})`)
154
+ res.json({ ok: true })
155
+ })
156
+
136
157
  // Web UI submits user actions
137
158
  app.post('/api/sessions/:id/complete', async (req, res) => {
138
159
  const session = getSession(req.params.id)
139
160
  if (!session) return res.status(404).json({ error: 'Session not found' })
140
161
 
162
+ // If user requested regeneration, set to rewriting (not completed) so agent can update
163
+ if (req.body.regenerate) {
164
+ setSessionRewriting(req.params.id, req.body)
165
+ console.log(`[agentclick] Session ${session.id} → rewriting:`, JSON.stringify(req.body, null, 2))
166
+ res.json({ ok: true, rewriting: true })
167
+ return
168
+ }
169
+
141
170
  completeSession(req.params.id, req.body)
142
171
 
143
172
  console.log(`[agentclick] Session ${session.id} completed:`, JSON.stringify(req.body, null, 2))
@@ -206,6 +235,11 @@ function buildActionSummary(result: Record<string, unknown>): string {
206
235
  lines.push('- User requested full regeneration.')
207
236
  }
208
237
 
238
+ const intents = (result.selectedIntents ?? []) as Array<{ id: string; accepted: boolean }>
239
+ if (intents.length > 0) {
240
+ lines.push(`- Intent decisions: ${intents.map(i => `${i.id} → ${i.accepted ? 'accepted' : 'rejected'}`).join(', ')}`)
241
+ }
242
+
209
243
  return lines.join('\n')
210
244
  }
211
245
 
@@ -18,27 +18,41 @@ db.exec(`
18
18
  status TEXT NOT NULL DEFAULT 'pending',
19
19
  result TEXT,
20
20
  sessionKey TEXT,
21
- createdAt INTEGER NOT NULL
21
+ createdAt INTEGER NOT NULL,
22
+ updatedAt INTEGER NOT NULL DEFAULT 0,
23
+ revision INTEGER NOT NULL DEFAULT 0
22
24
  )
23
25
  `)
24
26
 
27
+ // Migrate existing tables: add updatedAt and revision if missing
28
+ try {
29
+ db.exec(`ALTER TABLE sessions ADD COLUMN updatedAt INTEGER NOT NULL DEFAULT 0`)
30
+ } catch { /* column already exists */ }
31
+ try {
32
+ db.exec(`ALTER TABLE sessions ADD COLUMN revision INTEGER NOT NULL DEFAULT 0`)
33
+ } catch { /* column already exists */ }
34
+
25
35
  export interface Session {
26
36
  id: string
27
37
  type: string
28
38
  payload: unknown
29
- status: 'pending' | 'completed'
39
+ status: 'pending' | 'rewriting' | 'completed'
30
40
  result?: unknown
31
41
  sessionKey?: string
32
42
  createdAt: number
43
+ updatedAt: number
44
+ revision: number
33
45
  }
34
46
 
35
47
  export function createSession(session: Session): void {
36
48
  db.prepare(`
37
- INSERT INTO sessions (id, type, payload, status, sessionKey, createdAt)
38
- VALUES (@id, @type, @payload, @status, @sessionKey, @createdAt)
49
+ INSERT INTO sessions (id, type, payload, status, sessionKey, createdAt, updatedAt, revision)
50
+ VALUES (@id, @type, @payload, @status, @sessionKey, @createdAt, @updatedAt, @revision)
39
51
  `).run({
40
52
  ...session,
41
53
  payload: JSON.stringify(session.payload),
54
+ updatedAt: session.updatedAt || Date.now(),
55
+ revision: session.revision || 0,
42
56
  })
43
57
  }
44
58
 
@@ -55,8 +69,20 @@ export function listSessions(limit = 20): Session[] {
55
69
 
56
70
  export function completeSession(id: string, result: unknown): void {
57
71
  db.prepare(`
58
- UPDATE sessions SET status = 'completed', result = ? WHERE id = ?
59
- `).run(JSON.stringify(result), id)
72
+ UPDATE sessions SET status = 'completed', result = ?, updatedAt = ? WHERE id = ?
73
+ `).run(JSON.stringify(result), Date.now(), id)
74
+ }
75
+
76
+ export function setSessionRewriting(id: string, result: unknown): void {
77
+ db.prepare(`
78
+ UPDATE sessions SET status = 'rewriting', result = ?, updatedAt = ? WHERE id = ?
79
+ `).run(JSON.stringify(result), Date.now(), id)
80
+ }
81
+
82
+ export function updateSessionPayload(id: string, payload: unknown): void {
83
+ db.prepare(`
84
+ UPDATE sessions SET payload = ?, status = 'pending', result = NULL, updatedAt = ?, revision = revision + 1 WHERE id = ?
85
+ `).run(JSON.stringify(payload), Date.now(), id)
60
86
  }
61
87
 
62
88
  function deserialize(row: Record<string, unknown>): Session {
@@ -64,9 +90,11 @@ function deserialize(row: Record<string, unknown>): Session {
64
90
  id: row.id as string,
65
91
  type: row.type as string,
66
92
  payload: JSON.parse(row.payload as string),
67
- status: row.status as 'pending' | 'completed',
93
+ status: row.status as 'pending' | 'rewriting' | 'completed',
68
94
  result: row.result ? JSON.parse(row.result as string) : undefined,
69
95
  sessionKey: row.sessionKey as string | undefined,
70
96
  createdAt: row.createdAt as number,
97
+ updatedAt: (row.updatedAt as number) || 0,
98
+ revision: (row.revision as number) || 0,
71
99
  }
72
100
  }