dude-claude-plugin 2026.2.18 → 2026.2.20
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 +11 -1
- package/bin/dude-claude.js +42 -27
- package/package.json +1 -1
- package/scripts/dump-local.js +67 -0
- package/src/db-libsql.js +71 -11
package/README.md
CHANGED
|
@@ -63,7 +63,17 @@ npx dude-claude-plugin serve
|
|
|
63
63
|
dude-claude serve
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
Opens a local dashboard
|
|
66
|
+
Opens a local dashboard at `http://127.0.0.1:3456` (auto-opens in your browser). The UI has two panels: a sidebar listing records with filters, and a main panel for viewing and editing.
|
|
67
|
+
|
|
68
|
+
**What you can do:**
|
|
69
|
+
|
|
70
|
+
- **Project selector** — filter records by project or view all projects at once
|
|
71
|
+
- **Semantic search** — type in the search bar to find records by meaning, not just keywords (results ranked by similarity)
|
|
72
|
+
- **Filter by kind** — narrow to issues, specs, arch decisions, updates, or tests
|
|
73
|
+
- **Filter by status** — open, resolved, archived, active, or inactive
|
|
74
|
+
- **Create / edit / delete** — click "+ New" to add a record, click any record to edit it, or delete from the detail view
|
|
75
|
+
|
|
76
|
+
Set `DUDE_PORT` to change the default port (see [Configuration](#configuration)).
|
|
67
77
|
|
|
68
78
|
## Configuration
|
|
69
79
|
|
package/bin/dude-claude.js
CHANGED
|
@@ -1,32 +1,43 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
process.on('uncaughtException', (err) => {
|
|
4
|
+
console.error(`[dude] Fatal error: ${err.message}`);
|
|
5
|
+
process.exit(1);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
process.on('unhandledRejection', (err) => {
|
|
9
|
+
console.error(`[dude] Unhandled rejection: ${err?.message || err}`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
});
|
|
2
12
|
|
|
3
13
|
const command = process.argv[2] || 'mcp';
|
|
4
14
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
try {
|
|
16
|
+
switch (command) {
|
|
17
|
+
case 'mcp': {
|
|
18
|
+
const { startServer } = await import('../src/server.js');
|
|
19
|
+
await startServer();
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
case 'serve': {
|
|
23
|
+
const { startWebServer } = await import('../src/web.js');
|
|
24
|
+
await startWebServer();
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
case 'auto-retrieve': {
|
|
28
|
+
await import('../hooks/auto-retrieve.js');
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case 'auto-persist': {
|
|
32
|
+
await import('../hooks/auto-persist.js');
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case 'auto-persist-plan': {
|
|
36
|
+
await import('../hooks/auto-persist-plan.js');
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
default:
|
|
40
|
+
console.error(`Usage: dude-claude [mcp|serve|auto-retrieve|auto-persist|auto-persist-plan]
|
|
30
41
|
|
|
31
42
|
Commands:
|
|
32
43
|
mcp Start the MCP stdio server (default)
|
|
@@ -34,5 +45,9 @@ Commands:
|
|
|
34
45
|
auto-retrieve Run the auto-retrieve hook (reads prompt from stdin)
|
|
35
46
|
auto-persist Run the auto-persist utility (reads classification JSON from stdin)
|
|
36
47
|
auto-persist-plan Run the auto-persist-plan utility (reads classification JSON from stdin)`);
|
|
37
|
-
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(`[dude] Startup failed: ${err.message}`);
|
|
52
|
+
process.exit(1);
|
|
38
53
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Export the local libsql database as SQL INSERT statements.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/dump-local.js > dump.sql
|
|
7
|
+
* turso db shell <dbname> < dump.sql
|
|
8
|
+
*
|
|
9
|
+
* Reads from ~/.dude-claude/dude-libsql.db (or DUDE_DB_PATH env override).
|
|
10
|
+
* Outputs projects first, then records (with vector embeddings), to stdout.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createClient } from '@libsql/client';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
|
|
17
|
+
const DATA_DIR = join(homedir(), '.dude-claude');
|
|
18
|
+
const DB_PATH = process.env.DUDE_DB_PATH || join(DATA_DIR, 'dude-libsql.db');
|
|
19
|
+
|
|
20
|
+
function esc(str) {
|
|
21
|
+
if (str == null) return 'NULL';
|
|
22
|
+
return "'" + String(str).replace(/'/g, "''") + "'";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseEmbedding(blob) {
|
|
26
|
+
if (!blob) return null;
|
|
27
|
+
if (blob instanceof Float32Array) return blob;
|
|
28
|
+
if (blob instanceof ArrayBuffer) return new Float32Array(blob);
|
|
29
|
+
if (ArrayBuffer.isView(blob)) {
|
|
30
|
+
return new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
|
|
31
|
+
}
|
|
32
|
+
if (typeof blob === 'string') return new Float32Array(JSON.parse(blob));
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function main() {
|
|
37
|
+
const db = createClient({ url: `file:${DB_PATH}` });
|
|
38
|
+
|
|
39
|
+
// -- Projects --
|
|
40
|
+
const projects = await db.execute('SELECT * FROM project ORDER BY id');
|
|
41
|
+
for (const p of projects.rows) {
|
|
42
|
+
console.log(
|
|
43
|
+
`INSERT INTO project (id, name, created_at, updated_at) VALUES (${p.id}, ${esc(p.name)}, ${esc(p.created_at)}, ${esc(p.updated_at)});`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// -- Records (with embeddings) --
|
|
48
|
+
const records = await db.execute('SELECT * FROM record ORDER BY id');
|
|
49
|
+
for (const r of records.rows) {
|
|
50
|
+
const emb = parseEmbedding(r.embedding);
|
|
51
|
+
const embExpr = emb
|
|
52
|
+
? `vector('${JSON.stringify(Array.from(emb))}')`
|
|
53
|
+
: 'NULL';
|
|
54
|
+
|
|
55
|
+
console.log(
|
|
56
|
+
`INSERT INTO record (id, project_id, kind, title, body, status, embedding, created_at, updated_at) VALUES (${r.id}, ${r.project_id}, ${esc(r.kind)}, ${esc(r.title)}, ${esc(r.body)}, ${esc(r.status)}, ${embExpr}, ${esc(r.created_at)}, ${esc(r.updated_at)});`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
db.close();
|
|
61
|
+
console.error(`[dump] Exported ${projects.rows.length} projects, ${records.rows.length} records`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main().catch(err => {
|
|
65
|
+
console.error('dump-local failed:', err);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
});
|
package/src/db-libsql.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createClient } from '@libsql/client';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
4
4
|
import { join, basename } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { DbAdapter } from './db-adapter.js';
|
|
@@ -21,6 +21,7 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
21
21
|
this.config = config;
|
|
22
22
|
this.db = null;
|
|
23
23
|
this.currentProject = null;
|
|
24
|
+
this._syncError = null;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
@@ -30,13 +31,38 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
30
31
|
async init() {
|
|
31
32
|
if (this.db) return;
|
|
32
33
|
this._ensureDataDir();
|
|
33
|
-
this.
|
|
34
|
-
|
|
34
|
+
this._syncError = null;
|
|
35
|
+
|
|
36
|
+
if (this._hasSyncConfig()) {
|
|
37
|
+
try {
|
|
38
|
+
this.db = this._createClient(); // tries sync-enabled client
|
|
39
|
+
await this._runSchema();
|
|
40
|
+
} catch (err) {
|
|
41
|
+
// Close the bad client if it was created
|
|
42
|
+
if (this.db) try { this.db.close(); } catch {}
|
|
43
|
+
this._syncError = err.message;
|
|
44
|
+
this._logToFile(`Cloud sync failed: ${err.message}\n${err.stack}`);
|
|
45
|
+
console.error(`[dude] Cloud sync failed: ${err.message}`);
|
|
46
|
+
console.error('[dude] Falling back to local-only mode. Details in ~/.dude-claude/dude.log');
|
|
47
|
+
// Recreate with local-only client
|
|
48
|
+
this.db = this._createLocalOnlyClient();
|
|
49
|
+
await this._runSchema();
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
this.db = this._createLocalOnlyClient();
|
|
53
|
+
await this._runSchema();
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
const projectName = this._detectProject();
|
|
36
57
|
this.currentProject = await this._upsertProject(projectName);
|
|
37
58
|
await this._migrateProjectNames(projectName);
|
|
59
|
+
|
|
38
60
|
const syncInfo = await this.syncStatus();
|
|
39
|
-
const syncMsg = syncInfo.enabled
|
|
61
|
+
const syncMsg = syncInfo.enabled
|
|
62
|
+
? ` | cloud sync → ${syncInfo.syncUrl}`
|
|
63
|
+
: syncInfo.error
|
|
64
|
+
? ' | cloud sync FAILED — local-only mode'
|
|
65
|
+
: '';
|
|
40
66
|
console.error(`[dude] LibSQL DB ready — project "${this.currentProject.name}" (id=${this.currentProject.id})${syncMsg}`);
|
|
41
67
|
}
|
|
42
68
|
|
|
@@ -46,6 +72,23 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
46
72
|
}
|
|
47
73
|
}
|
|
48
74
|
|
|
75
|
+
_logToFile(message) {
|
|
76
|
+
try {
|
|
77
|
+
const logPath = join(DATA_DIR, 'dude.log');
|
|
78
|
+
const timestamp = new Date().toISOString();
|
|
79
|
+
appendFileSync(logPath, `[${timestamp}] ${message}\n`);
|
|
80
|
+
} catch { /* never let logging crash the server */ }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_hasSyncConfig() {
|
|
84
|
+
return !!(this.config.syncUrl || process.env.DUDE_TURSO_URL);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_createLocalOnlyClient() {
|
|
88
|
+
const url = this.config.url || `file:${this.config.dbPath || DB_PATH}`;
|
|
89
|
+
return createClient({ url });
|
|
90
|
+
}
|
|
91
|
+
|
|
49
92
|
_createClient() {
|
|
50
93
|
const url = this.config.url
|
|
51
94
|
|| `file:${this.config.dbPath || DB_PATH}`;
|
|
@@ -55,8 +98,8 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
55
98
|
if (this.config.syncUrl || process.env.DUDE_TURSO_URL) {
|
|
56
99
|
opts.syncUrl = this.config.syncUrl || process.env.DUDE_TURSO_URL;
|
|
57
100
|
opts.authToken = this.config.authToken || process.env.DUDE_TURSO_TOKEN;
|
|
58
|
-
const interval = this.config.syncInterval || process.env.DUDE_SYNC_INTERVAL;
|
|
59
|
-
|
|
101
|
+
const interval = this.config.syncInterval || process.env.DUDE_SYNC_INTERVAL || 60000;
|
|
102
|
+
opts.syncInterval = parseInt(interval);
|
|
60
103
|
}
|
|
61
104
|
|
|
62
105
|
return createClient(opts);
|
|
@@ -419,22 +462,39 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
419
462
|
const syncUrl = this.config.syncUrl || process.env.DUDE_TURSO_URL || null;
|
|
420
463
|
const syncInterval = this.config.syncInterval
|
|
421
464
|
|| process.env.DUDE_SYNC_INTERVAL
|
|
422
|
-
|| null;
|
|
465
|
+
|| (syncUrl ? 60000 : null);
|
|
423
466
|
return {
|
|
424
|
-
enabled: !!syncUrl,
|
|
467
|
+
enabled: !!syncUrl && !this._syncError,
|
|
425
468
|
...(syncUrl ? { syncUrl } : {}),
|
|
426
469
|
...(syncInterval ? { syncInterval: parseInt(syncInterval) } : {}),
|
|
470
|
+
...(this._syncError ? { error: this._syncError, degraded: true } : {}),
|
|
427
471
|
};
|
|
428
472
|
}
|
|
429
473
|
|
|
430
474
|
async sync() {
|
|
431
|
-
|
|
432
|
-
if (!status.enabled) {
|
|
475
|
+
if (!this._hasSyncConfig()) {
|
|
433
476
|
return { synced: false, message: 'Cloud sync not configured. Set DUDE_TURSO_URL and DUDE_TURSO_TOKEN to enable.' };
|
|
434
477
|
}
|
|
478
|
+
|
|
479
|
+
// If degraded, attempt to reconnect with sync-enabled client
|
|
480
|
+
if (this._syncError) {
|
|
481
|
+
try {
|
|
482
|
+
const newClient = this._createClient();
|
|
483
|
+
// Run a test query to verify the connection works
|
|
484
|
+
await newClient.execute('SELECT 1');
|
|
485
|
+
this.db.close();
|
|
486
|
+
this.db = newClient;
|
|
487
|
+
this._syncError = null;
|
|
488
|
+
console.error('[dude] Cloud sync reconnected successfully.');
|
|
489
|
+
return { synced: true, message: `Reconnected and synced with ${this.config.syncUrl || process.env.DUDE_TURSO_URL}` };
|
|
490
|
+
} catch (err) {
|
|
491
|
+
return { synced: false, message: `Reconnection failed: ${err.message}` };
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
435
495
|
try {
|
|
436
496
|
await this.db.sync();
|
|
437
|
-
return { synced: true, message: `Synced with ${
|
|
497
|
+
return { synced: true, message: `Synced with ${this.config.syncUrl || process.env.DUDE_TURSO_URL}` };
|
|
438
498
|
} catch (err) {
|
|
439
499
|
return { synced: false, message: `Sync failed: ${err.message}` };
|
|
440
500
|
}
|