@nicnocquee/dataqueue 1.33.0 → 1.35.0-beta.20260224075710

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.
Files changed (54) hide show
  1. package/ai/build-docs-content.ts +96 -0
  2. package/ai/build-llms-full.ts +42 -0
  3. package/ai/docs-content.json +290 -0
  4. package/ai/rules/advanced.md +170 -0
  5. package/ai/rules/basic.md +159 -0
  6. package/ai/rules/react-dashboard.md +87 -0
  7. package/ai/skills/dataqueue-advanced/SKILL.md +370 -0
  8. package/ai/skills/dataqueue-core/SKILL.md +235 -0
  9. package/ai/skills/dataqueue-react/SKILL.md +201 -0
  10. package/dist/cli.cjs +577 -32
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.d.cts +52 -2
  13. package/dist/cli.d.ts +52 -2
  14. package/dist/cli.js +575 -32
  15. package/dist/cli.js.map +1 -1
  16. package/dist/index.cjs +937 -108
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +358 -11
  19. package/dist/index.d.ts +358 -11
  20. package/dist/index.js +937 -108
  21. package/dist/index.js.map +1 -1
  22. package/dist/mcp-server.cjs +186 -0
  23. package/dist/mcp-server.cjs.map +1 -0
  24. package/dist/mcp-server.d.cts +32 -0
  25. package/dist/mcp-server.d.ts +32 -0
  26. package/dist/mcp-server.js +175 -0
  27. package/dist/mcp-server.js.map +1 -0
  28. package/migrations/1781200000005_add_retry_config_to_job_queue.sql +17 -0
  29. package/migrations/1781200000006_add_output_to_job_queue.sql +3 -0
  30. package/package.json +10 -4
  31. package/src/backend.ts +36 -3
  32. package/src/backends/postgres.ts +344 -42
  33. package/src/backends/redis-scripts.ts +173 -8
  34. package/src/backends/redis.test.ts +668 -0
  35. package/src/backends/redis.ts +244 -15
  36. package/src/cli.test.ts +65 -0
  37. package/src/cli.ts +56 -19
  38. package/src/db-util.ts +1 -1
  39. package/src/index.test.ts +811 -12
  40. package/src/index.ts +106 -14
  41. package/src/install-mcp-command.test.ts +216 -0
  42. package/src/install-mcp-command.ts +185 -0
  43. package/src/install-rules-command.test.ts +218 -0
  44. package/src/install-rules-command.ts +233 -0
  45. package/src/install-skills-command.test.ts +176 -0
  46. package/src/install-skills-command.ts +124 -0
  47. package/src/mcp-server.test.ts +162 -0
  48. package/src/mcp-server.ts +231 -0
  49. package/src/processor.ts +133 -49
  50. package/src/queue.test.ts +477 -0
  51. package/src/queue.ts +20 -3
  52. package/src/supervisor.test.ts +340 -0
  53. package/src/supervisor.ts +177 -0
  54. package/src/types.ts +318 -3
package/src/index.ts CHANGED
@@ -1,9 +1,13 @@
1
+ import { EventEmitter } from 'node:events';
1
2
  import { createProcessor } from './processor.js';
3
+ import { createSupervisor } from './supervisor.js';
2
4
  import {
3
5
  JobQueueConfig,
4
6
  JobQueue,
5
7
  JobOptions,
8
+ AddJobOptions,
6
9
  ProcessorOptions,
10
+ SupervisorOptions,
7
11
  JobHandlers,
8
12
  JobType,
9
13
  PostgresJobQueueConfig,
@@ -11,6 +15,9 @@ import {
11
15
  CronScheduleOptions,
12
16
  CronScheduleStatus,
13
17
  EditCronScheduleOptions,
18
+ QueueEventMap,
19
+ QueueEventName,
20
+ QueueEmitFn,
14
21
  } from './types.js';
15
22
  import { QueueBackend, CronScheduleInput } from './backend.js';
16
23
  import { setLogContext } from './log-context.js';
@@ -23,6 +30,8 @@ import { getNextCronOccurrence, validateCronExpression } from './cron.js';
23
30
  * Initialize the job queue system.
24
31
  *
25
32
  * Defaults to PostgreSQL when `backend` is omitted.
33
+ * For PostgreSQL, provide either `databaseConfig` or `pool` (bring your own).
34
+ * For Redis, provide either `redisConfig` or `client` (bring your own).
26
35
  */
27
36
  export const initJobQueue = <PayloadMap = any>(
28
37
  config: JobQueueConfig,
@@ -34,15 +43,39 @@ export const initJobQueue = <PayloadMap = any>(
34
43
 
35
44
  if (backendType === 'postgres') {
36
45
  const pgConfig = config as PostgresJobQueueConfig;
37
- const pool = createPool(pgConfig.databaseConfig);
38
- backend = new PostgresBackend(pool);
46
+ if (pgConfig.pool) {
47
+ backend = new PostgresBackend(pgConfig.pool);
48
+ } else if (pgConfig.databaseConfig) {
49
+ const pool = createPool(pgConfig.databaseConfig);
50
+ backend = new PostgresBackend(pool);
51
+ } else {
52
+ throw new Error(
53
+ 'PostgreSQL backend requires either "databaseConfig" or "pool" to be provided.',
54
+ );
55
+ }
39
56
  } else if (backendType === 'redis') {
40
- const redisConfig = (config as RedisJobQueueConfig).redisConfig;
41
- backend = new RedisBackend(redisConfig);
57
+ const redisConfig = config as RedisJobQueueConfig;
58
+ if (redisConfig.client) {
59
+ backend = new RedisBackend(
60
+ redisConfig.client as any,
61
+ redisConfig.keyPrefix,
62
+ );
63
+ } else if (redisConfig.redisConfig) {
64
+ backend = new RedisBackend(redisConfig.redisConfig);
65
+ } else {
66
+ throw new Error(
67
+ 'Redis backend requires either "redisConfig" or "client" to be provided.',
68
+ );
69
+ }
42
70
  } else {
43
71
  throw new Error(`Unknown backend: ${backendType}`);
44
72
  }
45
73
 
74
+ const emitter = new EventEmitter();
75
+ const emit: QueueEmitFn = (event, data) => {
76
+ emitter.emit(event, data);
77
+ };
78
+
46
79
  /**
47
80
  * Enqueue due cron jobs. Shared by the public API and the processor hook.
48
81
  */
@@ -84,6 +117,9 @@ export const initJobQueue = <PayloadMap = any>(
84
117
  timeoutMs: schedule.timeoutMs ?? undefined,
85
118
  forceKillOnTimeout: schedule.forceKillOnTimeout,
86
119
  tags: schedule.tags,
120
+ retryDelay: schedule.retryDelay ?? undefined,
121
+ retryBackoff: schedule.retryBackoff ?? undefined,
122
+ retryDelayMax: schedule.retryDelayMax ?? undefined,
87
123
  });
88
124
 
89
125
  // Advance to next occurrence
@@ -107,8 +143,21 @@ export const initJobQueue = <PayloadMap = any>(
107
143
  return {
108
144
  // Job queue operations
109
145
  addJob: withLogContext(
110
- (job: JobOptions<PayloadMap, any>) =>
111
- backend.addJob<PayloadMap, any>(job),
146
+ async (job: JobOptions<PayloadMap, any>, options?: AddJobOptions) => {
147
+ const jobId = await backend.addJob<PayloadMap, any>(job, options);
148
+ emit('job:added', { jobId, jobType: job.jobType });
149
+ return jobId;
150
+ },
151
+ config.verbose ?? false,
152
+ ),
153
+ addJobs: withLogContext(
154
+ async (jobs: JobOptions<PayloadMap, any>[], options?: AddJobOptions) => {
155
+ const jobIds = await backend.addJobs<PayloadMap, any>(jobs, options);
156
+ for (let i = 0; i < jobIds.length; i++) {
157
+ emit('job:added', { jobId: jobIds[i], jobType: jobs[i].jobType });
158
+ }
159
+ return jobIds;
160
+ },
112
161
  config.verbose ?? false,
113
162
  ),
114
163
  getJob: withLogContext(
@@ -140,15 +189,18 @@ export const initJobQueue = <PayloadMap = any>(
140
189
  ) => backend.getJobs<PayloadMap, any>(filters, limit, offset),
141
190
  config.verbose ?? false,
142
191
  ),
143
- retryJob: (jobId: number) => backend.retryJob(jobId),
192
+ retryJob: async (jobId: number) => {
193
+ await backend.retryJob(jobId);
194
+ emit('job:retried', { jobId });
195
+ },
144
196
  cleanupOldJobs: (daysToKeep?: number, batchSize?: number) =>
145
197
  backend.cleanupOldJobs(daysToKeep, batchSize),
146
198
  cleanupOldJobEvents: (daysToKeep?: number, batchSize?: number) =>
147
199
  backend.cleanupOldJobEvents(daysToKeep, batchSize),
148
- cancelJob: withLogContext(
149
- (jobId: number) => backend.cancelJob(jobId),
150
- config.verbose ?? false,
151
- ),
200
+ cancelJob: withLogContext(async (jobId: number) => {
201
+ await backend.cancelJob(jobId);
202
+ emit('job:cancelled', { jobId });
203
+ }, config.verbose ?? false),
152
204
  editJob: withLogContext(
153
205
  <T extends JobType<PayloadMap>>(
154
206
  jobId: number,
@@ -206,9 +258,19 @@ export const initJobQueue = <PayloadMap = any>(
206
258
  handlers: JobHandlers<PayloadMap>,
207
259
  options?: ProcessorOptions,
208
260
  ) =>
209
- createProcessor<PayloadMap>(backend, handlers, options, async () => {
210
- await enqueueDueCronJobsImpl();
211
- }),
261
+ createProcessor<PayloadMap>(
262
+ backend,
263
+ handlers,
264
+ options,
265
+ async () => {
266
+ await enqueueDueCronJobsImpl();
267
+ },
268
+ emit,
269
+ ),
270
+
271
+ // Background supervisor — automated maintenance
272
+ createSupervisor: (options?: SupervisorOptions) =>
273
+ createSupervisor(backend, options, emit),
212
274
 
213
275
  // Job events
214
276
  getJobEvents: withLogContext(
@@ -262,6 +324,9 @@ export const initJobQueue = <PayloadMap = any>(
262
324
  timezone: options.timezone ?? 'UTC',
263
325
  allowOverlap: options.allowOverlap ?? false,
264
326
  nextRunAt,
327
+ retryDelay: options.retryDelay ?? null,
328
+ retryBackoff: options.retryBackoff ?? null,
329
+ retryDelayMax: options.retryDelayMax ?? null,
265
330
  };
266
331
  return backend.addCronSchedule(input);
267
332
  },
@@ -320,6 +385,33 @@ export const initJobQueue = <PayloadMap = any>(
320
385
  config.verbose ?? false,
321
386
  ),
322
387
 
388
+ // Event hooks
389
+ on: <K extends QueueEventName>(
390
+ event: K,
391
+ listener: (data: QueueEventMap[K]) => void,
392
+ ) => {
393
+ emitter.on(event, listener as (...args: any[]) => void);
394
+ },
395
+ once: <K extends QueueEventName>(
396
+ event: K,
397
+ listener: (data: QueueEventMap[K]) => void,
398
+ ) => {
399
+ emitter.once(event, listener as (...args: any[]) => void);
400
+ },
401
+ off: <K extends QueueEventName>(
402
+ event: K,
403
+ listener: (data: QueueEventMap[K]) => void,
404
+ ) => {
405
+ emitter.off(event, listener as (...args: any[]) => void);
406
+ },
407
+ removeAllListeners: (event?: QueueEventName) => {
408
+ if (event) {
409
+ emitter.removeAllListeners(event);
410
+ } else {
411
+ emitter.removeAllListeners();
412
+ }
413
+ },
414
+
323
415
  // Advanced access
324
416
  getPool: () => {
325
417
  if (!(backend instanceof PostgresBackend)) {
@@ -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
+ });
@@ -0,0 +1,185 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import readline from 'readline';
4
+
5
+ export interface InstallMcpDeps {
6
+ log?: (...args: unknown[]) => void;
7
+ error?: (...args: unknown[]) => void;
8
+ exit?: (code: number) => void;
9
+ cwd?: string;
10
+ readFileSync?: (p: string, enc: BufferEncoding) => string;
11
+ writeFileSync?: (p: string, data: string) => void;
12
+ mkdirSync?: (p: string, opts?: fs.MakeDirectoryOptions) => void;
13
+ existsSync?: (p: string) => boolean;
14
+ /** Override for selecting the client (skips interactive prompt). */
15
+ selectedClient?: string;
16
+ }
17
+
18
+ interface McpClientConfig {
19
+ label: string;
20
+ install: (
21
+ deps: Required<
22
+ Pick<
23
+ InstallMcpDeps,
24
+ | 'cwd'
25
+ | 'readFileSync'
26
+ | 'writeFileSync'
27
+ | 'mkdirSync'
28
+ | 'existsSync'
29
+ | 'log'
30
+ >
31
+ >,
32
+ ) => void;
33
+ }
34
+
35
+ /**
36
+ * Merges the dataqueue MCP server config into an existing JSON config file.
37
+ *
38
+ * @param filePath - Path to the MCP config file.
39
+ * @param serverKey - Key name for the server entry.
40
+ * @param serverConfig - Server configuration object.
41
+ * @param deps - Injectable file system functions.
42
+ */
43
+ export function upsertMcpConfig(
44
+ filePath: string,
45
+ serverKey: string,
46
+ serverConfig: Record<string, unknown>,
47
+ deps: {
48
+ readFileSync: (p: string, enc: BufferEncoding) => string;
49
+ writeFileSync: (p: string, data: string) => void;
50
+ existsSync: (p: string) => boolean;
51
+ },
52
+ ): void {
53
+ let config: Record<string, unknown> = {};
54
+
55
+ if (deps.existsSync(filePath)) {
56
+ try {
57
+ config = JSON.parse(deps.readFileSync(filePath, 'utf-8'));
58
+ } catch {
59
+ config = {};
60
+ }
61
+ }
62
+
63
+ if (!config.mcpServers || typeof config.mcpServers !== 'object') {
64
+ config.mcpServers = {};
65
+ }
66
+
67
+ (config.mcpServers as Record<string, unknown>)[serverKey] = serverConfig;
68
+ deps.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
69
+ }
70
+
71
+ const MCP_SERVER_CONFIG = {
72
+ command: 'npx',
73
+ args: ['dataqueue-cli', 'mcp'],
74
+ };
75
+
76
+ const MCP_CLIENTS: Record<string, McpClientConfig> = {
77
+ '1': {
78
+ label: 'Cursor',
79
+ install: (deps) => {
80
+ const configDir = path.join(deps.cwd, '.cursor');
81
+ deps.mkdirSync(configDir, { recursive: true });
82
+ const configFile = path.join(configDir, 'mcp.json');
83
+ upsertMcpConfig(configFile, 'dataqueue', MCP_SERVER_CONFIG, deps);
84
+ deps.log(` ✓ .cursor/mcp.json`);
85
+ },
86
+ },
87
+ '2': {
88
+ label: 'Claude Code',
89
+ install: (deps) => {
90
+ const configFile = path.join(deps.cwd, '.mcp.json');
91
+ upsertMcpConfig(configFile, 'dataqueue', MCP_SERVER_CONFIG, deps);
92
+ deps.log(` ✓ .mcp.json`);
93
+ },
94
+ },
95
+ '3': {
96
+ label: 'VS Code (Copilot)',
97
+ install: (deps) => {
98
+ const configDir = path.join(deps.cwd, '.vscode');
99
+ deps.mkdirSync(configDir, { recursive: true });
100
+ const configFile = path.join(configDir, 'mcp.json');
101
+ upsertMcpConfig(configFile, 'dataqueue', MCP_SERVER_CONFIG, deps);
102
+ deps.log(` ✓ .vscode/mcp.json`);
103
+ },
104
+ },
105
+ '4': {
106
+ label: 'Windsurf',
107
+ install: (deps) => {
108
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
109
+ const configFile = path.join(
110
+ homeDir,
111
+ '.codeium',
112
+ 'windsurf',
113
+ 'mcp_config.json',
114
+ );
115
+ deps.mkdirSync(path.dirname(configFile), { recursive: true });
116
+ upsertMcpConfig(configFile, 'dataqueue', MCP_SERVER_CONFIG, deps);
117
+ deps.log(` ✓ ~/.codeium/windsurf/mcp_config.json`);
118
+ },
119
+ },
120
+ };
121
+
122
+ /**
123
+ * Installs the DataQueue MCP server config for the selected AI client.
124
+ *
125
+ * @param deps - Injectable dependencies for testing.
126
+ */
127
+ export async function runInstallMcp({
128
+ log = console.log,
129
+ error = console.error,
130
+ exit = (code: number) => process.exit(code),
131
+ cwd = process.cwd(),
132
+ readFileSync = fs.readFileSync,
133
+ writeFileSync = fs.writeFileSync,
134
+ mkdirSync = fs.mkdirSync,
135
+ existsSync = fs.existsSync,
136
+ selectedClient,
137
+ }: InstallMcpDeps = {}): Promise<void> {
138
+ log('DataQueue MCP Server Installer\n');
139
+ log('Select your AI client:\n');
140
+
141
+ for (const [key, client] of Object.entries(MCP_CLIENTS)) {
142
+ log(` ${key}) ${client.label}`);
143
+ }
144
+ log('');
145
+
146
+ let choice = selectedClient;
147
+
148
+ if (!choice) {
149
+ const rl = readline.createInterface({
150
+ input: process.stdin,
151
+ output: process.stdout,
152
+ });
153
+
154
+ choice = await new Promise<string>((resolve) => {
155
+ rl.question('Enter choice (1-4): ', (answer) => {
156
+ rl.close();
157
+ resolve(answer.trim());
158
+ });
159
+ });
160
+ }
161
+
162
+ const client = MCP_CLIENTS[choice];
163
+ if (!client) {
164
+ error(`Invalid choice: "${choice}". Expected 1-4.`);
165
+ exit(1);
166
+ return;
167
+ }
168
+
169
+ log(`\nInstalling MCP config for ${client.label}...`);
170
+
171
+ try {
172
+ client.install({
173
+ cwd,
174
+ readFileSync,
175
+ writeFileSync,
176
+ mkdirSync,
177
+ existsSync,
178
+ log,
179
+ });
180
+ log('\nDone! The MCP server will run via: npx dataqueue-cli mcp');
181
+ } catch (err) {
182
+ error('Failed to install MCP config:', err);
183
+ exit(1);
184
+ }
185
+ }