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.
Files changed (61) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +282 -0
  3. package/bin/cli.js +8 -0
  4. package/bin/daemon.js +7 -0
  5. package/package.json +70 -0
  6. package/src/agent/AGENTS.md +249 -0
  7. package/src/agent/agent.prompts.ts +66 -0
  8. package/src/agent/agent.test-runner.schemas.ts +158 -0
  9. package/src/agent/agent.test-runner.ts +436 -0
  10. package/src/agent/agent.ts +371 -0
  11. package/src/agent/agent.types.ts +94 -0
  12. package/src/backend/AGENTS.md +112 -0
  13. package/src/backend/backend.protocol.ts +95 -0
  14. package/src/backend/backend.schemas.ts +123 -0
  15. package/src/backend/backend.services.ts +151 -0
  16. package/src/backend/backend.ts +111 -0
  17. package/src/backend/backend.types.ts +34 -0
  18. package/src/cli/AGENTS.md +213 -0
  19. package/src/cli/cli.agent.ts +197 -0
  20. package/src/cli/cli.chat.ts +369 -0
  21. package/src/cli/cli.client.ts +55 -0
  22. package/src/cli/cli.collections.ts +491 -0
  23. package/src/cli/cli.config.ts +252 -0
  24. package/src/cli/cli.daemon.ts +160 -0
  25. package/src/cli/cli.documents.ts +413 -0
  26. package/src/cli/cli.mcp.ts +177 -0
  27. package/src/cli/cli.ts +28 -0
  28. package/src/cli/cli.utils.ts +122 -0
  29. package/src/client/AGENTS.md +135 -0
  30. package/src/client/client.adapters.ts +279 -0
  31. package/src/client/client.ts +86 -0
  32. package/src/client/client.types.ts +17 -0
  33. package/src/collections/AGENTS.md +185 -0
  34. package/src/collections/collections.schemas.ts +195 -0
  35. package/src/collections/collections.ts +1160 -0
  36. package/src/config/config.ts +118 -0
  37. package/src/daemon/AGENTS.md +168 -0
  38. package/src/daemon/daemon.config.ts +23 -0
  39. package/src/daemon/daemon.manager.ts +215 -0
  40. package/src/daemon/daemon.schemas.ts +22 -0
  41. package/src/daemon/daemon.ts +205 -0
  42. package/src/database/AGENTS.md +211 -0
  43. package/src/database/database.ts +64 -0
  44. package/src/database/migrations/migrations.001-init.ts +56 -0
  45. package/src/database/migrations/migrations.002-fts5.ts +32 -0
  46. package/src/database/migrations/migrations.ts +20 -0
  47. package/src/database/migrations/migrations.types.ts +9 -0
  48. package/src/documents/AGENTS.md +301 -0
  49. package/src/documents/documents.schemas.ts +190 -0
  50. package/src/documents/documents.ts +734 -0
  51. package/src/embedder/embedder.ts +53 -0
  52. package/src/exports.ts +0 -0
  53. package/src/mcp/AGENTS.md +264 -0
  54. package/src/mcp/mcp.ts +105 -0
  55. package/src/tools/AGENTS.md +228 -0
  56. package/src/tools/agent/agent.ts +45 -0
  57. package/src/tools/documents/documents.ts +401 -0
  58. package/src/tools/tools.langchain.ts +37 -0
  59. package/src/tools/tools.mcp.ts +46 -0
  60. package/src/tools/tools.types.ts +35 -0
  61. package/src/utils/utils.services.ts +46 -0
@@ -0,0 +1,491 @@
1
+ import { existsSync, writeFileSync } from 'node:fs';
2
+ import { resolve, basename } from 'node:path';
3
+
4
+ import type { Command } from 'commander';
5
+
6
+ import {
7
+ formatHeader,
8
+ formatSuccess,
9
+ formatError,
10
+ formatInfo,
11
+ formatWarning,
12
+ formatTableHeader,
13
+ formatTableRow,
14
+ withErrorHandling,
15
+ chalk,
16
+ } from './cli.utils.ts';
17
+ import { createCliClient } from './cli.client.ts';
18
+
19
+ import { Services } from '#root/utils/utils.services.ts';
20
+ import { CollectionsService } from '#root/collections/collections.ts';
21
+ import type { CollectionSpec, Manifest } from '#root/collections/collections.schemas.ts';
22
+ import { config } from '#root/config/config.ts';
23
+ import type { GetBackendAPIResponse } from '#root/backend/backend.types.ts';
24
+
25
+ type SyncResult = GetBackendAPIResponse<'collections', 'sync'>;
26
+
27
+ const createCollectionsCli = (command: Command) => {
28
+ command.description('Manage collection packages for AI context');
29
+
30
+ // collections init
31
+ command
32
+ .command('init')
33
+ .description('Create a new project config file')
34
+ .option('-f, --force', 'Overwrite existing file')
35
+ .action(
36
+ withErrorHandling(async (options: { force?: boolean }) => {
37
+ const services = new Services();
38
+ try {
39
+ const collectionsService = services.get(CollectionsService);
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ const configFile = (config as any).get('project.configFile') as string;
42
+
43
+ if (collectionsService.projectConfigExists() && !options.force) {
44
+ formatError(`${configFile} already exists. Use --force to overwrite.`);
45
+ return;
46
+ }
47
+
48
+ collectionsService.initProjectConfig(process.cwd(), options.force);
49
+ formatSuccess(`Created ${configFile}`);
50
+ } finally {
51
+ await services.destroy();
52
+ }
53
+ }),
54
+ );
55
+
56
+ // collections add
57
+ command
58
+ .command('add')
59
+ .argument('<name>', 'Name/alias for the collection')
60
+ .argument(
61
+ '<url>',
62
+ 'Manifest or bundle URL (supports https://, file://, git+https://, git+ssh://, or relative paths)',
63
+ )
64
+ .description('Add a collection to project or global config')
65
+ .option('-g, --global', 'Add to global config instead of project config')
66
+ .action(
67
+ withErrorHandling(async (name: string, url: string, options: { global?: boolean }) => {
68
+ const services = new Services();
69
+ try {
70
+ const collectionsService = services.get(CollectionsService);
71
+
72
+ if (!options.global && !collectionsService.projectConfigExists()) {
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
+ const configFile = (config as any).get('project.configFile') as string;
75
+ formatError(`No ${configFile} found. Run 'collections init' first, or use -g for global.`);
76
+ return;
77
+ }
78
+
79
+ // Normalize local paths to file:// URLs (but not git URLs)
80
+ let normalizedUrl = url;
81
+ if (
82
+ !url.startsWith('http://') &&
83
+ !url.startsWith('https://') &&
84
+ !url.startsWith('file://') &&
85
+ !url.startsWith('git+')
86
+ ) {
87
+ normalizedUrl = `file://${url}`;
88
+ }
89
+
90
+ const spec: CollectionSpec = { url: normalizedUrl };
91
+ collectionsService.addToConfig(name, spec, { global: options.global });
92
+
93
+ const scope = options.global ? 'global config' : 'project config';
94
+ formatSuccess(`Added collection "${name}" to ${scope} (${normalizedUrl})`);
95
+ } finally {
96
+ await services.destroy();
97
+ }
98
+ }),
99
+ );
100
+
101
+ // collections remove
102
+ command
103
+ .command('remove')
104
+ .argument('<name>', 'Name of the collection to remove')
105
+ .description('Remove a collection from project or global config')
106
+ .option('-g, --global', 'Remove from global config instead of project config')
107
+ .option('--drop', 'Also drop indexed data from database')
108
+ .action(
109
+ withErrorHandling(async (name: string, options: { global?: boolean; drop?: boolean }) => {
110
+ const services = new Services();
111
+ const client = options.drop ? await createCliClient() : null;
112
+ try {
113
+ const collectionsService = services.get(CollectionsService);
114
+
115
+ if (options.global) {
116
+ if (!collectionsService.globalConfigExists()) {
117
+ formatError('No global config found.');
118
+ return;
119
+ }
120
+ } else {
121
+ if (!collectionsService.projectConfigExists()) {
122
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
+ const configFile = (config as any).get('project.configFile') as string;
124
+ formatError(`No ${configFile} found.`);
125
+ return;
126
+ }
127
+ }
128
+
129
+ const spec = collectionsService.getFromConfig(name, { global: options.global });
130
+ if (!spec) {
131
+ const scope = options.global ? 'global config' : 'project config';
132
+ formatError(`Collection "${name}" not found in ${scope}.`);
133
+ return;
134
+ }
135
+
136
+ if (options.drop && client) {
137
+ const collectionId = collectionsService.computeCollectionId(spec);
138
+ await client.documents.dropCollection({ collection: collectionId });
139
+ await client.collections.delete({ id: collectionId });
140
+ formatInfo(`Dropped indexed data for "${name}"`);
141
+ }
142
+
143
+ collectionsService.removeFromConfig(name, { global: options.global });
144
+ const scope = options.global ? 'global config' : 'project config';
145
+ formatSuccess(`Removed "${name}" from ${scope}`);
146
+ } finally {
147
+ if (client) {
148
+ await client.disconnect();
149
+ }
150
+ await services.destroy();
151
+ }
152
+ }),
153
+ );
154
+
155
+ // collections list
156
+ command
157
+ .command('list')
158
+ .alias('ls')
159
+ .description('List configured collections and their status')
160
+ .option('-g, --global', 'Show only global collections')
161
+ .option('--no-global', 'Show only local collections')
162
+ .action(
163
+ withErrorHandling(async (options: { global?: boolean }) => {
164
+ const services = new Services();
165
+ const client = await createCliClient();
166
+ try {
167
+ const collectionsService = services.get(CollectionsService);
168
+
169
+ // Determine which collections to show
170
+ let entries: [string, CollectionSpec, 'local' | 'global'][] = [];
171
+
172
+ if (options.global === true) {
173
+ // Show only global
174
+ if (!collectionsService.globalConfigExists()) {
175
+ formatInfo('No global collections configured. Use "collections add -g" to add one.');
176
+ return;
177
+ }
178
+ const globalConfig = collectionsService.readGlobalConfig();
179
+ entries = Object.entries(globalConfig.collections).map(([name, spec]) => [name, spec, 'global']);
180
+ } else if (options.global === false) {
181
+ // Show only local
182
+ if (!collectionsService.projectConfigExists()) {
183
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
+ const configFile = (config as any).get('project.configFile') as string;
185
+ formatInfo(`No ${configFile} found. Run 'collections init' first.`);
186
+ return;
187
+ }
188
+ const projectConfig = collectionsService.readProjectConfig();
189
+ entries = Object.entries(projectConfig.collections).map(([name, spec]) => [name, spec, 'local']);
190
+ } else {
191
+ // Show both (default)
192
+ const allCollections = collectionsService.getAllCollections();
193
+ entries = Array.from(allCollections.entries()).map(([name, { spec, source }]) => [name, spec, source]);
194
+ }
195
+
196
+ if (entries.length === 0) {
197
+ formatInfo('No collections configured. Use "collections add" to add one.');
198
+ return;
199
+ }
200
+
201
+ formatHeader('Collections');
202
+
203
+ const maxNameLen = Math.max(...entries.map(([name]) => name.length), 10);
204
+ const urlLengths = entries.map(([, spec]) => spec.url.length);
205
+ const maxUrlLen = Math.min(Math.max(...urlLengths, 20), 45);
206
+ const showSource = options.global === undefined; // Show source column when showing both
207
+
208
+ const columns = [
209
+ { name: 'Name', width: maxNameLen },
210
+ { name: 'URL', width: maxUrlLen },
211
+ ...(showSource ? [{ name: 'Source', width: 8 }] : []),
212
+ { name: 'Status', width: 14 },
213
+ ];
214
+
215
+ formatTableHeader(columns);
216
+
217
+ for (const [name, spec, source] of entries) {
218
+ const status = await client.collections.getSyncStatus({ spec });
219
+ const statusText = status === 'synced' ? chalk.green('✓ synced') : chalk.yellow('⚠ not synced');
220
+
221
+ let url = spec.url;
222
+ if (url.length > maxUrlLen) {
223
+ url = url.substring(0, maxUrlLen - 3) + '...';
224
+ }
225
+
226
+ const sourceColor = source === 'local' ? chalk.blue : chalk.magenta;
227
+
228
+ const row = [
229
+ { value: name, width: maxNameLen, color: chalk.cyan },
230
+ { value: url, width: maxUrlLen, color: chalk.white },
231
+ ...(showSource ? [{ value: source, width: 8, color: sourceColor }] : []),
232
+ { value: statusText, width: 14 },
233
+ ];
234
+
235
+ formatTableRow(row);
236
+ }
237
+
238
+ console.log();
239
+ } finally {
240
+ await client.disconnect();
241
+ await services.destroy();
242
+ }
243
+ }),
244
+ );
245
+
246
+ // collections sync
247
+ command
248
+ .command('sync')
249
+ .argument('[name]', 'Name of specific collection to sync (omit for all)')
250
+ .description('Sync collection(s) from config')
251
+ .option('-g, --global', 'Sync only global collections')
252
+ .option('--no-global', 'Sync only local collections')
253
+ .option('-f, --force', 'Re-index all documents (ignore hash cache)')
254
+ .option('--dry-run', 'Show what would happen without making changes')
255
+ .action(
256
+ withErrorHandling(
257
+ async (name: string | undefined, options: { global?: boolean; force?: boolean; dryRun?: boolean }) => {
258
+ const services = new Services();
259
+ try {
260
+ const collectionsService = services.get(CollectionsService);
261
+
262
+ // Build list of collections to sync based on options
263
+ let toSync: [string, CollectionSpec, 'local' | 'global'][] = [];
264
+
265
+ if (name) {
266
+ // Syncing a specific collection by name
267
+ if (options.global === true) {
268
+ // Explicitly global
269
+ const spec = collectionsService.getFromConfig(name, { global: true });
270
+ if (!spec) {
271
+ formatError(`Collection "${name}" not found in global config.`);
272
+ return;
273
+ }
274
+ toSync = [[name, spec, 'global']];
275
+ } else if (options.global === false) {
276
+ // Explicitly local
277
+ const spec = collectionsService.getFromConfig(name, { global: false });
278
+ if (!spec) {
279
+ formatError(`Collection "${name}" not found in project config.`);
280
+ return;
281
+ }
282
+ toSync = [[name, spec, 'local']];
283
+ } else {
284
+ // Search local first, then global
285
+ const localSpec = collectionsService.getFromConfig(name, { global: false });
286
+ if (localSpec) {
287
+ toSync = [[name, localSpec, 'local']];
288
+ } else {
289
+ const globalSpec = collectionsService.getFromConfig(name, { global: true });
290
+ if (globalSpec) {
291
+ toSync = [[name, globalSpec, 'global']];
292
+ } else {
293
+ formatError(`Collection "${name}" not found in project or global config.`);
294
+ return;
295
+ }
296
+ }
297
+ }
298
+ } else {
299
+ // Syncing all collections
300
+ if (options.global === true) {
301
+ // Only global
302
+ if (!collectionsService.globalConfigExists()) {
303
+ formatInfo('No global collections configured. Use "collections add -g" to add one.');
304
+ return;
305
+ }
306
+ const globalConfig = collectionsService.readGlobalConfig();
307
+ toSync = Object.entries(globalConfig.collections).map(([n, spec]) => [n, spec, 'global']);
308
+ } else if (options.global === false) {
309
+ // Only local
310
+ if (!collectionsService.projectConfigExists()) {
311
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
312
+ const configFile = (config as any).get('project.configFile') as string;
313
+ formatError(`No ${configFile} found. Run 'collections init' first.`);
314
+ return;
315
+ }
316
+ const projectConfig = collectionsService.readProjectConfig();
317
+ toSync = Object.entries(projectConfig.collections).map(([n, spec]) => [n, spec, 'local']);
318
+ } else {
319
+ // Both local and global (default)
320
+ const allCollections = collectionsService.getAllCollections();
321
+ toSync = Array.from(allCollections.entries()).map(([n, { spec, source }]) => [n, spec, source]);
322
+ }
323
+ }
324
+
325
+ if (toSync.length === 0) {
326
+ formatInfo('No collections configured. Use "collections add" to add one.');
327
+ return;
328
+ }
329
+
330
+ // Handle dry-run mode separately
331
+ if (options.dryRun) {
332
+ formatWarning('Dry run mode - no changes will be made');
333
+ console.log();
334
+
335
+ for (const [collectionName, , source] of toSync) {
336
+ console.log(chalk.bold(`Syncing ${collectionName}`) + chalk.dim(` (${source})...`));
337
+ formatInfo(' Would sync this collection');
338
+ console.log();
339
+ }
340
+
341
+ console.log(chalk.green.bold('All collections synced.'));
342
+ return;
343
+ }
344
+
345
+ // Actually sync collections
346
+ const client = await createCliClient();
347
+ try {
348
+ for (const [collectionName, spec, source] of toSync) {
349
+ console.log(chalk.bold(`Syncing ${collectionName}`) + chalk.dim(` (${source})...`));
350
+
351
+ const result = await client.collections.sync({
352
+ name: collectionName,
353
+ spec,
354
+ cwd: process.cwd(),
355
+ force: options.force,
356
+ });
357
+
358
+ printSyncResult(collectionName, result);
359
+ }
360
+ } finally {
361
+ await client.disconnect();
362
+ }
363
+
364
+ console.log(chalk.green.bold('All collections synced.'));
365
+ } finally {
366
+ await services.destroy();
367
+ }
368
+ },
369
+ ),
370
+ );
371
+
372
+ // collections manifest (subcommand group)
373
+ const manifestCmd = command.command('manifest').description('Manifest management commands');
374
+
375
+ // collections manifest init
376
+ manifestCmd
377
+ .command('init')
378
+ .description('Create a manifest.json for publishing')
379
+ .option('-n, --name <name>', 'Package name')
380
+ .option('-v, --version <version>', 'Package version', '1.0.0')
381
+ .action(
382
+ withErrorHandling(async (options: { name?: string; version?: string }) => {
383
+ const manifestPath = resolve(process.cwd(), 'manifest.json');
384
+
385
+ if (existsSync(manifestPath)) {
386
+ formatError('manifest.json already exists');
387
+ return;
388
+ }
389
+
390
+ const manifest: Manifest = {
391
+ name: options.name || basename(process.cwd()),
392
+ version: options.version || '1.0.0',
393
+ description: '',
394
+ sources: {
395
+ glob: ['**/*.md'],
396
+ },
397
+ };
398
+
399
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
400
+ formatSuccess('Created manifest.json');
401
+ }),
402
+ );
403
+
404
+ // collections pack
405
+ command
406
+ .command('pack')
407
+ .description('Create a distributable bundle from a manifest')
408
+ .option('-m, --manifest <path>', 'Path to manifest file', 'manifest.json')
409
+ .option('-o, --output <path>', 'Output path for bundle')
410
+ .action(
411
+ withErrorHandling(async (options: { manifest: string; output?: string }) => {
412
+ const { createHash } = await import('node:crypto');
413
+ const { readFile, glob } = await import('node:fs/promises');
414
+ const tar = await import('tar');
415
+
416
+ const manifestPath = resolve(process.cwd(), options.manifest);
417
+
418
+ if (!existsSync(manifestPath)) {
419
+ formatError(`Manifest not found: ${manifestPath}`);
420
+ return;
421
+ }
422
+
423
+ const manifestContent = await readFile(manifestPath, 'utf8');
424
+ const manifest = JSON.parse(manifestContent) as Manifest;
425
+
426
+ formatHeader('Creating Bundle');
427
+ formatInfo(`Name: ${chalk.cyan(manifest.name)}`);
428
+ formatInfo(`Version: ${chalk.cyan(manifest.version)}`);
429
+ console.log();
430
+
431
+ const manifestDir = manifestPath.substring(0, manifestPath.lastIndexOf('/'));
432
+
433
+ // Collect files to include
434
+ const filesToInclude: string[] = ['manifest.json'];
435
+
436
+ if ('glob' in manifest.sources) {
437
+ for (const pattern of manifest.sources.glob) {
438
+ for await (const file of glob(pattern, { cwd: manifestDir || process.cwd() })) {
439
+ filesToInclude.push(file);
440
+ }
441
+ }
442
+ } else if ('files' in manifest.sources) {
443
+ for (const entry of manifest.sources.files) {
444
+ const path = typeof entry === 'string' ? entry : entry.path;
445
+ if (path) {
446
+ filesToInclude.push(path);
447
+ }
448
+ }
449
+ }
450
+
451
+ formatInfo(`Including ${filesToInclude.length} files`);
452
+
453
+ // Create bundle
454
+ const outputPath = options.output || `${manifest.name}-${manifest.version}.tar.gz`;
455
+ const fullOutputPath = resolve(process.cwd(), outputPath);
456
+
457
+ await tar.create(
458
+ {
459
+ gzip: true,
460
+ file: fullOutputPath,
461
+ cwd: manifestDir || process.cwd(),
462
+ },
463
+ filesToInclude,
464
+ );
465
+
466
+ // Calculate hash
467
+ const bundleContent = await readFile(fullOutputPath);
468
+ const hash = createHash('sha256').update(bundleContent).digest('hex');
469
+
470
+ console.log();
471
+ formatSuccess(`Created ${outputPath}`);
472
+ formatInfo(`SHA256: ${chalk.dim(hash)}`);
473
+ }),
474
+ );
475
+ };
476
+
477
+ const printSyncResult = (name: string, result: SyncResult) => {
478
+ const parts = [];
479
+ if (result.added > 0) parts.push(chalk.green(`${result.added} added`));
480
+ if (result.updated > 0) parts.push(chalk.yellow(`${result.updated} updated`));
481
+ if (result.removed > 0) parts.push(chalk.red(`${result.removed} removed`));
482
+
483
+ if (parts.length === 0) {
484
+ console.log(chalk.green(` ✓ ${result.total} documents (no changes)`));
485
+ } else {
486
+ console.log(chalk.green(` ✓ ${result.total} documents (${parts.join(', ')})`));
487
+ }
488
+ console.log();
489
+ };
490
+
491
+ export { createCollectionsCli };