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 +52 -108
- package/bin/agentclick.mjs +24 -1
- package/package.json +21 -3
- package/packages/server/dist/index.js +35 -4
- package/packages/server/dist/store.js +30 -5
- package/packages/server/src/index.ts +38 -4
- package/packages/server/src/store.ts +35 -7
- package/packages/web/dist/assets/index-AqoWN0xJ.js +67 -0
- package/packages/web/dist/assets/index-wTVix_xH.css +1 -0
- package/packages/web/dist/index.html +2 -2
- package/AGENTS.md +0 -148
- package/packages/web/dist/assets/index-CLd6pAEL.js +0 -67
- package/packages/web/dist/assets/index-DAT8x7ee.css +0 -1
package/README.md
CHANGED
|
@@ -1,154 +1,98 @@
|
|
|
1
1
|
# AgentClick
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AI agents fail silently and take irreversible actions. AgentClick puts a human review step between your agent and the world.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/agentclick)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://www.npmjs.com/package/agentclick)
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Why AgentClick
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
CLI options:
|
|
26
|
+
Then test it with a mock session:
|
|
73
27
|
|
|
74
28
|
```bash
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
34
|
+
A browser tab opens automatically. Review, approve or reject, and close the tab.
|
|
80
35
|
|
|
81
|
-
|
|
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
|
-
|
|
38
|
+
## How It Works
|
|
91
39
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
44
|
+
No WebSockets. No framework plugins. One HTTP endpoint in, one HTTP endpoint out.
|
|
98
45
|
|
|
99
|
-
|
|
46
|
+
---
|
|
100
47
|
|
|
101
|
-
|
|
48
|
+
## Comparison
|
|
102
49
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
##
|
|
60
|
+
## Session Types
|
|
112
61
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
##
|
|
68
|
+
## API
|
|
128
69
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
-
|
|
134
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
88
|
+
Server runs on `http://localhost:3001`, UI on `http://localhost:5173`.
|
|
148
89
|
|
|
149
|
-
|
|
90
|
+
For production-style single-port serving:
|
|
150
91
|
|
|
151
|
-
|
|
92
|
+
```bash
|
|
93
|
+
npm run build
|
|
94
|
+
npm start
|
|
95
|
+
```
|
|
152
96
|
|
|
153
97
|
---
|
|
154
98
|
|
package/bin/agentclick.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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'
|
|
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
|
}
|