agentclick 0.1.0 → 0.2.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/README.md +52 -99
- package/bin/agentclick.mjs +75 -1
- package/package.json +21 -3
- package/packages/server/dist/index.js +33 -0
- package/packages/server/src/index.ts +39 -0
- package/packages/web/dist/assets/index-BfM7acF_.js +67 -0
- package/packages/web/dist/assets/index-CrlEXNVh.css +1 -0
- package/packages/web/dist/index.html +2 -2
- package/AGENTS.md +0 -146
- package/packages/web/dist/assets/index-D8TPv7du.css +0 -1
- package/packages/web/dist/assets/index-DABc1gsV.js +0 -67
package/README.md
CHANGED
|
@@ -1,145 +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
|
-
Optional server config (defaults shown in `.env.example`):
|
|
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
|
-
|
|
36
|
+
---
|
|
82
37
|
|
|
83
|
-
|
|
84
|
-
npm run build
|
|
85
|
-
npm start # serves API + built web UI on localhost:3001
|
|
86
|
-
```
|
|
38
|
+
## How It Works
|
|
87
39
|
|
|
88
|
-
|
|
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.
|
|
89
43
|
|
|
90
|
-
|
|
44
|
+
No WebSockets. No framework plugins. One HTTP endpoint in, one HTTP endpoint out.
|
|
91
45
|
|
|
92
|
-
|
|
46
|
+
---
|
|
93
47
|
|
|
94
|
-
|
|
95
|
-
cp -r skills/clawui-email ~/.openclaw/skills/
|
|
96
|
-
```
|
|
48
|
+
## Comparison
|
|
97
49
|
|
|
98
|
-
|
|
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 |
|
|
99
57
|
|
|
100
58
|
---
|
|
101
59
|
|
|
102
|
-
##
|
|
60
|
+
## Session Types
|
|
103
61
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
│ ├── server/ # Node.js + Express — receives agent data, handles callbacks
|
|
108
|
-
│ └── web/ # React + Vite + Tailwind — the interaction UI
|
|
109
|
-
├── skills/
|
|
110
|
-
│ └── clawui-email/
|
|
111
|
-
│ └── SKILL.md # OpenClaw skill definition
|
|
112
|
-
└── docs/
|
|
113
|
-
└── research.md # Market & technical research notes
|
|
114
|
-
```
|
|
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.
|
|
115
65
|
|
|
116
66
|
---
|
|
117
67
|
|
|
118
|
-
##
|
|
68
|
+
## API
|
|
119
69
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
-
|
|
125
|
-
|
|
126
|
-
- [ ] **Next** — Remote mode UX + link delivery polish
|
|
127
|
-
- [ ] **Later** — Agent task visualization (Mission Control view)
|
|
128
|
-
- [ ] **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. |
|
|
129
76
|
|
|
130
77
|
---
|
|
131
78
|
|
|
132
|
-
##
|
|
79
|
+
## Development
|
|
133
80
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
81
|
+
```bash
|
|
82
|
+
git clone https://github.com/agentlayer-io/AgentClick.git
|
|
83
|
+
cd AgentClick
|
|
84
|
+
npm install
|
|
85
|
+
npm run dev
|
|
86
|
+
```
|
|
137
87
|
|
|
138
|
-
|
|
88
|
+
Server runs on `http://localhost:3001`, UI on `http://localhost:5173`.
|
|
139
89
|
|
|
140
|
-
|
|
90
|
+
For production-style single-port serving:
|
|
141
91
|
|
|
142
|
-
|
|
92
|
+
```bash
|
|
93
|
+
npm run build
|
|
94
|
+
npm start
|
|
95
|
+
```
|
|
143
96
|
|
|
144
97
|
---
|
|
145
98
|
|
package/bin/agentclick.mjs
CHANGED
|
@@ -4,11 +4,39 @@ import { existsSync } 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'
|
|
7
|
+
import net from 'node:net'
|
|
7
8
|
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url)
|
|
9
10
|
const rootDir = dirname(dirname(__filename))
|
|
10
11
|
const webDistIndex = join(rootDir, 'packages', 'web', 'dist', 'index.html')
|
|
11
12
|
const serverDistEntry = join(rootDir, 'packages', 'server', 'dist', 'index.js')
|
|
13
|
+
const args = process.argv.slice(2)
|
|
14
|
+
|
|
15
|
+
function printHelp() {
|
|
16
|
+
console.log(`AgentClick CLI
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
agentclick
|
|
20
|
+
agentclick --help
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--help, -h Show this help message
|
|
24
|
+
`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseArgs(argv) {
|
|
28
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
29
|
+
const arg = argv[i]
|
|
30
|
+
if (arg === '--help' || arg === '-h') {
|
|
31
|
+
printHelp()
|
|
32
|
+
process.exit(0)
|
|
33
|
+
}
|
|
34
|
+
console.error(`[agentclick] Unknown argument: ${arg}`)
|
|
35
|
+
console.error('[agentclick] Run "agentclick --help" for usage.')
|
|
36
|
+
process.exit(1)
|
|
37
|
+
}
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
12
40
|
|
|
13
41
|
function run(command, args) {
|
|
14
42
|
const result = spawnSync(command, args, {
|
|
@@ -25,6 +53,8 @@ function run(command, args) {
|
|
|
25
53
|
}
|
|
26
54
|
}
|
|
27
55
|
|
|
56
|
+
parseArgs(args)
|
|
57
|
+
|
|
28
58
|
if (!existsSync(webDistIndex) || !existsSync(serverDistEntry)) {
|
|
29
59
|
console.log('[agentclick] Build artifacts not found, running npm run build...')
|
|
30
60
|
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
@@ -36,4 +66,48 @@ if (!existsSync(serverDistEntry)) {
|
|
|
36
66
|
process.exit(1)
|
|
37
67
|
}
|
|
38
68
|
|
|
39
|
-
|
|
69
|
+
async function canListen(port) {
|
|
70
|
+
return await new Promise(resolve => {
|
|
71
|
+
const server = net.createServer()
|
|
72
|
+
server.once('error', err => {
|
|
73
|
+
if ((err).code === 'EADDRINUSE') {
|
|
74
|
+
resolve(false)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
console.error(`[agentclick] Port check failed for ${port}: ${err.message}`)
|
|
78
|
+
process.exit(1)
|
|
79
|
+
})
|
|
80
|
+
server.once('listening', () => {
|
|
81
|
+
server.close(() => resolve(true))
|
|
82
|
+
})
|
|
83
|
+
server.listen(port)
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function resolvePort() {
|
|
88
|
+
if (process.env.PORT) return process.env.PORT
|
|
89
|
+
|
|
90
|
+
let port = 3001
|
|
91
|
+
while (true) {
|
|
92
|
+
const available = await canListen(port)
|
|
93
|
+
if (available) return String(port)
|
|
94
|
+
console.log(`[agentclick] Port ${port} in use, trying ${port + 1}...`)
|
|
95
|
+
port += 1
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const childEnv = { ...process.env }
|
|
100
|
+
childEnv.PORT = await resolvePort()
|
|
101
|
+
|
|
102
|
+
const result = spawnSync(process.execPath, [serverDistEntry], {
|
|
103
|
+
cwd: rootDir,
|
|
104
|
+
stdio: 'inherit',
|
|
105
|
+
env: childEnv,
|
|
106
|
+
})
|
|
107
|
+
if (result.error) {
|
|
108
|
+
console.error('[agentclick] Failed to start server:', result.error.message)
|
|
109
|
+
process.exit(1)
|
|
110
|
+
}
|
|
111
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
112
|
+
process.exit(result.status)
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentclick",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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/*"
|
|
@@ -74,6 +74,35 @@ app.get('/api/sessions/:id', (req, res) => {
|
|
|
74
74
|
return res.status(404).json({ error: 'Session not found' });
|
|
75
75
|
res.json(session);
|
|
76
76
|
});
|
|
77
|
+
// Mock summary endpoint for inbox items (UI integration first)
|
|
78
|
+
app.get('/api/sessions/:id/summary', (req, res) => {
|
|
79
|
+
const session = getSession(req.params.id);
|
|
80
|
+
if (!session)
|
|
81
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
82
|
+
const payload = session.payload;
|
|
83
|
+
const inbox = payload?.inbox ?? [];
|
|
84
|
+
const emailId = req.query.emailId;
|
|
85
|
+
const email = inbox.find(item => item.id === emailId);
|
|
86
|
+
if (!email) {
|
|
87
|
+
return res.status(404).json({ error: 'Email not found in session' });
|
|
88
|
+
}
|
|
89
|
+
const preview = String(email.preview ?? '');
|
|
90
|
+
const summaryText = preview.length > 0
|
|
91
|
+
? `This email appears to be about: ${preview}`
|
|
92
|
+
: 'No preview text is available for this email yet.';
|
|
93
|
+
const from = String(email.from ?? 'Unknown sender');
|
|
94
|
+
const category = String(email.category ?? 'Unknown');
|
|
95
|
+
res.json({
|
|
96
|
+
emailId,
|
|
97
|
+
summary: summaryText,
|
|
98
|
+
bullets: [
|
|
99
|
+
`From: ${from}`,
|
|
100
|
+
`Category: ${category}`,
|
|
101
|
+
'Source: mock summary endpoint (replace with agent summary later)',
|
|
102
|
+
],
|
|
103
|
+
confidence: 'mock',
|
|
104
|
+
});
|
|
105
|
+
});
|
|
77
106
|
// Long-poll: agent calls this and blocks until user completes review (up to 5 min)
|
|
78
107
|
app.get('/api/sessions/:id/wait', async (req, res) => {
|
|
79
108
|
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -154,6 +183,10 @@ function buildActionSummary(result) {
|
|
|
154
183
|
if (result.regenerate) {
|
|
155
184
|
lines.push('- User requested full regeneration.');
|
|
156
185
|
}
|
|
186
|
+
const intents = (result.selectedIntents ?? []);
|
|
187
|
+
if (intents.length > 0) {
|
|
188
|
+
lines.push(`- Intent decisions: ${intents.map(i => `${i.id} → ${i.accepted ? 'accepted' : 'rejected'}`).join(', ')}`);
|
|
189
|
+
}
|
|
157
190
|
return lines.join('\n');
|
|
158
191
|
}
|
|
159
192
|
if (SHOULD_SERVE_BUILT_WEB) {
|
|
@@ -83,6 +83,40 @@ app.get('/api/sessions/:id', (req, res) => {
|
|
|
83
83
|
res.json(session)
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
+
// Mock summary endpoint for inbox items (UI integration first)
|
|
87
|
+
app.get('/api/sessions/:id/summary', (req, res) => {
|
|
88
|
+
const session = getSession(req.params.id)
|
|
89
|
+
if (!session) return res.status(404).json({ error: 'Session not found' })
|
|
90
|
+
|
|
91
|
+
const payload = session.payload as Record<string, unknown> | undefined
|
|
92
|
+
const inbox = (payload?.inbox as Array<Record<string, unknown>> | undefined) ?? []
|
|
93
|
+
const emailId = req.query.emailId as string | undefined
|
|
94
|
+
const email = inbox.find(item => item.id === emailId)
|
|
95
|
+
|
|
96
|
+
if (!email) {
|
|
97
|
+
return res.status(404).json({ error: 'Email not found in session' })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const preview = String(email.preview ?? '')
|
|
101
|
+
const summaryText = preview.length > 0
|
|
102
|
+
? `This email appears to be about: ${preview}`
|
|
103
|
+
: 'No preview text is available for this email yet.'
|
|
104
|
+
|
|
105
|
+
const from = String(email.from ?? 'Unknown sender')
|
|
106
|
+
const category = String(email.category ?? 'Unknown')
|
|
107
|
+
|
|
108
|
+
res.json({
|
|
109
|
+
emailId,
|
|
110
|
+
summary: summaryText,
|
|
111
|
+
bullets: [
|
|
112
|
+
`From: ${from}`,
|
|
113
|
+
`Category: ${category}`,
|
|
114
|
+
'Source: mock summary endpoint (replace with agent summary later)',
|
|
115
|
+
],
|
|
116
|
+
confidence: 'mock',
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
86
120
|
// Long-poll: agent calls this and blocks until user completes review (up to 5 min)
|
|
87
121
|
app.get('/api/sessions/:id/wait', async (req, res) => {
|
|
88
122
|
const TIMEOUT_MS = 5 * 60 * 1000
|
|
@@ -172,6 +206,11 @@ function buildActionSummary(result: Record<string, unknown>): string {
|
|
|
172
206
|
lines.push('- User requested full regeneration.')
|
|
173
207
|
}
|
|
174
208
|
|
|
209
|
+
const intents = (result.selectedIntents ?? []) as Array<{ id: string; accepted: boolean }>
|
|
210
|
+
if (intents.length > 0) {
|
|
211
|
+
lines.push(`- Intent decisions: ${intents.map(i => `${i.id} → ${i.accepted ? 'accepted' : 'rejected'}`).join(', ')}`)
|
|
212
|
+
}
|
|
213
|
+
|
|
175
214
|
return lines.join('\n')
|
|
176
215
|
}
|
|
177
216
|
|