claudit 0.1.0
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/README.md +139 -0
- package/bin/claudit-mcp.js +3 -0
- package/bin/claudit.js +11 -0
- package/client/dist/assets/index-DhjH_2Wd.css +32 -0
- package/client/dist/assets/index-Dwom-XdC.js +98 -0
- package/client/dist/index.html +13 -0
- package/package.json +40 -0
- package/server/dist/server/src/index.js +170 -0
- package/server/dist/server/src/mcp-server.js +144 -0
- package/server/dist/server/src/routes/cron.js +101 -0
- package/server/dist/server/src/routes/filesystem.js +71 -0
- package/server/dist/server/src/routes/groups.js +60 -0
- package/server/dist/server/src/routes/sessions.js +206 -0
- package/server/dist/server/src/routes/todo.js +93 -0
- package/server/dist/server/src/routes/todoProviders.js +179 -0
- package/server/dist/server/src/services/claudeProcess.js +220 -0
- package/server/dist/server/src/services/cronScheduler.js +163 -0
- package/server/dist/server/src/services/cronStorage.js +154 -0
- package/server/dist/server/src/services/database.js +103 -0
- package/server/dist/server/src/services/eventBus.js +11 -0
- package/server/dist/server/src/services/groupStorage.js +52 -0
- package/server/dist/server/src/services/historyIndex.js +100 -0
- package/server/dist/server/src/services/jsonStore.js +41 -0
- package/server/dist/server/src/services/managedSessions.js +96 -0
- package/server/dist/server/src/services/providerConfigStorage.js +80 -0
- package/server/dist/server/src/services/providers/TodoProvider.js +1 -0
- package/server/dist/server/src/services/providers/larkDocsProvider.js +75 -0
- package/server/dist/server/src/services/providers/mcpClient.js +151 -0
- package/server/dist/server/src/services/providers/meegoProvider.js +99 -0
- package/server/dist/server/src/services/providers/registry.js +17 -0
- package/server/dist/server/src/services/providers/supabaseProvider.js +172 -0
- package/server/dist/server/src/services/ptyManager.js +263 -0
- package/server/dist/server/src/services/sessionIndexCache.js +24 -0
- package/server/dist/server/src/services/sessionParser.js +98 -0
- package/server/dist/server/src/services/sessionScanner.js +244 -0
- package/server/dist/server/src/services/todoStorage.js +112 -0
- package/server/dist/server/src/services/todoSyncEngine.js +170 -0
- package/server/dist/server/src/types.js +1 -0
- package/server/dist/shared/src/index.js +1 -0
- package/server/dist/shared/src/types.js +2 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { db } from './database.js';
|
|
2
|
+
// --- Prepared statements ---
|
|
3
|
+
const stmtAll = db.prepare('SELECT * FROM managed_sessions');
|
|
4
|
+
const stmtById = db.prepare('SELECT * FROM managed_sessions WHERE sessionId = ?');
|
|
5
|
+
const stmtInsert = db.prepare(`
|
|
6
|
+
INSERT INTO managed_sessions (sessionId, projectPath, displayName, archived, pinned, createdAt)
|
|
7
|
+
VALUES (@sessionId, @projectPath, @displayName, @archived, @pinned, @createdAt)
|
|
8
|
+
`);
|
|
9
|
+
const stmtDelete = db.prepare('DELETE FROM managed_sessions WHERE sessionId = ?');
|
|
10
|
+
const stmtUpsertArchive = db.prepare(`
|
|
11
|
+
INSERT INTO managed_sessions (sessionId, projectPath, displayName, archived, pinned, createdAt)
|
|
12
|
+
VALUES (@sessionId, '', NULL, @archived, 0, @createdAt)
|
|
13
|
+
ON CONFLICT(sessionId) DO UPDATE SET archived = @archived
|
|
14
|
+
`);
|
|
15
|
+
const stmtUpsertPin = db.prepare(`
|
|
16
|
+
INSERT INTO managed_sessions (sessionId, projectPath, displayName, archived, pinned, createdAt)
|
|
17
|
+
VALUES (@sessionId, '', NULL, 0, @pinned, @createdAt)
|
|
18
|
+
ON CONFLICT(sessionId) DO UPDATE SET pinned = @pinned
|
|
19
|
+
`);
|
|
20
|
+
const stmtRename = db.prepare('UPDATE managed_sessions SET displayName = ? WHERE sessionId = ?');
|
|
21
|
+
const stmtPinned = db.prepare('SELECT sessionId FROM managed_sessions WHERE pinned = 1');
|
|
22
|
+
const stmtArchived = db.prepare('SELECT sessionId FROM managed_sessions WHERE archived = 1');
|
|
23
|
+
// --- Row mapper ---
|
|
24
|
+
function rowToSession(row) {
|
|
25
|
+
const s = {
|
|
26
|
+
sessionId: row.sessionId,
|
|
27
|
+
projectPath: row.projectPath,
|
|
28
|
+
createdAt: row.createdAt,
|
|
29
|
+
};
|
|
30
|
+
if (row.displayName != null)
|
|
31
|
+
s.displayName = row.displayName;
|
|
32
|
+
if (row.archived === 1)
|
|
33
|
+
s.archived = true;
|
|
34
|
+
if (row.pinned === 1)
|
|
35
|
+
s.pinned = true;
|
|
36
|
+
return s;
|
|
37
|
+
}
|
|
38
|
+
export function getManagedSessions() {
|
|
39
|
+
return stmtAll.all().map(rowToSession);
|
|
40
|
+
}
|
|
41
|
+
export function addManagedSession(sessionId, projectPath) {
|
|
42
|
+
const entry = {
|
|
43
|
+
sessionId,
|
|
44
|
+
projectPath,
|
|
45
|
+
createdAt: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
stmtInsert.run({
|
|
48
|
+
sessionId,
|
|
49
|
+
projectPath,
|
|
50
|
+
displayName: null,
|
|
51
|
+
archived: 0,
|
|
52
|
+
pinned: 0,
|
|
53
|
+
createdAt: entry.createdAt,
|
|
54
|
+
});
|
|
55
|
+
return entry;
|
|
56
|
+
}
|
|
57
|
+
export function renameManagedSession(sessionId, name) {
|
|
58
|
+
const result = stmtRename.run(name, sessionId);
|
|
59
|
+
if (result.changes === 0)
|
|
60
|
+
return null;
|
|
61
|
+
const row = stmtById.get(sessionId);
|
|
62
|
+
return row ? rowToSession(row) : null;
|
|
63
|
+
}
|
|
64
|
+
export function archiveManagedSession(sessionId, archived) {
|
|
65
|
+
stmtUpsertArchive.run({
|
|
66
|
+
sessionId,
|
|
67
|
+
archived: archived ? 1 : 0,
|
|
68
|
+
createdAt: new Date().toISOString(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export function removeManagedSession(sessionId) {
|
|
72
|
+
stmtDelete.run(sessionId);
|
|
73
|
+
}
|
|
74
|
+
export function pinManagedSession(sessionId, pinned) {
|
|
75
|
+
stmtUpsertPin.run({
|
|
76
|
+
sessionId,
|
|
77
|
+
pinned: pinned ? 1 : 0,
|
|
78
|
+
createdAt: new Date().toISOString(),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function getPinnedSessionIds() {
|
|
82
|
+
const rows = stmtPinned.all();
|
|
83
|
+
return new Set(rows.map(r => r.sessionId));
|
|
84
|
+
}
|
|
85
|
+
export function getArchivedSessionIds() {
|
|
86
|
+
const rows = stmtArchived.all();
|
|
87
|
+
return new Set(rows.map(r => r.sessionId));
|
|
88
|
+
}
|
|
89
|
+
/** Returns Map<sessionId, ManagedSession> for fast lookup */
|
|
90
|
+
export function getManagedSessionMap() {
|
|
91
|
+
const map = new Map();
|
|
92
|
+
for (const s of getManagedSessions()) {
|
|
93
|
+
map.set(s.sessionId, s);
|
|
94
|
+
}
|
|
95
|
+
return map;
|
|
96
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { db } from './database.js';
|
|
3
|
+
// --- Prepared statements ---
|
|
4
|
+
const stmtAll = db.prepare('SELECT * FROM provider_configs');
|
|
5
|
+
const stmtById = db.prepare('SELECT * FROM provider_configs WHERE id = ?');
|
|
6
|
+
const stmtInsert = db.prepare(`
|
|
7
|
+
INSERT INTO provider_configs (id, providerId, name, enabled, config, syncIntervalMinutes, lastSyncAt, lastSyncError, createdAt)
|
|
8
|
+
VALUES (@id, @providerId, @name, @enabled, @config, @syncIntervalMinutes, @lastSyncAt, @lastSyncError, @createdAt)
|
|
9
|
+
`);
|
|
10
|
+
const stmtDelete = db.prepare('DELETE FROM provider_configs WHERE id = ?');
|
|
11
|
+
// --- Row mapper ---
|
|
12
|
+
function rowToConfig(row) {
|
|
13
|
+
const config = {
|
|
14
|
+
id: row.id,
|
|
15
|
+
providerId: row.providerId,
|
|
16
|
+
name: row.name,
|
|
17
|
+
enabled: row.enabled === 1,
|
|
18
|
+
config: JSON.parse(row.config),
|
|
19
|
+
createdAt: row.createdAt,
|
|
20
|
+
};
|
|
21
|
+
if (row.syncIntervalMinutes != null)
|
|
22
|
+
config.syncIntervalMinutes = row.syncIntervalMinutes;
|
|
23
|
+
if (row.lastSyncAt != null)
|
|
24
|
+
config.lastSyncAt = row.lastSyncAt;
|
|
25
|
+
if (row.lastSyncError != null)
|
|
26
|
+
config.lastSyncError = row.lastSyncError;
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
29
|
+
function configToParams(config) {
|
|
30
|
+
return {
|
|
31
|
+
id: config.id,
|
|
32
|
+
providerId: config.providerId,
|
|
33
|
+
name: config.name,
|
|
34
|
+
enabled: config.enabled ? 1 : 0,
|
|
35
|
+
config: JSON.stringify(config.config),
|
|
36
|
+
syncIntervalMinutes: config.syncIntervalMinutes ?? null,
|
|
37
|
+
lastSyncAt: config.lastSyncAt ?? null,
|
|
38
|
+
lastSyncError: config.lastSyncError ?? null,
|
|
39
|
+
createdAt: config.createdAt,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Trim whitespace from all string values in config (prevents pasted keys with spaces) */
|
|
43
|
+
function sanitizeConfig(config) {
|
|
44
|
+
const result = {};
|
|
45
|
+
for (const [key, value] of Object.entries(config)) {
|
|
46
|
+
result[key] = typeof value === 'string' ? value.replace(/\s+/g, '') : value;
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
export function getAllConfigs() {
|
|
51
|
+
return stmtAll.all().map(rowToConfig);
|
|
52
|
+
}
|
|
53
|
+
export function getConfig(id) {
|
|
54
|
+
const row = stmtById.get(id);
|
|
55
|
+
return row ? rowToConfig(row) : undefined;
|
|
56
|
+
}
|
|
57
|
+
export function createConfig(data) {
|
|
58
|
+
const config = {
|
|
59
|
+
...data,
|
|
60
|
+
config: sanitizeConfig(data.config),
|
|
61
|
+
id: crypto.randomUUID(),
|
|
62
|
+
createdAt: new Date().toISOString(),
|
|
63
|
+
};
|
|
64
|
+
stmtInsert.run(configToParams(config));
|
|
65
|
+
return config;
|
|
66
|
+
}
|
|
67
|
+
export function updateConfig(id, updates) {
|
|
68
|
+
const existing = getConfig(id);
|
|
69
|
+
if (!existing)
|
|
70
|
+
return null;
|
|
71
|
+
const sanitized = updates.config ? { ...updates, config: sanitizeConfig(updates.config) } : updates;
|
|
72
|
+
const merged = { ...existing, ...sanitized, id };
|
|
73
|
+
stmtDelete.run(id);
|
|
74
|
+
stmtInsert.run(configToParams(merged));
|
|
75
|
+
return merged;
|
|
76
|
+
}
|
|
77
|
+
export function deleteConfig(id) {
|
|
78
|
+
const result = stmtDelete.run(id);
|
|
79
|
+
return result.changes > 0;
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { callMcpTool } from './mcpClient.js';
|
|
2
|
+
const configSchema = [
|
|
3
|
+
{
|
|
4
|
+
key: 'doc_token',
|
|
5
|
+
label: 'Document Token',
|
|
6
|
+
type: 'string',
|
|
7
|
+
required: true,
|
|
8
|
+
placeholder: 'e.g. doxcnXyz123 (from the doc URL)',
|
|
9
|
+
},
|
|
10
|
+
];
|
|
11
|
+
export const larkDocsProvider = {
|
|
12
|
+
id: 'lark-docs',
|
|
13
|
+
displayName: 'Lark Docs',
|
|
14
|
+
configSchema,
|
|
15
|
+
async fetchItems(config) {
|
|
16
|
+
const docToken = config.doc_token;
|
|
17
|
+
const result = await callMcpTool('lark-docs', 'get_lark_doc_content', {
|
|
18
|
+
doc_token: docToken,
|
|
19
|
+
});
|
|
20
|
+
const items = [];
|
|
21
|
+
// Parse document content to find checklist items
|
|
22
|
+
const content = result?.content?.[0]?.text || result?.text || '';
|
|
23
|
+
const text = typeof content === 'string' ? content : JSON.stringify(content);
|
|
24
|
+
// Parse checklist items from the document content
|
|
25
|
+
// Lark docs may return structured blocks or markdown-like text
|
|
26
|
+
const lines = text.split('\n');
|
|
27
|
+
let itemIndex = 0;
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
// Match markdown-style checkboxes: - [ ] or - [x]
|
|
31
|
+
const checkboxMatch = trimmed.match(/^[-*]\s*\[([ xX])\]\s*(.+)/);
|
|
32
|
+
if (checkboxMatch) {
|
|
33
|
+
const completed = checkboxMatch[1].toLowerCase() === 'x';
|
|
34
|
+
const title = checkboxMatch[2].trim();
|
|
35
|
+
items.push({
|
|
36
|
+
externalId: `${docToken}_${itemIndex++}`,
|
|
37
|
+
externalUrl: `https://bytedance.larkoffice.com/docx/${docToken}`,
|
|
38
|
+
title,
|
|
39
|
+
completed,
|
|
40
|
+
priority: 'medium',
|
|
41
|
+
});
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// Match TODO/DONE markers
|
|
45
|
+
const todoMatch = trimmed.match(/^(?:TODO|TASK|ACTION):\s*(.+)/i);
|
|
46
|
+
if (todoMatch) {
|
|
47
|
+
items.push({
|
|
48
|
+
externalId: `${docToken}_${itemIndex++}`,
|
|
49
|
+
externalUrl: `https://bytedance.larkoffice.com/docx/${docToken}`,
|
|
50
|
+
title: todoMatch[1].trim(),
|
|
51
|
+
completed: false,
|
|
52
|
+
priority: 'medium',
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const doneMatch = trimmed.match(/^(?:DONE|COMPLETED|FINISHED):\s*(.+)/i);
|
|
57
|
+
if (doneMatch) {
|
|
58
|
+
items.push({
|
|
59
|
+
externalId: `${docToken}_${itemIndex++}`,
|
|
60
|
+
externalUrl: `https://bytedance.larkoffice.com/docx/${docToken}`,
|
|
61
|
+
title: doneMatch[1].trim(),
|
|
62
|
+
completed: true,
|
|
63
|
+
priority: 'medium',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return items;
|
|
68
|
+
},
|
|
69
|
+
// completeItem not supported — Lark Docs is read-only for checklist parsing
|
|
70
|
+
async validateConfig(config) {
|
|
71
|
+
if (!config.doc_token)
|
|
72
|
+
return 'doc_token is required';
|
|
73
|
+
return null;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
const connections = new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Read MCP server configurations from Claude settings.
|
|
7
|
+
*/
|
|
8
|
+
function loadMcpConfig() {
|
|
9
|
+
const configs = {};
|
|
10
|
+
const settingsPath = path.join(process.env.HOME || '~', '.claude', 'settings.json');
|
|
11
|
+
try {
|
|
12
|
+
if (fs.existsSync(settingsPath)) {
|
|
13
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
14
|
+
if (settings.mcpServers) {
|
|
15
|
+
Object.assign(configs, settings.mcpServers);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// ignore
|
|
21
|
+
}
|
|
22
|
+
// Also check project-level .mcp.json
|
|
23
|
+
const mcpJsonPath = path.join(process.cwd(), '.mcp.json');
|
|
24
|
+
try {
|
|
25
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
26
|
+
const mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'));
|
|
27
|
+
if (mcpJson.mcpServers) {
|
|
28
|
+
Object.assign(configs, mcpJson.mcpServers);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
35
|
+
return configs;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Connect to an MCP server via stdio JSON-RPC.
|
|
39
|
+
*/
|
|
40
|
+
function connectToServer(serverName) {
|
|
41
|
+
const existing = connections.get(serverName);
|
|
42
|
+
if (existing && existing.process.exitCode === null) {
|
|
43
|
+
return existing;
|
|
44
|
+
}
|
|
45
|
+
const configs = loadMcpConfig();
|
|
46
|
+
const serverConfig = configs[serverName];
|
|
47
|
+
if (!serverConfig) {
|
|
48
|
+
throw new Error(`MCP server "${serverName}" not found in configuration`);
|
|
49
|
+
}
|
|
50
|
+
const proc = spawn(serverConfig.command, serverConfig.args || [], {
|
|
51
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
52
|
+
env: { ...process.env, ...serverConfig.env },
|
|
53
|
+
});
|
|
54
|
+
const conn = {
|
|
55
|
+
process: proc,
|
|
56
|
+
requestId: 0,
|
|
57
|
+
pending: new Map(),
|
|
58
|
+
};
|
|
59
|
+
let buffer = '';
|
|
60
|
+
proc.stdout.on('data', (chunk) => {
|
|
61
|
+
buffer += chunk.toString();
|
|
62
|
+
// Handle JSON-RPC messages separated by Content-Length headers or newlines
|
|
63
|
+
const lines = buffer.split('\n');
|
|
64
|
+
buffer = lines.pop() || '';
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
if (!trimmed || trimmed.startsWith('Content-Length'))
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
const msg = JSON.parse(trimmed);
|
|
71
|
+
if (msg.id !== undefined && conn.pending.has(msg.id)) {
|
|
72
|
+
const p = conn.pending.get(msg.id);
|
|
73
|
+
conn.pending.delete(msg.id);
|
|
74
|
+
if (msg.error) {
|
|
75
|
+
p.reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
p.resolve(msg.result);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// not a JSON line, skip
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
proc.on('exit', () => {
|
|
88
|
+
// Reject all pending requests
|
|
89
|
+
for (const [, p] of conn.pending) {
|
|
90
|
+
p.reject(new Error(`MCP server "${serverName}" exited`));
|
|
91
|
+
}
|
|
92
|
+
conn.pending.clear();
|
|
93
|
+
connections.delete(serverName);
|
|
94
|
+
});
|
|
95
|
+
// Initialize the server
|
|
96
|
+
const initId = ++conn.requestId;
|
|
97
|
+
const initMsg = JSON.stringify({
|
|
98
|
+
jsonrpc: '2.0',
|
|
99
|
+
id: initId,
|
|
100
|
+
method: 'initialize',
|
|
101
|
+
params: {
|
|
102
|
+
protocolVersion: '2024-11-05',
|
|
103
|
+
capabilities: {},
|
|
104
|
+
clientInfo: { name: 'claudit', version: '1.0.0' },
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
proc.stdin.write(initMsg + '\n');
|
|
108
|
+
connections.set(serverName, conn);
|
|
109
|
+
return conn;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Call an MCP tool on the specified server.
|
|
113
|
+
*/
|
|
114
|
+
export async function callMcpTool(serverName, toolName, args) {
|
|
115
|
+
const conn = connectToServer(serverName);
|
|
116
|
+
const id = ++conn.requestId;
|
|
117
|
+
const request = JSON.stringify({
|
|
118
|
+
jsonrpc: '2.0',
|
|
119
|
+
id,
|
|
120
|
+
method: 'tools/call',
|
|
121
|
+
params: {
|
|
122
|
+
name: toolName,
|
|
123
|
+
arguments: args,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
conn.pending.set(id, { resolve, reject });
|
|
128
|
+
conn.process.stdin.write(request + '\n');
|
|
129
|
+
// Timeout after 30 seconds
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
if (conn.pending.has(id)) {
|
|
132
|
+
conn.pending.delete(id);
|
|
133
|
+
reject(new Error(`MCP tool call "${toolName}" timed out`));
|
|
134
|
+
}
|
|
135
|
+
}, 30000);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Clean up all MCP connections on shutdown.
|
|
140
|
+
*/
|
|
141
|
+
export function closeMcpConnections() {
|
|
142
|
+
for (const [, conn] of connections) {
|
|
143
|
+
try {
|
|
144
|
+
conn.process.kill();
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// ignore
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
connections.clear();
|
|
151
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { callMcpTool } from './mcpClient.js';
|
|
2
|
+
const configSchema = [
|
|
3
|
+
{
|
|
4
|
+
key: 'project_key',
|
|
5
|
+
label: 'Project Key',
|
|
6
|
+
type: 'string',
|
|
7
|
+
required: true,
|
|
8
|
+
placeholder: 'e.g. my-project',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
key: 'work_item_type',
|
|
12
|
+
label: 'Work Item Type',
|
|
13
|
+
type: 'string',
|
|
14
|
+
required: true,
|
|
15
|
+
placeholder: 'e.g. story, task, bug',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: 'moql_filter',
|
|
19
|
+
label: 'MOQL Filter (optional)',
|
|
20
|
+
type: 'string',
|
|
21
|
+
required: false,
|
|
22
|
+
placeholder: 'e.g. assignee = "me" AND status != "done"',
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
export const meegoProvider = {
|
|
26
|
+
id: 'meego',
|
|
27
|
+
displayName: 'Meego',
|
|
28
|
+
configSchema,
|
|
29
|
+
async fetchItems(config) {
|
|
30
|
+
const projectKey = config.project_key;
|
|
31
|
+
const workItemType = config.work_item_type;
|
|
32
|
+
const moqlFilter = config.moql_filter;
|
|
33
|
+
let mql = `project_key = "${projectKey}" AND work_item_type_key = "${workItemType}"`;
|
|
34
|
+
if (moqlFilter) {
|
|
35
|
+
mql += ` AND (${moqlFilter})`;
|
|
36
|
+
}
|
|
37
|
+
const result = await callMcpTool('meego', 'search_by_mql', {
|
|
38
|
+
project_key: projectKey,
|
|
39
|
+
work_item_type_key: workItemType,
|
|
40
|
+
mql,
|
|
41
|
+
});
|
|
42
|
+
const items = [];
|
|
43
|
+
// Parse the MCP result — expected to contain work items
|
|
44
|
+
const workItems = result?.content?.[0]?.text
|
|
45
|
+
? JSON.parse(result.content[0].text)
|
|
46
|
+
: result?.work_items || result || [];
|
|
47
|
+
const list = Array.isArray(workItems) ? workItems : workItems.work_items || [];
|
|
48
|
+
for (const item of list) {
|
|
49
|
+
items.push({
|
|
50
|
+
externalId: String(item.id || item.work_item_id),
|
|
51
|
+
externalUrl: item.url || item.web_url,
|
|
52
|
+
title: item.name || item.title || item.summary || `Work Item ${item.id}`,
|
|
53
|
+
description: item.description,
|
|
54
|
+
completed: isCompletedStatus(item.status?.name || item.status_name || item.status),
|
|
55
|
+
priority: mapPriority(item.priority?.name || item.priority_name || item.priority),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return items;
|
|
59
|
+
},
|
|
60
|
+
async completeItem(config, externalId) {
|
|
61
|
+
const projectKey = config.project_key;
|
|
62
|
+
const workItemType = config.work_item_type;
|
|
63
|
+
await callMcpTool('meego', 'finish_node', {
|
|
64
|
+
project_key: projectKey,
|
|
65
|
+
work_item_type_key: workItemType,
|
|
66
|
+
work_item_id: Number(externalId),
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
async validateConfig(config) {
|
|
70
|
+
if (!config.project_key)
|
|
71
|
+
return 'project_key is required';
|
|
72
|
+
if (!config.work_item_type)
|
|
73
|
+
return 'work_item_type is required';
|
|
74
|
+
return null;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
function isCompletedStatus(status) {
|
|
78
|
+
if (!status)
|
|
79
|
+
return false;
|
|
80
|
+
const lower = status.toLowerCase();
|
|
81
|
+
return lower === 'done' || lower === 'closed' || lower === 'completed' || lower === 'resolved';
|
|
82
|
+
}
|
|
83
|
+
function mapPriority(priority) {
|
|
84
|
+
if (!priority)
|
|
85
|
+
return 'medium';
|
|
86
|
+
if (typeof priority === 'number') {
|
|
87
|
+
if (priority >= 3)
|
|
88
|
+
return 'high';
|
|
89
|
+
if (priority >= 2)
|
|
90
|
+
return 'medium';
|
|
91
|
+
return 'low';
|
|
92
|
+
}
|
|
93
|
+
const lower = priority.toLowerCase();
|
|
94
|
+
if (lower.includes('high') || lower.includes('urgent') || lower.includes('critical'))
|
|
95
|
+
return 'high';
|
|
96
|
+
if (lower.includes('low') || lower.includes('minor'))
|
|
97
|
+
return 'low';
|
|
98
|
+
return 'medium';
|
|
99
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { meegoProvider } from './meegoProvider.js';
|
|
2
|
+
import { larkDocsProvider } from './larkDocsProvider.js';
|
|
3
|
+
import { supabaseProvider } from './supabaseProvider.js';
|
|
4
|
+
const providers = new Map();
|
|
5
|
+
function register(p) {
|
|
6
|
+
providers.set(p.id, p);
|
|
7
|
+
}
|
|
8
|
+
// Register built-in providers
|
|
9
|
+
register(meegoProvider);
|
|
10
|
+
register(larkDocsProvider);
|
|
11
|
+
register(supabaseProvider);
|
|
12
|
+
export function getProvider(id) {
|
|
13
|
+
return providers.get(id);
|
|
14
|
+
}
|
|
15
|
+
export function getAllProviders() {
|
|
16
|
+
return Array.from(providers.values());
|
|
17
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const configSchema = [
|
|
2
|
+
{
|
|
3
|
+
key: 'supabase_url',
|
|
4
|
+
label: 'Supabase Project URL',
|
|
5
|
+
type: 'string',
|
|
6
|
+
required: true,
|
|
7
|
+
placeholder: 'e.g. https://xxx.supabase.co',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
key: 'anon_key',
|
|
11
|
+
label: 'API Key (anon/publishable)',
|
|
12
|
+
type: 'string',
|
|
13
|
+
required: true,
|
|
14
|
+
placeholder: 'eyJhbGciOiJIUzI1NiIs...',
|
|
15
|
+
secret: true,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: 'table',
|
|
19
|
+
label: 'Table Name',
|
|
20
|
+
type: 'string',
|
|
21
|
+
required: true,
|
|
22
|
+
placeholder: 'e.g. bug_reports',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: 'title_column',
|
|
26
|
+
label: 'Title Column',
|
|
27
|
+
type: 'string',
|
|
28
|
+
required: true,
|
|
29
|
+
placeholder: 'e.g. message',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: 'status_column',
|
|
33
|
+
label: 'Status Column',
|
|
34
|
+
type: 'string',
|
|
35
|
+
required: false,
|
|
36
|
+
placeholder: 'e.g. status',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: 'done_value',
|
|
40
|
+
label: 'Done Status Value',
|
|
41
|
+
type: 'string',
|
|
42
|
+
required: false,
|
|
43
|
+
placeholder: 'e.g. done',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: 'description_columns',
|
|
47
|
+
label: 'Description Columns (comma-separated)',
|
|
48
|
+
type: 'string',
|
|
49
|
+
required: false,
|
|
50
|
+
placeholder: 'e.g. device_info,app_version,current_tab',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: 'filter',
|
|
54
|
+
label: 'PostgREST Filter (optional)',
|
|
55
|
+
type: 'string',
|
|
56
|
+
required: false,
|
|
57
|
+
placeholder: 'e.g. status=eq.open or assigned_to=eq.me',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
async function supabaseFetch(url, apiKey, path, options = {}) {
|
|
61
|
+
const res = await fetch(`${url}/rest/v1/${path}`, {
|
|
62
|
+
...options,
|
|
63
|
+
headers: {
|
|
64
|
+
'apikey': apiKey,
|
|
65
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
'Prefer': options.method === 'PATCH' ? 'return=representation' : '',
|
|
68
|
+
...options.headers,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
const body = await res.text();
|
|
73
|
+
throw new Error(`Supabase API error ${res.status}: ${body}`);
|
|
74
|
+
}
|
|
75
|
+
if (res.status === 204)
|
|
76
|
+
return null;
|
|
77
|
+
return res.json();
|
|
78
|
+
}
|
|
79
|
+
export const supabaseProvider = {
|
|
80
|
+
id: 'supabase',
|
|
81
|
+
displayName: 'Supabase',
|
|
82
|
+
configSchema,
|
|
83
|
+
async fetchItems(config) {
|
|
84
|
+
const url = config.supabase_url.replace(/\/$/, '');
|
|
85
|
+
const apiKey = config.anon_key;
|
|
86
|
+
const table = config.table;
|
|
87
|
+
const titleCol = config.title_column;
|
|
88
|
+
const statusCol = config.status_column;
|
|
89
|
+
const doneValue = config.done_value;
|
|
90
|
+
const descCols = config.description_columns;
|
|
91
|
+
const filter = config.filter;
|
|
92
|
+
// Build select columns
|
|
93
|
+
const selectCols = new Set(['id', titleCol, 'created_at']);
|
|
94
|
+
if (statusCol)
|
|
95
|
+
selectCols.add(statusCol);
|
|
96
|
+
if (descCols) {
|
|
97
|
+
descCols.split(',').map(c => c.trim()).filter(Boolean).forEach(c => selectCols.add(c));
|
|
98
|
+
}
|
|
99
|
+
let path = `${table}?select=${Array.from(selectCols).join(',')}&order=created_at.desc`;
|
|
100
|
+
if (filter) {
|
|
101
|
+
path += `&${filter}`;
|
|
102
|
+
}
|
|
103
|
+
const rows = await supabaseFetch(url, apiKey, path);
|
|
104
|
+
return rows.map(row => {
|
|
105
|
+
// Build description from extra columns
|
|
106
|
+
let description;
|
|
107
|
+
if (descCols) {
|
|
108
|
+
const parts = descCols.split(',').map(c => c.trim()).filter(Boolean);
|
|
109
|
+
const lines = [];
|
|
110
|
+
for (const col of parts) {
|
|
111
|
+
const val = row[col];
|
|
112
|
+
if (val == null || val === '' || (Array.isArray(val) && val.length === 0))
|
|
113
|
+
continue;
|
|
114
|
+
if (Array.isArray(val)) {
|
|
115
|
+
lines.push(`${col}: ${val.join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
lines.push(`${col}: ${val}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (lines.length > 0)
|
|
122
|
+
description = lines.join('\n');
|
|
123
|
+
}
|
|
124
|
+
const completed = statusCol && doneValue
|
|
125
|
+
? String(row[statusCol]) === doneValue
|
|
126
|
+
: false;
|
|
127
|
+
return {
|
|
128
|
+
externalId: String(row.id),
|
|
129
|
+
externalUrl: `${url}/project/default/editor?table=${table}`,
|
|
130
|
+
title: String(row[titleCol] || `Row ${row.id}`),
|
|
131
|
+
description,
|
|
132
|
+
completed,
|
|
133
|
+
priority: mapStatusToPriority(statusCol ? row[statusCol] : undefined),
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
async completeItem(config, externalId) {
|
|
138
|
+
const url = config.supabase_url.replace(/\/$/, '');
|
|
139
|
+
const apiKey = config.anon_key;
|
|
140
|
+
const table = config.table;
|
|
141
|
+
const statusCol = config.status_column;
|
|
142
|
+
const doneValue = config.done_value;
|
|
143
|
+
if (!statusCol || !doneValue) {
|
|
144
|
+
throw new Error('Cannot complete: status_column and done_value not configured');
|
|
145
|
+
}
|
|
146
|
+
await supabaseFetch(url, apiKey, `${table}?id=eq.${externalId}`, {
|
|
147
|
+
method: 'PATCH',
|
|
148
|
+
body: JSON.stringify({ [statusCol]: doneValue }),
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
async validateConfig(config) {
|
|
152
|
+
if (!config.supabase_url)
|
|
153
|
+
return 'supabase_url is required';
|
|
154
|
+
if (!config.anon_key)
|
|
155
|
+
return 'anon_key is required';
|
|
156
|
+
if (!config.table)
|
|
157
|
+
return 'table is required';
|
|
158
|
+
if (!config.title_column)
|
|
159
|
+
return 'title_column is required';
|
|
160
|
+
return null;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
function mapStatusToPriority(status) {
|
|
164
|
+
if (!status)
|
|
165
|
+
return 'medium';
|
|
166
|
+
const lower = String(status).toLowerCase();
|
|
167
|
+
if (lower === 'open')
|
|
168
|
+
return 'high';
|
|
169
|
+
if (lower === 'in_progress')
|
|
170
|
+
return 'medium';
|
|
171
|
+
return 'low';
|
|
172
|
+
}
|