ctxpkg 0.0.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 +661 -0
- package/README.md +282 -0
- package/bin/cli.js +8 -0
- package/bin/daemon.js +7 -0
- package/package.json +70 -0
- package/src/agent/AGENTS.md +249 -0
- package/src/agent/agent.prompts.ts +66 -0
- package/src/agent/agent.test-runner.schemas.ts +158 -0
- package/src/agent/agent.test-runner.ts +436 -0
- package/src/agent/agent.ts +371 -0
- package/src/agent/agent.types.ts +94 -0
- package/src/backend/AGENTS.md +112 -0
- package/src/backend/backend.protocol.ts +95 -0
- package/src/backend/backend.schemas.ts +123 -0
- package/src/backend/backend.services.ts +151 -0
- package/src/backend/backend.ts +111 -0
- package/src/backend/backend.types.ts +34 -0
- package/src/cli/AGENTS.md +213 -0
- package/src/cli/cli.agent.ts +197 -0
- package/src/cli/cli.chat.ts +369 -0
- package/src/cli/cli.client.ts +55 -0
- package/src/cli/cli.collections.ts +491 -0
- package/src/cli/cli.config.ts +252 -0
- package/src/cli/cli.daemon.ts +160 -0
- package/src/cli/cli.documents.ts +413 -0
- package/src/cli/cli.mcp.ts +177 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/cli.utils.ts +122 -0
- package/src/client/AGENTS.md +135 -0
- package/src/client/client.adapters.ts +279 -0
- package/src/client/client.ts +86 -0
- package/src/client/client.types.ts +17 -0
- package/src/collections/AGENTS.md +185 -0
- package/src/collections/collections.schemas.ts +195 -0
- package/src/collections/collections.ts +1160 -0
- package/src/config/config.ts +118 -0
- package/src/daemon/AGENTS.md +168 -0
- package/src/daemon/daemon.config.ts +23 -0
- package/src/daemon/daemon.manager.ts +215 -0
- package/src/daemon/daemon.schemas.ts +22 -0
- package/src/daemon/daemon.ts +205 -0
- package/src/database/AGENTS.md +211 -0
- package/src/database/database.ts +64 -0
- package/src/database/migrations/migrations.001-init.ts +56 -0
- package/src/database/migrations/migrations.002-fts5.ts +32 -0
- package/src/database/migrations/migrations.ts +20 -0
- package/src/database/migrations/migrations.types.ts +9 -0
- package/src/documents/AGENTS.md +301 -0
- package/src/documents/documents.schemas.ts +190 -0
- package/src/documents/documents.ts +734 -0
- package/src/embedder/embedder.ts +53 -0
- package/src/exports.ts +0 -0
- package/src/mcp/AGENTS.md +264 -0
- package/src/mcp/mcp.ts +105 -0
- package/src/tools/AGENTS.md +228 -0
- package/src/tools/agent/agent.ts +45 -0
- package/src/tools/documents/documents.ts +401 -0
- package/src/tools/tools.langchain.ts +37 -0
- package/src/tools/tools.mcp.ts +46 -0
- package/src/tools/tools.types.ts +35 -0
- package/src/utils/utils.services.ts +46 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { select, confirm, input } from '@inquirer/prompts';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
formatHeader,
|
|
6
|
+
formatSuccess,
|
|
7
|
+
formatError,
|
|
8
|
+
formatInfo,
|
|
9
|
+
formatTableHeader,
|
|
10
|
+
formatTableRow,
|
|
11
|
+
withErrorHandling,
|
|
12
|
+
chalk,
|
|
13
|
+
} from './cli.utils.ts';
|
|
14
|
+
import { createCliClient } from './cli.client.ts';
|
|
15
|
+
|
|
16
|
+
import { Services } from '#root/utils/utils.services.ts';
|
|
17
|
+
import { CollectionsService } from '#root/collections/collections.ts';
|
|
18
|
+
|
|
19
|
+
const createDocumentsCli = (command: Command) => {
|
|
20
|
+
command.description('Manage reference document collections');
|
|
21
|
+
|
|
22
|
+
// List collections command
|
|
23
|
+
command
|
|
24
|
+
.command('list-collections')
|
|
25
|
+
.alias('ls')
|
|
26
|
+
.description('List all reference collections')
|
|
27
|
+
.action(
|
|
28
|
+
withErrorHandling(async () => {
|
|
29
|
+
const client = await createCliClient();
|
|
30
|
+
try {
|
|
31
|
+
const list = await client.documents.listCollections();
|
|
32
|
+
|
|
33
|
+
if (list.length === 0) {
|
|
34
|
+
formatInfo('No collections found.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
formatHeader('Reference Collections');
|
|
39
|
+
|
|
40
|
+
const maxCollectionLen = Math.max(...list.map((c) => c.collection.length), 10);
|
|
41
|
+
|
|
42
|
+
formatTableHeader([
|
|
43
|
+
{ name: 'Collection', width: maxCollectionLen },
|
|
44
|
+
{ name: 'Documents', width: 10 },
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
for (const item of list) {
|
|
48
|
+
formatTableRow([
|
|
49
|
+
{ value: item.collection, width: maxCollectionLen, color: chalk.white },
|
|
50
|
+
{ value: String(item.document_count), width: 10, color: chalk.yellow },
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log();
|
|
55
|
+
} finally {
|
|
56
|
+
await client.disconnect();
|
|
57
|
+
}
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Drop collection command
|
|
62
|
+
command
|
|
63
|
+
.command('drop-collection')
|
|
64
|
+
.alias('drop')
|
|
65
|
+
.argument('[name]', 'Name of collection to drop')
|
|
66
|
+
.description('Drop a reference collection')
|
|
67
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
68
|
+
.action(
|
|
69
|
+
withErrorHandling(async (name: string | undefined, options: { force?: boolean }) => {
|
|
70
|
+
const client = await createCliClient();
|
|
71
|
+
try {
|
|
72
|
+
const collections = await client.documents.listCollections();
|
|
73
|
+
|
|
74
|
+
if (collections.length === 0) {
|
|
75
|
+
formatInfo('No collections found.');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let collectionName: string;
|
|
80
|
+
|
|
81
|
+
// If no name provided, prompt user to select
|
|
82
|
+
if (!name) {
|
|
83
|
+
collectionName = await select({
|
|
84
|
+
message: 'Select a collection to drop:',
|
|
85
|
+
choices: collections.map((c) => ({
|
|
86
|
+
name: `${c.collection} (${c.document_count} documents)`,
|
|
87
|
+
value: c.collection,
|
|
88
|
+
})),
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
// Verify collection exists
|
|
92
|
+
const exists = collections.find((c) => c.collection === name);
|
|
93
|
+
if (!exists) {
|
|
94
|
+
formatError(`Collection "${name}" not found.`);
|
|
95
|
+
console.log();
|
|
96
|
+
formatInfo('Available collections:');
|
|
97
|
+
for (const c of collections) {
|
|
98
|
+
console.log(chalk.dim(' •'), c.collection);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
collectionName = name;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Confirm deletion unless --force is used
|
|
106
|
+
if (!options.force) {
|
|
107
|
+
const confirmed = await confirm({
|
|
108
|
+
message: chalk.yellow(`Are you sure you want to drop "${collectionName}"? This cannot be undone.`),
|
|
109
|
+
default: false,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!confirmed) {
|
|
113
|
+
formatInfo('Operation cancelled.');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await client.documents.dropCollection({ collection: collectionName });
|
|
119
|
+
formatSuccess(`Collection "${collectionName}" dropped successfully.`);
|
|
120
|
+
} finally {
|
|
121
|
+
await client.disconnect();
|
|
122
|
+
}
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Search command
|
|
127
|
+
command
|
|
128
|
+
.command('search')
|
|
129
|
+
.description('Search for documents in reference collections using hybrid semantic + keyword search')
|
|
130
|
+
.argument('<query>', 'Search query')
|
|
131
|
+
.option('-c, --collections <names...>', 'Limit search to specific collections (can be aliases)')
|
|
132
|
+
.option('--no-global', 'Exclude global collections from search')
|
|
133
|
+
.option('-l, --limit <number>', 'Maximum number of results', '10')
|
|
134
|
+
.option('--max-distance <number>', 'Maximum distance threshold (0-2, lower = stricter)')
|
|
135
|
+
.option('--no-hybrid', 'Disable hybrid search (use pure vector search)')
|
|
136
|
+
.option('--rerank', 'Enable re-ranking for higher precision (slower)')
|
|
137
|
+
.action(
|
|
138
|
+
withErrorHandling(
|
|
139
|
+
async (
|
|
140
|
+
query: string,
|
|
141
|
+
options: {
|
|
142
|
+
collections?: string[];
|
|
143
|
+
global: boolean;
|
|
144
|
+
limit: string;
|
|
145
|
+
default: boolean;
|
|
146
|
+
maxDistance?: string;
|
|
147
|
+
hybrid: boolean;
|
|
148
|
+
rerank?: boolean;
|
|
149
|
+
},
|
|
150
|
+
) => {
|
|
151
|
+
const services = new Services();
|
|
152
|
+
const client = await createCliClient();
|
|
153
|
+
try {
|
|
154
|
+
const collectionsService = services.get(CollectionsService);
|
|
155
|
+
|
|
156
|
+
// Helper to resolve alias to collection ID
|
|
157
|
+
const resolveCollection = (name: string, includeGlobal: boolean): string => {
|
|
158
|
+
// Try to resolve as alias (local first, then global if allowed)
|
|
159
|
+
if (includeGlobal) {
|
|
160
|
+
const spec = collectionsService.getFromConfig(name);
|
|
161
|
+
if (spec) {
|
|
162
|
+
return collectionsService.computeCollectionId(spec);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const spec = collectionsService.getFromConfig(name, { global: false });
|
|
166
|
+
if (spec) {
|
|
167
|
+
return collectionsService.computeCollectionId(spec);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Return as-is (might be a raw collection ID or path)
|
|
171
|
+
return name;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
let collectionsToSearch: string[] = [];
|
|
175
|
+
const includeGlobal = options.global !== false;
|
|
176
|
+
|
|
177
|
+
if (options.collections && options.collections.length > 0) {
|
|
178
|
+
// Explicit collections provided - resolve aliases
|
|
179
|
+
const collectionsSet = new Set<string>();
|
|
180
|
+
for (const c of options.collections) {
|
|
181
|
+
collectionsSet.add(resolveCollection(c, includeGlobal));
|
|
182
|
+
}
|
|
183
|
+
collectionsToSearch = Array.from(collectionsSet);
|
|
184
|
+
} else {
|
|
185
|
+
// No explicit collections - default to all from local + global configs
|
|
186
|
+
const allCollections = includeGlobal
|
|
187
|
+
? collectionsService.getAllCollections()
|
|
188
|
+
: new Map(
|
|
189
|
+
Object.entries(collectionsService.readProjectConfig().collections).map(([name, spec]) => [
|
|
190
|
+
name,
|
|
191
|
+
{ spec, source: 'local' as const },
|
|
192
|
+
]),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (allCollections.size === 0) {
|
|
196
|
+
formatError('No collections configured. Use "collections add" to add one.');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
collectionsToSearch = Array.from(allCollections.values()).map(({ spec }) =>
|
|
201
|
+
collectionsService.computeCollectionId(spec),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
formatHeader('Search Results');
|
|
206
|
+
formatInfo(`Query: ${chalk.cyan(query)}`);
|
|
207
|
+
if (collectionsToSearch.length > 0) {
|
|
208
|
+
const displayCollections =
|
|
209
|
+
collectionsToSearch.length > 3
|
|
210
|
+
? `${collectionsToSearch.slice(0, 3).join(', ')} (+${collectionsToSearch.length - 3} more)`
|
|
211
|
+
: collectionsToSearch.join(', ');
|
|
212
|
+
formatInfo(`Collections: ${chalk.cyan(displayCollections)}`);
|
|
213
|
+
}
|
|
214
|
+
if (!includeGlobal) {
|
|
215
|
+
formatInfo(`Scope: ${chalk.cyan('local only')}`);
|
|
216
|
+
}
|
|
217
|
+
if (!options.hybrid) {
|
|
218
|
+
formatInfo(`Mode: ${chalk.cyan('vector-only (hybrid disabled)')}`);
|
|
219
|
+
}
|
|
220
|
+
if (options.rerank) {
|
|
221
|
+
formatInfo(`Re-ranking: ${chalk.cyan('enabled')}`);
|
|
222
|
+
}
|
|
223
|
+
console.log();
|
|
224
|
+
|
|
225
|
+
const results = await client.documents.search({
|
|
226
|
+
query,
|
|
227
|
+
collections: collectionsToSearch,
|
|
228
|
+
limit: parseInt(options.limit, 10),
|
|
229
|
+
maxDistance: options.maxDistance ? parseFloat(options.maxDistance) : undefined,
|
|
230
|
+
hybridSearch: options.hybrid,
|
|
231
|
+
rerank: options.rerank,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (results.length === 0) {
|
|
235
|
+
formatInfo('No results found.');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < results.length; i++) {
|
|
240
|
+
const result = results[i];
|
|
241
|
+
const distanceColor =
|
|
242
|
+
result.distance < 0.5 ? chalk.green : result.distance < 1 ? chalk.yellow : chalk.red;
|
|
243
|
+
|
|
244
|
+
console.log(
|
|
245
|
+
chalk.bold.white(`${i + 1}.`) +
|
|
246
|
+
' ' +
|
|
247
|
+
chalk.cyan(result.document) +
|
|
248
|
+
chalk.dim(' in ') +
|
|
249
|
+
chalk.magenta(result.collection),
|
|
250
|
+
);
|
|
251
|
+
const scoreInfo =
|
|
252
|
+
result.score !== undefined
|
|
253
|
+
? chalk.dim(' Score: ') + chalk.green(result.score.toFixed(4)) + chalk.dim(' | ')
|
|
254
|
+
: chalk.dim(' ');
|
|
255
|
+
console.log(scoreInfo + chalk.dim('Distance: ') + distanceColor(result.distance.toFixed(4)));
|
|
256
|
+
console.log();
|
|
257
|
+
|
|
258
|
+
// Format content with indentation and truncation
|
|
259
|
+
const contentLines = result.content.split('\n').slice(0, 6);
|
|
260
|
+
for (const line of contentLines) {
|
|
261
|
+
const truncated = line.length > 100 ? line.slice(0, 97) + '...' : line;
|
|
262
|
+
console.log(chalk.dim(' │ ') + chalk.white(truncated));
|
|
263
|
+
}
|
|
264
|
+
if (result.content.split('\n').length > 6) {
|
|
265
|
+
console.log(chalk.dim(' │ ...'));
|
|
266
|
+
}
|
|
267
|
+
console.log();
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
await client.disconnect();
|
|
271
|
+
await services.destroy();
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// Interactive search command
|
|
278
|
+
command
|
|
279
|
+
.command('interactive-search')
|
|
280
|
+
.alias('isearch')
|
|
281
|
+
.description('Interactive search mode')
|
|
282
|
+
.action(
|
|
283
|
+
withErrorHandling(async () => {
|
|
284
|
+
const services = new Services();
|
|
285
|
+
const client = await createCliClient();
|
|
286
|
+
try {
|
|
287
|
+
const collectionsService = services.get(CollectionsService);
|
|
288
|
+
const collections = await client.documents.listCollections();
|
|
289
|
+
|
|
290
|
+
if (collections.length === 0) {
|
|
291
|
+
formatInfo('No collections found. Add some documents first.');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Build a map of collection ID → {alias, source} from both configs
|
|
296
|
+
const idToInfo = new Map<string, { alias: string; source: 'local' | 'global' }>();
|
|
297
|
+
const allCollections = collectionsService.getAllCollections();
|
|
298
|
+
for (const [alias, { spec, source }] of allCollections) {
|
|
299
|
+
const id = collectionsService.computeCollectionId(spec);
|
|
300
|
+
idToInfo.set(id, { alias, source });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
formatHeader('Interactive Search');
|
|
304
|
+
|
|
305
|
+
// Sort collections: local first, then global, then unaliased
|
|
306
|
+
const sortedCollections = [...collections].sort((a, b) => {
|
|
307
|
+
const aInfo = idToInfo.get(a.collection);
|
|
308
|
+
const bInfo = idToInfo.get(b.collection);
|
|
309
|
+
|
|
310
|
+
// Collections with aliases come before those without
|
|
311
|
+
if (aInfo && !bInfo) return -1;
|
|
312
|
+
if (!aInfo && bInfo) return 1;
|
|
313
|
+
|
|
314
|
+
// Both have aliases - sort by source (local first) then alias name
|
|
315
|
+
if (aInfo && bInfo) {
|
|
316
|
+
if (aInfo.source !== bInfo.source) {
|
|
317
|
+
return aInfo.source === 'local' ? -1 : 1;
|
|
318
|
+
}
|
|
319
|
+
return aInfo.alias.localeCompare(bInfo.alias);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Neither has alias - keep original order
|
|
323
|
+
return 0;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Select collections to search - show alias and source indicator
|
|
327
|
+
const selectedCollections = await select({
|
|
328
|
+
message: 'Search in:',
|
|
329
|
+
choices: [
|
|
330
|
+
{ name: 'All collections', value: undefined },
|
|
331
|
+
...sortedCollections.map((c) => {
|
|
332
|
+
const info = idToInfo.get(c.collection);
|
|
333
|
+
let displayName: string;
|
|
334
|
+
if (info) {
|
|
335
|
+
const sourceIndicator = info.source === 'local' ? 'local' : 'global';
|
|
336
|
+
displayName = `${info.alias} (${sourceIndicator}, ${c.document_count} docs)`;
|
|
337
|
+
} else {
|
|
338
|
+
displayName = `${c.collection} (${c.document_count} docs)`;
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
name: displayName,
|
|
342
|
+
value: c.collection,
|
|
343
|
+
};
|
|
344
|
+
}),
|
|
345
|
+
],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const query = await input({
|
|
349
|
+
message: 'Enter search query:',
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (!query.trim()) {
|
|
353
|
+
formatInfo('Empty query. Exiting.');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const limitStr = await input({
|
|
358
|
+
message: 'Number of results:',
|
|
359
|
+
default: '10',
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const results = await client.documents.search({
|
|
363
|
+
query,
|
|
364
|
+
collections: selectedCollections ? [selectedCollections] : undefined,
|
|
365
|
+
limit: parseInt(limitStr, 10) || 10,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
console.log();
|
|
369
|
+
|
|
370
|
+
if (results.length === 0) {
|
|
371
|
+
formatInfo('No results found.');
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
formatHeader(`Found ${results.length} results`);
|
|
376
|
+
|
|
377
|
+
for (let i = 0; i < results.length; i++) {
|
|
378
|
+
const result = results[i];
|
|
379
|
+
const distanceColor = result.distance < 0.5 ? chalk.green : result.distance < 1 ? chalk.yellow : chalk.red;
|
|
380
|
+
|
|
381
|
+
console.log(
|
|
382
|
+
chalk.bold.white(`${i + 1}.`) +
|
|
383
|
+
' ' +
|
|
384
|
+
chalk.cyan(result.document) +
|
|
385
|
+
chalk.dim(' in ') +
|
|
386
|
+
chalk.magenta(result.collection),
|
|
387
|
+
);
|
|
388
|
+
const scoreInfo =
|
|
389
|
+
result.score !== undefined
|
|
390
|
+
? chalk.dim(' Score: ') + chalk.green(result.score.toFixed(4)) + chalk.dim(' | ')
|
|
391
|
+
: chalk.dim(' ');
|
|
392
|
+
console.log(scoreInfo + chalk.dim('Distance: ') + distanceColor(result.distance.toFixed(4)));
|
|
393
|
+
console.log();
|
|
394
|
+
|
|
395
|
+
const contentLines = result.content.split('\n').slice(0, 6);
|
|
396
|
+
for (const line of contentLines) {
|
|
397
|
+
const truncated = line.length > 100 ? line.slice(0, 97) + '...' : line;
|
|
398
|
+
console.log(chalk.dim(' │ ') + chalk.white(truncated));
|
|
399
|
+
}
|
|
400
|
+
if (result.content.split('\n').length > 6) {
|
|
401
|
+
console.log(chalk.dim(' │ ...'));
|
|
402
|
+
}
|
|
403
|
+
console.log();
|
|
404
|
+
}
|
|
405
|
+
} finally {
|
|
406
|
+
await client.disconnect();
|
|
407
|
+
await services.destroy();
|
|
408
|
+
}
|
|
409
|
+
}),
|
|
410
|
+
);
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
export { createDocumentsCli };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
|
|
3
|
+
import { getLLMConfigFromAppConfig } from '../agent/agent.ts';
|
|
4
|
+
import { createAgentMcpServer, createDocumentsMcpServer, runMcpServer } from '../mcp/mcp.ts';
|
|
5
|
+
|
|
6
|
+
import { createCliClient } from './cli.client.ts';
|
|
7
|
+
import { formatError, withErrorHandling } from './cli.utils.ts';
|
|
8
|
+
|
|
9
|
+
import { CollectionsService } from '#root/collections/collections.ts';
|
|
10
|
+
import { Services } from '#root/utils/utils.services.ts';
|
|
11
|
+
|
|
12
|
+
const createMcpCli = (command: Command) => {
|
|
13
|
+
command.description('Start MCP servers for tool integration');
|
|
14
|
+
|
|
15
|
+
// Documents MCP server command
|
|
16
|
+
command
|
|
17
|
+
.command('documents')
|
|
18
|
+
.alias('docs')
|
|
19
|
+
.description('Start an MCP server with document tools')
|
|
20
|
+
.option('-c, --collections <names...>', 'Limit searches to specific collections')
|
|
21
|
+
.option('--no-global', 'Exclude global collections from searches')
|
|
22
|
+
.option('--name <name>', 'MCP server name', 'ctxpkg-documents')
|
|
23
|
+
.option('--version <version>', 'MCP server version', '1.0.0')
|
|
24
|
+
.action(
|
|
25
|
+
withErrorHandling(async (options: { collections?: string[]; global: boolean; name: string; version: string }) => {
|
|
26
|
+
const client = await createCliClient();
|
|
27
|
+
|
|
28
|
+
// Build alias map from both local and global configs (respecting --no-global)
|
|
29
|
+
const aliasMap = new Map<string, string>();
|
|
30
|
+
const services = new Services();
|
|
31
|
+
let defaultCollections: string[] | undefined;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const collectionsService = services.get(CollectionsService);
|
|
35
|
+
const includeGlobal = options.global !== false;
|
|
36
|
+
|
|
37
|
+
if (includeGlobal) {
|
|
38
|
+
// Include both local and global collections
|
|
39
|
+
const allCollections = collectionsService.getAllCollections();
|
|
40
|
+
for (const [alias, { spec }] of allCollections) {
|
|
41
|
+
const collectionId = collectionsService.computeCollectionId(spec);
|
|
42
|
+
aliasMap.set(alias, collectionId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Default to all configured collections when no -c option
|
|
46
|
+
if (!options.collections) {
|
|
47
|
+
defaultCollections = Array.from(allCollections.values()).map(({ spec }) =>
|
|
48
|
+
collectionsService.computeCollectionId(spec),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Local only
|
|
53
|
+
if (collectionsService.projectConfigExists()) {
|
|
54
|
+
const projectConfig = collectionsService.readProjectConfig();
|
|
55
|
+
for (const [alias, spec] of Object.entries(projectConfig.collections)) {
|
|
56
|
+
const collectionId = collectionsService.computeCollectionId(spec);
|
|
57
|
+
aliasMap.set(alias, collectionId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Default to local collections only when no -c option
|
|
61
|
+
if (!options.collections) {
|
|
62
|
+
defaultCollections = Object.values(projectConfig.collections).map((spec) =>
|
|
63
|
+
collectionsService.computeCollectionId(spec),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} finally {
|
|
69
|
+
await services.destroy();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Build collections list - use explicit collections or defaults
|
|
73
|
+
let collectionsToUse: string[] | undefined;
|
|
74
|
+
|
|
75
|
+
if (options.collections) {
|
|
76
|
+
const collectionsSet = new Set<string>();
|
|
77
|
+
for (const c of options.collections) {
|
|
78
|
+
// Resolve alias if available
|
|
79
|
+
const resolved = aliasMap.get(c) || c;
|
|
80
|
+
collectionsSet.add(resolved);
|
|
81
|
+
}
|
|
82
|
+
collectionsToUse = [...collectionsSet];
|
|
83
|
+
} else {
|
|
84
|
+
collectionsToUse = defaultCollections;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create and run MCP server
|
|
88
|
+
const server = createDocumentsMcpServer({
|
|
89
|
+
client,
|
|
90
|
+
aliasMap,
|
|
91
|
+
collections: collectionsToUse,
|
|
92
|
+
name: options.name,
|
|
93
|
+
version: options.version,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await runMcpServer(server);
|
|
97
|
+
|
|
98
|
+
// Note: cleanup happens on SIGINT/SIGTERM in runMcpServer
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Agent mode MCP server command
|
|
103
|
+
command
|
|
104
|
+
.command('agent')
|
|
105
|
+
.description('Start an MCP server with agent mode (single ask_documents tool)')
|
|
106
|
+
.option('-c, --collections <names...>', 'Limit searches to specific collections')
|
|
107
|
+
.option('--no-global', 'Exclude global collections from searches')
|
|
108
|
+
.option('--name <name>', 'MCP server name', 'ctxpkg-agent')
|
|
109
|
+
.option('--version <version>', 'MCP server version', '1.0.0')
|
|
110
|
+
.option('--model <model>', 'Override LLM model from config')
|
|
111
|
+
.action(
|
|
112
|
+
withErrorHandling(
|
|
113
|
+
async (options: { collections?: string[]; global: boolean; name: string; version: string; model?: string }) => {
|
|
114
|
+
// Get LLM config and validate
|
|
115
|
+
const llmConfig = await getLLMConfigFromAppConfig();
|
|
116
|
+
|
|
117
|
+
if (!llmConfig.apiKey) {
|
|
118
|
+
formatError('LLM API key not configured. Set it with: ctxpkg config set llm.apiKey <key>');
|
|
119
|
+
formatError('Or use environment variable: CTXPKG_LLM_API_KEY=<key>');
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Override model if specified
|
|
125
|
+
if (options.model) {
|
|
126
|
+
llmConfig.model = options.model;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const client = await createCliClient();
|
|
130
|
+
|
|
131
|
+
// Build alias map from both local and global configs (respecting --no-global)
|
|
132
|
+
const aliasMap = new Map<string, string>();
|
|
133
|
+
const services = new Services();
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const collectionsService = services.get(CollectionsService);
|
|
137
|
+
const includeGlobal = options.global !== false;
|
|
138
|
+
|
|
139
|
+
if (includeGlobal) {
|
|
140
|
+
// Include both local and global collections
|
|
141
|
+
const allCollections = collectionsService.getAllCollections();
|
|
142
|
+
for (const [alias, { spec }] of allCollections) {
|
|
143
|
+
const collectionId = collectionsService.computeCollectionId(spec);
|
|
144
|
+
aliasMap.set(alias, collectionId);
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
// Local only
|
|
148
|
+
if (collectionsService.projectConfigExists()) {
|
|
149
|
+
const projectConfig = collectionsService.readProjectConfig();
|
|
150
|
+
for (const [alias, spec] of Object.entries(projectConfig.collections)) {
|
|
151
|
+
const collectionId = collectionsService.computeCollectionId(spec);
|
|
152
|
+
aliasMap.set(alias, collectionId);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} finally {
|
|
157
|
+
await services.destroy();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Create and run MCP server with agent mode
|
|
161
|
+
const server = createAgentMcpServer({
|
|
162
|
+
client,
|
|
163
|
+
llmConfig,
|
|
164
|
+
aliasMap,
|
|
165
|
+
name: options.name,
|
|
166
|
+
version: options.version,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await runMcpServer(server);
|
|
170
|
+
|
|
171
|
+
// Note: cleanup happens on SIGINT/SIGTERM in runMcpServer
|
|
172
|
+
},
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export { createMcpCli };
|
package/src/cli/cli.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
|
|
3
|
+
import { createAgentCli } from './cli.agent.ts';
|
|
4
|
+
import { createChatCli } from './cli.chat.ts';
|
|
5
|
+
import { createCollectionsCli } from './cli.collections.ts';
|
|
6
|
+
import { createConfigCli } from './cli.config.ts';
|
|
7
|
+
import { createDaemonCli } from './cli.daemon.ts';
|
|
8
|
+
import { createDocumentsCli } from './cli.documents.ts';
|
|
9
|
+
import { createMcpCli } from './cli.mcp.ts';
|
|
10
|
+
|
|
11
|
+
const createProgram = () => {
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program.name('ctxpkg').description('Context package manager - manage AI agent context collections').version('1.0.0');
|
|
15
|
+
|
|
16
|
+
// Create subcommand groups
|
|
17
|
+
createCollectionsCli(program.command('collections').alias('col').description('Manage collection packages'));
|
|
18
|
+
createDocumentsCli(program.command('documents').alias('docs').description('Query indexed documents'));
|
|
19
|
+
createConfigCli(program.command('config').alias('cfg').description('Manage configuration'));
|
|
20
|
+
createDaemonCli(program.command('daemon').description('Manage the background daemon'));
|
|
21
|
+
createMcpCli(program.command('mcp').description('Start MCP servers for tool integration'));
|
|
22
|
+
createChatCli(program.command('chat').description('Chat with your documentation using AI'));
|
|
23
|
+
createAgentCli(program.command('agent').description('Agent testing and evaluation tools'));
|
|
24
|
+
|
|
25
|
+
return program;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export { createProgram };
|