@dinoxx/dinox-cli 1.0.15 → 1.0.17
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 +102 -6
- package/dist/cli.js +8 -0
- package/dist/commands/graph/index.d.ts +2 -0
- package/dist/commands/graph/index.js +113 -0
- package/dist/commands/graph/repo.d.ts +40 -0
- package/dist/commands/graph/repo.js +126 -0
- package/dist/commands/mcp/auth.d.ts +4 -0
- package/dist/commands/mcp/auth.js +75 -0
- package/dist/commands/mcp/index.d.ts +2 -0
- package/dist/commands/mcp/index.js +85 -0
- package/dist/commands/mcp/server.d.ts +31 -0
- package/dist/commands/mcp/server.js +392 -0
- package/dist/commands/mcp/tools.d.ts +2 -0
- package/dist/commands/mcp/tools.js +104 -0
- package/dist/commands/mcp/types.d.ts +28 -0
- package/dist/commands/mcp/types.js +1 -0
- package/dist/commands/meta/index.d.ts +2 -0
- package/dist/commands/meta/index.js +45 -0
- package/dist/commands/meta/repo.d.ts +27 -0
- package/dist/commands/meta/repo.js +71 -0
- package/dist/commands/notes/index.d.ts +13 -3
- package/dist/commands/notes/index.js +135 -6
- package/dist/commands/notes/repo.d.ts +26 -0
- package/dist/commands/notes/repo.js +99 -0
- package/dist/commands/notes/searchOutput.d.ts +6 -0
- package/dist/commands/notes/searchOutput.js +41 -0
- package/dist/daemon/client.d.ts +2 -0
- package/dist/daemon/client.js +86 -0
- package/dist/daemon/ensure.d.ts +1 -0
- package/dist/daemon/ensure.js +23 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +151 -0
- package/dist/daemon/pid.d.ts +10 -0
- package/dist/daemon/pid.js +55 -0
- package/dist/daemon/server.d.ts +10 -0
- package/dist/daemon/server.js +140 -0
- package/dist/daemon/types.d.ts +19 -0
- package/dist/daemon/types.js +1 -0
- package/dist/dinox.js +6 -14
- package/dist/utils/errors.d.ts +22 -5
- package/dist/utils/errors.js +29 -0
- package/package.json +1 -1
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { DEFAULT_MCP_PORT } from './types.js';
|
|
3
|
+
import { mcpTools } from './tools.js';
|
|
4
|
+
import { isAuthorizedMCPRequest } from './auth.js';
|
|
5
|
+
import { resolveAuthIdentity } from '../../auth/userInfo.js';
|
|
6
|
+
import { loadConfig } from '../../config/store.js';
|
|
7
|
+
import { resolveConfig } from '../../config/resolve.js';
|
|
8
|
+
import { connectPowerSync } from '../../powersync/runtime.js';
|
|
9
|
+
import { parseCreatedAtRange } from '../notes/searchTime.js';
|
|
10
|
+
import { parseNoteSearchFields, projectNoteSearchFields } from '../notes/searchOutput.js';
|
|
11
|
+
import { createNote, getNoteDetails, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, searchNotes, validateTagsForCreate, } from '../notes/repo.js';
|
|
12
|
+
import { getBacklinks, getGraphStats, getOutlinks, getRelatedNotes } from '../graph/repo.js';
|
|
13
|
+
import { searchTodoTasks } from '../todo/repo.js';
|
|
14
|
+
import { convertMarkdownToNoteContent } from '../../utils/tiptapMarkdown.js';
|
|
15
|
+
import { generateId } from '../../utils/id.js';
|
|
16
|
+
import { getPackageVersion } from '../../utils/version.js';
|
|
17
|
+
const MAX_MCP_REQUEST_BYTES = 1 * 1024 * 1024;
|
|
18
|
+
const DEFAULT_SEARCH_LIMIT = 50;
|
|
19
|
+
const MAX_SEARCH_LIMIT = 500;
|
|
20
|
+
function normalizeLimit(value) {
|
|
21
|
+
if (value === undefined || value === null) {
|
|
22
|
+
return DEFAULT_SEARCH_LIMIT;
|
|
23
|
+
}
|
|
24
|
+
const raw = typeof value === 'number'
|
|
25
|
+
? value
|
|
26
|
+
: typeof value === 'string'
|
|
27
|
+
? Number(value)
|
|
28
|
+
: NaN;
|
|
29
|
+
if (!Number.isFinite(raw)) {
|
|
30
|
+
return DEFAULT_SEARCH_LIMIT;
|
|
31
|
+
}
|
|
32
|
+
const int = Math.trunc(raw);
|
|
33
|
+
if (int <= 0) {
|
|
34
|
+
return DEFAULT_SEARCH_LIMIT;
|
|
35
|
+
}
|
|
36
|
+
return Math.min(int, MAX_SEARCH_LIMIT);
|
|
37
|
+
}
|
|
38
|
+
function normalizeOffset(value) {
|
|
39
|
+
if (value === undefined || value === null) {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
const raw = typeof value === 'number'
|
|
43
|
+
? value
|
|
44
|
+
: typeof value === 'string'
|
|
45
|
+
? Number(value)
|
|
46
|
+
: NaN;
|
|
47
|
+
if (!Number.isFinite(raw)) {
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
const int = Math.trunc(raw);
|
|
51
|
+
if (int < 0) {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
return int;
|
|
55
|
+
}
|
|
56
|
+
function normalizeStringArray(value) {
|
|
57
|
+
if (!Array.isArray(value)) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return value.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
function parseBoxFilter(value) {
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
return { names: normalizeStringArray(value), matchEmpty: false };
|
|
65
|
+
}
|
|
66
|
+
if (typeof value !== 'string') {
|
|
67
|
+
return { names: [], matchEmpty: false };
|
|
68
|
+
}
|
|
69
|
+
const trimmed = value.trim();
|
|
70
|
+
if (!trimmed) {
|
|
71
|
+
return { names: [], matchEmpty: false };
|
|
72
|
+
}
|
|
73
|
+
if (trimmed === '[]') {
|
|
74
|
+
return { names: [], matchEmpty: true };
|
|
75
|
+
}
|
|
76
|
+
const names = trimmed
|
|
77
|
+
.split(/[,\n]/)
|
|
78
|
+
.map((name) => name.trim())
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
return { names, matchEmpty: false };
|
|
81
|
+
}
|
|
82
|
+
function isJsonRpcId(value) {
|
|
83
|
+
return value === null || typeof value === 'string' || typeof value === 'number';
|
|
84
|
+
}
|
|
85
|
+
function parseMCPRequest(value) {
|
|
86
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
87
|
+
throw new Error('Invalid JSON-RPC request');
|
|
88
|
+
}
|
|
89
|
+
const record = value;
|
|
90
|
+
if (record.jsonrpc !== '2.0') {
|
|
91
|
+
throw new Error('Invalid JSON-RPC request: jsonrpc must be "2.0"');
|
|
92
|
+
}
|
|
93
|
+
if (typeof record.method !== 'string' || !record.method.trim()) {
|
|
94
|
+
throw new Error('Invalid JSON-RPC request: method is required');
|
|
95
|
+
}
|
|
96
|
+
if (record.id !== undefined && !isJsonRpcId(record.id)) {
|
|
97
|
+
throw new Error('Invalid JSON-RPC request: id must be a string, number, or null');
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
jsonrpc: '2.0',
|
|
101
|
+
id: record.id,
|
|
102
|
+
method: record.method.trim(),
|
|
103
|
+
params: record.params,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function hasRequestId(req) {
|
|
107
|
+
return req.id !== undefined;
|
|
108
|
+
}
|
|
109
|
+
function buildResponse(req, payload) {
|
|
110
|
+
return {
|
|
111
|
+
jsonrpc: '2.0',
|
|
112
|
+
id: req.id ?? null,
|
|
113
|
+
...payload,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function readJsonBody(req) {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const chunks = [];
|
|
119
|
+
let totalBytes = 0;
|
|
120
|
+
req.on('data', (chunk) => {
|
|
121
|
+
totalBytes += chunk.length;
|
|
122
|
+
if (totalBytes > MAX_MCP_REQUEST_BYTES) {
|
|
123
|
+
req.destroy(new Error('Request body too large'));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
chunks.push(chunk);
|
|
127
|
+
});
|
|
128
|
+
req.on('end', () => {
|
|
129
|
+
try {
|
|
130
|
+
const body = Buffer.concat(chunks).toString('utf-8');
|
|
131
|
+
resolve(JSON.parse(body));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
reject(error);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
req.on('error', reject);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
export class MCPServer {
|
|
141
|
+
db = null;
|
|
142
|
+
async initialize() {
|
|
143
|
+
const config = resolveConfig(await loadConfig());
|
|
144
|
+
const { db } = await connectPowerSync({ config, offline: false, timeoutMs: config.sync.timeoutMs });
|
|
145
|
+
this.db = db;
|
|
146
|
+
}
|
|
147
|
+
async handleRequest(req) {
|
|
148
|
+
if (req.method === 'notifications/initialized') {
|
|
149
|
+
return hasRequestId(req) ? buildResponse(req, { result: {} }) : null;
|
|
150
|
+
}
|
|
151
|
+
const shouldRespond = hasRequestId(req);
|
|
152
|
+
try {
|
|
153
|
+
switch (req.method) {
|
|
154
|
+
case 'initialize':
|
|
155
|
+
if (!shouldRespond) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return this.handleInitialize(req);
|
|
159
|
+
case 'tools/list':
|
|
160
|
+
if (!shouldRespond) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return this.handleToolsList(req);
|
|
164
|
+
case 'tools/call':
|
|
165
|
+
if (!shouldRespond) {
|
|
166
|
+
await this.handleToolsCall(req);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return await this.handleToolsCall(req);
|
|
170
|
+
default:
|
|
171
|
+
if (!shouldRespond) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return buildResponse(req, {
|
|
175
|
+
error: { code: -32601, message: 'Method not found' },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (!shouldRespond) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return buildResponse(req, {
|
|
184
|
+
error: { code: -32603, message: error.message || 'Internal error' },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
handleInitialize(req) {
|
|
189
|
+
const result = {
|
|
190
|
+
protocolVersion: '2024-11-05',
|
|
191
|
+
capabilities: {
|
|
192
|
+
tools: {},
|
|
193
|
+
},
|
|
194
|
+
serverInfo: {
|
|
195
|
+
name: 'dinox-mcp-server',
|
|
196
|
+
version: getPackageVersion(),
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
return buildResponse(req, { result });
|
|
200
|
+
}
|
|
201
|
+
handleToolsList(req) {
|
|
202
|
+
return buildResponse(req, { result: { tools: mcpTools } });
|
|
203
|
+
}
|
|
204
|
+
async handleToolsCall(req) {
|
|
205
|
+
if (!this.db) {
|
|
206
|
+
throw new Error('Database not initialized');
|
|
207
|
+
}
|
|
208
|
+
const name = req.params?.name;
|
|
209
|
+
const args = req.params?.arguments ?? {};
|
|
210
|
+
if (typeof name !== 'string') {
|
|
211
|
+
throw new Error('Invalid tools/call: missing tool name');
|
|
212
|
+
}
|
|
213
|
+
switch (name) {
|
|
214
|
+
case 'dinox_search_notes': {
|
|
215
|
+
const keyword = typeof args.query === 'string' ? args.query : '';
|
|
216
|
+
const tagsExpression = typeof args.tags === 'string' ? args.tags.trim() : '';
|
|
217
|
+
const { names: boxNames, matchEmpty: matchEmptyBoxes } = parseBoxFilter(args.box);
|
|
218
|
+
const zettelBoxIds = await resolveZettelBoxIdsForSearch(this.db, boxNames);
|
|
219
|
+
const daysInput = typeof args.days === 'number' ? String(args.days) : args.days;
|
|
220
|
+
const { createdAtFrom, createdAtTo } = parseCreatedAtRange({
|
|
221
|
+
from: args.from,
|
|
222
|
+
to: args.to,
|
|
223
|
+
days: daysInput,
|
|
224
|
+
});
|
|
225
|
+
const limit = normalizeLimit(args.limit);
|
|
226
|
+
const offset = normalizeOffset(args.offset);
|
|
227
|
+
const fields = parseNoteSearchFields(args.fields, 'dinox_search_notes.fields');
|
|
228
|
+
const rows = await searchNotes(this.db, keyword, {
|
|
229
|
+
includeDeleted: false,
|
|
230
|
+
tagsExpression: tagsExpression || undefined,
|
|
231
|
+
createdAtFrom,
|
|
232
|
+
createdAtTo,
|
|
233
|
+
zettelBoxIds,
|
|
234
|
+
matchEmptyZettelBoxes: matchEmptyBoxes,
|
|
235
|
+
});
|
|
236
|
+
const totalCount = rows.length;
|
|
237
|
+
const paged = rows.slice(offset, offset + limit);
|
|
238
|
+
const result = projectNoteSearchFields(paged, fields);
|
|
239
|
+
const text = JSON.stringify({
|
|
240
|
+
meta: {
|
|
241
|
+
total_count: totalCount,
|
|
242
|
+
returned_count: result.length,
|
|
243
|
+
limit,
|
|
244
|
+
offset,
|
|
245
|
+
has_more: offset + result.length < totalCount,
|
|
246
|
+
fields: fields ?? null,
|
|
247
|
+
},
|
|
248
|
+
notes: result,
|
|
249
|
+
});
|
|
250
|
+
return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
|
|
251
|
+
}
|
|
252
|
+
case 'dinox_get_note': {
|
|
253
|
+
if (typeof args.id !== 'string' || !args.id.trim()) {
|
|
254
|
+
throw new Error('dinox_get_note: id is required');
|
|
255
|
+
}
|
|
256
|
+
const notes = await getNoteDetails(this.db, [args.id.trim()]);
|
|
257
|
+
const text = JSON.stringify(notes[0] ?? null);
|
|
258
|
+
return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
|
|
259
|
+
}
|
|
260
|
+
case 'dinox_create_note': {
|
|
261
|
+
const title = typeof args.title === 'string' ? args.title.trim() : '';
|
|
262
|
+
const contentMd = typeof args.content === 'string' ? args.content : '';
|
|
263
|
+
if (!title) {
|
|
264
|
+
throw new Error('dinox_create_note: title is required');
|
|
265
|
+
}
|
|
266
|
+
if (!contentMd) {
|
|
267
|
+
throw new Error('dinox_create_note: content is required');
|
|
268
|
+
}
|
|
269
|
+
const rawConfig = await loadConfig();
|
|
270
|
+
const config = resolveConfig(rawConfig);
|
|
271
|
+
const identity = await resolveAuthIdentity(rawConfig, config);
|
|
272
|
+
const { contentJson, contentText } = convertMarkdownToNoteContent(contentMd);
|
|
273
|
+
const tags = await validateTagsForCreate(this.db, normalizeStringArray(args.tags));
|
|
274
|
+
const zettelBoxIds = await resolveZettelBoxIdsForCreate(this.db, normalizeStringArray(args.boxes));
|
|
275
|
+
const id = generateId();
|
|
276
|
+
await createNote(this.db, {
|
|
277
|
+
id,
|
|
278
|
+
title,
|
|
279
|
+
noteType: 'crawl',
|
|
280
|
+
contentMd,
|
|
281
|
+
contentText,
|
|
282
|
+
contentJson,
|
|
283
|
+
tags,
|
|
284
|
+
zettelBoxIds,
|
|
285
|
+
userId: identity.userId,
|
|
286
|
+
});
|
|
287
|
+
const text = JSON.stringify({ id });
|
|
288
|
+
return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
|
|
289
|
+
}
|
|
290
|
+
case 'dinox_graph_backlinks': {
|
|
291
|
+
if (typeof args.id !== 'string' || !args.id.trim()) {
|
|
292
|
+
throw new Error('dinox_graph_backlinks: id is required');
|
|
293
|
+
}
|
|
294
|
+
const result = await getBacklinks(this.db, args.id.trim());
|
|
295
|
+
const text = JSON.stringify(result);
|
|
296
|
+
return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
|
|
297
|
+
}
|
|
298
|
+
case 'dinox_graph_outlinks': {
|
|
299
|
+
if (typeof args.id !== 'string' || !args.id.trim()) {
|
|
300
|
+
throw new Error('dinox_graph_outlinks: id is required');
|
|
301
|
+
}
|
|
302
|
+
const result = await getOutlinks(this.db, args.id.trim());
|
|
303
|
+
const text = JSON.stringify(result);
|
|
304
|
+
return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
|
|
305
|
+
}
|
|
306
|
+
case 'dinox_graph_related': {
|
|
307
|
+
if (typeof args.id !== 'string' || !args.id.trim()) {
|
|
308
|
+
throw new Error('dinox_graph_related: id is required');
|
|
309
|
+
}
|
|
310
|
+
const depthRaw = typeof args.depth === 'number' && Number.isFinite(args.depth)
|
|
311
|
+
? Math.trunc(args.depth)
|
|
312
|
+
: 2;
|
|
313
|
+
const depth = Math.min(Math.max(depthRaw, 1), 5);
|
|
314
|
+
const result = await getRelatedNotes(this.db, args.id.trim(), depth);
|
|
315
|
+
const text = JSON.stringify(result);
|
|
316
|
+
return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
|
|
317
|
+
}
|
|
318
|
+
case 'dinox_graph_stats': {
|
|
319
|
+
const result = await getGraphStats(this.db);
|
|
320
|
+
const text = JSON.stringify(result);
|
|
321
|
+
return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
|
|
322
|
+
}
|
|
323
|
+
case 'dinox_search_todos': {
|
|
324
|
+
const query = typeof args.query === 'string' ? args.query : '';
|
|
325
|
+
const status = typeof args.status === 'string' && ['all', 'completed', 'uncompleted'].includes(args.status)
|
|
326
|
+
? args.status
|
|
327
|
+
: 'all';
|
|
328
|
+
const limit = normalizeLimit(args.limit);
|
|
329
|
+
const result = await searchTodoTasks(this.db, query, {
|
|
330
|
+
includeDeleted: false,
|
|
331
|
+
taskStatus: status || 'all',
|
|
332
|
+
taskTagFilter: normalizeStringArray(args.tags),
|
|
333
|
+
limit,
|
|
334
|
+
scanLimit: 200,
|
|
335
|
+
});
|
|
336
|
+
const text = JSON.stringify(result);
|
|
337
|
+
return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
|
|
338
|
+
}
|
|
339
|
+
default:
|
|
340
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async close() {
|
|
344
|
+
if (this.db) {
|
|
345
|
+
await this.db.close().catch(() => undefined);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
export async function startMCPServer(port = DEFAULT_MCP_PORT, host = '127.0.0.1', authToken) {
|
|
350
|
+
const mcpServer = new MCPServer();
|
|
351
|
+
await mcpServer.initialize();
|
|
352
|
+
const server = createServer(async (req, res) => {
|
|
353
|
+
if (req.method === 'POST') {
|
|
354
|
+
if (!isAuthorizedMCPRequest(req.headers, authToken)) {
|
|
355
|
+
res.writeHead(401, {
|
|
356
|
+
'Content-Type': 'application/json',
|
|
357
|
+
'WWW-Authenticate': 'Bearer realm="dinox-mcp"',
|
|
358
|
+
});
|
|
359
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const body = await readJsonBody(req);
|
|
364
|
+
const mcpReq = parseMCPRequest(body);
|
|
365
|
+
const mcpRes = await mcpServer.handleRequest(mcpReq);
|
|
366
|
+
if (!mcpRes) {
|
|
367
|
+
res.writeHead(204);
|
|
368
|
+
res.end();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
372
|
+
res.end(JSON.stringify(mcpRes));
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
376
|
+
const statusCode = message.includes('too large') ? 413 : 400;
|
|
377
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
378
|
+
res.end(JSON.stringify({ error: message }));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
res.writeHead(405);
|
|
383
|
+
res.end();
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
server.headersTimeout = 5_000;
|
|
387
|
+
server.requestTimeout = 10_000;
|
|
388
|
+
server.listen(port, host, () => {
|
|
389
|
+
console.log(`MCP server listening on http://${host}:${port}`);
|
|
390
|
+
});
|
|
391
|
+
return { server, mcpServer };
|
|
392
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export const mcpTools = [
|
|
2
|
+
{
|
|
3
|
+
name: 'dinox_search_notes',
|
|
4
|
+
description: 'Search notes by keyword, tags, date range, or SQL expression',
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
query: { type: 'string', description: 'Search keyword' },
|
|
9
|
+
tags: { type: 'string', description: 'Tag expression (AND/OR/NOT)' },
|
|
10
|
+
from: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
11
|
+
to: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
12
|
+
days: { type: 'number', description: 'Recent N days' },
|
|
13
|
+
box: { type: 'string', description: 'Zettel box name' },
|
|
14
|
+
limit: { type: 'number', description: 'Max results (default: 50)' },
|
|
15
|
+
offset: { type: 'number', description: 'Result offset (default: 0)' },
|
|
16
|
+
fields: {
|
|
17
|
+
type: 'array',
|
|
18
|
+
items: { type: 'string' },
|
|
19
|
+
description: 'Returned fields: id,title,summary,tags,created_at,zettel_boxes',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'dinox_get_note',
|
|
26
|
+
description: 'Get note details by ID',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
id: { type: 'string', description: 'Note ID' },
|
|
31
|
+
},
|
|
32
|
+
required: ['id'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'dinox_create_note',
|
|
37
|
+
description: 'Create a new note',
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
title: { type: 'string', description: 'Note title' },
|
|
42
|
+
content: { type: 'string', description: 'Markdown content' },
|
|
43
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
|
|
44
|
+
boxes: { type: 'array', items: { type: 'string' }, description: 'Zettel boxes' },
|
|
45
|
+
},
|
|
46
|
+
required: ['title', 'content'],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'dinox_graph_backlinks',
|
|
51
|
+
description: 'Get notes that link to this note',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
id: { type: 'string', description: 'Note ID' },
|
|
56
|
+
},
|
|
57
|
+
required: ['id'],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'dinox_graph_outlinks',
|
|
62
|
+
description: 'Get notes that this note links to',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
id: { type: 'string', description: 'Note ID' },
|
|
67
|
+
},
|
|
68
|
+
required: ['id'],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'dinox_graph_related',
|
|
73
|
+
description: 'Get related notes within N degrees',
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: {
|
|
77
|
+
id: { type: 'string', description: 'Note ID' },
|
|
78
|
+
depth: { type: 'number', description: 'Degrees (1-5, default: 2)' },
|
|
79
|
+
},
|
|
80
|
+
required: ['id'],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'dinox_graph_stats',
|
|
85
|
+
description: 'Get graph statistics for notes and links',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'dinox_search_todos',
|
|
93
|
+
description: 'Search todo tasks',
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
query: { type: 'string', description: 'Task text query' },
|
|
98
|
+
status: { type: 'string', enum: ['all', 'completed', 'uncompleted'], description: 'Task status' },
|
|
99
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Task tags' },
|
|
100
|
+
limit: { type: 'number', description: 'Max results (default: 50)' },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type MCPTool = {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: 'object';
|
|
6
|
+
properties: Record<string, any>;
|
|
7
|
+
required?: string[];
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export type MCPResource = {
|
|
11
|
+
uri: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
mimeType?: string;
|
|
15
|
+
};
|
|
16
|
+
export type MCPServerInfo = {
|
|
17
|
+
name: string;
|
|
18
|
+
version: string;
|
|
19
|
+
};
|
|
20
|
+
export type MCPInitializeResult = {
|
|
21
|
+
protocolVersion: string;
|
|
22
|
+
capabilities: {
|
|
23
|
+
tools?: Record<string, never>;
|
|
24
|
+
resources?: Record<string, never>;
|
|
25
|
+
};
|
|
26
|
+
serverInfo: MCPServerInfo;
|
|
27
|
+
};
|
|
28
|
+
export declare const DEFAULT_MCP_PORT = 45137;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_MCP_PORT = 45137;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { loadConfig } from '../../config/store.js';
|
|
2
|
+
import { resolveConfig } from '../../config/resolve.js';
|
|
3
|
+
import { printYaml } from '../../utils/output.js';
|
|
4
|
+
import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
|
|
5
|
+
import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
|
|
6
|
+
import { getMetaSchema, getMetaStats } from './repo.js';
|
|
7
|
+
async function syncBeforeMetaCommand(db, offline, timeoutMs) {
|
|
8
|
+
if (offline) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
|
|
12
|
+
}
|
|
13
|
+
export function registerMetaCommands(program) {
|
|
14
|
+
const meta = program.command('meta').description('Query Dinox metadata');
|
|
15
|
+
meta
|
|
16
|
+
.command('stats')
|
|
17
|
+
.description('Get metadata stats (notes/tags/boxes)')
|
|
18
|
+
.action(async (_options, command) => {
|
|
19
|
+
const globals = command.optsWithGlobals?.() ?? {};
|
|
20
|
+
const offline = Boolean(globals.offline);
|
|
21
|
+
const config = resolveConfig(await loadConfig());
|
|
22
|
+
const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
|
|
23
|
+
const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
|
|
24
|
+
try {
|
|
25
|
+
await syncBeforeMetaCommand(db, offline, timeoutMs);
|
|
26
|
+
const stats = await getMetaStats(db);
|
|
27
|
+
printYaml({
|
|
28
|
+
stale: offline ? true : stale,
|
|
29
|
+
stats,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await db.close().catch(() => undefined);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
meta
|
|
37
|
+
.command('schema')
|
|
38
|
+
.description('Get structured Dinox data schema for AI integrations')
|
|
39
|
+
.action(() => {
|
|
40
|
+
printYaml(getMetaSchema());
|
|
41
|
+
});
|
|
42
|
+
meta.action((_options, command) => {
|
|
43
|
+
command.outputHelp();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
export type MetaStats = {
|
|
3
|
+
notes_count: number;
|
|
4
|
+
tags_count: number;
|
|
5
|
+
boxes_count: number;
|
|
6
|
+
latest_note_updated_at: string | null;
|
|
7
|
+
};
|
|
8
|
+
export type MetaSchema = {
|
|
9
|
+
version: number;
|
|
10
|
+
entities: {
|
|
11
|
+
note_search_result: {
|
|
12
|
+
fields: string[];
|
|
13
|
+
meta_fields: string[];
|
|
14
|
+
};
|
|
15
|
+
note_context: {
|
|
16
|
+
fields: string[];
|
|
17
|
+
};
|
|
18
|
+
graph_stats: {
|
|
19
|
+
fields: string[];
|
|
20
|
+
};
|
|
21
|
+
meta_stats: {
|
|
22
|
+
fields: string[];
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export declare function getMetaStats(db: AbstractPowerSyncDatabase): Promise<MetaStats>;
|
|
27
|
+
export declare function getMetaSchema(): MetaSchema;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export async function getMetaStats(db) {
|
|
2
|
+
const [notesRow, tagsRow, boxesRow, latestNoteRow] = await Promise.all([
|
|
3
|
+
db.getOptional(`
|
|
4
|
+
SELECT COUNT(1) AS count
|
|
5
|
+
FROM c_note n
|
|
6
|
+
WHERE n.is_del IS NULL OR n.is_del = 0
|
|
7
|
+
`, []),
|
|
8
|
+
db.getOptional(`
|
|
9
|
+
SELECT COUNT(1) AS count
|
|
10
|
+
FROM c_tag_node t
|
|
11
|
+
WHERE t.is_del IS NULL OR t.is_del = 0
|
|
12
|
+
`, []),
|
|
13
|
+
db.getOptional(`
|
|
14
|
+
SELECT COUNT(1) AS count
|
|
15
|
+
FROM c_zettel_box b
|
|
16
|
+
WHERE b.is_del IS NULL OR b.is_del = 0
|
|
17
|
+
`, []),
|
|
18
|
+
db.getOptional(`
|
|
19
|
+
SELECT MAX(COALESCE(n.updated_at, n.created_at)) AS latest
|
|
20
|
+
FROM c_note n
|
|
21
|
+
WHERE n.is_del IS NULL OR n.is_del = 0
|
|
22
|
+
`, []),
|
|
23
|
+
]);
|
|
24
|
+
return {
|
|
25
|
+
notes_count: Number(notesRow?.count ?? 0),
|
|
26
|
+
tags_count: Number(tagsRow?.count ?? 0),
|
|
27
|
+
boxes_count: Number(boxesRow?.count ?? 0),
|
|
28
|
+
latest_note_updated_at: latestNoteRow?.latest ?? null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function getMetaSchema() {
|
|
32
|
+
return {
|
|
33
|
+
version: 1,
|
|
34
|
+
entities: {
|
|
35
|
+
note_search_result: {
|
|
36
|
+
fields: ['id', 'title', 'summary', 'tags', 'created_at', 'zettel_boxes'],
|
|
37
|
+
meta_fields: [
|
|
38
|
+
'query',
|
|
39
|
+
'returned_count',
|
|
40
|
+
'total_count',
|
|
41
|
+
'stale',
|
|
42
|
+
'pagination.limit',
|
|
43
|
+
'pagination.offset',
|
|
44
|
+
'pagination.has_more',
|
|
45
|
+
'fields',
|
|
46
|
+
'applied_filters',
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
note_context: {
|
|
50
|
+
fields: [
|
|
51
|
+
'id',
|
|
52
|
+
'title',
|
|
53
|
+
'summary',
|
|
54
|
+
'tags',
|
|
55
|
+
'created_at',
|
|
56
|
+
'updated_at',
|
|
57
|
+
'is_del',
|
|
58
|
+
'zettel_boxes',
|
|
59
|
+
'links.backlinks',
|
|
60
|
+
'links.outlinks',
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
graph_stats: {
|
|
64
|
+
fields: ['notes_count', 'links_count', 'connected_notes_count', 'isolated_notes_count'],
|
|
65
|
+
},
|
|
66
|
+
meta_stats: {
|
|
67
|
+
fields: ['notes_count', 'tags_count', 'boxes_count', 'latest_note_updated_at'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|