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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dude",
3
- "version": "2026.2.14",
3
+ "version": "2026.2.18",
4
4
  "description": "Ultra-minimal RAG and cross-project memory for Claude CLI",
5
5
  "author": {
6
6
  "name": "Fingerskier"
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 6 MCP tools without auto-hooks:
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** | 6 tools: `search`, `upsert_record`, `get_record`, `list_records`, `delete_record`, `list_projects` |
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 + sqlite-vec at `~/.dude-claude/dude.db` |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dude-claude-plugin",
3
- "version": "2026.2.14",
3
+ "version": "2026.2.18",
4
4
  "description": "Ultra-minimal RAG and cross-project memory for Claude CLI",
5
5
  "type": "module",
6
6
  "bin": {
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
- console.error(`[dude] LibSQL DB ready — project "${this.currentProject.name}" (id=${this.currentProject.id})`);
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 CHECK (kind IN ('issue','spec','arch','update')),
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' CHECK (status IN ('open','resolved','archived')),
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 & updates). Returns cross-project results ranked by similarity.',
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">