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 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
+ [![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)
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
+ }