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,197 @@
1
+ import type { Command } from 'commander';
2
+
3
+ import { createTestRunner, type TestProgressEvent } from '../agent/agent.test-runner.ts';
4
+ import type { ValidationMode } from '../agent/agent.test-runner.schemas.ts';
5
+
6
+ import {
7
+ chalk,
8
+ formatError,
9
+ formatHeader,
10
+ formatInfo,
11
+ formatSuccess,
12
+ formatWarning,
13
+ withErrorHandling,
14
+ } from './cli.utils.ts';
15
+
16
+ type TestOptions = {
17
+ json?: boolean;
18
+ verbose?: boolean;
19
+ validationMode?: ValidationMode;
20
+ threshold?: number;
21
+ model?: string;
22
+ };
23
+
24
+ /**
25
+ * Format duration in human-readable form
26
+ */
27
+ const formatDuration = (ms: number): string => {
28
+ if (ms < 1000) return `${ms}ms`;
29
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
30
+ return `${(ms / 60000).toFixed(1)}m`;
31
+ };
32
+
33
+ /**
34
+ * Progress callback for verbose mode
35
+ */
36
+ const createProgressCallback = (verbose: boolean) => {
37
+ return (event: TestProgressEvent) => {
38
+ switch (event.type) {
39
+ case 'suite_start':
40
+ formatHeader(`Running: ${event.suiteName}`);
41
+ console.log(`${event.totalTests} test(s) to run\n`);
42
+ break;
43
+
44
+ case 'sync_start':
45
+ if (verbose) {
46
+ formatInfo('Syncing collections...');
47
+ }
48
+ break;
49
+
50
+ case 'sync_complete':
51
+ if (verbose) {
52
+ formatSuccess('Collections synced');
53
+ console.log();
54
+ }
55
+ break;
56
+
57
+ case 'test_start':
58
+ if (verbose) {
59
+ console.log(chalk.dim(`[${event.index + 1}] Running: ${event.testId}`));
60
+ }
61
+ break;
62
+
63
+ case 'test_complete': {
64
+ const { result } = event;
65
+ const icon = result.skipped ? chalk.yellow('○') : result.passed ? chalk.green('✓') : chalk.red('✗');
66
+ const status = result.skipped ? chalk.yellow('SKIP') : result.passed ? chalk.green('PASS') : chalk.red('FAIL');
67
+ const duration = chalk.dim(`(${formatDuration(result.durationMs)})`);
68
+
69
+ console.log(`${icon} ${result.id} ${status} ${duration}`);
70
+
71
+ if (verbose && !result.skipped) {
72
+ if (result.score !== undefined) {
73
+ console.log(chalk.dim(` Score: ${(result.score * 100).toFixed(1)}%`));
74
+ }
75
+ if (result.reasoning) {
76
+ console.log(chalk.dim(` ${result.reasoning}`));
77
+ }
78
+ if (result.error) {
79
+ console.log(chalk.red(` Error: ${result.error}`));
80
+ }
81
+ if (result.keywordsFound?.length || result.keywordsMissing?.length) {
82
+ if (result.keywordsFound?.length) {
83
+ console.log(chalk.dim(` Found: ${result.keywordsFound.join(', ')}`));
84
+ }
85
+ if (result.keywordsMissing?.length) {
86
+ console.log(chalk.dim(` Missing: ${result.keywordsMissing.join(', ')}`));
87
+ }
88
+ }
89
+ }
90
+ break;
91
+ }
92
+
93
+ case 'suite_complete':
94
+ // Summary is handled separately
95
+ break;
96
+ }
97
+ };
98
+ };
99
+
100
+ const createAgentCli = (command: Command) => {
101
+ command.description('Agent testing and evaluation tools');
102
+
103
+ command
104
+ .command('test')
105
+ .description('Run agent tests from a YAML test file')
106
+ .argument('<test-file>', 'Path to YAML test file')
107
+ .option('--json', 'Output results as JSON')
108
+ .option('-v, --verbose', 'Show detailed progress and results')
109
+ .option('-m, --validation-mode <mode>', 'Override validation mode (semantic, llm, keywords)')
110
+ .option('-t, --threshold <number>', 'Override pass threshold (0-1)', parseFloat)
111
+ .option('--model <model>', 'Model to use for LLM validation (defaults to configured model)')
112
+ .action(
113
+ withErrorHandling(async (testFile: string, options: TestOptions) => {
114
+ const runner = createTestRunner();
115
+
116
+ try {
117
+ // Load test suite
118
+ const { suite, baseDir } = await runner.loadTestSuite(testFile);
119
+
120
+ // Validate options
121
+ if (options.validationMode && !['semantic', 'llm', 'keywords'].includes(options.validationMode)) {
122
+ formatError(`Invalid validation mode: ${options.validationMode}. Use: semantic, llm, or keywords`);
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+
127
+ if (options.threshold !== undefined && (options.threshold < 0 || options.threshold > 1)) {
128
+ formatError('Threshold must be between 0 and 1');
129
+ process.exitCode = 1;
130
+ return;
131
+ }
132
+
133
+ // Run tests
134
+ const result = await runner.runTestSuite(suite, {
135
+ onProgress: options.json ? undefined : createProgressCallback(options.verbose ?? false),
136
+ validationMode: options.validationMode as ValidationMode | undefined,
137
+ passThreshold: options.threshold,
138
+ validationModel: options.model,
139
+ baseDir,
140
+ });
141
+
142
+ // Output results
143
+ if (options.json) {
144
+ console.log(JSON.stringify(result, null, 2));
145
+ } else {
146
+ // Summary
147
+ console.log();
148
+ formatHeader('Summary');
149
+ console.log(`Total: ${result.summary.total}`);
150
+ console.log(`Passed: ${chalk.green(result.summary.passed)}`);
151
+ console.log(
152
+ `Failed: ${result.summary.failed > 0 ? chalk.red(result.summary.failed) : result.summary.failed}`,
153
+ );
154
+ console.log(
155
+ `Skipped: ${result.summary.skipped > 0 ? chalk.yellow(result.summary.skipped) : result.summary.skipped}`,
156
+ );
157
+ console.log(`Time: ${formatDuration(result.durationMs)}`);
158
+ console.log();
159
+
160
+ // Show failed test details if not verbose (verbose already shows them)
161
+ if (!options.verbose && result.summary.failed > 0) {
162
+ formatHeader('Failed Tests');
163
+ for (const testResult of result.results) {
164
+ if (!testResult.passed && !testResult.skipped) {
165
+ console.log(chalk.red(`✗ ${testResult.id}`));
166
+ if (testResult.score !== undefined) {
167
+ console.log(chalk.dim(` Score: ${(testResult.score * 100).toFixed(1)}%`));
168
+ }
169
+ if (testResult.reasoning) {
170
+ console.log(chalk.dim(` ${testResult.reasoning}`));
171
+ }
172
+ if (testResult.error) {
173
+ console.log(chalk.red(` Error: ${testResult.error}`));
174
+ }
175
+ console.log();
176
+ }
177
+ }
178
+ }
179
+
180
+ // Final status
181
+ if (result.summary.failed === 0) {
182
+ formatSuccess('All tests passed!');
183
+ } else {
184
+ formatWarning(`${result.summary.failed} test(s) failed`);
185
+ process.exitCode = 1;
186
+ }
187
+ }
188
+ } finally {
189
+ // Clean up - TypeScript doesn't know about the destroy symbol
190
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
191
+ await (runner as any)[Symbol.for('destroy')]?.();
192
+ }
193
+ }),
194
+ );
195
+ };
196
+
197
+ export { createAgentCli };
@@ -0,0 +1,369 @@
1
+ import { input } from '@inquirer/prompts';
2
+ import type { Command } from 'commander';
3
+
4
+ import { createDocumentAgent, getLLMConfigFromAppConfig } from '../agent/agent.ts';
5
+ import type { AgentStep } from '../agent/agent.types.ts';
6
+
7
+ import { createCliClient } from './cli.client.ts';
8
+ import { chalk, formatError, formatHeader, formatInfo, withErrorHandling } from './cli.utils.ts';
9
+
10
+ import { CollectionsService } from '#root/collections/collections.ts';
11
+ import { Services } from '#root/utils/utils.services.ts';
12
+
13
+ type ChatOptions = {
14
+ interactive?: boolean;
15
+ useCase?: string;
16
+ collections?: string[];
17
+ global: boolean;
18
+ model?: string;
19
+ verbose?: boolean;
20
+ };
21
+
22
+ /**
23
+ * Create a verbose step callback that logs agent reasoning to console
24
+ */
25
+ const createVerboseCallback = () => {
26
+ return (step: AgentStep) => {
27
+ switch (step.type) {
28
+ case 'thinking':
29
+ console.log(chalk.dim(` [thinking] ${step.content}`));
30
+ break;
31
+ case 'tool_call':
32
+ console.log(chalk.blue(` [tool] ${step.toolName}`));
33
+ if (step.toolInput) {
34
+ const inputPreview = JSON.stringify(step.toolInput).slice(0, 100);
35
+ console.log(chalk.dim(` Input: ${inputPreview}${inputPreview.length >= 100 ? '...' : ''}`));
36
+ }
37
+ break;
38
+ case 'tool_result':
39
+ console.log(chalk.green(` [result] ${step.content}`));
40
+ break;
41
+ case 'error':
42
+ console.log(chalk.yellow(` [retry] ${step.content}`));
43
+ break;
44
+ }
45
+ };
46
+ };
47
+
48
+ /**
49
+ * Resolve collection names to IDs, returning a list of resolved collection IDs
50
+ */
51
+ const resolveCollectionIds = (
52
+ collections: string[] | undefined,
53
+ aliasMap: Map<string, string>,
54
+ ): string[] | undefined => {
55
+ if (!collections || collections.length === 0) return undefined;
56
+ return collections.map((c) => aliasMap.get(c) ?? c);
57
+ };
58
+
59
+ /**
60
+ * Run one-shot chat mode - ask a single question and get an answer.
61
+ */
62
+ const runOneShotChat = async (query: string, options: ChatOptions) => {
63
+ // Get LLM config
64
+ const llmConfig = await getLLMConfigFromAppConfig();
65
+
66
+ if (!llmConfig.apiKey) {
67
+ formatError('LLM API key not configured. Set it with: ctxpkg config set llm.apiKey <key>');
68
+ formatError('Or use environment variable: CTXPKG_LLM_API_KEY=<key>');
69
+ process.exitCode = 1;
70
+ return;
71
+ }
72
+
73
+ // Override model if specified
74
+ if (options.model) {
75
+ llmConfig.model = options.model;
76
+ }
77
+
78
+ // Prompt for use case if not provided
79
+ let useCase = options.useCase;
80
+ if (!useCase) {
81
+ useCase = await input({
82
+ message: 'What is your use case? (helps find relevant information)',
83
+ });
84
+ }
85
+
86
+ if (!useCase) {
87
+ formatError('Use case is required to help the agent find relevant information.');
88
+ process.exitCode = 1;
89
+ return;
90
+ }
91
+
92
+ const client = await createCliClient();
93
+ const services = new Services();
94
+
95
+ try {
96
+ // Build alias map
97
+ const aliasMap = new Map<string, string>();
98
+ const collectionsService = services.get(CollectionsService);
99
+ const includeGlobal = options.global !== false;
100
+
101
+ if (includeGlobal) {
102
+ const allCollections = collectionsService.getAllCollections();
103
+ for (const [alias, { spec }] of allCollections) {
104
+ const collectionId = collectionsService.computeCollectionId(spec);
105
+ aliasMap.set(alias, collectionId);
106
+ }
107
+ } else if (collectionsService.projectConfigExists()) {
108
+ const projectConfig = collectionsService.readProjectConfig();
109
+ for (const [alias, spec] of Object.entries(projectConfig.collections)) {
110
+ const collectionId = collectionsService.computeCollectionId(spec);
111
+ aliasMap.set(alias, collectionId);
112
+ }
113
+ }
114
+
115
+ // Resolve collection filtering
116
+ const resolvedCollections = resolveCollectionIds(options.collections, aliasMap);
117
+
118
+ // Create verbose callback if needed
119
+ const onStep = options.verbose ? createVerboseCallback() : undefined;
120
+
121
+ // Create agent with collection filtering
122
+ const agent = createDocumentAgent({
123
+ client,
124
+ llmConfig,
125
+ aliasMap,
126
+ collections: resolvedCollections,
127
+ onStep,
128
+ });
129
+
130
+ formatInfo('Searching documentation...\n');
131
+
132
+ // Ask the question
133
+ const response = await agent.ask(query, useCase);
134
+
135
+ // Display answer
136
+ formatHeader('Answer');
137
+ console.log(response.answer);
138
+ console.log();
139
+
140
+ // Display sources
141
+ if (response.sources.length > 0) {
142
+ formatHeader('Sources');
143
+ for (const source of response.sources) {
144
+ const section = source.section ? ` → "${source.section}"` : '';
145
+ console.log(`${chalk.dim('•')} ${chalk.cyan(source.collection)}: ${source.document}${section}`);
146
+ }
147
+ console.log();
148
+ }
149
+
150
+ // Display confidence and note
151
+ const confidenceColor =
152
+ response.confidence === 'high' ? chalk.green : response.confidence === 'medium' ? chalk.yellow : chalk.red;
153
+ console.log(`Confidence: ${confidenceColor(response.confidence)}`);
154
+
155
+ if (response.note) {
156
+ console.log(`\n${chalk.dim('Note:')} ${response.note}`);
157
+ }
158
+ } finally {
159
+ await client.disconnect();
160
+ await services.destroy();
161
+ }
162
+ };
163
+
164
+ /**
165
+ * Run interactive chat mode - continuous conversation with the agent.
166
+ */
167
+ const runInteractiveChat = async (options: ChatOptions) => {
168
+ // Get LLM config
169
+ const llmConfig = await getLLMConfigFromAppConfig();
170
+
171
+ if (!llmConfig.apiKey) {
172
+ formatError('LLM API key not configured. Set it with: ctxpkg config set llm.apiKey <key>');
173
+ formatError('Or use environment variable: CTXPKG_LLM_API_KEY=<key>');
174
+ process.exitCode = 1;
175
+ return;
176
+ }
177
+
178
+ // Override model if specified
179
+ if (options.model) {
180
+ llmConfig.model = options.model;
181
+ }
182
+
183
+ const client = await createCliClient();
184
+ const services = new Services();
185
+
186
+ try {
187
+ // Build alias map
188
+ const aliasMap = new Map<string, string>();
189
+ const collectionsService = services.get(CollectionsService);
190
+ const includeGlobal = options.global !== false;
191
+
192
+ if (includeGlobal) {
193
+ const allCollections = collectionsService.getAllCollections();
194
+ for (const [alias, { spec }] of allCollections) {
195
+ const collectionId = collectionsService.computeCollectionId(spec);
196
+ aliasMap.set(alias, collectionId);
197
+ }
198
+ } else if (collectionsService.projectConfigExists()) {
199
+ const projectConfig = collectionsService.readProjectConfig();
200
+ for (const [alias, spec] of Object.entries(projectConfig.collections)) {
201
+ const collectionId = collectionsService.computeCollectionId(spec);
202
+ aliasMap.set(alias, collectionId);
203
+ }
204
+ }
205
+
206
+ // Resolve collection filtering
207
+ const resolvedCollections = resolveCollectionIds(options.collections, aliasMap);
208
+
209
+ // Track verbose mode state
210
+ let verbose = options.verbose ?? false;
211
+
212
+ // Create agent with collection filtering
213
+ const agent = createDocumentAgent({
214
+ client,
215
+ llmConfig,
216
+ aliasMap,
217
+ collections: resolvedCollections,
218
+ });
219
+
220
+ // Display header
221
+ formatHeader('ctxpkg Chat');
222
+ console.log('Type your questions. Commands: /help, /use-case, /clear, /verbose, /quit');
223
+ console.log();
224
+
225
+ // Get initial use case
226
+ let useCase = options.useCase;
227
+ if (!useCase) {
228
+ useCase = await input({
229
+ message: 'What are you trying to accomplish?',
230
+ });
231
+ }
232
+
233
+ if (!useCase) {
234
+ formatError('Use case is required.');
235
+ process.exitCode = 1;
236
+ return;
237
+ }
238
+
239
+ console.log();
240
+ console.log(chalk.dim(`Use case: ${useCase}`));
241
+ if (verbose) {
242
+ console.log(chalk.dim('Verbose mode: on'));
243
+ }
244
+ console.log();
245
+
246
+ // Chat loop
247
+ while (true) {
248
+ let message: string;
249
+ try {
250
+ message = await input({ message: chalk.cyan('You:') });
251
+ } catch {
252
+ // Handle Ctrl+C
253
+ console.log('\nGoodbye!');
254
+ break;
255
+ }
256
+
257
+ if (!message.trim()) {
258
+ continue;
259
+ }
260
+
261
+ // Handle commands
262
+ if (message.startsWith('/')) {
263
+ const cmd = message.toLowerCase().trim();
264
+
265
+ if (cmd === '/quit' || cmd === '/exit' || cmd === '/q') {
266
+ console.log('Goodbye!');
267
+ break;
268
+ }
269
+
270
+ if (cmd === '/help' || cmd === '/h') {
271
+ console.log('\nCommands:');
272
+ console.log(' /help, /h Show this help');
273
+ console.log(' /use-case, /u Change use case');
274
+ console.log(' /clear, /c Clear conversation history');
275
+ console.log(' /verbose, /v Toggle verbose mode');
276
+ console.log(' /quit, /q Exit chat\n');
277
+ continue;
278
+ }
279
+
280
+ if (cmd === '/use-case' || cmd === '/u') {
281
+ console.log(chalk.dim(`Current use case: ${useCase}`));
282
+ const newUseCase = await input({ message: 'New use case (or press enter to keep current):' });
283
+ if (newUseCase.trim()) {
284
+ useCase = newUseCase;
285
+ agent.clearHistory(); // Clear history when use case changes
286
+ console.log(chalk.dim(`Use case updated: ${useCase}`));
287
+ console.log(chalk.dim('Conversation history cleared.\n'));
288
+ }
289
+ continue;
290
+ }
291
+
292
+ if (cmd === '/clear' || cmd === '/c') {
293
+ agent.clearHistory();
294
+ console.log(chalk.dim('Conversation history cleared.\n'));
295
+ continue;
296
+ }
297
+
298
+ if (cmd === '/verbose' || cmd === '/v') {
299
+ verbose = !verbose;
300
+ console.log(chalk.dim(`Verbose mode: ${verbose ? 'on' : 'off'}\n`));
301
+ continue;
302
+ }
303
+
304
+ console.log(chalk.yellow(`Unknown command: ${message}. Type /help for available commands.\n`));
305
+ continue;
306
+ }
307
+
308
+ // Ask the question using chat (maintains history)
309
+ console.log();
310
+ formatInfo('Searching...');
311
+
312
+ try {
313
+ const onStep = verbose ? createVerboseCallback() : undefined;
314
+ const response = await agent.chat(message, useCase, { onStep });
315
+
316
+ console.log();
317
+ console.log(response.answer);
318
+ console.log();
319
+
320
+ if (response.sources.length > 0) {
321
+ console.log(chalk.dim('Sources:'));
322
+ for (const source of response.sources) {
323
+ const section = source.section ? ` → "${source.section}"` : '';
324
+ console.log(chalk.dim(` • ${source.collection}: ${source.document}${section}`));
325
+ }
326
+ console.log();
327
+ }
328
+
329
+ // Show conversation length in verbose mode
330
+ if (verbose) {
331
+ console.log(chalk.dim(`[${agent.getHistoryLength()} messages in history]\n`));
332
+ }
333
+ } catch (error) {
334
+ const errMessage = error instanceof Error ? error.message : String(error);
335
+ formatError(`Error: ${errMessage}`);
336
+ console.log();
337
+ }
338
+ }
339
+ } finally {
340
+ await client.disconnect();
341
+ await services.destroy();
342
+ }
343
+ };
344
+
345
+ const createChatCli = (command: Command) => {
346
+ command
347
+ .description('Chat with your documentation using AI')
348
+ .argument('[query]', 'Question to ask (starts one-shot mode)')
349
+ .option('-i, --interactive', 'Start interactive chat session')
350
+ .option('-u, --use-case <text>', 'Context for why you need this information')
351
+ .option('-c, --collections <names...>', 'Limit to specific collections')
352
+ .option('--no-global', 'Exclude global collections')
353
+ .option('--model <model>', 'Override LLM model from config')
354
+ .option('--verbose', 'Show agent reasoning (not yet implemented)')
355
+ .action(
356
+ withErrorHandling(async (query: string | undefined, options: ChatOptions) => {
357
+ if (options.interactive) {
358
+ await runInteractiveChat(options);
359
+ } else if (query) {
360
+ await runOneShotChat(query, options);
361
+ } else {
362
+ // No query and not interactive - start interactive mode
363
+ await runInteractiveChat(options);
364
+ }
365
+ }),
366
+ );
367
+ };
368
+
369
+ export { createChatCli };
@@ -0,0 +1,55 @@
1
+ import { BackendClient } from '#root/client/client.ts';
2
+ import { DaemonManager } from '#root/daemon/daemon.manager.ts';
3
+
4
+ type CliClientMode = 'auto' | 'direct' | 'daemon';
5
+
6
+ type CliClientOptions = {
7
+ mode?: CliClientMode;
8
+ socketPath?: string;
9
+ timeout?: number;
10
+ };
11
+
12
+ // Factory function for CLI commands
13
+ const createCliClient = async (options?: CliClientOptions): Promise<BackendClient> => {
14
+ const mode = options?.mode ?? 'auto';
15
+
16
+ if (mode === 'auto') {
17
+ // Try daemon first, fall back to direct
18
+ const manager = new DaemonManager({ socketPath: options?.socketPath });
19
+ const isDaemonRunning = await manager.isRunning();
20
+
21
+ if (isDaemonRunning) {
22
+ const client = new BackendClient({
23
+ mode: 'daemon',
24
+ socketPath: options?.socketPath,
25
+ timeout: options?.timeout,
26
+ });
27
+ await client.connect();
28
+ return client;
29
+ }
30
+
31
+ // Fall back to direct mode
32
+ const client = new BackendClient({ mode: 'direct' });
33
+ await client.connect();
34
+ return client;
35
+ }
36
+
37
+ if (mode === 'daemon') {
38
+ const client = new BackendClient({
39
+ mode: 'daemon',
40
+ socketPath: options?.socketPath,
41
+ autoStartDaemon: true,
42
+ timeout: options?.timeout,
43
+ });
44
+ await client.connect();
45
+ return client;
46
+ }
47
+
48
+ // Direct mode
49
+ const client = new BackendClient({ mode: 'direct' });
50
+ await client.connect();
51
+ return client;
52
+ };
53
+
54
+ export { createCliClient };
55
+ export type { CliClientOptions, CliClientMode };