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,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 };
|