@nicnocquee/dataqueue 1.33.0 → 1.34.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/ai/build-docs-content.ts +96 -0
- package/ai/build-llms-full.ts +42 -0
- package/ai/docs-content.json +278 -0
- package/ai/rules/advanced.md +94 -0
- package/ai/rules/basic.md +90 -0
- package/ai/rules/react-dashboard.md +83 -0
- package/ai/skills/dataqueue-advanced/SKILL.md +211 -0
- package/ai/skills/dataqueue-core/SKILL.md +131 -0
- package/ai/skills/dataqueue-react/SKILL.md +189 -0
- package/dist/cli.cjs +577 -32
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +52 -2
- package/dist/cli.d.ts +52 -2
- package/dist/cli.js +575 -32
- package/dist/cli.js.map +1 -1
- package/dist/mcp-server.cjs +186 -0
- package/dist/mcp-server.cjs.map +1 -0
- package/dist/mcp-server.d.cts +32 -0
- package/dist/mcp-server.d.ts +32 -0
- package/dist/mcp-server.js +175 -0
- package/dist/mcp-server.js.map +1 -0
- package/package.json +10 -4
- package/src/cli.test.ts +65 -0
- package/src/cli.ts +56 -19
- package/src/install-mcp-command.test.ts +216 -0
- package/src/install-mcp-command.ts +185 -0
- package/src/install-rules-command.test.ts +218 -0
- package/src/install-rules-command.ts +233 -0
- package/src/install-skills-command.test.ts +176 -0
- package/src/install-skills-command.ts +124 -0
- package/src/mcp-server.test.ts +162 -0
- package/src/mcp-server.ts +231 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
var __dirname = path.dirname(__filename);
|
|
11
|
+
function loadDocsContent(docsPath = path.join(__dirname, "../ai/docs-content.json")) {
|
|
12
|
+
const raw = fs.readFileSync(docsPath, "utf-8");
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
}
|
|
15
|
+
function scorePageForQuery(page, queryTerms) {
|
|
16
|
+
const titleLower = page.title.toLowerCase();
|
|
17
|
+
const descLower = page.description.toLowerCase();
|
|
18
|
+
const contentLower = page.content.toLowerCase();
|
|
19
|
+
let score = 0;
|
|
20
|
+
for (const term of queryTerms) {
|
|
21
|
+
if (titleLower.includes(term)) score += 10;
|
|
22
|
+
if (descLower.includes(term)) score += 5;
|
|
23
|
+
const contentMatches = contentLower.split(term).length - 1;
|
|
24
|
+
score += Math.min(contentMatches, 10);
|
|
25
|
+
}
|
|
26
|
+
return score;
|
|
27
|
+
}
|
|
28
|
+
function extractExcerpt(content, queryTerms, maxLength = 500) {
|
|
29
|
+
const lower = content.toLowerCase();
|
|
30
|
+
let earliestIndex = -1;
|
|
31
|
+
for (const term of queryTerms) {
|
|
32
|
+
const idx = lower.indexOf(term);
|
|
33
|
+
if (idx !== -1 && (earliestIndex === -1 || idx < earliestIndex)) {
|
|
34
|
+
earliestIndex = idx;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (earliestIndex === -1) {
|
|
38
|
+
return content.slice(0, maxLength);
|
|
39
|
+
}
|
|
40
|
+
const start = Math.max(0, earliestIndex - 100);
|
|
41
|
+
const end = Math.min(content.length, start + maxLength);
|
|
42
|
+
let excerpt = content.slice(start, end);
|
|
43
|
+
if (start > 0) excerpt = "..." + excerpt;
|
|
44
|
+
if (end < content.length) excerpt = excerpt + "...";
|
|
45
|
+
return excerpt;
|
|
46
|
+
}
|
|
47
|
+
async function startMcpServer(deps = {}) {
|
|
48
|
+
const pages = loadDocsContent(deps.docsPath);
|
|
49
|
+
const server = new McpServer({
|
|
50
|
+
name: "dataqueue-docs",
|
|
51
|
+
version: "1.0.0"
|
|
52
|
+
});
|
|
53
|
+
server.resource("llms-txt", "dataqueue://llms.txt", async () => {
|
|
54
|
+
const llmsPath = path.join(
|
|
55
|
+
__dirname,
|
|
56
|
+
"../ai/skills/dataqueue-core/SKILL.md"
|
|
57
|
+
);
|
|
58
|
+
let content;
|
|
59
|
+
try {
|
|
60
|
+
content = fs.readFileSync(llmsPath, "utf-8");
|
|
61
|
+
} catch {
|
|
62
|
+
content = pages.map((p) => `## ${p.title}
|
|
63
|
+
|
|
64
|
+
Slug: ${p.slug}
|
|
65
|
+
|
|
66
|
+
${p.description}`).join("\n\n");
|
|
67
|
+
}
|
|
68
|
+
return { contents: [{ uri: "dataqueue://llms.txt", text: content }] };
|
|
69
|
+
});
|
|
70
|
+
server.tool(
|
|
71
|
+
"list-doc-pages",
|
|
72
|
+
"List all available DataQueue documentation pages with titles and descriptions.",
|
|
73
|
+
{},
|
|
74
|
+
async () => {
|
|
75
|
+
const listing = pages.map((p) => ({
|
|
76
|
+
slug: p.slug,
|
|
77
|
+
title: p.title,
|
|
78
|
+
description: p.description
|
|
79
|
+
}));
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{ type: "text", text: JSON.stringify(listing, null, 2) }
|
|
83
|
+
]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
server.tool(
|
|
88
|
+
"get-doc-page",
|
|
89
|
+
"Fetch a specific DataQueue doc page by slug. Returns full page content as markdown.",
|
|
90
|
+
{
|
|
91
|
+
slug: z.string().describe('The doc page slug, e.g. "usage/add-job" or "api/job-queue"')
|
|
92
|
+
},
|
|
93
|
+
async ({ slug }) => {
|
|
94
|
+
const page = pages.find((p) => p.slug === slug);
|
|
95
|
+
if (!page) {
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: `Page not found: "${slug}". Use list-doc-pages to see available slugs.`
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
isError: true
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const header = page.description ? `# ${page.title}
|
|
107
|
+
|
|
108
|
+
> ${page.description}
|
|
109
|
+
|
|
110
|
+
` : `# ${page.title}
|
|
111
|
+
|
|
112
|
+
`;
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: "text", text: header + page.content }]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
server.tool(
|
|
119
|
+
"search-docs",
|
|
120
|
+
"Full-text search across all DataQueue documentation pages. Returns matching sections with page titles and content excerpts.",
|
|
121
|
+
{
|
|
122
|
+
query: z.string().describe('Search query, e.g. "cron scheduling" or "waitForToken"')
|
|
123
|
+
},
|
|
124
|
+
async ({ query }) => {
|
|
125
|
+
const queryTerms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 1);
|
|
126
|
+
if (queryTerms.length === 0) {
|
|
127
|
+
return {
|
|
128
|
+
content: [
|
|
129
|
+
{ type: "text", text: "Please provide a search query." }
|
|
130
|
+
],
|
|
131
|
+
isError: true
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const scored = pages.map((page) => ({
|
|
135
|
+
page,
|
|
136
|
+
score: scorePageForQuery(page, queryTerms)
|
|
137
|
+
})).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, 5);
|
|
138
|
+
if (scored.length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: "text",
|
|
143
|
+
text: `No results for "${query}". Try different keywords or use list-doc-pages to browse.`
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const results = scored.map((r) => {
|
|
149
|
+
const excerpt = extractExcerpt(r.page.content, queryTerms);
|
|
150
|
+
return `## ${r.page.title} (${r.page.slug})
|
|
151
|
+
|
|
152
|
+
${r.page.description}
|
|
153
|
+
|
|
154
|
+
${excerpt}`;
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
content: [{ type: "text", text: results.join("\n\n---\n\n") }]
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
const transport = deps.transport ?? new StdioServerTransport();
|
|
162
|
+
await server.connect(transport);
|
|
163
|
+
return server;
|
|
164
|
+
}
|
|
165
|
+
var isDirectRun = process.argv[1] && (process.argv[1].endsWith("/mcp-server.js") || process.argv[1].endsWith("/mcp-server.cjs"));
|
|
166
|
+
if (isDirectRun) {
|
|
167
|
+
startMcpServer().catch((err) => {
|
|
168
|
+
console.error("Failed to start MCP server:", err);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export { extractExcerpt, loadDocsContent, scorePageForQuery, startMcpServer };
|
|
174
|
+
//# sourceMappingURL=mcp-server.js.map
|
|
175
|
+
//# sourceMappingURL=mcp-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mcp-server.ts"],"names":[],"mappings":"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","file":"mcp-server.js","sourcesContent":["#!/usr/bin/env node\n\n/**\n * DataQueue MCP Server — exposes documentation search over stdio.\n * Run via: dataqueue-cli mcp\n */\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod';\nimport fs from 'fs';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\ninterface DocPage {\n slug: string;\n title: string;\n description: string;\n content: string;\n}\n\n/** @internal Loads docs-content.json from the ai/ directory bundled with the package. */\nexport function loadDocsContent(\n docsPath: string = path.join(__dirname, '../ai/docs-content.json'),\n): DocPage[] {\n const raw = fs.readFileSync(docsPath, 'utf-8');\n return JSON.parse(raw) as DocPage[];\n}\n\n/** @internal Scores a doc page against a search query using simple term matching. */\nexport function scorePageForQuery(page: DocPage, queryTerms: string[]): number {\n const titleLower = page.title.toLowerCase();\n const descLower = page.description.toLowerCase();\n const contentLower = page.content.toLowerCase();\n\n let score = 0;\n for (const term of queryTerms) {\n if (titleLower.includes(term)) score += 10;\n if (descLower.includes(term)) score += 5;\n\n const contentMatches = contentLower.split(term).length - 1;\n score += Math.min(contentMatches, 10);\n }\n return score;\n}\n\n/** @internal Extracts a relevant excerpt around the first match of any query term. */\nexport function extractExcerpt(\n content: string,\n queryTerms: string[],\n maxLength = 500,\n): string {\n const lower = content.toLowerCase();\n let earliestIndex = -1;\n\n for (const term of queryTerms) {\n const idx = lower.indexOf(term);\n if (idx !== -1 && (earliestIndex === -1 || idx < earliestIndex)) {\n earliestIndex = idx;\n }\n }\n\n if (earliestIndex === -1) {\n return content.slice(0, maxLength);\n }\n\n const start = Math.max(0, earliestIndex - 100);\n const end = Math.min(content.length, start + maxLength);\n let excerpt = content.slice(start, end);\n\n if (start > 0) excerpt = '...' + excerpt;\n if (end < content.length) excerpt = excerpt + '...';\n\n return excerpt;\n}\n\n/**\n * Creates and starts the DataQueue MCP server over stdio.\n *\n * @param deps - Injectable dependencies for testing.\n */\nexport async function startMcpServer(\n deps: {\n docsPath?: string;\n transport?: InstanceType<typeof StdioServerTransport>;\n } = {},\n): Promise<McpServer> {\n const pages = loadDocsContent(deps.docsPath);\n\n const server = new McpServer({\n name: 'dataqueue-docs',\n version: '1.0.0',\n });\n\n server.resource('llms-txt', 'dataqueue://llms.txt', async () => {\n const llmsPath = path.join(\n __dirname,\n '../ai/skills/dataqueue-core/SKILL.md',\n );\n let content: string;\n try {\n content = fs.readFileSync(llmsPath, 'utf-8');\n } catch {\n content = pages\n .map((p) => `## ${p.title}\\n\\nSlug: ${p.slug}\\n\\n${p.description}`)\n .join('\\n\\n');\n }\n return { contents: [{ uri: 'dataqueue://llms.txt', text: content }] };\n });\n\n server.tool(\n 'list-doc-pages',\n 'List all available DataQueue documentation pages with titles and descriptions.',\n {},\n async () => {\n const listing = pages.map((p) => ({\n slug: p.slug,\n title: p.title,\n description: p.description,\n }));\n return {\n content: [\n { type: 'text' as const, text: JSON.stringify(listing, null, 2) },\n ],\n };\n },\n );\n\n server.tool(\n 'get-doc-page',\n 'Fetch a specific DataQueue doc page by slug. Returns full page content as markdown.',\n {\n slug: z\n .string()\n .describe('The doc page slug, e.g. \"usage/add-job\" or \"api/job-queue\"'),\n },\n async ({ slug }) => {\n const page = pages.find((p) => p.slug === slug);\n if (!page) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Page not found: \"${slug}\". Use list-doc-pages to see available slugs.`,\n },\n ],\n isError: true,\n };\n }\n const header = page.description\n ? `# ${page.title}\\n\\n> ${page.description}\\n\\n`\n : `# ${page.title}\\n\\n`;\n return {\n content: [{ type: 'text' as const, text: header + page.content }],\n };\n },\n );\n\n server.tool(\n 'search-docs',\n 'Full-text search across all DataQueue documentation pages. Returns matching sections with page titles and content excerpts.',\n {\n query: z\n .string()\n .describe('Search query, e.g. \"cron scheduling\" or \"waitForToken\"'),\n },\n async ({ query }) => {\n const queryTerms = query\n .toLowerCase()\n .split(/\\s+/)\n .filter((t) => t.length > 1);\n\n if (queryTerms.length === 0) {\n return {\n content: [\n { type: 'text' as const, text: 'Please provide a search query.' },\n ],\n isError: true,\n };\n }\n\n const scored = pages\n .map((page) => ({\n page,\n score: scorePageForQuery(page, queryTerms),\n }))\n .filter((r) => r.score > 0)\n .sort((a, b) => b.score - a.score)\n .slice(0, 5);\n\n if (scored.length === 0) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `No results for \"${query}\". Try different keywords or use list-doc-pages to browse.`,\n },\n ],\n };\n }\n\n const results = scored.map((r) => {\n const excerpt = extractExcerpt(r.page.content, queryTerms);\n return `## ${r.page.title} (${r.page.slug})\\n\\n${r.page.description}\\n\\n${excerpt}`;\n });\n\n return {\n content: [{ type: 'text' as const, text: results.join('\\n\\n---\\n\\n') }],\n };\n },\n );\n\n const transport = deps.transport ?? new StdioServerTransport();\n await server.connect(transport);\n return server;\n}\n\nconst isDirectRun =\n process.argv[1] &&\n (process.argv[1].endsWith('/mcp-server.js') ||\n process.argv[1].endsWith('/mcp-server.cjs'));\n\nif (isDirectRun) {\n startMcpServer().catch((err) => {\n console.error('Failed to start MCP server:', err);\n process.exit(1);\n });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nicnocquee/dataqueue",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.0",
|
|
4
4
|
"description": "PostgreSQL or Redis-backed job queue for Node.js applications with support for serverless environments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"dist/",
|
|
16
16
|
"src/",
|
|
17
|
-
"migrations/"
|
|
17
|
+
"migrations/",
|
|
18
|
+
"ai/"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
21
|
+
"prebuild": "npx tsx ai/build-docs-content.ts && npx tsx ai/build-llms-full.ts",
|
|
20
22
|
"build": "tsup",
|
|
21
23
|
"ci": "npm run build && npm run check-format && npm run check-exports && npm run lint && npm run test",
|
|
22
24
|
"lint": "tsc",
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
"format": "prettier --write .",
|
|
25
27
|
"check-format": "prettier --check .",
|
|
26
28
|
"check-exports": "attw --pack .",
|
|
29
|
+
"build:docs-content": "npx tsx ai/build-docs-content.ts && npx tsx ai/build-llms-full.ts",
|
|
27
30
|
"dev": "tsup --watch",
|
|
28
31
|
"migrate": "node-pg-migrate -d $PG_DATAQUEUE_DATABASE -m ./migrations"
|
|
29
32
|
},
|
|
@@ -43,9 +46,11 @@
|
|
|
43
46
|
"directory": "packages/dataqueue"
|
|
44
47
|
},
|
|
45
48
|
"dependencies": {
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
46
50
|
"croner": "^10.0.1",
|
|
47
51
|
"pg": "^8.0.0",
|
|
48
|
-
"pg-connection-string": "^2.9.1"
|
|
52
|
+
"pg-connection-string": "^2.9.1",
|
|
53
|
+
"zod": "^3.25.67"
|
|
49
54
|
},
|
|
50
55
|
"devDependencies": {
|
|
51
56
|
"@arethetypeswrong/cli": "^0.18.2",
|
|
@@ -76,6 +81,7 @@
|
|
|
76
81
|
}
|
|
77
82
|
},
|
|
78
83
|
"bin": {
|
|
79
|
-
"dataqueue-cli": "./cli.cjs"
|
|
84
|
+
"dataqueue-cli": "./cli.cjs",
|
|
85
|
+
"dataqueue-mcp": "./dist/mcp-server.js"
|
|
80
86
|
}
|
|
81
87
|
}
|
package/src/cli.test.ts
CHANGED
|
@@ -23,6 +23,10 @@ function makeDeps() {
|
|
|
23
23
|
spawnSyncImpl: vi.fn(() => makeSpawnSyncReturns(0)),
|
|
24
24
|
migrationsDir: '/migrations',
|
|
25
25
|
runInitImpl: vi.fn(),
|
|
26
|
+
runInstallSkillsImpl: vi.fn(),
|
|
27
|
+
runInstallRulesImpl: vi.fn(async () => {}),
|
|
28
|
+
runInstallMcpImpl: vi.fn(async () => {}),
|
|
29
|
+
startMcpServerImpl: vi.fn(async () => ({}) as any),
|
|
26
30
|
} satisfies CliDeps;
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -138,4 +142,65 @@ describe('runCli', () => {
|
|
|
138
142
|
runCli(['node', 'cli.js', 'migrate'], deps);
|
|
139
143
|
expect(deps.exit).toHaveBeenCalledWith(1);
|
|
140
144
|
});
|
|
145
|
+
|
|
146
|
+
it('routes install-skills command to runInstallSkillsImpl', () => {
|
|
147
|
+
// Act
|
|
148
|
+
runCli(['node', 'cli.js', 'install-skills'], deps);
|
|
149
|
+
|
|
150
|
+
// Assert
|
|
151
|
+
expect(deps.runInstallSkillsImpl).toHaveBeenCalledWith(
|
|
152
|
+
expect.objectContaining({
|
|
153
|
+
log: deps.log,
|
|
154
|
+
error: deps.error,
|
|
155
|
+
exit: deps.exit,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('routes install-rules command to runInstallRulesImpl', () => {
|
|
161
|
+
// Act
|
|
162
|
+
runCli(['node', 'cli.js', 'install-rules'], deps);
|
|
163
|
+
|
|
164
|
+
// Assert
|
|
165
|
+
expect(deps.runInstallRulesImpl).toHaveBeenCalledWith(
|
|
166
|
+
expect.objectContaining({
|
|
167
|
+
log: deps.log,
|
|
168
|
+
error: deps.error,
|
|
169
|
+
exit: deps.exit,
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('routes install-mcp command to runInstallMcpImpl', () => {
|
|
175
|
+
// Act
|
|
176
|
+
runCli(['node', 'cli.js', 'install-mcp'], deps);
|
|
177
|
+
|
|
178
|
+
// Assert
|
|
179
|
+
expect(deps.runInstallMcpImpl).toHaveBeenCalledWith(
|
|
180
|
+
expect.objectContaining({
|
|
181
|
+
log: deps.log,
|
|
182
|
+
error: deps.error,
|
|
183
|
+
exit: deps.exit,
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('routes mcp command to startMcpServerImpl', () => {
|
|
189
|
+
// Act
|
|
190
|
+
runCli(['node', 'cli.js', 'mcp'], deps);
|
|
191
|
+
|
|
192
|
+
// Assert
|
|
193
|
+
expect(deps.startMcpServerImpl).toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('shows new commands in usage output', () => {
|
|
197
|
+
// Act
|
|
198
|
+
runCli(['node', 'cli.js'], deps);
|
|
199
|
+
|
|
200
|
+
// Assert
|
|
201
|
+
expect(deps.log).toHaveBeenCalledWith(' dataqueue-cli install-skills');
|
|
202
|
+
expect(deps.log).toHaveBeenCalledWith(' dataqueue-cli install-rules');
|
|
203
|
+
expect(deps.log).toHaveBeenCalledWith(' dataqueue-cli install-mcp');
|
|
204
|
+
expect(deps.log).toHaveBeenCalledWith(' dataqueue-cli mcp');
|
|
205
|
+
});
|
|
141
206
|
});
|
package/src/cli.ts
CHANGED
|
@@ -3,6 +3,13 @@ import { spawnSync, SpawnSyncReturns } from 'child_process';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { InitDeps, runInit } from './init-command.js';
|
|
6
|
+
import {
|
|
7
|
+
runInstallSkills,
|
|
8
|
+
InstallSkillsDeps,
|
|
9
|
+
} from './install-skills-command.js';
|
|
10
|
+
import { runInstallRules, InstallRulesDeps } from './install-rules-command.js';
|
|
11
|
+
import { runInstallMcp, InstallMcpDeps } from './install-mcp-command.js';
|
|
12
|
+
import { startMcpServer } from './mcp-server.js';
|
|
6
13
|
|
|
7
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
15
|
const __dirname = path.dirname(__filename);
|
|
@@ -15,6 +22,13 @@ export interface CliDeps {
|
|
|
15
22
|
migrationsDir?: string;
|
|
16
23
|
initDeps?: InitDeps;
|
|
17
24
|
runInitImpl?: (deps?: InitDeps) => void;
|
|
25
|
+
installSkillsDeps?: InstallSkillsDeps;
|
|
26
|
+
runInstallSkillsImpl?: (deps?: InstallSkillsDeps) => void;
|
|
27
|
+
installRulesDeps?: InstallRulesDeps;
|
|
28
|
+
runInstallRulesImpl?: (deps?: InstallRulesDeps) => Promise<void>;
|
|
29
|
+
installMcpDeps?: InstallMcpDeps;
|
|
30
|
+
runInstallMcpImpl?: (deps?: InstallMcpDeps) => Promise<void>;
|
|
31
|
+
startMcpServerImpl?: typeof startMcpServer;
|
|
18
32
|
}
|
|
19
33
|
|
|
20
34
|
export function runCli(
|
|
@@ -27,19 +41,27 @@ export function runCli(
|
|
|
27
41
|
migrationsDir = path.join(__dirname, '../migrations'),
|
|
28
42
|
initDeps,
|
|
29
43
|
runInitImpl = runInit,
|
|
44
|
+
installSkillsDeps,
|
|
45
|
+
runInstallSkillsImpl = runInstallSkills,
|
|
46
|
+
installRulesDeps,
|
|
47
|
+
runInstallRulesImpl = runInstallRules,
|
|
48
|
+
installMcpDeps,
|
|
49
|
+
runInstallMcpImpl = runInstallMcp,
|
|
50
|
+
startMcpServerImpl = startMcpServer,
|
|
30
51
|
}: CliDeps = {},
|
|
31
52
|
): void {
|
|
32
53
|
const [, , command, ...restArgs] = argv;
|
|
33
54
|
|
|
34
|
-
/**
|
|
35
|
-
* Prints CLI usage and exits with non-zero code.
|
|
36
|
-
*/
|
|
37
55
|
function printUsage() {
|
|
38
56
|
log('Usage:');
|
|
39
57
|
log(
|
|
40
58
|
' dataqueue-cli migrate [--envPath <path>] [-s <schema> | --schema <schema>]',
|
|
41
59
|
);
|
|
42
60
|
log(' dataqueue-cli init');
|
|
61
|
+
log(' dataqueue-cli install-skills');
|
|
62
|
+
log(' dataqueue-cli install-rules');
|
|
63
|
+
log(' dataqueue-cli install-mcp');
|
|
64
|
+
log(' dataqueue-cli mcp');
|
|
43
65
|
log('');
|
|
44
66
|
log('Options for migrate:');
|
|
45
67
|
log(
|
|
@@ -49,24 +71,13 @@ export function runCli(
|
|
|
49
71
|
' -s, --schema <schema> Set the schema to use (passed to node-pg-migrate)',
|
|
50
72
|
);
|
|
51
73
|
log('');
|
|
52
|
-
log('
|
|
74
|
+
log('AI tooling commands:');
|
|
75
|
+
log(' install-skills Install DataQueue skill files for AI assistants');
|
|
76
|
+
log(' install-rules Install DataQueue agent rules for AI clients');
|
|
53
77
|
log(
|
|
54
|
-
' -
|
|
55
|
-
);
|
|
56
|
-
log(
|
|
57
|
-
' - For managed Postgres (e.g., DigitalOcean) with SSL, set PGSSLMODE=require and PGSSLROOTCERT to your CA .crt file.',
|
|
58
|
-
);
|
|
59
|
-
log(
|
|
60
|
-
' Example: PGSSLMODE=require NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.crt PG_DATAQUEUE_DATABASE=... npx dataqueue-cli migrate',
|
|
61
|
-
);
|
|
62
|
-
log('');
|
|
63
|
-
log('Notes for init:');
|
|
64
|
-
log(
|
|
65
|
-
' - Supports both Next.js App Router and Pages Router (prefers App Router if both exist).',
|
|
66
|
-
);
|
|
67
|
-
log(
|
|
68
|
-
' - Scaffolds endpoint, cron.sh, queue placeholder, and package.json entries.',
|
|
78
|
+
' install-mcp Configure the DataQueue MCP server for AI clients',
|
|
69
79
|
);
|
|
80
|
+
log(' mcp Start the DataQueue MCP server (stdio)');
|
|
70
81
|
exit(1);
|
|
71
82
|
}
|
|
72
83
|
|
|
@@ -115,6 +126,32 @@ export function runCli(
|
|
|
115
126
|
exit,
|
|
116
127
|
...initDeps,
|
|
117
128
|
});
|
|
129
|
+
} else if (command === 'install-skills') {
|
|
130
|
+
runInstallSkillsImpl({
|
|
131
|
+
log,
|
|
132
|
+
error,
|
|
133
|
+
exit,
|
|
134
|
+
...installSkillsDeps,
|
|
135
|
+
});
|
|
136
|
+
} else if (command === 'install-rules') {
|
|
137
|
+
runInstallRulesImpl({
|
|
138
|
+
log,
|
|
139
|
+
error,
|
|
140
|
+
exit,
|
|
141
|
+
...installRulesDeps,
|
|
142
|
+
});
|
|
143
|
+
} else if (command === 'install-mcp') {
|
|
144
|
+
runInstallMcpImpl({
|
|
145
|
+
log,
|
|
146
|
+
error,
|
|
147
|
+
exit,
|
|
148
|
+
...installMcpDeps,
|
|
149
|
+
});
|
|
150
|
+
} else if (command === 'mcp') {
|
|
151
|
+
startMcpServerImpl().catch((err) => {
|
|
152
|
+
error('Failed to start MCP server:', err);
|
|
153
|
+
exit(1);
|
|
154
|
+
});
|
|
118
155
|
} else {
|
|
119
156
|
printUsage();
|
|
120
157
|
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
runInstallMcp,
|
|
4
|
+
upsertMcpConfig,
|
|
5
|
+
InstallMcpDeps,
|
|
6
|
+
} from './install-mcp-command.js';
|
|
7
|
+
|
|
8
|
+
describe('upsertMcpConfig', () => {
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.restoreAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('creates new config file when it does not exist', () => {
|
|
14
|
+
// Setup
|
|
15
|
+
const deps = {
|
|
16
|
+
existsSync: vi.fn(() => false),
|
|
17
|
+
readFileSync: vi.fn(),
|
|
18
|
+
writeFileSync: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Act
|
|
22
|
+
upsertMcpConfig(
|
|
23
|
+
'/path/mcp.json',
|
|
24
|
+
'dataqueue',
|
|
25
|
+
{ command: 'npx', args: ['dataqueue-cli', 'mcp'] },
|
|
26
|
+
deps,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Assert
|
|
30
|
+
const written = JSON.parse(deps.writeFileSync.mock.calls[0][1] as string);
|
|
31
|
+
expect(written.mcpServers.dataqueue).toEqual({
|
|
32
|
+
command: 'npx',
|
|
33
|
+
args: ['dataqueue-cli', 'mcp'],
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('adds to existing config without overwriting other servers', () => {
|
|
38
|
+
// Setup
|
|
39
|
+
const existing = JSON.stringify({
|
|
40
|
+
mcpServers: { other: { command: 'other' } },
|
|
41
|
+
});
|
|
42
|
+
const deps = {
|
|
43
|
+
existsSync: vi.fn(() => true),
|
|
44
|
+
readFileSync: vi.fn(() => existing),
|
|
45
|
+
writeFileSync: vi.fn(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
upsertMcpConfig(
|
|
50
|
+
'/path/mcp.json',
|
|
51
|
+
'dataqueue',
|
|
52
|
+
{ command: 'npx', args: ['dataqueue-cli', 'mcp'] },
|
|
53
|
+
deps,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
const written = JSON.parse(deps.writeFileSync.mock.calls[0][1] as string);
|
|
58
|
+
expect(written.mcpServers.other).toEqual({ command: 'other' });
|
|
59
|
+
expect(written.mcpServers.dataqueue).toEqual({
|
|
60
|
+
command: 'npx',
|
|
61
|
+
args: ['dataqueue-cli', 'mcp'],
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('overwrites existing dataqueue entry', () => {
|
|
66
|
+
// Setup
|
|
67
|
+
const existing = JSON.stringify({
|
|
68
|
+
mcpServers: { dataqueue: { command: 'old' } },
|
|
69
|
+
});
|
|
70
|
+
const deps = {
|
|
71
|
+
existsSync: vi.fn(() => true),
|
|
72
|
+
readFileSync: vi.fn(() => existing),
|
|
73
|
+
writeFileSync: vi.fn(),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Act
|
|
77
|
+
upsertMcpConfig('/path/mcp.json', 'dataqueue', { command: 'new' }, deps);
|
|
78
|
+
|
|
79
|
+
// Assert
|
|
80
|
+
const written = JSON.parse(deps.writeFileSync.mock.calls[0][1] as string);
|
|
81
|
+
expect(written.mcpServers.dataqueue).toEqual({ command: 'new' });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('handles malformed JSON in existing file', () => {
|
|
85
|
+
// Setup
|
|
86
|
+
const deps = {
|
|
87
|
+
existsSync: vi.fn(() => true),
|
|
88
|
+
readFileSync: vi.fn(() => 'not json'),
|
|
89
|
+
writeFileSync: vi.fn(),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Act
|
|
93
|
+
upsertMcpConfig('/path/mcp.json', 'dataqueue', { command: 'npx' }, deps);
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
const written = JSON.parse(deps.writeFileSync.mock.calls[0][1] as string);
|
|
97
|
+
expect(written.mcpServers.dataqueue).toEqual({ command: 'npx' });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('runInstallMcp', () => {
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
vi.restoreAllMocks();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
function makeDeps(overrides: Partial<InstallMcpDeps> = {}): InstallMcpDeps {
|
|
107
|
+
return {
|
|
108
|
+
log: vi.fn(),
|
|
109
|
+
error: vi.fn(),
|
|
110
|
+
exit: vi.fn(),
|
|
111
|
+
cwd: '/project',
|
|
112
|
+
readFileSync: vi.fn(() => '{}'),
|
|
113
|
+
writeFileSync: vi.fn(),
|
|
114
|
+
mkdirSync: vi.fn(),
|
|
115
|
+
existsSync: vi.fn(() => false),
|
|
116
|
+
...overrides,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
it('installs MCP config for Cursor (option 1)', async () => {
|
|
121
|
+
// Setup
|
|
122
|
+
const deps = makeDeps({ selectedClient: '1' });
|
|
123
|
+
|
|
124
|
+
// Act
|
|
125
|
+
await runInstallMcp(deps);
|
|
126
|
+
|
|
127
|
+
// Assert
|
|
128
|
+
expect(deps.mkdirSync).toHaveBeenCalledWith('/project/.cursor', {
|
|
129
|
+
recursive: true,
|
|
130
|
+
});
|
|
131
|
+
const written = JSON.parse(
|
|
132
|
+
(deps.writeFileSync as ReturnType<typeof vi.fn>).mock
|
|
133
|
+
.calls[0][1] as string,
|
|
134
|
+
);
|
|
135
|
+
expect(written.mcpServers.dataqueue.command).toBe('npx');
|
|
136
|
+
expect(written.mcpServers.dataqueue.args).toEqual(['dataqueue-cli', 'mcp']);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('installs MCP config for Claude Code (option 2)', async () => {
|
|
140
|
+
// Setup
|
|
141
|
+
const deps = makeDeps({ selectedClient: '2' });
|
|
142
|
+
|
|
143
|
+
// Act
|
|
144
|
+
await runInstallMcp(deps);
|
|
145
|
+
|
|
146
|
+
// Assert
|
|
147
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
148
|
+
'/project/.mcp.json',
|
|
149
|
+
expect.any(String),
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('installs MCP config for VS Code (option 3)', async () => {
|
|
154
|
+
// Setup
|
|
155
|
+
const deps = makeDeps({ selectedClient: '3' });
|
|
156
|
+
|
|
157
|
+
// Act
|
|
158
|
+
await runInstallMcp(deps);
|
|
159
|
+
|
|
160
|
+
// Assert
|
|
161
|
+
expect(deps.mkdirSync).toHaveBeenCalledWith('/project/.vscode', {
|
|
162
|
+
recursive: true,
|
|
163
|
+
});
|
|
164
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
165
|
+
expect.stringContaining('.vscode/mcp.json'),
|
|
166
|
+
expect.any(String),
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('exits with error for invalid choice', async () => {
|
|
171
|
+
// Setup
|
|
172
|
+
const deps = makeDeps({ selectedClient: '99' });
|
|
173
|
+
|
|
174
|
+
// Act
|
|
175
|
+
await runInstallMcp(deps);
|
|
176
|
+
|
|
177
|
+
// Assert
|
|
178
|
+
expect(deps.error).toHaveBeenCalledWith(
|
|
179
|
+
expect.stringContaining('Invalid choice'),
|
|
180
|
+
);
|
|
181
|
+
expect(deps.exit).toHaveBeenCalledWith(1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('handles install errors', async () => {
|
|
185
|
+
// Setup
|
|
186
|
+
const deps = makeDeps({
|
|
187
|
+
selectedClient: '1',
|
|
188
|
+
writeFileSync: vi.fn(() => {
|
|
189
|
+
throw new Error('permission denied');
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Act
|
|
194
|
+
await runInstallMcp(deps);
|
|
195
|
+
|
|
196
|
+
// Assert
|
|
197
|
+
expect(deps.error).toHaveBeenCalledWith(
|
|
198
|
+
'Failed to install MCP config:',
|
|
199
|
+
expect.any(Error),
|
|
200
|
+
);
|
|
201
|
+
expect(deps.exit).toHaveBeenCalledWith(1);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('logs done message on success', async () => {
|
|
205
|
+
// Setup
|
|
206
|
+
const deps = makeDeps({ selectedClient: '1' });
|
|
207
|
+
|
|
208
|
+
// Act
|
|
209
|
+
await runInstallMcp(deps);
|
|
210
|
+
|
|
211
|
+
// Assert
|
|
212
|
+
expect(deps.log).toHaveBeenCalledWith(
|
|
213
|
+
expect.stringContaining('npx dataqueue-cli mcp'),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
});
|