agentclick 0.1.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/AGENTS.md +146 -0
- package/README.md +148 -0
- package/bin/agentclick.mjs +39 -0
- package/package.json +35 -0
- package/packages/server/dist/index.js +170 -0
- package/packages/server/dist/preference.js +68 -0
- package/packages/server/dist/store.js +55 -0
- package/packages/server/package.json +25 -0
- package/packages/server/src/index.ts +189 -0
- package/packages/server/src/preference.ts +101 -0
- package/packages/server/src/store.ts +72 -0
- package/packages/web/dist/assets/index-D8TPv7du.css +1 -0
- package/packages/web/dist/assets/index-DABc1gsV.js +67 -0
- package/packages/web/dist/index.html +13 -0
- package/packages/web/package.json +25 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# AgentClick — Claude Code Context
|
|
2
|
+
|
|
3
|
+
## What This Project Is
|
|
4
|
+
|
|
5
|
+
AgentClick is a human-in-the-loop UI layer for AI agents. Agents POST structured data here; users review/approve/edit in the browser; results callback to the agent. AgentClick never connects directly to email or external services — all data flows through the agent.
|
|
6
|
+
|
|
7
|
+
GitHub: https://github.com/agentlayer-io/AgentClick
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Start the Dev Server
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm run dev
|
|
15
|
+
# Server: http://localhost:3001
|
|
16
|
+
# UI: http://localhost:5173
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The server auto-loads root `.env` via `dotenv` for `PORT` and `OPENCLAW_WEBHOOK` overrides.
|
|
20
|
+
|
|
21
|
+
Production-style local run (single port):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm run build
|
|
25
|
+
npm start
|
|
26
|
+
# API + UI: http://localhost:3001
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Global CLI install target (packaging in progress):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g agentclick
|
|
33
|
+
agentclick
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Deployment / production notes:
|
|
37
|
+
- `docs/deployment.md` (single-port serving, `.env`, Nginx reverse proxy example, Docker/OpenClaw host mapping)
|
|
38
|
+
|
|
39
|
+
To test without a live agent, POST a mock session:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
curl -s -X POST http://localhost:3001/api/review \
|
|
43
|
+
-H "Content-Type: application/json" \
|
|
44
|
+
-d '{
|
|
45
|
+
"type": "email_review",
|
|
46
|
+
"sessionKey": "test-key",
|
|
47
|
+
"payload": {
|
|
48
|
+
"inbox": [
|
|
49
|
+
{ "id": "e1", "from": "john@example.com", "subject": "Q1 Follow-up",
|
|
50
|
+
"preview": "Hi, just wanted to follow up...", "category": "Work",
|
|
51
|
+
"isRead": false, "timestamp": 1771747653333 }
|
|
52
|
+
],
|
|
53
|
+
"draft": {
|
|
54
|
+
"replyTo": "e1", "to": "john@example.com",
|
|
55
|
+
"subject": "Re: Q1 Follow-up",
|
|
56
|
+
"paragraphs": [
|
|
57
|
+
{ "id": "p1", "content": "Hi John, thanks for following up." },
|
|
58
|
+
{ "id": "p2", "content": "We are aligned on the timeline." },
|
|
59
|
+
{ "id": "p3", "content": "Let me know if anything else is needed." }
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}'
|
|
64
|
+
# Browser opens automatically to the review session
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Key Files
|
|
70
|
+
|
|
71
|
+
| File | Purpose |
|
|
72
|
+
|------|---------|
|
|
73
|
+
| `packages/server/src/index.ts` | Express API: all routes, long-poll `/wait` endpoint, browser auto-open |
|
|
74
|
+
| `packages/server/src/store.ts` | SQLite session store via `better-sqlite3` → `~/.openclaw/clawui-sessions.db` |
|
|
75
|
+
| `packages/server/src/preference.ts` | Writes AVOID rules to `~/.openclaw/workspace/MEMORY.md` |
|
|
76
|
+
| `packages/web/src/pages/ReviewPage.tsx` | Main UI — two-column inbox + draft review (Format B) and legacy single-column (Format A) |
|
|
77
|
+
| `packages/web/src/pages/ApprovalPage.tsx` | Action approval UI |
|
|
78
|
+
| `packages/web/src/pages/CodeReviewPage.tsx` | Shell command review UI |
|
|
79
|
+
| `packages/web/src/pages/HomePage.tsx` | Session history list |
|
|
80
|
+
| `skills/` | OpenClaw SKILL.md files (use `host.docker.internal:3001` if OpenClaw runs in Docker) |
|
|
81
|
+
| `scripts/demo.sh` | One-command test: `./scripts/demo.sh [email\|approval\|code]` |
|
|
82
|
+
| `scripts/install-skills.sh` | Copies skills to `~/.openclaw/skills/` and `~/.openclaw/workspace/skills/` |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## API Shape
|
|
87
|
+
|
|
88
|
+
**POST /api/review** — agent creates a session, browser opens automatically
|
|
89
|
+
**GET /api/sessions** — list sessions (max 20, sorted by createdAt desc)
|
|
90
|
+
**GET /api/sessions/:id** — get single session
|
|
91
|
+
**GET /api/sessions/:id/wait** — long-poll, blocks up to 5 min until user submits (agent calls this to wait for review result)
|
|
92
|
+
**POST /api/sessions/:id/complete** — UI submits user decision
|
|
93
|
+
|
|
94
|
+
OpenClaw webhook (optional): `POST http://localhost:18789/hooks/agent`
|
|
95
|
+
Body: `{ message: string, sessionKey: string, deliver: true }`
|
|
96
|
+
|
|
97
|
+
> For agents running in Docker: use `host.docker.internal:3001` instead of `localhost:3001`.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Two Payload Formats
|
|
102
|
+
|
|
103
|
+
**Format A (legacy):** `{ to, subject, paragraphs[] }`
|
|
104
|
+
**Format B (v2):** `{ inbox[], draft: { replyTo, to, subject, paragraphs[], ccSuggestions? } }`
|
|
105
|
+
|
|
106
|
+
`ReviewPage.tsx` auto-detects via `'inbox' in payload`.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Rules
|
|
111
|
+
|
|
112
|
+
Read `docs/dev-rules.md` before writing any code. Key rules:
|
|
113
|
+
- TypeScript only, no `any`
|
|
114
|
+
- Tailwind only, no inline styles
|
|
115
|
+
- Linear/Vercel aesthetic: zinc/gray palette, `shadow-sm` max, no gradients
|
|
116
|
+
- Commit format: `type: short description` (no emoji, no Claude attribution)
|
|
117
|
+
- No over-abstraction — if one file handles it, keep it in one file
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## What's Done / What's Pending
|
|
122
|
+
|
|
123
|
+
**Done:**
|
|
124
|
+
- ReviewPage v2 (two-column inbox + draft), ApprovalPage, CodeReviewPage, HomePage
|
|
125
|
+
- Preference learning (writes AVOID rules to MEMORY.md on paragraph delete)
|
|
126
|
+
- Risk-based color grading on session history
|
|
127
|
+
- SQLite session persistence (`~/.openclaw/clawui-sessions.db`)
|
|
128
|
+
- Long-poll `/wait` endpoint for agent integration
|
|
129
|
+
- Keyboard shortcuts: `Escape` closes dropdown, `Cmd+Enter` submits
|
|
130
|
+
- End-to-end tested with OpenClaw (Docker) via Feishu — full loop verified
|
|
131
|
+
- Production single-port serving (`npm run build && npm start`)
|
|
132
|
+
- Minimal CI build workflow (GitHub Actions: `npm ci && npm run build`)
|
|
133
|
+
- Deployment guide (`docs/deployment.md`) for single-port runtime, env vars, reverse proxy, and Docker/OpenClaw notes
|
|
134
|
+
- Global CLI packaging entrypoint (`bin/agentclick.mjs`) for `agentclick` command startup
|
|
135
|
+
|
|
136
|
+
**Pending:**
|
|
137
|
+
- npm publish execution (`npm publish`) after final package review
|
|
138
|
+
- CC suggestions as checkboxes (currently free-text input; agent can pass `ccSuggestions[]`)
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Recent Verification
|
|
143
|
+
|
|
144
|
+
- 2026-02-23 (local): Verified API session creation for `email_review`, `action_approval`, and `code_review` via `scripts/demo.sh`.
|
|
145
|
+
- 2026-02-23 (local): Verified `create -> GET session (pending) -> complete -> /wait (completed)` flow on `action_approval`.
|
|
146
|
+
- 2026-02-23 (local): Verified single-port production mode (`npm run build && PORT=3101 npm start`) serves `/`, SPA fallback routes, `/api/*`, and returns single-port review URLs.
|
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# AgentClick
|
|
2
|
+
|
|
3
|
+
**Rich web UI for AI agent interactions — click to edit, human-in-the-loop, preference learning.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/agentlayer-io/AgentClick/stargazers)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://github.com/agentlayer-io/AgentClick/pulls)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## The Problem
|
|
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
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
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
|
+
```bash
|
|
66
|
+
npm install -g agentclick
|
|
67
|
+
agentclick
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This starts the AgentClick server on `http://localhost:3001` and serves the built web UI on the same port.
|
|
71
|
+
|
|
72
|
+
Optional server config (defaults shown in `.env.example`):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
PORT=3001
|
|
76
|
+
OPENCLAW_WEBHOOK=http://localhost:18789/hooks/agent
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Create a local `.env` in the project root to override these values during development (server auto-loads it via `dotenv`).
|
|
80
|
+
|
|
81
|
+
Production-style local run (single port after build):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm run build
|
|
85
|
+
npm start # serves API + built web UI on localhost:3001
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Deployment notes (reverse proxy, Docker/OpenClaw host mapping, env vars):
|
|
89
|
+
|
|
90
|
+
- See `docs/deployment.md`
|
|
91
|
+
|
|
92
|
+
Copy the skill to your OpenClaw workspace:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
cp -r skills/clawui-email ~/.openclaw/skills/
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Restart OpenClaw. Ask it to write an email — the review page will open automatically.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Project Structure
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
AgentClick/
|
|
106
|
+
├── packages/
|
|
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
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Roadmap
|
|
119
|
+
|
|
120
|
+
- [x] **M0** — Email draft review (click to delete/rewrite paragraphs)
|
|
121
|
+
- [x] **M1** — Preference learning (auto-save rules to MEMORY.md)
|
|
122
|
+
- [x] **M2 (partial)** — Agent integration loop (`/review` create session + `/wait` long-poll + callback)
|
|
123
|
+
- [x] **Next** — Unified serving (single port; no separate Vite port in production)
|
|
124
|
+
- [x] **Next** — Production serve polish (deployment docs / environment examples)
|
|
125
|
+
- [ ] **Next** — npm global package (`agentclick`)
|
|
126
|
+
- [ ] **Next** — Remote mode UX + link delivery polish
|
|
127
|
+
- [ ] **Later** — Agent task visualization (Mission Control view)
|
|
128
|
+
- [ ] **Later** — Multi-framework support (beyond OpenClaw)
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Why Not ClawX?
|
|
133
|
+
|
|
134
|
+
[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.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Contributing
|
|
139
|
+
|
|
140
|
+
This is an early-stage open source project. All contributions welcome — UI components, new interaction patterns, OpenClaw integration improvements, documentation.
|
|
141
|
+
|
|
142
|
+
Open an issue to discuss before submitting large PRs.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
9
|
+
const rootDir = dirname(dirname(__filename))
|
|
10
|
+
const webDistIndex = join(rootDir, 'packages', 'web', 'dist', 'index.html')
|
|
11
|
+
const serverDistEntry = join(rootDir, 'packages', 'server', 'dist', 'index.js')
|
|
12
|
+
|
|
13
|
+
function run(command, args) {
|
|
14
|
+
const result = spawnSync(command, args, {
|
|
15
|
+
cwd: rootDir,
|
|
16
|
+
stdio: 'inherit',
|
|
17
|
+
env: process.env,
|
|
18
|
+
})
|
|
19
|
+
if (result.error) {
|
|
20
|
+
console.error(`[agentclick] Failed to run ${command}:`, result.error.message)
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
24
|
+
process.exit(result.status)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!existsSync(webDistIndex) || !existsSync(serverDistEntry)) {
|
|
29
|
+
console.log('[agentclick] Build artifacts not found, running npm run build...')
|
|
30
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
31
|
+
run(npmCmd, ['run', 'build'])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!existsSync(serverDistEntry)) {
|
|
35
|
+
console.error('[agentclick] Server build output missing after build. Expected packages/server/dist/index.js')
|
|
36
|
+
process.exit(1)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
run(process.execPath, [serverDistEntry])
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentclick",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"bin": {
|
|
5
|
+
"agentclick": "./bin/agentclick.mjs"
|
|
6
|
+
},
|
|
7
|
+
"files": [
|
|
8
|
+
"bin/",
|
|
9
|
+
"packages/server/dist/",
|
|
10
|
+
"packages/server/src/",
|
|
11
|
+
"packages/server/package.json",
|
|
12
|
+
"packages/web/dist/",
|
|
13
|
+
"packages/web/package.json",
|
|
14
|
+
"README.md",
|
|
15
|
+
"AGENTS.md"
|
|
16
|
+
],
|
|
17
|
+
"workspaces": [
|
|
18
|
+
"packages/*"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "concurrently \"npm run dev -w packages/server\" \"npm run dev -w packages/web\"",
|
|
22
|
+
"build": "npm run build -w packages/server && npm run build -w packages/web",
|
|
23
|
+
"start": "npm run start -w packages/server"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"better-sqlite3": "^12.6.2",
|
|
27
|
+
"cors": "^2.8.5",
|
|
28
|
+
"dotenv": "^17.3.1",
|
|
29
|
+
"express": "^4.19.2",
|
|
30
|
+
"open": "^10.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"concurrently": "^9.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { learnFromDeletions } from './preference.js';
|
|
9
|
+
import { createSession, getSession, listSessions, completeSession } from './store.js';
|
|
10
|
+
const app = express();
|
|
11
|
+
const PORT = Number(process.env.PORT || 3001);
|
|
12
|
+
const OPENCLAW_WEBHOOK = process.env.OPENCLAW_WEBHOOK || 'http://localhost:18789/hooks/agent';
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
const WEB_DIST_DIR = join(__dirname, '../../web/dist');
|
|
16
|
+
const SHOULD_SERVE_BUILT_WEB = existsSync(WEB_DIST_DIR) && (__filename.endsWith('/dist/index.js') || process.env.NODE_ENV === 'production');
|
|
17
|
+
const WEB_ORIGIN = SHOULD_SERVE_BUILT_WEB ? `http://localhost:${PORT}` : 'http://localhost:5173';
|
|
18
|
+
app.use(cors());
|
|
19
|
+
app.use(express.json());
|
|
20
|
+
// OpenClaw calls this when a review is needed
|
|
21
|
+
app.post('/api/review', async (req, res) => {
|
|
22
|
+
const { type, sessionKey, payload } = req.body;
|
|
23
|
+
if (!sessionKey) {
|
|
24
|
+
console.warn('[agentclick] Warning: sessionKey missing — callback will be skipped');
|
|
25
|
+
}
|
|
26
|
+
const id = `session_${Date.now()}`;
|
|
27
|
+
createSession({
|
|
28
|
+
id,
|
|
29
|
+
type: type || 'email_review',
|
|
30
|
+
payload,
|
|
31
|
+
sessionKey,
|
|
32
|
+
status: 'pending',
|
|
33
|
+
createdAt: Date.now(),
|
|
34
|
+
});
|
|
35
|
+
const routeMap = {
|
|
36
|
+
action_approval: 'approval',
|
|
37
|
+
code_review: 'code-review',
|
|
38
|
+
};
|
|
39
|
+
const path = routeMap[type] ?? 'review';
|
|
40
|
+
const url = `${WEB_ORIGIN}/${path}/${id}`;
|
|
41
|
+
console.log(`[agentclick] Review session created: ${id}`);
|
|
42
|
+
console.log(`[agentclick] Opening browser: ${url}`);
|
|
43
|
+
try {
|
|
44
|
+
await open(url);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.warn('[agentclick] Failed to open browser:', err);
|
|
48
|
+
}
|
|
49
|
+
res.json({ sessionId: id, url });
|
|
50
|
+
});
|
|
51
|
+
// List recent sessions for homepage
|
|
52
|
+
app.get('/api/sessions', (_req, res) => {
|
|
53
|
+
const list = listSessions(20).map(s => {
|
|
54
|
+
const p = s.payload;
|
|
55
|
+
// Format B (inbox+draft) stores subject/to inside draft
|
|
56
|
+
const draft = p?.draft;
|
|
57
|
+
return {
|
|
58
|
+
id: s.id,
|
|
59
|
+
type: s.type,
|
|
60
|
+
status: s.status,
|
|
61
|
+
createdAt: s.createdAt,
|
|
62
|
+
subject: (draft?.subject ?? p?.subject),
|
|
63
|
+
to: (draft?.to ?? p?.to),
|
|
64
|
+
risk: p?.risk,
|
|
65
|
+
command: p?.command,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
res.json(list);
|
|
69
|
+
});
|
|
70
|
+
// Web UI fetches session data
|
|
71
|
+
app.get('/api/sessions/:id', (req, res) => {
|
|
72
|
+
const session = getSession(req.params.id);
|
|
73
|
+
if (!session)
|
|
74
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
75
|
+
res.json(session);
|
|
76
|
+
});
|
|
77
|
+
// Long-poll: agent calls this and blocks until user completes review (up to 5 min)
|
|
78
|
+
app.get('/api/sessions/:id/wait', async (req, res) => {
|
|
79
|
+
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
80
|
+
const POLL_MS = 1500;
|
|
81
|
+
const start = Date.now();
|
|
82
|
+
while (Date.now() - start < TIMEOUT_MS) {
|
|
83
|
+
const session = getSession(req.params.id);
|
|
84
|
+
if (!session)
|
|
85
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
86
|
+
if (session.status === 'completed')
|
|
87
|
+
return res.json(session);
|
|
88
|
+
await new Promise(r => setTimeout(r, POLL_MS));
|
|
89
|
+
}
|
|
90
|
+
res.status(408).json({ error: 'timeout', message: 'User did not complete review within 5 minutes' });
|
|
91
|
+
});
|
|
92
|
+
// Web UI submits user actions
|
|
93
|
+
app.post('/api/sessions/:id/complete', async (req, res) => {
|
|
94
|
+
const session = getSession(req.params.id);
|
|
95
|
+
if (!session)
|
|
96
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
97
|
+
completeSession(req.params.id, req.body);
|
|
98
|
+
console.log(`[agentclick] Session ${session.id} completed:`, JSON.stringify(req.body, null, 2));
|
|
99
|
+
// Learn from delete actions and persist rules to MEMORY.md
|
|
100
|
+
const actions = (req.body.actions ?? []);
|
|
101
|
+
learnFromDeletions(actions, session.payload);
|
|
102
|
+
// Send result back to OpenClaw
|
|
103
|
+
let callbackFailed = false;
|
|
104
|
+
let callbackError = '';
|
|
105
|
+
if (session.sessionKey) {
|
|
106
|
+
try {
|
|
107
|
+
const summary = buildActionSummary(req.body);
|
|
108
|
+
await fetch(OPENCLAW_WEBHOOK, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {
|
|
111
|
+
'Content-Type': 'application/json',
|
|
112
|
+
'Authorization': `Bearer ${process.env.OPENCLAW_TOKEN || ''}`
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
message: summary,
|
|
116
|
+
sessionKey: session.sessionKey,
|
|
117
|
+
deliver: true
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
console.log(`[agentclick] Callback sent to OpenClaw`);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
callbackFailed = true;
|
|
124
|
+
callbackError = String(err);
|
|
125
|
+
console.error(`[agentclick] Failed to callback OpenClaw:`, err);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
res.json({ ok: true, callbackFailed, callbackError });
|
|
129
|
+
});
|
|
130
|
+
function buildActionSummary(result) {
|
|
131
|
+
// If result has approved field, it's an action_approval or code_review
|
|
132
|
+
if ('approved' in result) {
|
|
133
|
+
const approved = result.approved;
|
|
134
|
+
const note = result.note;
|
|
135
|
+
const lines = ['[agentclick] User reviewed the request:'];
|
|
136
|
+
lines.push(approved ? '- Approved: proceed.' : '- Rejected: do not proceed.');
|
|
137
|
+
if (note)
|
|
138
|
+
lines.push(`- Note: ${note}`);
|
|
139
|
+
return lines.join('\n');
|
|
140
|
+
}
|
|
141
|
+
const actions = result.actions || [];
|
|
142
|
+
const lines = ['[agentclick] User reviewed the draft:'];
|
|
143
|
+
const deleted = actions.filter(a => a.type === 'delete');
|
|
144
|
+
const rewritten = actions.filter(a => a.type === 'rewrite');
|
|
145
|
+
if (deleted.length > 0) {
|
|
146
|
+
lines.push(`- Deleted ${deleted.length} paragraph(s): ${deleted.map(a => `${a.paragraphId} (reason: ${a.reason})`).join(', ')}`);
|
|
147
|
+
}
|
|
148
|
+
if (rewritten.length > 0) {
|
|
149
|
+
lines.push(`- Requested rewrite for: ${rewritten.map(a => `${a.paragraphId} — "${a.instruction}"`).join(', ')}`);
|
|
150
|
+
}
|
|
151
|
+
if (result.confirmed) {
|
|
152
|
+
lines.push('- User confirmed: proceed with sending.');
|
|
153
|
+
}
|
|
154
|
+
if (result.regenerate) {
|
|
155
|
+
lines.push('- User requested full regeneration.');
|
|
156
|
+
}
|
|
157
|
+
return lines.join('\n');
|
|
158
|
+
}
|
|
159
|
+
if (SHOULD_SERVE_BUILT_WEB) {
|
|
160
|
+
app.use(express.static(WEB_DIST_DIR));
|
|
161
|
+
app.get('*', (req, res, next) => {
|
|
162
|
+
if (req.path.startsWith('/api/'))
|
|
163
|
+
return next();
|
|
164
|
+
res.sendFile(join(WEB_DIST_DIR, 'index.html'));
|
|
165
|
+
});
|
|
166
|
+
console.log(`[agentclick] Serving web UI from ${WEB_DIST_DIR}`);
|
|
167
|
+
}
|
|
168
|
+
app.listen(PORT, () => {
|
|
169
|
+
console.log(`[agentclick] Server running at http://localhost:${PORT}`);
|
|
170
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const MEMORY_PATH = path.join(os.homedir(), '.openclaw', 'workspace', 'MEMORY.md');
|
|
5
|
+
const SECTION_HEADER = '## Email Preferences (ClawUI Auto-Learned)';
|
|
6
|
+
// Map raw reason keys to human-readable descriptions
|
|
7
|
+
const REASON_LABELS = {
|
|
8
|
+
too_formal: 'too formal',
|
|
9
|
+
too_casual: 'too casual',
|
|
10
|
+
too_long: 'too long',
|
|
11
|
+
off_topic: 'off topic',
|
|
12
|
+
inaccurate: 'inaccurate',
|
|
13
|
+
repetitive: 'repetitive',
|
|
14
|
+
unnecessary: 'unnecessary',
|
|
15
|
+
wrong_tone: 'wrong tone',
|
|
16
|
+
too_polite: 'too polite',
|
|
17
|
+
redundant: 'redundant',
|
|
18
|
+
};
|
|
19
|
+
function ensureMemoryFile() {
|
|
20
|
+
const dir = path.dirname(MEMORY_PATH);
|
|
21
|
+
if (!fs.existsSync(dir)) {
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
if (!fs.existsSync(MEMORY_PATH)) {
|
|
25
|
+
fs.writeFileSync(MEMORY_PATH, '# ClawUI Learned Preferences\n', 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Truncate paragraph content into a short description for the rule
|
|
29
|
+
function summarize(content) {
|
|
30
|
+
const cleaned = content.trim().replace(/\s+/g, ' ');
|
|
31
|
+
if (cleaned.length <= 80)
|
|
32
|
+
return cleaned;
|
|
33
|
+
return cleaned.slice(0, 77) + '...';
|
|
34
|
+
}
|
|
35
|
+
function resolveReason(reason) {
|
|
36
|
+
if (!reason)
|
|
37
|
+
return 'user deleted';
|
|
38
|
+
return REASON_LABELS[reason] ?? reason;
|
|
39
|
+
}
|
|
40
|
+
export function learnFromDeletions(actions, payload) {
|
|
41
|
+
const deletions = actions.filter(a => a.type === 'delete');
|
|
42
|
+
if (deletions.length === 0)
|
|
43
|
+
return;
|
|
44
|
+
const paragraphMap = new Map((payload.paragraphs ?? []).map(p => [p.id, p.content]));
|
|
45
|
+
// Infer scope from session type
|
|
46
|
+
const scope = payload.type === 'email_review' ? 'email' : 'general';
|
|
47
|
+
const rules = [];
|
|
48
|
+
for (const action of deletions) {
|
|
49
|
+
const content = paragraphMap.get(action.paragraphId);
|
|
50
|
+
// Skip if we cannot find the original paragraph text
|
|
51
|
+
if (!content)
|
|
52
|
+
continue;
|
|
53
|
+
const description = summarize(content);
|
|
54
|
+
const reason = resolveReason(action.reason);
|
|
55
|
+
rules.push(`- AVOID: ${description} (reason: ${reason}) - SCOPE: ${scope}`);
|
|
56
|
+
}
|
|
57
|
+
if (rules.length === 0)
|
|
58
|
+
return;
|
|
59
|
+
ensureMemoryFile();
|
|
60
|
+
const existing = fs.readFileSync(MEMORY_PATH, 'utf-8');
|
|
61
|
+
// Append section header once if not present, then append rules
|
|
62
|
+
const needsHeader = !existing.includes(SECTION_HEADER);
|
|
63
|
+
const block = needsHeader
|
|
64
|
+
? `\n${SECTION_HEADER}\n${rules.join('\n')}\n`
|
|
65
|
+
: `${rules.join('\n')}\n`;
|
|
66
|
+
fs.appendFileSync(MEMORY_PATH, block, 'utf-8');
|
|
67
|
+
console.log(`[agentclick] Learned ${rules.length} preference rule(s) -> ${MEMORY_PATH}`);
|
|
68
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
const DB_DIR = join(homedir(), '.openclaw');
|
|
6
|
+
const DB_PATH = join(DB_DIR, 'clawui-sessions.db');
|
|
7
|
+
if (!existsSync(DB_DIR))
|
|
8
|
+
mkdirSync(DB_DIR, { recursive: true });
|
|
9
|
+
const db = new Database(DB_PATH);
|
|
10
|
+
db.exec(`
|
|
11
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
type TEXT NOT NULL,
|
|
14
|
+
payload TEXT NOT NULL,
|
|
15
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
16
|
+
result TEXT,
|
|
17
|
+
sessionKey TEXT,
|
|
18
|
+
createdAt INTEGER NOT NULL
|
|
19
|
+
)
|
|
20
|
+
`);
|
|
21
|
+
export function createSession(session) {
|
|
22
|
+
db.prepare(`
|
|
23
|
+
INSERT INTO sessions (id, type, payload, status, sessionKey, createdAt)
|
|
24
|
+
VALUES (@id, @type, @payload, @status, @sessionKey, @createdAt)
|
|
25
|
+
`).run({
|
|
26
|
+
...session,
|
|
27
|
+
payload: JSON.stringify(session.payload),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function getSession(id) {
|
|
31
|
+
const row = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
32
|
+
if (!row)
|
|
33
|
+
return null;
|
|
34
|
+
return deserialize(row);
|
|
35
|
+
}
|
|
36
|
+
export function listSessions(limit = 20) {
|
|
37
|
+
const rows = db.prepare('SELECT * FROM sessions ORDER BY createdAt DESC LIMIT ?').all(limit);
|
|
38
|
+
return rows.map(deserialize);
|
|
39
|
+
}
|
|
40
|
+
export function completeSession(id, result) {
|
|
41
|
+
db.prepare(`
|
|
42
|
+
UPDATE sessions SET status = 'completed', result = ? WHERE id = ?
|
|
43
|
+
`).run(JSON.stringify(result), id);
|
|
44
|
+
}
|
|
45
|
+
function deserialize(row) {
|
|
46
|
+
return {
|
|
47
|
+
id: row.id,
|
|
48
|
+
type: row.type,
|
|
49
|
+
payload: JSON.parse(row.payload),
|
|
50
|
+
status: row.status,
|
|
51
|
+
result: row.result ? JSON.parse(row.result) : undefined,
|
|
52
|
+
sessionKey: row.sessionKey,
|
|
53
|
+
createdAt: row.createdAt,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentclick/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "tsx watch src/index.ts",
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"start": "node dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"better-sqlite3": "^12.6.2",
|
|
12
|
+
"cors": "^2.8.5",
|
|
13
|
+
"dotenv": "^17.3.1",
|
|
14
|
+
"express": "^4.19.2",
|
|
15
|
+
"open": "^10.1.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
19
|
+
"@types/cors": "^2.8.17",
|
|
20
|
+
"@types/express": "^4.17.21",
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"tsx": "^4.19.0",
|
|
23
|
+
"typescript": "^5.6.0"
|
|
24
|
+
}
|
|
25
|
+
}
|