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,123 @@
1
+ import { z } from 'zod';
2
+
3
+ import {
4
+ referenceDocumentSchema,
5
+ searchChunksOptionsSchema,
6
+ searchChunkItemSchema,
7
+ listDocumentsParamsSchema,
8
+ listDocumentsResultSchema,
9
+ getOutlineParamsSchema,
10
+ outlineResultSchema,
11
+ getSectionParamsSchema,
12
+ sectionResultSchema,
13
+ findRelatedParamsSchema,
14
+ searchBatchParamsSchema,
15
+ searchBatchResultSchema,
16
+ } from '#root/documents/documents.schemas.ts';
17
+
18
+ // Collection info schema (enhanced with metadata for MCP tools v2)
19
+ const collectionInfoSchema = z.object({
20
+ collection: z.string(),
21
+ document_count: z.number(),
22
+ description: z.string().nullable(),
23
+ version: z.string().nullable(),
24
+ });
25
+
26
+ type CollectionInfo = z.infer<typeof collectionInfoSchema>;
27
+
28
+ // Update collection options
29
+ const updateCollectionOptionsSchema = z.object({
30
+ pattern: z.string(),
31
+ cwd: z.string(),
32
+ collection: z.string().optional(),
33
+ });
34
+
35
+ type UpdateCollectionOptions = z.infer<typeof updateCollectionOptionsSchema>;
36
+
37
+ // Drop collection params
38
+ const dropCollectionParamsSchema = z.object({
39
+ collection: z.string(),
40
+ });
41
+
42
+ // Get document params
43
+ const getDocumentParamsSchema = z.object({
44
+ collection: z.string(),
45
+ id: z.string(),
46
+ });
47
+
48
+ // System status schema
49
+ const systemStatusSchema = z.object({
50
+ uptime: z.number(),
51
+ connections: z.number(),
52
+ services: z.array(z.string()),
53
+ });
54
+
55
+ type SystemStatus = z.infer<typeof systemStatusSchema>;
56
+
57
+ // Ping response
58
+ const pingResponseSchema = z.object({
59
+ pong: z.literal(true),
60
+ timestamp: z.number(),
61
+ });
62
+
63
+ // Collections schemas
64
+ const syncCollectionParamsSchema = z.object({
65
+ name: z.string(),
66
+ spec: z.object({ url: z.string() }),
67
+ cwd: z.string(),
68
+ force: z.boolean().optional(),
69
+ });
70
+
71
+ type SyncCollectionParams = z.infer<typeof syncCollectionParamsSchema>;
72
+
73
+ const syncResultSchema = z.object({
74
+ added: z.number(),
75
+ updated: z.number(),
76
+ removed: z.number(),
77
+ total: z.number(),
78
+ });
79
+
80
+ type SyncResult = z.infer<typeof syncResultSchema>;
81
+
82
+ const collectionRecordInfoSchema = z.object({
83
+ id: z.string(),
84
+ url: z.string(),
85
+ name: z.string().nullable(),
86
+ version: z.string().nullable(),
87
+ lastSyncAt: z.string().nullable(),
88
+ });
89
+
90
+ type CollectionRecordInfo = z.infer<typeof collectionRecordInfoSchema>;
91
+
92
+ export type {
93
+ CollectionInfo,
94
+ UpdateCollectionOptions,
95
+ SystemStatus,
96
+ SyncCollectionParams,
97
+ SyncResult,
98
+ CollectionRecordInfo,
99
+ };
100
+ export {
101
+ referenceDocumentSchema,
102
+ searchChunksOptionsSchema,
103
+ searchChunkItemSchema,
104
+ collectionInfoSchema,
105
+ updateCollectionOptionsSchema,
106
+ dropCollectionParamsSchema,
107
+ getDocumentParamsSchema,
108
+ systemStatusSchema,
109
+ pingResponseSchema,
110
+ syncCollectionParamsSchema,
111
+ syncResultSchema,
112
+ collectionRecordInfoSchema,
113
+ // New schemas for MCP tools v2
114
+ listDocumentsParamsSchema,
115
+ listDocumentsResultSchema,
116
+ getOutlineParamsSchema,
117
+ outlineResultSchema,
118
+ getSectionParamsSchema,
119
+ sectionResultSchema,
120
+ findRelatedParamsSchema,
121
+ searchBatchParamsSchema,
122
+ searchBatchResultSchema,
123
+ };
@@ -0,0 +1,151 @@
1
+ import { z } from 'zod';
2
+
3
+ import { procedure } from './backend.protocol.ts';
4
+ import {
5
+ searchChunksOptionsSchema,
6
+ updateCollectionOptionsSchema,
7
+ dropCollectionParamsSchema,
8
+ getDocumentParamsSchema,
9
+ syncCollectionParamsSchema,
10
+ listDocumentsParamsSchema,
11
+ getOutlineParamsSchema,
12
+ getSectionParamsSchema,
13
+ findRelatedParamsSchema,
14
+ searchBatchParamsSchema,
15
+ } from './backend.schemas.ts';
16
+ import type { CollectionInfo, SystemStatus, SyncResult, CollectionRecordInfo } from './backend.schemas.ts';
17
+
18
+ import type { Services } from '#root/utils/utils.services.ts';
19
+ import { DocumentsService } from '#root/documents/documents.ts';
20
+ import { CollectionsService } from '#root/collections/collections.ts';
21
+ import type {
22
+ ReferenceDocument,
23
+ SearchChunkItem,
24
+ ListDocumentsResult,
25
+ OutlineResult,
26
+ SectionResult,
27
+ SearchBatchResult,
28
+ } from '#root/documents/documents.schemas.ts';
29
+
30
+ // Factory to create service procedures with access to Services container
31
+ const createBackendServices = (services: Services, getStatus: () => { uptime: number; connections: number }) => {
32
+ // Documents service procedures
33
+ const documents = {
34
+ listCollections: procedure(z.object({}), async (): Promise<CollectionInfo[]> => {
35
+ const docService = services.get(DocumentsService);
36
+ return docService.listCollections();
37
+ }),
38
+
39
+ dropCollection: procedure(dropCollectionParamsSchema, async (params): Promise<void> => {
40
+ const docService = services.get(DocumentsService);
41
+ await docService.dropCollection(params.collection);
42
+ }),
43
+
44
+ updateCollection: procedure(updateCollectionOptionsSchema, async (params): Promise<void> => {
45
+ const docService = services.get(DocumentsService);
46
+ await docService.updateCollectionFromGlob(params);
47
+ }),
48
+
49
+ search: procedure(searchChunksOptionsSchema, async (params): Promise<SearchChunkItem[]> => {
50
+ const docService = services.get(DocumentsService);
51
+ return docService.search(params);
52
+ }),
53
+
54
+ getDocument: procedure(getDocumentParamsSchema, async (params): Promise<ReferenceDocument | null> => {
55
+ const docService = services.get(DocumentsService);
56
+ return docService.getDocument(params.collection, params.id);
57
+ }),
58
+
59
+ // New procedures for MCP tools v2
60
+ listDocuments: procedure(listDocumentsParamsSchema, async (params): Promise<ListDocumentsResult> => {
61
+ const docService = services.get(DocumentsService);
62
+ return docService.listDocuments(params);
63
+ }),
64
+
65
+ getOutline: procedure(getOutlineParamsSchema, async (params): Promise<OutlineResult | null> => {
66
+ const docService = services.get(DocumentsService);
67
+ return docService.getOutline(params);
68
+ }),
69
+
70
+ getSection: procedure(getSectionParamsSchema, async (params): Promise<SectionResult | null> => {
71
+ const docService = services.get(DocumentsService);
72
+ return docService.getSection(params);
73
+ }),
74
+
75
+ findRelated: procedure(findRelatedParamsSchema, async (params): Promise<SearchChunkItem[]> => {
76
+ const docService = services.get(DocumentsService);
77
+ return docService.findRelated(params);
78
+ }),
79
+
80
+ searchBatch: procedure(searchBatchParamsSchema, async (params): Promise<SearchBatchResult> => {
81
+ const docService = services.get(DocumentsService);
82
+ return docService.searchBatch(params);
83
+ }),
84
+ };
85
+
86
+ // Collections service procedures
87
+ const collections = {
88
+ sync: procedure(syncCollectionParamsSchema, async (params): Promise<SyncResult> => {
89
+ const colService = services.get(CollectionsService);
90
+ return colService.syncCollection(params.name, params.spec, params.cwd, {
91
+ force: params.force,
92
+ });
93
+ }),
94
+
95
+ list: procedure(z.object({}), async (): Promise<CollectionRecordInfo[]> => {
96
+ const colService = services.get(CollectionsService);
97
+ const records = await colService.listCollections();
98
+ return records.map((r) => ({
99
+ id: r.id,
100
+ url: r.url,
101
+ name: r.name,
102
+ version: r.version,
103
+ lastSyncAt: r.last_sync_at,
104
+ }));
105
+ }),
106
+
107
+ getSyncStatus: procedure(
108
+ z.object({
109
+ spec: z.object({ url: z.string() }),
110
+ }),
111
+ async (params): Promise<'synced' | 'not_synced' | 'stale'> => {
112
+ const colService = services.get(CollectionsService);
113
+ return colService.getSyncStatus(params.spec);
114
+ },
115
+ ),
116
+
117
+ delete: procedure(z.object({ id: z.string() }), async (params): Promise<void> => {
118
+ const colService = services.get(CollectionsService);
119
+ await colService.deleteCollection(params.id);
120
+ }),
121
+ };
122
+
123
+ // System procedures
124
+ const system = {
125
+ ping: procedure(z.object({}), async (): Promise<{ pong: true; timestamp: number }> => {
126
+ return { pong: true, timestamp: Date.now() };
127
+ }),
128
+
129
+ status: procedure(z.object({}), async (): Promise<SystemStatus> => {
130
+ const status = getStatus();
131
+ return {
132
+ uptime: status.uptime,
133
+ connections: status.connections,
134
+ services: ['documents', 'collections'],
135
+ };
136
+ }),
137
+
138
+ shutdown: procedure(z.object({}), async (): Promise<void> => {
139
+ // Shutdown is handled by the daemon, this just signals intent
140
+ process.emit('SIGTERM');
141
+ }),
142
+ };
143
+
144
+ return { documents, collections, system };
145
+ };
146
+
147
+ // Type for the services object returned by createBackendServices
148
+ type BackendServices = ReturnType<typeof createBackendServices>;
149
+
150
+ export type { BackendServices };
151
+ export { createBackendServices };
@@ -0,0 +1,111 @@
1
+ import { z } from 'zod';
2
+
3
+ import {
4
+ requestSchema,
5
+ ErrorCodes,
6
+ createSuccessResponse,
7
+ createErrorResponse,
8
+ type Request,
9
+ type Response,
10
+ type Procedure,
11
+ } from './backend.protocol.ts';
12
+ import { createBackendServices, type BackendServices } from './backend.services.ts';
13
+
14
+ import { Services, destroy } from '#root/utils/utils.services.ts';
15
+
16
+ type BackendOptions = {
17
+ services?: Services;
18
+ };
19
+
20
+ class Backend {
21
+ #services: Services;
22
+ #backendServices: BackendServices;
23
+ #startTime: number;
24
+ #connectionCount = 0;
25
+
26
+ constructor(options?: BackendOptions) {
27
+ this.#services = options?.services ?? new Services();
28
+ this.#startTime = Date.now();
29
+ this.#backendServices = createBackendServices(this.#services, () => ({
30
+ uptime: Date.now() - this.#startTime,
31
+ connections: this.#connectionCount,
32
+ }));
33
+ }
34
+
35
+ // Update connection count (called by daemon)
36
+ setConnectionCount(count: number) {
37
+ this.#connectionCount = count;
38
+ }
39
+
40
+ // Get the services definition for type inference
41
+ getServices(): BackendServices {
42
+ return this.#backendServices;
43
+ }
44
+
45
+ // Handle incoming request
46
+ async handleRequest(raw: unknown): Promise<Response> {
47
+ // Parse request
48
+ const parseResult = requestSchema.safeParse(raw);
49
+ if (!parseResult.success) {
50
+ return createErrorResponse(
51
+ 'unknown',
52
+ ErrorCodes.ParseError,
53
+ 'Invalid request format',
54
+ parseResult.error.format(),
55
+ );
56
+ }
57
+
58
+ const request = parseResult.data;
59
+ return this.#routeRequest(request);
60
+ }
61
+
62
+ async #routeRequest(request: Request): Promise<Response> {
63
+ const { id, method, params } = request;
64
+
65
+ // Parse method as "service.method"
66
+ const [serviceName, methodName] = method.split('.');
67
+ if (!serviceName || !methodName) {
68
+ return createErrorResponse(id, ErrorCodes.MethodNotFound, `Invalid method format: ${method}`);
69
+ }
70
+
71
+ // Get procedure using explicit lookup
72
+ const procedure = this.#getProcedure(serviceName, methodName);
73
+ if (!procedure) {
74
+ return createErrorResponse(id, ErrorCodes.MethodNotFound, `Unknown method: ${method}`);
75
+ }
76
+
77
+ // Validate input
78
+ const inputResult = procedure.input.safeParse(params ?? {});
79
+ if (!inputResult.success) {
80
+ return createErrorResponse(id, ErrorCodes.InvalidParams, 'Invalid parameters', inputResult.error.format());
81
+ }
82
+
83
+ // Execute handler
84
+ try {
85
+ const result = await procedure.handler(inputResult.data);
86
+ return createSuccessResponse(id, result);
87
+ } catch (error) {
88
+ const message = error instanceof Error ? error.message : 'Unknown error';
89
+ return createErrorResponse(id, ErrorCodes.ServiceError, message);
90
+ }
91
+ }
92
+
93
+ #getProcedure(serviceName: string, methodName: string): Procedure<z.ZodTypeAny, unknown> | null {
94
+ const services = this.#backendServices;
95
+ if (!(serviceName in services)) {
96
+ return null;
97
+ }
98
+ const service = services[serviceName as keyof typeof services];
99
+ if (!(methodName in service)) {
100
+ return null;
101
+ }
102
+
103
+ return service[methodName as keyof typeof service] as unknown as Procedure<z.ZodTypeAny, unknown>;
104
+ }
105
+
106
+ [destroy] = async () => {
107
+ await this.#services.destroy();
108
+ };
109
+ }
110
+
111
+ export { Backend };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared type definitions for backend services.
3
+ * These types define the contract between backend and client.
4
+ */
5
+
6
+ import type { BackendServices } from './backend.services.ts';
7
+ import type { ProcedureToFunction } from './backend.protocol.ts';
8
+
9
+ /**
10
+ * Complete backend API definition.
11
+ * This is the main interface that clients use.
12
+ */
13
+ export type BackendAPI = {
14
+ [Namespace in keyof BackendServices]: {
15
+ [Method in keyof BackendServices[Namespace]]: ProcedureToFunction<BackendServices[Namespace][Method]>;
16
+ };
17
+ };
18
+
19
+ /**
20
+ * Extract the parameter type for a specific API method.
21
+ * Returns the first parameter type, or undefined if the method takes no parameters.
22
+ */
23
+ export type GetBackendAPIParams<
24
+ Namespace extends keyof BackendAPI,
25
+ Method extends keyof BackendAPI[Namespace],
26
+ > = Parameters<BackendAPI[Namespace][Method]>[0];
27
+
28
+ /**
29
+ * Extract the response type (unwrapped from Promise) for a specific API method.
30
+ */
31
+ export type GetBackendAPIResponse<
32
+ Namespace extends keyof BackendAPI,
33
+ Method extends keyof BackendAPI[Namespace],
34
+ > = Awaited<ReturnType<BackendAPI[Namespace][Method]>>;
@@ -0,0 +1,213 @@
1
+ # CLI — Agent Guidelines
2
+
3
+ This document describes the CLI module architecture for AI agents working on this codebase.
4
+
5
+ ## Overview
6
+
7
+ The CLI module provides the `ctxpkg` command-line interface using [Commander.js](https://github.com/tj/commander.js). Commands are organized by domain, with shared utilities for formatting and error handling.
8
+
9
+ ## File Structure
10
+
11
+ | File | Purpose |
12
+ |------|---------|
13
+ | `cli.ts` | Main entry point, creates program and mounts subcommand groups |
14
+ | `cli.utils.ts` | Shared formatting utilities and error handling |
15
+ | `cli.client.ts` | Factory for creating `BackendClient` with auto mode detection |
16
+ | `cli.collections.ts` | Collection management commands (`init`, `add`, `sync`, etc.) |
17
+ | `cli.documents.ts` | Document commands (`search`, `list-collections`, etc.) |
18
+ | `cli.config.ts` | Configuration commands (`set`, `get`, `list`, `edit`) |
19
+ | `cli.daemon.ts` | Daemon management commands (`start`, `stop`, `status`) |
20
+ | `cli.mcp.ts` | MCP server commands |
21
+
22
+ ## Architecture
23
+
24
+ ```
25
+ ┌─────────────────────────────────────────────────────────────┐
26
+ │ createProgram() │
27
+ │ cli.ts │
28
+ ├─────────────────────────────────────────────────────────────┤
29
+ │ collections documents config daemon mcp │
30
+ │ ─────────── ────────── ────── ────── ─── │
31
+ │ init search set start start │
32
+ │ add list-cols get stop │
33
+ │ remove drop-col list status │
34
+ │ list isearch edit │
35
+ │ sync reset │
36
+ │ pack │
37
+ └─────────────────────────────────────────────────────────────┘
38
+
39
+ ┌─────────────┴─────────────┐
40
+ ▼ ▼
41
+ ┌───────────────┐ ┌───────────────┐
42
+ │ cli.utils.ts │ │ cli.client.ts │
43
+ │ (formatting) │ │ (BackendClient│
44
+ └───────────────┘ │ factory) │
45
+ └───────────────┘
46
+ ```
47
+
48
+ ## Command Structure
49
+
50
+ Commands follow this pattern:
51
+
52
+ ```
53
+ ctxpkg <group> <command> [args] [options]
54
+
55
+ # Examples:
56
+ ctxpkg collections add my-docs ./docs
57
+ ctxpkg documents search "how to authenticate"
58
+ ctxpkg config set openai.apiKey sk-...
59
+ ctxpkg daemon status
60
+ ```
61
+
62
+ ## Adding a New Command
63
+
64
+ ### 1. Add to Existing Domain
65
+
66
+ Add command in the appropriate `cli.<domain>.ts`:
67
+
68
+ ```typescript
69
+ command
70
+ .command('my-command')
71
+ .alias('mc') // Optional short alias
72
+ .argument('<required>', 'Description')
73
+ .argument('[optional]', 'Description')
74
+ .description('What this command does')
75
+ .option('-f, --flag', 'Boolean flag')
76
+ .option('-v, --value <val>', 'With value', 'default')
77
+ .action(
78
+ withErrorHandling(async (required, optional, options) => {
79
+ // Implementation
80
+ formatSuccess('Done!');
81
+ }),
82
+ );
83
+ ```
84
+
85
+ ### 2. Add a New Domain
86
+
87
+ 1. Create `cli.<domain>.ts`:
88
+
89
+ ```typescript
90
+ import type { Command } from 'commander';
91
+ import { formatHeader, formatSuccess, withErrorHandling, chalk } from './cli.utils.ts';
92
+
93
+ const create<Domain>Cli = (command: Command) => {
94
+ command.description('Description of command group');
95
+
96
+ command
97
+ .command('subcommand')
98
+ .description('What this does')
99
+ .action(withErrorHandling(async () => {
100
+ // Implementation
101
+ }));
102
+ };
103
+
104
+ export { create<Domain>Cli };
105
+ ```
106
+
107
+ 2. Mount in `cli.ts`:
108
+
109
+ ```typescript
110
+ import { create<Domain>Cli } from './cli.<domain>.ts';
111
+
112
+ // In createProgram():
113
+ create<Domain>Cli(program.command('<domain>').description('...'));
114
+ ```
115
+
116
+ ## Key Patterns
117
+
118
+ ### Error Handling
119
+
120
+ Always wrap actions with `withErrorHandling()`:
121
+
122
+ ```typescript
123
+ .action(
124
+ withErrorHandling(async (args, options) => {
125
+ // Errors are caught and formatted with formatError()
126
+ // process.exitCode is set to 1 on error
127
+ }),
128
+ )
129
+ ```
130
+
131
+ ### Formatting Output
132
+
133
+ Use utilities from `cli.utils.ts` for consistent output:
134
+
135
+ ```typescript
136
+ formatHeader('Section Title'); // Cyan bordered header
137
+ formatSuccess('Done!'); // Green ✔ prefix
138
+ formatError('Failed'); // Red ✖ prefix
139
+ formatInfo('Note'); // Blue ℹ prefix
140
+ formatWarning('Careful'); // Yellow ⚠ prefix
141
+
142
+ // Tables
143
+ formatTableHeader([
144
+ { name: 'Name', width: 20 },
145
+ { name: 'Value', width: 30 },
146
+ ]);
147
+ formatTableRow([
148
+ { value: 'foo', width: 20, color: chalk.cyan },
149
+ { value: 'bar', width: 30 },
150
+ ]);
151
+ ```
152
+
153
+ ### Backend Client
154
+
155
+ Use `createCliClient()` from `cli.client.ts`:
156
+
157
+ ```typescript
158
+ const client = await createCliClient();
159
+ try {
160
+ const results = await client.documents.search({ query: 'foo' });
161
+ // ...
162
+ } finally {
163
+ await client.disconnect();
164
+ }
165
+ ```
166
+
167
+ Auto mode tries daemon first, falls back to direct (in-process).
168
+
169
+ ### Services Cleanup
170
+
171
+ Always clean up services in `finally` blocks:
172
+
173
+ ```typescript
174
+ const services = new Services();
175
+ const client = await createCliClient();
176
+ try {
177
+ // ...
178
+ } finally {
179
+ await client.disconnect();
180
+ await services.destroy();
181
+ }
182
+ ```
183
+
184
+ ### Interactive Prompts
185
+
186
+ Use `@inquirer/prompts` for interactive input:
187
+
188
+ ```typescript
189
+ import { input, confirm, select } from '@inquirer/prompts';
190
+
191
+ const name = await input({ message: 'Enter name:' });
192
+ const confirmed = await confirm({ message: 'Continue?', default: false });
193
+ const choice = await select({
194
+ message: 'Pick one:',
195
+ choices: [
196
+ { name: 'Option A', value: 'a' },
197
+ { name: 'Option B', value: 'b' },
198
+ ],
199
+ });
200
+ ```
201
+
202
+ ## Testing Commands
203
+
204
+ ```bash
205
+ # Run directly during development
206
+ ./bin/cli.js collections list
207
+ ./bin/cli.js documents search "query"
208
+ ./bin/cli.js config list
209
+
210
+ # With options
211
+ ./bin/cli.js collections sync --force
212
+ ./bin/cli.js documents search "query" --limit 5
213
+ ```