wp-devdocs-mcp 1.1.1
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/LICENSE +21 -0
- package/README.md +407 -0
- package/bin/wp-hooks.js +533 -0
- package/package.json +62 -0
- package/src/constants.js +22 -0
- package/src/db/sqlite.js +1012 -0
- package/src/docs/doc-index-manager.js +128 -0
- package/src/docs/parsers/admin-handbook-parser.js +48 -0
- package/src/docs/parsers/base-doc-parser.js +175 -0
- package/src/docs/parsers/block-editor-parser.js +49 -0
- package/src/docs/parsers/general-doc-parser.js +53 -0
- package/src/docs/parsers/plugin-handbook-parser.js +60 -0
- package/src/docs/parsers/rest-api-parser.js +57 -0
- package/src/docs/parsers/wp-cli-parser.js +61 -0
- package/src/indexer/index-manager.js +187 -0
- package/src/indexer/js-parser.js +306 -0
- package/src/indexer/parser-utils.js +158 -0
- package/src/indexer/php-parser.js +205 -0
- package/src/indexer/sources/github-private.js +57 -0
- package/src/indexer/sources/github-public.js +38 -0
- package/src/indexer/sources/index.js +19 -0
- package/src/indexer/sources/local-folder.js +17 -0
- package/src/mcp-entry.js +109 -0
- package/src/presets.js +68 -0
- package/src/server/tools/get-doc.js +79 -0
- package/src/server/tools/get-hook-context.js +67 -0
- package/src/server/tools/list-docs.js +71 -0
- package/src/server/tools/search-block-apis.js +72 -0
- package/src/server/tools/search-docs.js +55 -0
- package/src/server/tools/search-hooks.js +64 -0
- package/src/server/tools/validate-hook.js +66 -0
package/bin/wp-hooks.js
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import {
|
|
5
|
+
addSource,
|
|
6
|
+
listSources,
|
|
7
|
+
getSource,
|
|
8
|
+
removeSource,
|
|
9
|
+
searchHooks,
|
|
10
|
+
searchBlockApis,
|
|
11
|
+
searchDocs,
|
|
12
|
+
validateHook,
|
|
13
|
+
getStats,
|
|
14
|
+
rebuildFtsIndex,
|
|
15
|
+
isSourceIndexed,
|
|
16
|
+
getStaleSources,
|
|
17
|
+
closeDb,
|
|
18
|
+
} from '../src/db/sqlite.js';
|
|
19
|
+
import { indexSources } from '../src/indexer/index-manager.js';
|
|
20
|
+
import { getPreset, listPresets } from '../src/presets.js';
|
|
21
|
+
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('wp-hooks')
|
|
26
|
+
.description('WordPress hook indexer, doc searcher, and MCP server CLI')
|
|
27
|
+
.version('2.0.0');
|
|
28
|
+
|
|
29
|
+
function printIndexStats(stats) {
|
|
30
|
+
console.log(` Files processed: ${stats.files_processed}`);
|
|
31
|
+
console.log(` Files skipped: ${stats.files_skipped}`);
|
|
32
|
+
console.log(` Hooks inserted: ${stats.hooks_inserted}`);
|
|
33
|
+
console.log(` Hooks updated: ${stats.hooks_updated}`);
|
|
34
|
+
console.log(` Hooks unchanged: ${stats.hooks_skipped}`);
|
|
35
|
+
console.log(` Hooks removed: ${stats.hooks_removed}`);
|
|
36
|
+
console.log(` Blocks indexed: ${stats.blocks_indexed}`);
|
|
37
|
+
console.log(` APIs indexed: ${stats.apis_indexed}`);
|
|
38
|
+
console.log(` Docs inserted: ${stats.docs_inserted}`);
|
|
39
|
+
console.log(` Docs updated: ${stats.docs_updated}`);
|
|
40
|
+
console.log(` Docs unchanged: ${stats.docs_skipped}`);
|
|
41
|
+
console.log(` Docs removed: ${stats.docs_removed}`);
|
|
42
|
+
|
|
43
|
+
if (stats.errors.length > 0) {
|
|
44
|
+
console.log(`\n Errors (${stats.errors.length}):`);
|
|
45
|
+
for (const err of stats.errors) {
|
|
46
|
+
console.log(` - ${err}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- source:add ---
|
|
52
|
+
program
|
|
53
|
+
.command('source:add')
|
|
54
|
+
.description('Add a new source to index')
|
|
55
|
+
.requiredOption('--name <name>', 'Unique source name')
|
|
56
|
+
.requiredOption('--type <type>', 'Source type: github-public, github-private, local-folder')
|
|
57
|
+
.option('--repo <url>', 'Repository URL (for github types)')
|
|
58
|
+
.option('--subfolder <path>', 'Subfolder within repo to index')
|
|
59
|
+
.option('--path <path>', 'Local folder path (for local-folder type)')
|
|
60
|
+
.option('--token-env <var>', 'Environment variable name containing GitHub token')
|
|
61
|
+
.option('--branch <branch>', 'Git branch (default: main)', 'main')
|
|
62
|
+
.option('--content-type <type>', 'Content type: source or docs (default: source)', 'source')
|
|
63
|
+
.option('--no-index', 'Skip automatic indexing after adding')
|
|
64
|
+
.action(async (opts) => {
|
|
65
|
+
try {
|
|
66
|
+
const existing = getSource(opts.name);
|
|
67
|
+
if (existing) {
|
|
68
|
+
console.error(`Source "${opts.name}" already exists. Remove it first.`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
addSource({
|
|
73
|
+
name: opts.name,
|
|
74
|
+
type: opts.type,
|
|
75
|
+
repo_url: opts.repo || null,
|
|
76
|
+
subfolder: opts.subfolder || null,
|
|
77
|
+
local_path: opts.path || null,
|
|
78
|
+
token_env_var: opts.tokenEnv || null,
|
|
79
|
+
branch: opts.branch,
|
|
80
|
+
content_type: opts.contentType,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
console.log(`Source "${opts.name}" added successfully (content_type: ${opts.contentType}).`);
|
|
84
|
+
|
|
85
|
+
if (opts.index) {
|
|
86
|
+
console.log(`\nIndexing "${opts.name}"...`);
|
|
87
|
+
const stats = await indexSources({ sourceName: opts.name });
|
|
88
|
+
console.log('\nIndexing complete:');
|
|
89
|
+
printIndexStats(stats);
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(`Error: ${err.message}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
} finally {
|
|
95
|
+
closeDb();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// --- source:list ---
|
|
100
|
+
|
|
101
|
+
function formatRelativeTime(isoString) {
|
|
102
|
+
if (!isoString) return 'never';
|
|
103
|
+
const diff = Date.now() - new Date(isoString + 'Z').getTime();
|
|
104
|
+
const seconds = Math.floor(diff / 1000);
|
|
105
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
106
|
+
const minutes = Math.floor(seconds / 60);
|
|
107
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
108
|
+
const hours = Math.floor(minutes / 60);
|
|
109
|
+
if (hours < 24) return `${hours}h ago`;
|
|
110
|
+
const days = Math.floor(hours / 24);
|
|
111
|
+
return `${days}d ago`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
program
|
|
115
|
+
.command('source:list')
|
|
116
|
+
.description('List all configured sources')
|
|
117
|
+
.action(() => {
|
|
118
|
+
try {
|
|
119
|
+
const sources = listSources();
|
|
120
|
+
if (sources.length === 0) {
|
|
121
|
+
console.log('No sources configured. Use "wp-hooks source:add" to add one.');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(`\n${'Name'.padEnd(25)} ${'Type'.padEnd(18)} ${'Content'.padEnd(10)} ${'Branch'.padEnd(10)} ${'Indexed'.padEnd(10)} ${'Last indexed'.padEnd(14)} Details`);
|
|
126
|
+
console.log('-'.repeat(124));
|
|
127
|
+
|
|
128
|
+
for (const s of sources) {
|
|
129
|
+
const details = s.repo_url || s.local_path || '';
|
|
130
|
+
const subfolder = s.subfolder ? ` [${s.subfolder}]` : '';
|
|
131
|
+
const indexed = isSourceIndexed(s.id) ? 'yes' : 'no';
|
|
132
|
+
const contentType = s.content_type || 'source';
|
|
133
|
+
const lastIndexed = formatRelativeTime(s.last_indexed_at);
|
|
134
|
+
console.log(
|
|
135
|
+
`${s.name.padEnd(25)} ${s.type.padEnd(18)} ${contentType.padEnd(10)} ${(s.branch || 'main').padEnd(10)} ${indexed.padEnd(10)} ${lastIndexed.padEnd(14)} ${details}${subfolder}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
console.log('');
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error(`Error: ${err.message}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
} finally {
|
|
143
|
+
closeDb();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// --- source:remove ---
|
|
148
|
+
program
|
|
149
|
+
.command('source:remove <name>')
|
|
150
|
+
.description('Remove a source and all its indexed data')
|
|
151
|
+
.action((name) => {
|
|
152
|
+
try {
|
|
153
|
+
const removed = removeSource(name);
|
|
154
|
+
if (!removed) {
|
|
155
|
+
console.error(`Source "${name}" not found.`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
console.log(`Source "${name}" removed along with all indexed data.`);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error(`Error: ${err.message}`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
} finally {
|
|
163
|
+
closeDb();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// --- quick-add ---
|
|
168
|
+
program
|
|
169
|
+
.command('quick-add <preset>')
|
|
170
|
+
.description('Add and index a pre-configured source. Available presets: ' + listPresets().map(p => p.name).join(', '))
|
|
171
|
+
.option('--no-index', 'Skip automatic indexing after adding')
|
|
172
|
+
.action(async (presetName, opts) => {
|
|
173
|
+
try {
|
|
174
|
+
const preset = getPreset(presetName);
|
|
175
|
+
if (!preset) {
|
|
176
|
+
console.error(`Unknown preset: "${presetName}". Available: ${listPresets().map(p => p.name).join(', ')}`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const existing = getSource(preset.name);
|
|
181
|
+
if (existing) {
|
|
182
|
+
console.log(`Source "${preset.name}" already exists. Skipping add, running index...`);
|
|
183
|
+
} else {
|
|
184
|
+
addSource(preset);
|
|
185
|
+
console.log(`Source "${preset.name}" added (${preset.content_type}).`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (opts.index) {
|
|
189
|
+
console.log(`\nIndexing "${preset.name}"...`);
|
|
190
|
+
const stats = await indexSources({ sourceName: preset.name });
|
|
191
|
+
console.log('\nIndexing complete:');
|
|
192
|
+
printIndexStats(stats);
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error(`Error: ${err.message}`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
} finally {
|
|
198
|
+
closeDb();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// --- quick-add-all ---
|
|
203
|
+
program
|
|
204
|
+
.command('quick-add-all')
|
|
205
|
+
.description('Add and index all pre-configured sources')
|
|
206
|
+
.option('--no-index', 'Skip automatic indexing after adding')
|
|
207
|
+
.action(async (opts) => {
|
|
208
|
+
try {
|
|
209
|
+
const presets = listPresets();
|
|
210
|
+
for (const preset of presets) {
|
|
211
|
+
const existing = getSource(preset.name);
|
|
212
|
+
if (existing) {
|
|
213
|
+
console.log(`Source "${preset.name}" already exists — skipping.`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
addSource(preset);
|
|
217
|
+
console.log(`Added: ${preset.name} (${preset.content_type})`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (opts.index) {
|
|
221
|
+
console.log('\nIndexing all sources...');
|
|
222
|
+
const stats = await indexSources();
|
|
223
|
+
console.log('\nIndexing complete:');
|
|
224
|
+
console.log(` Sources processed: ${stats.sources_processed}`);
|
|
225
|
+
printIndexStats(stats);
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error(`Error: ${err.message}`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
} finally {
|
|
231
|
+
closeDb();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// --- index ---
|
|
236
|
+
program
|
|
237
|
+
.command('index')
|
|
238
|
+
.description('Index all enabled sources (or a specific one)')
|
|
239
|
+
.option('--source <name>', 'Index a specific source only')
|
|
240
|
+
.option('--force', 'Ignore mtime cache and re-index everything', false)
|
|
241
|
+
.action(async (opts) => {
|
|
242
|
+
try {
|
|
243
|
+
const stats = await indexSources({
|
|
244
|
+
sourceName: opts.source,
|
|
245
|
+
force: opts.force,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
console.log('\nIndexing complete:');
|
|
249
|
+
console.log(` Sources processed: ${stats.sources_processed}`);
|
|
250
|
+
printIndexStats(stats);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.error(`Error: ${err.message}`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
} finally {
|
|
255
|
+
closeDb();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// --- update ---
|
|
260
|
+
program
|
|
261
|
+
.command('update')
|
|
262
|
+
.description('Fetch and re-index stale sources (or all with --force)')
|
|
263
|
+
.option('--source <name>', 'Update a specific source only')
|
|
264
|
+
.option('--force', 'Re-index regardless of staleness', false)
|
|
265
|
+
.action(async (opts) => {
|
|
266
|
+
try {
|
|
267
|
+
let sources;
|
|
268
|
+
|
|
269
|
+
if (opts.source) {
|
|
270
|
+
const source = getSource(opts.source);
|
|
271
|
+
if (!source) {
|
|
272
|
+
console.error(`Source not found: ${opts.source}`);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
if (!source.enabled) {
|
|
276
|
+
console.error(`Source "${opts.source}" is disabled.`);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
sources = [source];
|
|
280
|
+
} else if (opts.force) {
|
|
281
|
+
sources = listSources().filter(s => s.enabled);
|
|
282
|
+
} else {
|
|
283
|
+
sources = getStaleSources(24 * 60 * 60 * 1000);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (sources.length === 0) {
|
|
287
|
+
console.log('All sources are up to date. Use --force to re-index anyway.');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
console.log(`Updating ${sources.length} source(s)...\n`);
|
|
292
|
+
|
|
293
|
+
for (const source of sources) {
|
|
294
|
+
console.log(`Updating: ${source.name}`);
|
|
295
|
+
const stats = await indexSources({
|
|
296
|
+
sourceName: source.name,
|
|
297
|
+
force: opts.force,
|
|
298
|
+
});
|
|
299
|
+
printIndexStats(stats);
|
|
300
|
+
console.log('');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.log('Update complete.');
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.error(`Error: ${err.message}`);
|
|
306
|
+
process.exit(1);
|
|
307
|
+
} finally {
|
|
308
|
+
closeDb();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// --- search ---
|
|
313
|
+
program
|
|
314
|
+
.command('search <query>')
|
|
315
|
+
.description('Search indexed hooks')
|
|
316
|
+
.option('--type <type>', 'Filter by hook type')
|
|
317
|
+
.option('--source <name>', 'Filter by source name')
|
|
318
|
+
.option('--limit <n>', 'Max results', '20')
|
|
319
|
+
.option('--include-removed', 'Include removed hooks', false)
|
|
320
|
+
.action((query, opts) => {
|
|
321
|
+
try {
|
|
322
|
+
const results = searchHooks(query, {
|
|
323
|
+
type: opts.type,
|
|
324
|
+
source: opts.source,
|
|
325
|
+
includeRemoved: opts.includeRemoved,
|
|
326
|
+
limit: parseInt(opts.limit, 10),
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (results.length === 0) {
|
|
330
|
+
console.log(`No hooks found matching "${query}".`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
console.log(`\nFound ${results.length} hook(s) matching "${query}":\n`);
|
|
335
|
+
|
|
336
|
+
for (const h of results) {
|
|
337
|
+
console.log(` ${h.name}`);
|
|
338
|
+
console.log(` Type: ${h.type} | Source: ${h.source_name}`);
|
|
339
|
+
console.log(` File: ${h.file_path}:${h.line_number}`);
|
|
340
|
+
if (h.is_dynamic) console.log(' Dynamic: yes');
|
|
341
|
+
if (h.status === 'removed') console.log(' Status: REMOVED');
|
|
342
|
+
if (h.class_name) console.log(` Class: ${h.class_name}`);
|
|
343
|
+
if (h.php_function) console.log(` Function: ${h.php_function}()`);
|
|
344
|
+
if (h.params) console.log(` Params: ${h.params}`);
|
|
345
|
+
if (h.inferred_description) console.log(` Description: ${h.inferred_description}`);
|
|
346
|
+
if (h.docblock) console.log(` Docblock: ${h.docblock.replace(/\n/g, '\n ')}`);
|
|
347
|
+
console.log(` ID: ${h.id}`);
|
|
348
|
+
console.log('');
|
|
349
|
+
}
|
|
350
|
+
} catch (err) {
|
|
351
|
+
console.error(`Error: ${err.message}`);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
} finally {
|
|
354
|
+
closeDb();
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// --- search-blocks ---
|
|
359
|
+
program
|
|
360
|
+
.command('search-blocks <query>')
|
|
361
|
+
.description('Search block registrations and WP JS API usages')
|
|
362
|
+
.option('--limit <n>', 'Max results per category', '20')
|
|
363
|
+
.action((query, opts) => {
|
|
364
|
+
try {
|
|
365
|
+
const { blocks, apis } = searchBlockApis(query, {
|
|
366
|
+
limit: parseInt(opts.limit, 10),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
if (blocks.length === 0 && apis.length === 0) {
|
|
370
|
+
console.log(`No block registrations or API usages found matching "${query}".`);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (blocks.length > 0) {
|
|
375
|
+
console.log(`\nBlock Registrations (${blocks.length}):\n`);
|
|
376
|
+
for (const b of blocks) {
|
|
377
|
+
console.log(` ${b.block_name || 'unknown'}`);
|
|
378
|
+
console.log(` Source: ${b.source_name} | File: ${b.file_path}:${b.line_number}`);
|
|
379
|
+
if (b.block_title) console.log(` Title: ${b.block_title}`);
|
|
380
|
+
if (b.block_category) console.log(` Category: ${b.block_category}`);
|
|
381
|
+
if (b.code_context) console.log(` Context: ${b.code_context.split('\n').slice(0, 5).join('\n ')}`);
|
|
382
|
+
console.log('');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (apis.length > 0) {
|
|
387
|
+
console.log(`API Usages (${apis.length}):\n`);
|
|
388
|
+
for (const a of apis) {
|
|
389
|
+
console.log(` ${a.api_call}`);
|
|
390
|
+
console.log(` Source: ${a.source_name} | File: ${a.file_path}:${a.line_number}`);
|
|
391
|
+
console.log(` Namespace: ${a.namespace} | Method: ${a.method}`);
|
|
392
|
+
if (a.code_context) console.log(` Context: ${a.code_context.split('\n').slice(0, 3).join('\n ')}`);
|
|
393
|
+
console.log('');
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} catch (err) {
|
|
397
|
+
console.error(`Error: ${err.message}`);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
} finally {
|
|
400
|
+
closeDb();
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// --- search-docs ---
|
|
405
|
+
program
|
|
406
|
+
.command('search-docs <query>')
|
|
407
|
+
.description('Search indexed WordPress documentation')
|
|
408
|
+
.option('--type <type>', 'Filter by doc type (guide, tutorial, reference, api, howto, faq, general)')
|
|
409
|
+
.option('--category <cat>', 'Filter by category (block-editor, plugins, rest-api, wp-cli, admin)')
|
|
410
|
+
.option('--source <name>', 'Filter by source name')
|
|
411
|
+
.option('--limit <n>', 'Max results', '20')
|
|
412
|
+
.action((query, opts) => {
|
|
413
|
+
try {
|
|
414
|
+
const results = searchDocs(query, {
|
|
415
|
+
doc_type: opts.type,
|
|
416
|
+
category: opts.category,
|
|
417
|
+
source: opts.source,
|
|
418
|
+
limit: parseInt(opts.limit, 10),
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (results.length === 0) {
|
|
422
|
+
console.log(`No documentation found matching "${query}".`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
console.log(`\nFound ${results.length} doc(s) matching "${query}":\n`);
|
|
427
|
+
|
|
428
|
+
for (const d of results) {
|
|
429
|
+
console.log(` ${d.title}`);
|
|
430
|
+
console.log(` Type: ${d.doc_type} | Category: ${d.category || 'general'} | Source: ${d.source_name}`);
|
|
431
|
+
console.log(` Slug: ${d.slug}`);
|
|
432
|
+
if (d.subcategory) console.log(` Subcategory: ${d.subcategory}`);
|
|
433
|
+
if (d.description) console.log(` Description: ${d.description.slice(0, 150)}${d.description.length > 150 ? '...' : ''}`);
|
|
434
|
+
console.log(` ID: ${d.id}`);
|
|
435
|
+
console.log('');
|
|
436
|
+
}
|
|
437
|
+
} catch (err) {
|
|
438
|
+
console.error(`Error: ${err.message}`);
|
|
439
|
+
process.exit(1);
|
|
440
|
+
} finally {
|
|
441
|
+
closeDb();
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// --- validate ---
|
|
446
|
+
program
|
|
447
|
+
.command('validate <hook-name>')
|
|
448
|
+
.description('Validate if a hook name exists (exit code 0=valid, 1=not found)')
|
|
449
|
+
.action((hookName) => {
|
|
450
|
+
try {
|
|
451
|
+
const result = validateHook(hookName);
|
|
452
|
+
|
|
453
|
+
if (result.status === 'VALID') {
|
|
454
|
+
console.log(`VALID — "${hookName}" found in ${result.hooks.length} location(s):`);
|
|
455
|
+
for (const h of result.hooks) {
|
|
456
|
+
console.log(` ${h.source_name}: ${h.file_path}:${h.line_number} (${h.type})`);
|
|
457
|
+
}
|
|
458
|
+
process.exit(0);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (result.status === 'REMOVED') {
|
|
462
|
+
console.log(`REMOVED — "${hookName}" was found but has been removed.`);
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
console.log(`NOT FOUND — "${hookName}" does not exist in any indexed source.`);
|
|
467
|
+
if (result.similar.length > 0) {
|
|
468
|
+
console.log('\nDid you mean:');
|
|
469
|
+
for (const s of result.similar) {
|
|
470
|
+
console.log(` ${s.name} (${s.type}) [${s.source_name}]`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
process.exit(1);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
console.error(`Error: ${err.message}`);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
} finally {
|
|
478
|
+
closeDb();
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// --- stats ---
|
|
483
|
+
program
|
|
484
|
+
.command('stats')
|
|
485
|
+
.description('Show indexing statistics')
|
|
486
|
+
.action(() => {
|
|
487
|
+
try {
|
|
488
|
+
const stats = getStats();
|
|
489
|
+
|
|
490
|
+
console.log('\nOverall Statistics:');
|
|
491
|
+
console.log(` Sources: ${stats.totals.sources}`);
|
|
492
|
+
console.log(` Active hooks: ${stats.totals.active_hooks}`);
|
|
493
|
+
console.log(` Removed hooks: ${stats.totals.removed_hooks}`);
|
|
494
|
+
console.log(` Block registrations: ${stats.totals.block_registrations}`);
|
|
495
|
+
console.log(` API usages: ${stats.totals.api_usages}`);
|
|
496
|
+
console.log(` Documentation pages: ${stats.totals.docs}`);
|
|
497
|
+
|
|
498
|
+
if (stats.per_source.length > 0) {
|
|
499
|
+
console.log('\nPer Source:');
|
|
500
|
+
console.log(` ${'Name'.padEnd(25)} ${'Type'.padEnd(8)} ${'Hooks'.padEnd(8)} ${'Removed'.padEnd(10)} ${'Blocks'.padEnd(8)} ${'APIs'.padEnd(8)} ${'Docs'.padEnd(8)} Files`);
|
|
501
|
+
console.log(' ' + '-'.repeat(90));
|
|
502
|
+
for (const s of stats.per_source) {
|
|
503
|
+
console.log(
|
|
504
|
+
` ${s.name.padEnd(25)} ${(s.content_type || 'source').padEnd(8)} ${String(s.hooks).padEnd(8)} ${String(s.removed_hooks).padEnd(10)} ${String(s.blocks).padEnd(8)} ${String(s.apis).padEnd(8)} ${String(s.docs).padEnd(8)} ${s.files}`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
console.log('');
|
|
509
|
+
} catch (err) {
|
|
510
|
+
console.error(`Error: ${err.message}`);
|
|
511
|
+
process.exit(1);
|
|
512
|
+
} finally {
|
|
513
|
+
closeDb();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// --- rebuild-index ---
|
|
518
|
+
program
|
|
519
|
+
.command('rebuild-index')
|
|
520
|
+
.description('Rebuild FTS indexes (recovery for out-of-sync full-text search)')
|
|
521
|
+
.action(() => {
|
|
522
|
+
try {
|
|
523
|
+
rebuildFtsIndex();
|
|
524
|
+
console.log('FTS indexes rebuilt successfully (hooks, blocks, APIs, docs).');
|
|
525
|
+
} catch (err) {
|
|
526
|
+
console.error(`Error: ${err.message}`);
|
|
527
|
+
process.exit(1);
|
|
528
|
+
} finally {
|
|
529
|
+
closeDb();
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wp-devdocs-mcp",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "MCP server that indexes WordPress hooks, blocks, APIs, and official documentation — exposing them as queryable tools to AI assistants",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.0.0"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"wp-hooks": "./bin/wp-hooks.js",
|
|
11
|
+
"wp-devdocs-mcp": "./src/mcp-entry.js",
|
|
12
|
+
"wp-mcp": "./src/mcp-entry.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src/",
|
|
16
|
+
"bin/",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"start": "node src/mcp-entry.js",
|
|
22
|
+
"test": "node test/docs-test.js && node test/search-test.js",
|
|
23
|
+
"lint": "eslint src/ bin/"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/pluginslab/wp-devdocs-mcp.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/pluginslab/wp-devdocs-mcp#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/pluginslab/wp-devdocs-mcp/issues"
|
|
32
|
+
},
|
|
33
|
+
"author": "Marcel Schmitz",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"mcp",
|
|
36
|
+
"wordpress",
|
|
37
|
+
"hooks",
|
|
38
|
+
"devdocs",
|
|
39
|
+
"ai",
|
|
40
|
+
"claude",
|
|
41
|
+
"model-context-protocol",
|
|
42
|
+
"gutenberg",
|
|
43
|
+
"blocks",
|
|
44
|
+
"wp-cli"
|
|
45
|
+
],
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
48
|
+
"better-sqlite3": "^11.8.2",
|
|
49
|
+
"commander": "^13.1.0",
|
|
50
|
+
"dotenv": "^17.3.1",
|
|
51
|
+
"fast-glob": "^3.3.3",
|
|
52
|
+
"simple-git": "^3.27.0",
|
|
53
|
+
"zod": "^3.24.2"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@anthropic-ai/sdk": "^0.78.0",
|
|
57
|
+
"@eslint/js": "^9.39.3",
|
|
58
|
+
"@google/generative-ai": "^0.24.1",
|
|
59
|
+
"eslint": "^9.39.3"
|
|
60
|
+
},
|
|
61
|
+
"license": "MIT"
|
|
62
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
|
|
4
|
+
export const BASE_DIR = join(homedir(), '.wp-devdocs-mcp');
|
|
5
|
+
export const DB_PATH = join(BASE_DIR, 'hooks.db');
|
|
6
|
+
export const CACHE_DIR = join(BASE_DIR, 'cache');
|
|
7
|
+
export const SOURCES_DIR = join(BASE_DIR, 'sources');
|
|
8
|
+
|
|
9
|
+
export const DOC_TYPES = {
|
|
10
|
+
GUIDE: 'guide',
|
|
11
|
+
TUTORIAL: 'tutorial',
|
|
12
|
+
REFERENCE: 'reference',
|
|
13
|
+
API: 'api',
|
|
14
|
+
HOWTO: 'howto',
|
|
15
|
+
FAQ: 'faq',
|
|
16
|
+
GENERAL: 'general',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const CONTENT_TYPES = {
|
|
20
|
+
SOURCE: 'source',
|
|
21
|
+
DOCS: 'docs',
|
|
22
|
+
};
|