agentclick 0.2.0 → 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.
@@ -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,6 @@
1
1
  {
2
2
  "name": "agentclick",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Human-in-the-loop approval UI for AI agents. Review, edit, and approve agent actions in your browser before they execute.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -6,7 +6,7 @@ import { existsSync } from 'fs';
6
6
  import { dirname, join } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { learnFromDeletions } from './preference.js';
9
- import { createSession, getSession, listSessions, completeSession } from './store.js';
9
+ import { createSession, getSession, listSessions, completeSession, setSessionRewriting, updateSessionPayload } from './store.js';
10
10
  const app = express();
11
11
  const PORT = Number(process.env.PORT || 3001);
12
12
  const OPENCLAW_WEBHOOK = process.env.OPENCLAW_WEBHOOK || 'http://localhost:18789/hooks/agent';
@@ -23,18 +23,23 @@ app.post('/api/review', async (req, res) => {
23
23
  if (!sessionKey) {
24
24
  console.warn('[agentclick] Warning: sessionKey missing — callback will be skipped');
25
25
  }
26
- const id = `session_${Date.now()}`;
26
+ const now = Date.now();
27
+ const id = `session_${now}`;
27
28
  createSession({
28
29
  id,
29
30
  type: type || 'email_review',
30
31
  payload,
31
32
  sessionKey,
32
33
  status: 'pending',
33
- createdAt: Date.now(),
34
+ createdAt: now,
35
+ updatedAt: now,
36
+ revision: 0,
34
37
  });
35
38
  const routeMap = {
36
39
  action_approval: 'approval',
37
40
  code_review: 'code-review',
41
+ form_review: 'form-review',
42
+ selection_review: 'selection',
38
43
  };
39
44
  const path = routeMap[type] ?? 'review';
40
45
  const url = `${WEB_ORIGIN}/${path}/${id}`;
@@ -112,17 +117,39 @@ app.get('/api/sessions/:id/wait', async (req, res) => {
112
117
  const session = getSession(req.params.id);
113
118
  if (!session)
114
119
  return res.status(404).json({ error: 'Session not found' });
115
- if (session.status === 'completed')
120
+ if (session.status === 'completed' || session.status === 'rewriting') {
121
+ console.log(`[agentclick] /wait returning ${session.status} for ${session.id} (revision=${session.revision})`);
116
122
  return res.json(session);
123
+ }
117
124
  await new Promise(r => setTimeout(r, POLL_MS));
118
125
  }
119
126
  res.status(408).json({ error: 'timeout', message: 'User did not complete review within 5 minutes' });
120
127
  });
128
+ // Agent updates session payload after rewriting
129
+ app.put('/api/sessions/:id/payload', (req, res) => {
130
+ const session = getSession(req.params.id);
131
+ if (!session)
132
+ return res.status(404).json({ error: 'Session not found' });
133
+ if (session.status !== 'rewriting')
134
+ return res.status(400).json({ error: 'Session is not in rewriting state' });
135
+ console.log(`[agentclick] Payload update requested for ${session.id} (status=${session.status}, revision=${session.revision})`);
136
+ updateSessionPayload(req.params.id, req.body.payload);
137
+ const updated = getSession(req.params.id);
138
+ console.log(`[agentclick] Session ${session.id} payload updated, back to pending (revision=${updated?.revision ?? 'unknown'})`);
139
+ res.json({ ok: true });
140
+ });
121
141
  // Web UI submits user actions
122
142
  app.post('/api/sessions/:id/complete', async (req, res) => {
123
143
  const session = getSession(req.params.id);
124
144
  if (!session)
125
145
  return res.status(404).json({ error: 'Session not found' });
146
+ // If user requested regeneration, set to rewriting (not completed) so agent can update
147
+ if (req.body.regenerate) {
148
+ setSessionRewriting(req.params.id, req.body);
149
+ console.log(`[agentclick] Session ${session.id} → rewriting:`, JSON.stringify(req.body, null, 2));
150
+ res.json({ ok: true, rewriting: true });
151
+ return;
152
+ }
126
153
  completeSession(req.params.id, req.body);
127
154
  console.log(`[agentclick] Session ${session.id} completed:`, JSON.stringify(req.body, null, 2));
128
155
  // Learn from delete actions and persist rules to MEMORY.md
@@ -15,16 +15,29 @@ db.exec(`
15
15
  status TEXT NOT NULL DEFAULT 'pending',
16
16
  result TEXT,
17
17
  sessionKey TEXT,
18
- createdAt INTEGER NOT NULL
18
+ createdAt INTEGER NOT NULL,
19
+ updatedAt INTEGER NOT NULL DEFAULT 0,
20
+ revision INTEGER NOT NULL DEFAULT 0
19
21
  )
20
22
  `);
23
+ // Migrate existing tables: add updatedAt and revision if missing
24
+ try {
25
+ db.exec(`ALTER TABLE sessions ADD COLUMN updatedAt INTEGER NOT NULL DEFAULT 0`);
26
+ }
27
+ catch { /* column already exists */ }
28
+ try {
29
+ db.exec(`ALTER TABLE sessions ADD COLUMN revision INTEGER NOT NULL DEFAULT 0`);
30
+ }
31
+ catch { /* column already exists */ }
21
32
  export function createSession(session) {
22
33
  db.prepare(`
23
- INSERT INTO sessions (id, type, payload, status, sessionKey, createdAt)
24
- VALUES (@id, @type, @payload, @status, @sessionKey, @createdAt)
34
+ INSERT INTO sessions (id, type, payload, status, sessionKey, createdAt, updatedAt, revision)
35
+ VALUES (@id, @type, @payload, @status, @sessionKey, @createdAt, @updatedAt, @revision)
25
36
  `).run({
26
37
  ...session,
27
38
  payload: JSON.stringify(session.payload),
39
+ updatedAt: session.updatedAt || Date.now(),
40
+ revision: session.revision || 0,
28
41
  });
29
42
  }
30
43
  export function getSession(id) {
@@ -39,8 +52,18 @@ export function listSessions(limit = 20) {
39
52
  }
40
53
  export function completeSession(id, result) {
41
54
  db.prepare(`
42
- UPDATE sessions SET status = 'completed', result = ? WHERE id = ?
43
- `).run(JSON.stringify(result), id);
55
+ UPDATE sessions SET status = 'completed', result = ?, updatedAt = ? WHERE id = ?
56
+ `).run(JSON.stringify(result), Date.now(), id);
57
+ }
58
+ export function setSessionRewriting(id, result) {
59
+ db.prepare(`
60
+ UPDATE sessions SET status = 'rewriting', result = ?, updatedAt = ? WHERE id = ?
61
+ `).run(JSON.stringify(result), Date.now(), id);
62
+ }
63
+ export function updateSessionPayload(id, payload) {
64
+ db.prepare(`
65
+ UPDATE sessions SET payload = ?, status = 'pending', result = NULL, updatedAt = ?, revision = revision + 1 WHERE id = ?
66
+ `).run(JSON.stringify(payload), Date.now(), id);
44
67
  }
45
68
  function deserialize(row) {
46
69
  return {
@@ -51,5 +74,7 @@ function deserialize(row) {
51
74
  result: row.result ? JSON.parse(row.result) : undefined,
52
75
  sessionKey: row.sessionKey,
53
76
  createdAt: row.createdAt,
77
+ updatedAt: row.updatedAt || 0,
78
+ revision: row.revision || 0,
54
79
  };
55
80
  }
@@ -6,7 +6,7 @@ import { existsSync } from 'fs'
6
6
  import { dirname, join } from 'path'
7
7
  import { fileURLToPath } from 'url'
8
8
  import { learnFromDeletions } from './preference.js'
9
- import { createSession, getSession, listSessions, completeSession } from './store.js'
9
+ import { createSession, getSession, listSessions, completeSession, setSessionRewriting, updateSessionPayload } from './store.js'
10
10
 
11
11
  const app = express()
12
12
  const PORT = Number(process.env.PORT || 3001)
@@ -28,19 +28,24 @@ app.post('/api/review', async (req, res) => {
28
28
  console.warn('[agentclick] Warning: sessionKey missing — callback will be skipped')
29
29
  }
30
30
 
31
- const id = `session_${Date.now()}`
31
+ const now = Date.now()
32
+ const id = `session_${now}`
32
33
  createSession({
33
34
  id,
34
35
  type: type || 'email_review',
35
36
  payload,
36
37
  sessionKey,
37
38
  status: 'pending',
38
- createdAt: Date.now(),
39
+ createdAt: now,
40
+ updatedAt: now,
41
+ revision: 0,
39
42
  })
40
43
 
41
44
  const routeMap: Record<string, string> = {
42
45
  action_approval: 'approval',
43
46
  code_review: 'code-review',
47
+ form_review: 'form-review',
48
+ selection_review: 'selection',
44
49
  }
45
50
  const path = routeMap[type] ?? 'review'
46
51
  const url = `${WEB_ORIGIN}/${path}/${id}`
@@ -126,18 +131,42 @@ app.get('/api/sessions/:id/wait', async (req, res) => {
126
131
  while (Date.now() - start < TIMEOUT_MS) {
127
132
  const session = getSession(req.params.id)
128
133
  if (!session) return res.status(404).json({ error: 'Session not found' })
129
- if (session.status === 'completed') return res.json(session)
134
+ if (session.status === 'completed' || session.status === 'rewriting') {
135
+ console.log(`[agentclick] /wait returning ${session.status} for ${session.id} (revision=${session.revision})`)
136
+ return res.json(session)
137
+ }
130
138
  await new Promise(r => setTimeout(r, POLL_MS))
131
139
  }
132
140
 
133
141
  res.status(408).json({ error: 'timeout', message: 'User did not complete review within 5 minutes' })
134
142
  })
135
143
 
144
+ // Agent updates session payload after rewriting
145
+ app.put('/api/sessions/:id/payload', (req, res) => {
146
+ const session = getSession(req.params.id)
147
+ if (!session) return res.status(404).json({ error: 'Session not found' })
148
+ if (session.status !== 'rewriting') return res.status(400).json({ error: 'Session is not in rewriting state' })
149
+
150
+ console.log(`[agentclick] Payload update requested for ${session.id} (status=${session.status}, revision=${session.revision})`)
151
+ updateSessionPayload(req.params.id, req.body.payload)
152
+ const updated = getSession(req.params.id)
153
+ console.log(`[agentclick] Session ${session.id} payload updated, back to pending (revision=${updated?.revision ?? 'unknown'})`)
154
+ res.json({ ok: true })
155
+ })
156
+
136
157
  // Web UI submits user actions
137
158
  app.post('/api/sessions/:id/complete', async (req, res) => {
138
159
  const session = getSession(req.params.id)
139
160
  if (!session) return res.status(404).json({ error: 'Session not found' })
140
161
 
162
+ // If user requested regeneration, set to rewriting (not completed) so agent can update
163
+ if (req.body.regenerate) {
164
+ setSessionRewriting(req.params.id, req.body)
165
+ console.log(`[agentclick] Session ${session.id} → rewriting:`, JSON.stringify(req.body, null, 2))
166
+ res.json({ ok: true, rewriting: true })
167
+ return
168
+ }
169
+
141
170
  completeSession(req.params.id, req.body)
142
171
 
143
172
  console.log(`[agentclick] Session ${session.id} completed:`, JSON.stringify(req.body, null, 2))
@@ -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
  }