Skip to content

Commit 051220d

Browse files
committed
feat: add user preference persistence via click_preferences.md and API endpoint
1 parent 02a81fc commit 051220d

File tree

6 files changed

+96
-1
lines changed

6 files changed

+96
-1
lines changed

packages/server/src/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { existsSync, readFileSync } from 'fs'
77
import { dirname, join } from 'path'
88
import { fileURLToPath } from 'url'
99
import { promisify } from 'util'
10-
import { learnFromDeletions, learnFromRewrite, learnFromTrajectoryRevisions, learnFromCodeRejection, learnFromActionRejection, getLearnedPreferences, clearPreferences, deletePreference } from './preference.js'
10+
import { learnFromDeletions, learnFromRewrite, learnFromTrajectoryRevisions, learnFromCodeRejection, learnFromActionRejection, learnFromUserIntention, getUserPreferences, getLearnedPreferences, clearPreferences, deletePreference } from './preference.js'
1111
import { createSession, getSession, listSessions, completeSession, setSessionRewriting, updateSessionPageStatus, updateSessionPayload, updateSessionPayloadKeepStatus } from './store.js'
1212
import {
1313
buildMemoryCatalog,
@@ -607,6 +607,11 @@ app.get('/api/preferences', (_req, res) => {
607607
res.json({ preferences: getLearnedPreferences() })
608608
})
609609

610+
// User style preferences from preferences.md
611+
app.get('/api/preferences/style', (_req, res) => {
612+
res.type('text/plain').send(getUserPreferences())
613+
})
614+
610615
app.delete('/api/preferences', (_req, res) => {
611616
clearPreferences()
612617
console.log('[agentclick] Cleared all learned preferences from MEMORY.md')
@@ -870,6 +875,12 @@ app.post('/api/sessions/:id/complete', async (req, res) => {
870875
const rewriteActions = (req.body.actions ?? []) as Array<{ type: string; paragraphId: string; reason?: string; instruction?: string; shouldLearn?: boolean }>
871876
learnFromDeletions(rewriteActions, session.payload as Record<string, unknown>)
872877
learnFromRewrite(rewriteActions, session.payload as Record<string, unknown>)
878+
// Save userIntention to preferences.md
879+
const userIntention = req.body.userIntention as string | undefined
880+
if (userIntention) {
881+
const scope = session.type?.replace('_review', '') ?? 'general'
882+
learnFromUserIntention(userIntention, scope)
883+
}
873884
res.json({ ok: true, rewriting: true })
874885
return
875886
}

packages/server/src/preference.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,52 @@ import path from 'path'
33
import os from 'os'
44

55
const MEMORY_PATH = path.join(os.homedir(), '.openclaw', 'workspace', 'MEMORY.md')
6+
const PREFERENCES_PATH = path.join(os.homedir(), '.openclaw', 'workspace', 'click_preferences.md')
7+
8+
const SCOPE_SECTIONS: Record<string, string> = {
9+
email: '## Email Reply Style',
10+
trajectory: '## Trajectory',
11+
code: '## Code Review',
12+
action: '## Action Approval',
13+
}
14+
15+
function ensurePrefFile(): void {
16+
const dir = path.dirname(PREFERENCES_PATH)
17+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
18+
if (!fs.existsSync(PREFERENCES_PATH)) {
19+
fs.writeFileSync(PREFERENCES_PATH, '# AgentClick Preferences\n', 'utf-8')
20+
}
21+
22+
}
23+
24+
export function getUserPreferences(): string {
25+
if (!fs.existsSync(PREFERENCES_PATH)) return ''
26+
return fs.readFileSync(PREFERENCES_PATH, 'utf-8')
27+
}
28+
29+
export function learnFromUserIntention(intention: string, scope: string): void {
30+
const trimmed = intention.trim()
31+
if (!trimmed) return
32+
33+
ensurePrefFile()
34+
const content = fs.readFileSync(PREFERENCES_PATH, 'utf-8')
35+
const section = SCOPE_SECTIONS[scope] ?? `## ${scope}`
36+
const rule = `- ${summarize(trimmed)}`
37+
38+
// Skip if exact rule already exists
39+
if (content.includes(rule)) return
40+
41+
if (content.includes(section)) {
42+
const lines = content.split('\n')
43+
const idx = lines.findIndex(l => l === section)
44+
lines.splice(idx + 1, 0, rule)
45+
fs.writeFileSync(PREFERENCES_PATH, lines.join('\n'), 'utf-8')
46+
} else {
47+
fs.appendFileSync(PREFERENCES_PATH, `\n${section}\n${rule}\n`, 'utf-8')
48+
}
49+
50+
console.log(`[agentclick] Saved user intention to click_preferences.md -> ${PREFERENCES_PATH}`)
51+
}
652
const SECTION_HEADER = '## Email Preferences (ClickUI Auto-Learned)'
753
const TRAJECTORY_SECTION_HEADER = '## Trajectory Guidance (ClickUI Auto-Learned)'
854
const CODE_SECTION_HEADER = '## Code Review Preferences (ClickUI Auto-Learned)'

skills/clickui-approve/SKILL.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ description: Use this skill when you are about to take a consequential or irreve
77

88
When you are about to take a high-risk action (sending messages, deleting data, calling external APIs), use these steps.
99

10+
## Preferences
11+
12+
Before submitting an action for approval, fetch user preferences and apply any rules under `## Action Approval`:
13+
14+
```bash
15+
curl -s "$AGENTCLICK_BASE/api/preferences/style"
16+
```
17+
1018
## Step 1: Submit for approval
1119

1220
```bash

skills/clickui-code/SKILL.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ description: Use this skill when you want to run a shell command that could be d
77

88
Before running risky shell commands, get user approval via this skill. Always show the user exactly which files will change and what the diffs look like — this is the default, not optional.
99

10+
## Preferences
11+
12+
Before submitting a command for review, fetch user preferences and apply any rules under `## Code Review`:
13+
14+
```bash
15+
curl -s "$AGENTCLICK_BASE/api/preferences/style"
16+
```
17+
1018
## Step 1: Generate the diff
1119

1220
Before submitting, capture the exact changes so the user can see them in the review UI.

skills/clickui-email/SKILL.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ If the task is email work plus live session handling in UI, use this skill over
3131
- Reply generation is lazy: generate only after the user clicks `Reply`.
3232
- The reply text comes from the agent's own generation unless the user wired some other drafting system explicitly.
3333

34+
## Preferences
35+
36+
Before opening a session, fetch user style preferences and apply any rules under `## Email Reply Style` to every reply draft in this session:
37+
38+
```bash
39+
curl -s "$AGENTCLICK_BASE/api/preferences/style"
40+
```
41+
42+
## Session Style Rules
43+
44+
When the poll result includes `userIntention`, treat it as a **session-level style rule** — apply it to every subsequent reply draft in this session, not just the current one. It is also automatically saved to preferences by the server.
45+
46+
Example: if `userIntention` is `"say Hiiii instead of Hi"`, every reply generated after that point must use "Hiiii".
47+
3448
## Step 1: Ensure AgentClick is running
3549

3650
```bash

skills/clickui-trajectory/SKILL.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ curl -s -X PUT "$AGENTCLICK_BASE/api/sessions/${SESSION_ID}/payload" \
189189
}
190190
```
191191

192+
## Preferences
193+
194+
Before submitting a trajectory, fetch user preferences and apply any rules under `## Trajectory`:
195+
196+
```bash
197+
curl -s "$AGENTCLICK_BASE/api/preferences/style"
198+
```
199+
192200
## Learning
193201

194202
When a human marks a step wrong with "Remember this for future runs" checked, a rule is written to `~/.openclaw/workspace/MEMORY.md`:

0 commit comments

Comments
 (0)