dude-claude-plugin 2026.2.14 → 2026.2.18
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/.claude-plugin/plugin.json +1 -1
- package/README.md +17 -3
- package/package.json +1 -1
- package/src/db-adapter.js +16 -0
- package/src/db-libsql.js +74 -3
- package/src/migrations/003-drop-checks.js +28 -0
- package/src/server.js +31 -6
- package/src/web.js +10 -0
- package/web/index.html +12 -0
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ claude plugin install dude-claude-plugin@fingerskier-plugins
|
|
|
20
20
|
|
|
21
21
|
### MCP server only (via npx)
|
|
22
22
|
|
|
23
|
-
If you just want the
|
|
23
|
+
If you just want the 7 MCP tools without auto-hooks:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
claude mcp add dude -- npx dude-claude-plugin mcp
|
|
@@ -37,12 +37,13 @@ claude mcp add dude -- dude-claude mcp
|
|
|
37
37
|
|
|
38
38
|
| Component | Description |
|
|
39
39
|
|-----------|-------------|
|
|
40
|
-
| **MCP server** |
|
|
40
|
+
| **MCP server** | 7 tools: `search`, `upsert_record`, `get_record`, `list_records`, `delete_record`, `list_projects`, `sync_status` |
|
|
41
41
|
| **Auto-retrieve hook** | On each prompt, searches memory for relevant context and injects it |
|
|
42
42
|
| **Auto-persist hook** | After each response, classifies the work and saves issues/specs |
|
|
43
43
|
| **Web UI** | Local dashboard at `http://127.0.0.1:3456` for manual CRUD |
|
|
44
|
-
| **Storage** | SQLite
|
|
44
|
+
| **Storage** | libsql (SQLite-compatible) at `~/.dude-claude/dude-libsql.db` |
|
|
45
45
|
| **Embeddings** | Local all-MiniLM-L6-v2 via @huggingface/transformers (no API keys) |
|
|
46
|
+
| **Cloud sync** | Optional Turso cloud sync via environment variables |
|
|
46
47
|
|
|
47
48
|
## How it works
|
|
48
49
|
|
|
@@ -70,6 +71,19 @@ Opens a local dashboard on port 3456 for browsing and editing projects, issues,
|
|
|
70
71
|
|---|---|---|
|
|
71
72
|
| `DUDE_PORT` | `3456` | Web UI port |
|
|
72
73
|
| `DUDE_CONTEXT_LIMIT` | `5` | Max records injected per prompt |
|
|
74
|
+
| `DUDE_RECENCY_HOURS` | `1` | Lookback window for recent records |
|
|
75
|
+
|
|
76
|
+
### Cloud Sync (optional)
|
|
77
|
+
|
|
78
|
+
The plugin works fully offline by default. To enable cloud sync with [Turso](https://turso.tech), set these environment variables:
|
|
79
|
+
|
|
80
|
+
| Env variable | Description |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `DUDE_TURSO_URL` | Turso database URL (e.g. `libsql://your-db.turso.io`) |
|
|
83
|
+
| `DUDE_TURSO_TOKEN` | Turso auth token |
|
|
84
|
+
| `DUDE_SYNC_INTERVAL` | Sync interval in ms (default: `60000`) |
|
|
85
|
+
|
|
86
|
+
When configured, `@libsql/client` maintains a local embedded replica that auto-syncs with Turso. You can also trigger a manual sync via the `sync_status` MCP tool or `POST /api/sync`.
|
|
73
87
|
|
|
74
88
|
## Requirements
|
|
75
89
|
|
package/package.json
CHANGED
package/src/db-adapter.js
CHANGED
|
@@ -77,6 +77,22 @@ export class DbAdapter {
|
|
|
77
77
|
throw new Error('Not implemented');
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Get cloud sync status.
|
|
82
|
+
* @returns {Promise<{ enabled: boolean, syncUrl?: string, syncInterval?: number }>}
|
|
83
|
+
*/
|
|
84
|
+
async syncStatus() {
|
|
85
|
+
return { enabled: false };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Trigger a manual cloud sync (no-op if sync is not configured).
|
|
90
|
+
* @returns {Promise<{ synced: boolean, message: string }>}
|
|
91
|
+
*/
|
|
92
|
+
async sync() {
|
|
93
|
+
return { synced: false, message: 'Cloud sync not configured' };
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
/**
|
|
81
97
|
* Close the database connection.
|
|
82
98
|
*/
|
package/src/db-libsql.js
CHANGED
|
@@ -35,7 +35,9 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
35
35
|
const projectName = this._detectProject();
|
|
36
36
|
this.currentProject = await this._upsertProject(projectName);
|
|
37
37
|
await this._migrateProjectNames(projectName);
|
|
38
|
-
|
|
38
|
+
const syncInfo = await this.syncStatus();
|
|
39
|
+
const syncMsg = syncInfo.enabled ? ` | cloud sync → ${syncInfo.syncUrl}` : '';
|
|
40
|
+
console.error(`[dude] LibSQL DB ready — project "${this.currentProject.name}" (id=${this.currentProject.id})${syncMsg}`);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
_ensureDataDir() {
|
|
@@ -74,10 +76,10 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
74
76
|
sql: `CREATE TABLE IF NOT EXISTS record (
|
|
75
77
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
76
78
|
project_id INTEGER NOT NULL REFERENCES project(id) ON DELETE CASCADE,
|
|
77
|
-
kind TEXT NOT NULL
|
|
79
|
+
kind TEXT NOT NULL,
|
|
78
80
|
title TEXT NOT NULL,
|
|
79
81
|
body TEXT NOT NULL DEFAULT '',
|
|
80
|
-
status TEXT NOT NULL DEFAULT 'open'
|
|
82
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
81
83
|
embedding F32_BLOB(384),
|
|
82
84
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
83
85
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
@@ -92,6 +94,50 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
92
94
|
ON record(libsql_vector_idx(embedding, 'metric=cosine'))`,
|
|
93
95
|
},
|
|
94
96
|
], 'write');
|
|
97
|
+
|
|
98
|
+
// Migrate existing databases that still have CHECK constraints
|
|
99
|
+
await this._migrateDropChecks();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Detect whether the record table was created with CHECK constraints
|
|
104
|
+
* (from a previous schema version) and recreate it without them.
|
|
105
|
+
* This is needed because SQLite/libsql doesn't support ALTER TABLE
|
|
106
|
+
* to drop constraints — the table must be recreated.
|
|
107
|
+
*/
|
|
108
|
+
async _migrateDropChecks() {
|
|
109
|
+
const result = await this.db.execute(
|
|
110
|
+
"SELECT sql FROM sqlite_master WHERE type='table' AND name='record'",
|
|
111
|
+
);
|
|
112
|
+
const ddl = result.rows[0]?.sql || '';
|
|
113
|
+
if (!ddl.includes('CHECK')) return; // already migrated or fresh DB
|
|
114
|
+
|
|
115
|
+
await this.db.executeMultiple(`
|
|
116
|
+
CREATE TABLE record_new (
|
|
117
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
118
|
+
project_id INTEGER NOT NULL REFERENCES project(id) ON DELETE CASCADE,
|
|
119
|
+
kind TEXT NOT NULL,
|
|
120
|
+
title TEXT NOT NULL,
|
|
121
|
+
body TEXT NOT NULL DEFAULT '',
|
|
122
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
123
|
+
embedding F32_BLOB(384),
|
|
124
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
125
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
INSERT INTO record_new SELECT * FROM record;
|
|
129
|
+
|
|
130
|
+
DROP TABLE record;
|
|
131
|
+
|
|
132
|
+
ALTER TABLE record_new RENAME TO record;
|
|
133
|
+
|
|
134
|
+
CREATE INDEX IF NOT EXISTS idx_record_project_kind
|
|
135
|
+
ON record(project_id, kind);
|
|
136
|
+
|
|
137
|
+
CREATE INDEX IF NOT EXISTS idx_record_embedding
|
|
138
|
+
ON record(libsql_vector_idx(embedding, 'metric=cosine'));
|
|
139
|
+
`);
|
|
140
|
+
console.error('[dude] Migrated record table: removed CHECK constraints');
|
|
95
141
|
}
|
|
96
142
|
|
|
97
143
|
_detectProject() {
|
|
@@ -369,6 +415,31 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
369
415
|
return this.get(Number(result.lastInsertRowid));
|
|
370
416
|
}
|
|
371
417
|
|
|
418
|
+
async syncStatus() {
|
|
419
|
+
const syncUrl = this.config.syncUrl || process.env.DUDE_TURSO_URL || null;
|
|
420
|
+
const syncInterval = this.config.syncInterval
|
|
421
|
+
|| process.env.DUDE_SYNC_INTERVAL
|
|
422
|
+
|| null;
|
|
423
|
+
return {
|
|
424
|
+
enabled: !!syncUrl,
|
|
425
|
+
...(syncUrl ? { syncUrl } : {}),
|
|
426
|
+
...(syncInterval ? { syncInterval: parseInt(syncInterval) } : {}),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async sync() {
|
|
431
|
+
const status = await this.syncStatus();
|
|
432
|
+
if (!status.enabled) {
|
|
433
|
+
return { synced: false, message: 'Cloud sync not configured. Set DUDE_TURSO_URL and DUDE_TURSO_TOKEN to enable.' };
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
await this.db.sync();
|
|
437
|
+
return { synced: true, message: `Synced with ${status.syncUrl}` };
|
|
438
|
+
} catch (err) {
|
|
439
|
+
return { synced: false, message: `Sync failed: ${err.message}` };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
372
443
|
async close() {
|
|
373
444
|
if (this.db) {
|
|
374
445
|
this.db.close();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const version = 3;
|
|
2
|
+
|
|
3
|
+
export function up(db) {
|
|
4
|
+
// Remove CHECK constraints from the record table.
|
|
5
|
+
// Constraints are now enforced at the business logic layer (Zod validation).
|
|
6
|
+
// This also enables new kinds (e.g. 'test') and statuses (e.g. 'active', 'inactive')
|
|
7
|
+
// without requiring further schema migrations.
|
|
8
|
+
db.exec(`
|
|
9
|
+
CREATE TABLE record_new (
|
|
10
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
|
+
project_id INTEGER NOT NULL REFERENCES project(id) ON DELETE CASCADE,
|
|
12
|
+
kind TEXT NOT NULL,
|
|
13
|
+
title TEXT NOT NULL,
|
|
14
|
+
body TEXT NOT NULL DEFAULT '',
|
|
15
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
16
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
17
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
INSERT INTO record_new SELECT * FROM record;
|
|
21
|
+
|
|
22
|
+
DROP TABLE record;
|
|
23
|
+
|
|
24
|
+
ALTER TABLE record_new RENAME TO record;
|
|
25
|
+
|
|
26
|
+
CREATE INDEX idx_record_project_kind ON record(project_id, kind);
|
|
27
|
+
`);
|
|
28
|
+
}
|
package/src/server.js
CHANGED
|
@@ -15,10 +15,10 @@ export async function startServer() {
|
|
|
15
15
|
// ---- search ----
|
|
16
16
|
server.tool(
|
|
17
17
|
'search',
|
|
18
|
-
'Semantic search across records (issues, specs, arch decisions &
|
|
18
|
+
'Semantic search across records (issues, specs, arch decisions, updates & tests). Returns cross-project results ranked by similarity.',
|
|
19
19
|
{
|
|
20
20
|
query: z.string().describe('Natural language search query'),
|
|
21
|
-
kind: z.enum(['issue', 'spec', 'arch', 'update', 'all']).optional().describe('Filter by record kind'),
|
|
21
|
+
kind: z.enum(['issue', 'spec', 'arch', 'update', 'test', 'all']).optional().describe('Filter by record kind'),
|
|
22
22
|
project: z.string().optional().describe('Project name to boost; "*" for equal weight'),
|
|
23
23
|
limit: z.number().int().positive().optional().describe('Max results (default 5)'),
|
|
24
24
|
},
|
|
@@ -42,10 +42,10 @@ export async function startServer() {
|
|
|
42
42
|
'Create or update a record. If id is provided, updates that record. Otherwise inserts with dedup.',
|
|
43
43
|
{
|
|
44
44
|
id: z.number().int().optional().describe('Record ID to update (omit for new)'),
|
|
45
|
-
kind: z.enum(['issue', 'spec', 'arch', 'update']).describe('Record kind: issue (bug), spec (plan), arch (architecture decision), update (feature change)'),
|
|
45
|
+
kind: z.enum(['issue', 'spec', 'arch', 'update', 'test']).describe('Record kind: issue (bug), spec (plan), arch (architecture decision), update (feature change), test (verification procedure)'),
|
|
46
46
|
title: z.string().describe('Short summary'),
|
|
47
47
|
body: z.string().optional().describe('Full description'),
|
|
48
|
-
status: z.enum(['open', 'resolved', 'archived']).optional().describe('Defaults to open'),
|
|
48
|
+
status: z.enum(['open', 'resolved', 'archived', 'active', 'inactive']).optional().describe('Defaults to open. For tests: active = should be performed, inactive = disabled'),
|
|
49
49
|
},
|
|
50
50
|
async ({ id, kind, title, body, status }) => {
|
|
51
51
|
try {
|
|
@@ -96,8 +96,8 @@ export async function startServer() {
|
|
|
96
96
|
'list_records',
|
|
97
97
|
'List records with optional filters.',
|
|
98
98
|
{
|
|
99
|
-
kind: z.enum(['issue', 'spec', 'arch', 'update', 'all']).optional().describe('Filter by kind'),
|
|
100
|
-
status: z.enum(['open', 'resolved', 'archived', 'all']).optional().describe('Filter by status'),
|
|
99
|
+
kind: z.enum(['issue', 'spec', 'arch', 'update', 'test', 'all']).optional().describe('Filter by kind'),
|
|
100
|
+
status: z.enum(['open', 'resolved', 'archived', 'active', 'inactive', 'all']).optional().describe('Filter by status'),
|
|
101
101
|
project: z.string().optional().describe('Project name, or "*" for all'),
|
|
102
102
|
},
|
|
103
103
|
async ({ kind, status, project }) => {
|
|
@@ -151,6 +151,31 @@ export async function startServer() {
|
|
|
151
151
|
},
|
|
152
152
|
);
|
|
153
153
|
|
|
154
|
+
// ---- sync_status ----
|
|
155
|
+
server.tool(
|
|
156
|
+
'sync_status',
|
|
157
|
+
'Check cloud sync status and optionally trigger a manual sync.',
|
|
158
|
+
{
|
|
159
|
+
trigger_sync: z.boolean().optional().describe('If true, trigger an immediate sync'),
|
|
160
|
+
},
|
|
161
|
+
async ({ trigger_sync }) => {
|
|
162
|
+
try {
|
|
163
|
+
const status = await db.syncStatus();
|
|
164
|
+
let result = { ...status };
|
|
165
|
+
if (trigger_sync && status.enabled) {
|
|
166
|
+
const syncResult = await db.sync();
|
|
167
|
+
result.sync = syncResult;
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
171
|
+
};
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error('[dude] sync_status failed:', err);
|
|
174
|
+
return { content: [{ type: 'text', text: `Error in sync_status: ${err.message}` }], isError: true };
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
154
179
|
// Start transport
|
|
155
180
|
const transport = new StdioServerTransport();
|
|
156
181
|
await server.connect(transport);
|
package/src/web.js
CHANGED
|
@@ -61,6 +61,16 @@ async function handleRequest(db, req, res) {
|
|
|
61
61
|
|
|
62
62
|
// --- API routes ---
|
|
63
63
|
|
|
64
|
+
// GET /api/sync-status
|
|
65
|
+
if (method === 'GET' && path === '/api/sync-status') {
|
|
66
|
+
return json(res, await db.syncStatus());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// POST /api/sync
|
|
70
|
+
if (method === 'POST' && path === '/api/sync') {
|
|
71
|
+
return json(res, await db.sync());
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
// GET /api/projects
|
|
65
75
|
if (method === 'GET' && path === '/api/projects') {
|
|
66
76
|
return json(res, await db.listProjects());
|
package/web/index.html
CHANGED
|
@@ -56,9 +56,12 @@
|
|
|
56
56
|
.badge-spec { background: #e3f2fd; color: #1565c0; }
|
|
57
57
|
.badge-arch { background: #fff3e0; color: #e65100; }
|
|
58
58
|
.badge-update { background: #e8f5e9; color: #1b5e20; }
|
|
59
|
+
.badge-test { background: #e0f7fa; color: #00695c; }
|
|
59
60
|
.badge-open { background: #e8f5e9; color: #2e7d32; }
|
|
60
61
|
.badge-resolved { background: #f3e5f5; color: #6a1b9a; }
|
|
61
62
|
.badge-archived { background: #eceff1; color: #546e7a; }
|
|
63
|
+
.badge-active { background: #e8f5e9; color: #2e7d32; }
|
|
64
|
+
.badge-inactive { background: #fce4e4; color: #c62828; }
|
|
62
65
|
.main-panel {
|
|
63
66
|
flex: 1; padding: 24px; overflow-y: auto;
|
|
64
67
|
}
|
|
@@ -114,12 +117,15 @@
|
|
|
114
117
|
<option value="spec">Specs</option>
|
|
115
118
|
<option value="arch">Arch</option>
|
|
116
119
|
<option value="update">Updates</option>
|
|
120
|
+
<option value="test">Tests</option>
|
|
117
121
|
</select>
|
|
118
122
|
<select id="statusFilter">
|
|
119
123
|
<option value="">All statuses</option>
|
|
120
124
|
<option value="open">Open</option>
|
|
121
125
|
<option value="resolved">Resolved</option>
|
|
122
126
|
<option value="archived">Archived</option>
|
|
127
|
+
<option value="active">Active</option>
|
|
128
|
+
<option value="inactive">Inactive</option>
|
|
123
129
|
</select>
|
|
124
130
|
<button id="newBtn">+ New</button>
|
|
125
131
|
</div>
|
|
@@ -214,6 +220,7 @@
|
|
|
214
220
|
<option value="spec" ${record.kind === 'spec' ? 'selected' : ''}>Spec</option>
|
|
215
221
|
<option value="arch" ${record.kind === 'arch' ? 'selected' : ''}>Arch</option>
|
|
216
222
|
<option value="update" ${record.kind === 'update' ? 'selected' : ''}>Update</option>
|
|
223
|
+
<option value="test" ${record.kind === 'test' ? 'selected' : ''}>Test</option>
|
|
217
224
|
</select>
|
|
218
225
|
</div>
|
|
219
226
|
<div class="form-group">
|
|
@@ -230,6 +237,8 @@
|
|
|
230
237
|
<option value="open" ${record.status === 'open' ? 'selected' : ''}>Open</option>
|
|
231
238
|
<option value="resolved" ${record.status === 'resolved' ? 'selected' : ''}>Resolved</option>
|
|
232
239
|
<option value="archived" ${record.status === 'archived' ? 'selected' : ''}>Archived</option>
|
|
240
|
+
<option value="active" ${record.status === 'active' ? 'selected' : ''}>Active</option>
|
|
241
|
+
<option value="inactive" ${record.status === 'inactive' ? 'selected' : ''}>Inactive</option>
|
|
233
242
|
</select>
|
|
234
243
|
</div>
|
|
235
244
|
<div class="btn-row">
|
|
@@ -254,6 +263,7 @@
|
|
|
254
263
|
<option value="spec">Spec</option>
|
|
255
264
|
<option value="arch">Arch</option>
|
|
256
265
|
<option value="update">Update</option>
|
|
266
|
+
<option value="test">Test</option>
|
|
257
267
|
</select>
|
|
258
268
|
</div>
|
|
259
269
|
<div class="form-group">
|
|
@@ -270,6 +280,8 @@
|
|
|
270
280
|
<option value="open">Open</option>
|
|
271
281
|
<option value="resolved">Resolved</option>
|
|
272
282
|
<option value="archived">Archived</option>
|
|
283
|
+
<option value="active">Active</option>
|
|
284
|
+
<option value="inactive">Inactive</option>
|
|
273
285
|
</select>
|
|
274
286
|
</div>
|
|
275
287
|
<div class="btn-row">
|