dude-claude-plugin 2026.2.14 → 2026.2.19
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 +28 -4
- package/package.json +1 -1
- package/scripts/dump-local.js +67 -0
- package/src/db-adapter.js +16 -0
- package/src/db-libsql.js +123 -6
- 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
|
|
|
@@ -62,7 +63,17 @@ npx dude-claude-plugin serve
|
|
|
62
63
|
dude-claude serve
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
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)).
|
|
66
77
|
|
|
67
78
|
## Configuration
|
|
68
79
|
|
|
@@ -70,6 +81,19 @@ Opens a local dashboard on port 3456 for browsing and editing projects, issues,
|
|
|
70
81
|
|---|---|---|
|
|
71
82
|
| `DUDE_PORT` | `3456` | Web UI port |
|
|
72
83
|
| `DUDE_CONTEXT_LIMIT` | `5` | Max records injected per prompt |
|
|
84
|
+
| `DUDE_RECENCY_HOURS` | `1` | Lookback window for recent records |
|
|
85
|
+
|
|
86
|
+
### Cloud Sync (optional)
|
|
87
|
+
|
|
88
|
+
The plugin works fully offline by default. To enable cloud sync with [Turso](https://turso.tech), set these environment variables:
|
|
89
|
+
|
|
90
|
+
| Env variable | Description |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `DUDE_TURSO_URL` | Turso database URL (e.g. `libsql://your-db.turso.io`) |
|
|
93
|
+
| `DUDE_TURSO_TOKEN` | Turso auth token |
|
|
94
|
+
| `DUDE_SYNC_INTERVAL` | Sync interval in ms (default: `60000`) |
|
|
95
|
+
|
|
96
|
+
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
97
|
|
|
74
98
|
## Requirements
|
|
75
99
|
|
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-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
|
@@ -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,12 +31,33 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
30
31
|
async init() {
|
|
31
32
|
if (this.db) return;
|
|
32
33
|
this._ensureDataDir();
|
|
33
|
-
this.
|
|
34
|
+
this._syncError = null;
|
|
35
|
+
|
|
36
|
+
if (this._hasSyncConfig()) {
|
|
37
|
+
try {
|
|
38
|
+
this.db = this._createClient(); // tries sync-enabled client
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(`[dude] Cloud sync connection failed: ${err.message}`);
|
|
41
|
+
console.error('[dude] Falling back to local-only mode.');
|
|
42
|
+
this._syncError = err.message;
|
|
43
|
+
this.db = this._createLocalOnlyClient();
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
this.db = this._createLocalOnlyClient();
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
await this._runSchema();
|
|
35
50
|
const projectName = this._detectProject();
|
|
36
51
|
this.currentProject = await this._upsertProject(projectName);
|
|
37
52
|
await this._migrateProjectNames(projectName);
|
|
38
|
-
|
|
53
|
+
|
|
54
|
+
const syncInfo = await this.syncStatus();
|
|
55
|
+
const syncMsg = syncInfo.enabled
|
|
56
|
+
? ` | cloud sync → ${syncInfo.syncUrl}`
|
|
57
|
+
: syncInfo.error
|
|
58
|
+
? ' | cloud sync FAILED — local-only mode'
|
|
59
|
+
: '';
|
|
60
|
+
console.error(`[dude] LibSQL DB ready — project "${this.currentProject.name}" (id=${this.currentProject.id})${syncMsg}`);
|
|
39
61
|
}
|
|
40
62
|
|
|
41
63
|
_ensureDataDir() {
|
|
@@ -44,6 +66,15 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
44
66
|
}
|
|
45
67
|
}
|
|
46
68
|
|
|
69
|
+
_hasSyncConfig() {
|
|
70
|
+
return !!(this.config.syncUrl || process.env.DUDE_TURSO_URL);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_createLocalOnlyClient() {
|
|
74
|
+
const url = this.config.url || `file:${this.config.dbPath || DB_PATH}`;
|
|
75
|
+
return createClient({ url });
|
|
76
|
+
}
|
|
77
|
+
|
|
47
78
|
_createClient() {
|
|
48
79
|
const url = this.config.url
|
|
49
80
|
|| `file:${this.config.dbPath || DB_PATH}`;
|
|
@@ -53,8 +84,8 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
53
84
|
if (this.config.syncUrl || process.env.DUDE_TURSO_URL) {
|
|
54
85
|
opts.syncUrl = this.config.syncUrl || process.env.DUDE_TURSO_URL;
|
|
55
86
|
opts.authToken = this.config.authToken || process.env.DUDE_TURSO_TOKEN;
|
|
56
|
-
const interval = this.config.syncInterval || process.env.DUDE_SYNC_INTERVAL;
|
|
57
|
-
|
|
87
|
+
const interval = this.config.syncInterval || process.env.DUDE_SYNC_INTERVAL || 60000;
|
|
88
|
+
opts.syncInterval = parseInt(interval);
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
return createClient(opts);
|
|
@@ -74,10 +105,10 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
74
105
|
sql: `CREATE TABLE IF NOT EXISTS record (
|
|
75
106
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
76
107
|
project_id INTEGER NOT NULL REFERENCES project(id) ON DELETE CASCADE,
|
|
77
|
-
kind TEXT NOT NULL
|
|
108
|
+
kind TEXT NOT NULL,
|
|
78
109
|
title TEXT NOT NULL,
|
|
79
110
|
body TEXT NOT NULL DEFAULT '',
|
|
80
|
-
status TEXT NOT NULL DEFAULT 'open'
|
|
111
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
81
112
|
embedding F32_BLOB(384),
|
|
82
113
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
83
114
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
@@ -92,6 +123,50 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
92
123
|
ON record(libsql_vector_idx(embedding, 'metric=cosine'))`,
|
|
93
124
|
},
|
|
94
125
|
], 'write');
|
|
126
|
+
|
|
127
|
+
// Migrate existing databases that still have CHECK constraints
|
|
128
|
+
await this._migrateDropChecks();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Detect whether the record table was created with CHECK constraints
|
|
133
|
+
* (from a previous schema version) and recreate it without them.
|
|
134
|
+
* This is needed because SQLite/libsql doesn't support ALTER TABLE
|
|
135
|
+
* to drop constraints — the table must be recreated.
|
|
136
|
+
*/
|
|
137
|
+
async _migrateDropChecks() {
|
|
138
|
+
const result = await this.db.execute(
|
|
139
|
+
"SELECT sql FROM sqlite_master WHERE type='table' AND name='record'",
|
|
140
|
+
);
|
|
141
|
+
const ddl = result.rows[0]?.sql || '';
|
|
142
|
+
if (!ddl.includes('CHECK')) return; // already migrated or fresh DB
|
|
143
|
+
|
|
144
|
+
await this.db.executeMultiple(`
|
|
145
|
+
CREATE TABLE record_new (
|
|
146
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
147
|
+
project_id INTEGER NOT NULL REFERENCES project(id) ON DELETE CASCADE,
|
|
148
|
+
kind TEXT NOT NULL,
|
|
149
|
+
title TEXT NOT NULL,
|
|
150
|
+
body TEXT NOT NULL DEFAULT '',
|
|
151
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
152
|
+
embedding F32_BLOB(384),
|
|
153
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
154
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
INSERT INTO record_new SELECT * FROM record;
|
|
158
|
+
|
|
159
|
+
DROP TABLE record;
|
|
160
|
+
|
|
161
|
+
ALTER TABLE record_new RENAME TO record;
|
|
162
|
+
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_record_project_kind
|
|
164
|
+
ON record(project_id, kind);
|
|
165
|
+
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_record_embedding
|
|
167
|
+
ON record(libsql_vector_idx(embedding, 'metric=cosine'));
|
|
168
|
+
`);
|
|
169
|
+
console.error('[dude] Migrated record table: removed CHECK constraints');
|
|
95
170
|
}
|
|
96
171
|
|
|
97
172
|
_detectProject() {
|
|
@@ -369,6 +444,48 @@ export class LibsqlAdapter extends DbAdapter {
|
|
|
369
444
|
return this.get(Number(result.lastInsertRowid));
|
|
370
445
|
}
|
|
371
446
|
|
|
447
|
+
async syncStatus() {
|
|
448
|
+
const syncUrl = this.config.syncUrl || process.env.DUDE_TURSO_URL || null;
|
|
449
|
+
const syncInterval = this.config.syncInterval
|
|
450
|
+
|| process.env.DUDE_SYNC_INTERVAL
|
|
451
|
+
|| (syncUrl ? 60000 : null);
|
|
452
|
+
return {
|
|
453
|
+
enabled: !!syncUrl && !this._syncError,
|
|
454
|
+
...(syncUrl ? { syncUrl } : {}),
|
|
455
|
+
...(syncInterval ? { syncInterval: parseInt(syncInterval) } : {}),
|
|
456
|
+
...(this._syncError ? { error: this._syncError, degraded: true } : {}),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async sync() {
|
|
461
|
+
if (!this._hasSyncConfig()) {
|
|
462
|
+
return { synced: false, message: 'Cloud sync not configured. Set DUDE_TURSO_URL and DUDE_TURSO_TOKEN to enable.' };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// If degraded, attempt to reconnect with sync-enabled client
|
|
466
|
+
if (this._syncError) {
|
|
467
|
+
try {
|
|
468
|
+
const newClient = this._createClient();
|
|
469
|
+
// Run a test query to verify the connection works
|
|
470
|
+
await newClient.execute('SELECT 1');
|
|
471
|
+
this.db.close();
|
|
472
|
+
this.db = newClient;
|
|
473
|
+
this._syncError = null;
|
|
474
|
+
console.error('[dude] Cloud sync reconnected successfully.');
|
|
475
|
+
return { synced: true, message: `Reconnected and synced with ${this.config.syncUrl || process.env.DUDE_TURSO_URL}` };
|
|
476
|
+
} catch (err) {
|
|
477
|
+
return { synced: false, message: `Reconnection failed: ${err.message}` };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
await this.db.sync();
|
|
483
|
+
return { synced: true, message: `Synced with ${this.config.syncUrl || process.env.DUDE_TURSO_URL}` };
|
|
484
|
+
} catch (err) {
|
|
485
|
+
return { synced: false, message: `Sync failed: ${err.message}` };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
372
489
|
async close() {
|
|
373
490
|
if (this.db) {
|
|
374
491
|
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">
|