@inkeep/agents-cli 0.1.0
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.md +51 -0
- package/README.md +512 -0
- package/dist/__tests__/api.test.d.ts +1 -0
- package/dist/__tests__/api.test.js +257 -0
- package/dist/__tests__/cli.test.d.ts +1 -0
- package/dist/__tests__/cli.test.js +153 -0
- package/dist/__tests__/commands/config.test.d.ts +1 -0
- package/dist/__tests__/commands/config.test.js +154 -0
- package/dist/__tests__/commands/init.test.d.ts +1 -0
- package/dist/__tests__/commands/init.test.js +186 -0
- package/dist/__tests__/commands/pull-retry.test.d.ts +1 -0
- package/dist/__tests__/commands/pull-retry.test.js +156 -0
- package/dist/__tests__/commands/pull.test.d.ts +1 -0
- package/dist/__tests__/commands/pull.test.js +54 -0
- package/dist/__tests__/commands/push-spinner.test.d.ts +1 -0
- package/dist/__tests__/commands/push-spinner.test.js +127 -0
- package/dist/__tests__/commands/push.test.d.ts +1 -0
- package/dist/__tests__/commands/push.test.js +265 -0
- package/dist/__tests__/config-validation.test.d.ts +1 -0
- package/dist/__tests__/config-validation.test.js +106 -0
- package/dist/__tests__/package.test.d.ts +1 -0
- package/dist/__tests__/package.test.js +82 -0
- package/dist/__tests__/utils/json-comparator.test.d.ts +1 -0
- package/dist/__tests__/utils/json-comparator.test.js +174 -0
- package/dist/__tests__/utils/port-manager.test.d.ts +1 -0
- package/dist/__tests__/utils/port-manager.test.js +144 -0
- package/dist/__tests__/utils/ts-loader.test.d.ts +1 -0
- package/dist/__tests__/utils/ts-loader.test.js +233 -0
- package/dist/api.d.ts +23 -0
- package/dist/api.js +140 -0
- package/dist/commands/chat-enhanced.d.ts +7 -0
- package/dist/commands/chat-enhanced.js +396 -0
- package/dist/commands/chat.d.ts +5 -0
- package/dist/commands/chat.js +125 -0
- package/dist/commands/config.d.ts +6 -0
- package/dist/commands/config.js +128 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +171 -0
- package/dist/commands/list-graphs.d.ts +6 -0
- package/dist/commands/list-graphs.js +131 -0
- package/dist/commands/mcp-list.d.ts +4 -0
- package/dist/commands/mcp-list.js +156 -0
- package/dist/commands/mcp-start-simple.d.ts +5 -0
- package/dist/commands/mcp-start-simple.js +193 -0
- package/dist/commands/mcp-start.d.ts +5 -0
- package/dist/commands/mcp-start.js +217 -0
- package/dist/commands/mcp-status.d.ts +1 -0
- package/dist/commands/mcp-status.js +96 -0
- package/dist/commands/mcp-stop.d.ts +5 -0
- package/dist/commands/mcp-stop.js +160 -0
- package/dist/commands/pull.d.ts +15 -0
- package/dist/commands/pull.js +313 -0
- package/dist/commands/pull.llm-generate.d.ts +10 -0
- package/dist/commands/pull.llm-generate.js +184 -0
- package/dist/commands/push.d.ts +6 -0
- package/dist/commands/push.js +268 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +292 -0
- package/dist/exports.d.ts +2 -0
- package/dist/exports.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +98 -0
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.js +3 -0
- package/dist/types/graph.d.ts +10 -0
- package/dist/types/graph.js +1 -0
- package/dist/utils/json-comparator.d.ts +60 -0
- package/dist/utils/json-comparator.js +222 -0
- package/dist/utils/mcp-runner.d.ts +6 -0
- package/dist/utils/mcp-runner.js +147 -0
- package/dist/utils/port-manager.d.ts +43 -0
- package/dist/utils/port-manager.js +92 -0
- package/dist/utils/ts-loader.d.ts +5 -0
- package/dist/utils/ts-loader.js +146 -0
- package/package.json +77 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, extname, resolve } from 'node:path';
|
|
4
|
+
import * as readline from 'node:readline';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
import { ManagementApiClient, ExecutionApiClient } from '../api.js';
|
|
10
|
+
import { validateConfiguration } from '../config.js';
|
|
11
|
+
export async function chatCommandEnhanced(graphIdInput, options) {
|
|
12
|
+
// Check if we need to re-run with tsx for TypeScript config files
|
|
13
|
+
if (!process.env.TSX_RUNNING) {
|
|
14
|
+
// Helper function to find config file
|
|
15
|
+
function findConfigFile(startPath = process.cwd()) {
|
|
16
|
+
let currentPath = resolve(startPath);
|
|
17
|
+
const root = '/';
|
|
18
|
+
const configNames = ['inkeep.config.ts', 'inkeep.config.js', '.inkeeprc.ts', '.inkeeprc.js'];
|
|
19
|
+
while (currentPath !== root) {
|
|
20
|
+
// Check for config files at this level
|
|
21
|
+
for (const configName of configNames) {
|
|
22
|
+
const configPath = resolve(currentPath, configName);
|
|
23
|
+
if (existsSync(configPath)) {
|
|
24
|
+
return configPath;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const parentPath = dirname(currentPath);
|
|
28
|
+
if (parentPath === currentPath) {
|
|
29
|
+
break; // Reached filesystem root
|
|
30
|
+
}
|
|
31
|
+
currentPath = parentPath;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
// Determine if we have a TypeScript config that needs tsx
|
|
36
|
+
let configPath = null;
|
|
37
|
+
if (options?.configFilePath) {
|
|
38
|
+
// User specified a config path
|
|
39
|
+
configPath = resolve(process.cwd(), options.configFilePath);
|
|
40
|
+
if (!existsSync(configPath)) {
|
|
41
|
+
// Config file doesn't exist, let the normal flow handle the error
|
|
42
|
+
configPath = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Search for config file
|
|
47
|
+
configPath = findConfigFile();
|
|
48
|
+
}
|
|
49
|
+
// If we found a TypeScript config file, re-run with tsx
|
|
50
|
+
if (configPath && extname(configPath) === '.ts') {
|
|
51
|
+
// Re-run this command with tsx
|
|
52
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
53
|
+
const __dirname = dirname(__filename);
|
|
54
|
+
const cliPath = resolve(__dirname, '../index.js');
|
|
55
|
+
const args = [cliPath, 'chat'];
|
|
56
|
+
if (graphIdInput)
|
|
57
|
+
args.push(graphIdInput);
|
|
58
|
+
if (options?.tenantId)
|
|
59
|
+
args.push('--tenant-id', options.tenantId);
|
|
60
|
+
if (options?.managementApiUrl)
|
|
61
|
+
args.push('--management-api-url', options.managementApiUrl);
|
|
62
|
+
if (options?.executionApiUrl)
|
|
63
|
+
args.push('--execution-api-url', options.executionApiUrl);
|
|
64
|
+
if (options?.configFilePath)
|
|
65
|
+
args.push('--config-file-path', options.configFilePath);
|
|
66
|
+
const child = spawn('npx', ['tsx', ...args], {
|
|
67
|
+
cwd: process.cwd(),
|
|
68
|
+
stdio: 'inherit',
|
|
69
|
+
env: { ...process.env, TSX_RUNNING: '1' },
|
|
70
|
+
});
|
|
71
|
+
child.on('error', (error) => {
|
|
72
|
+
console.error(chalk.red('Failed to load TypeScript configuration:'), error.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
|
75
|
+
child.on('exit', (code) => {
|
|
76
|
+
process.exit(code || 0);
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Validate configuration
|
|
82
|
+
let config;
|
|
83
|
+
try {
|
|
84
|
+
config = await validateConfiguration(options?.tenantId, options?.managementApiUrl, options?.executionApiUrl, options?.configFilePath);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error(chalk.red(error.message));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
// Log configuration sources for debugging
|
|
91
|
+
console.log(chalk.gray('Using configuration:'));
|
|
92
|
+
console.log(chalk.gray(` • Tenant ID: ${config.sources.tenantId}`));
|
|
93
|
+
console.log(chalk.gray(` • Management API: ${config.sources.managementApiUrl}`));
|
|
94
|
+
console.log(chalk.gray(` • Execution API: ${config.sources.executionApiUrl}`));
|
|
95
|
+
console.log();
|
|
96
|
+
const managementApi = await ManagementApiClient.create(config.managementApiUrl, options?.configFilePath, config.tenantId);
|
|
97
|
+
const executionApi = await ExecutionApiClient.create(config.executionApiUrl, options?.configFilePath, config.tenantId);
|
|
98
|
+
let graphId = graphIdInput;
|
|
99
|
+
// If no graph ID provided, show autocomplete selection
|
|
100
|
+
if (!graphId) {
|
|
101
|
+
const spinner = ora('Fetching available graphs...').start();
|
|
102
|
+
try {
|
|
103
|
+
const graphs = await managementApi.listGraphs();
|
|
104
|
+
spinner.stop();
|
|
105
|
+
if (graphs.length === 0) {
|
|
106
|
+
console.error(chalk.red('No graphs available. Push a graph first with: inkeep push <graph-path>'));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
// Create searchable source for autocomplete
|
|
110
|
+
const graphChoices = graphs.map((g) => ({
|
|
111
|
+
name: `${chalk.cyan(g.id)} - ${g.name || 'Unnamed Graph'}`,
|
|
112
|
+
value: g.id,
|
|
113
|
+
short: g.id,
|
|
114
|
+
searchText: `${g.id} ${g.name || ''}`.toLowerCase(),
|
|
115
|
+
}));
|
|
116
|
+
// Use list prompt for interactive selection
|
|
117
|
+
const answer = await inquirer.prompt([
|
|
118
|
+
{
|
|
119
|
+
type: 'list',
|
|
120
|
+
name: 'graphId',
|
|
121
|
+
message: 'Select a graph to chat with:',
|
|
122
|
+
choices: graphChoices,
|
|
123
|
+
pageSize: 10,
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
graphId = answer.graphId;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
spinner.fail('Failed to fetch graphs');
|
|
130
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Check if graph exists
|
|
135
|
+
const spinner = ora('Connecting to graph...').start();
|
|
136
|
+
try {
|
|
137
|
+
const graph = await managementApi.getGraph(graphId);
|
|
138
|
+
if (!graph) {
|
|
139
|
+
spinner.fail(`Graph "${graphId}" not found`);
|
|
140
|
+
// Show available graphs
|
|
141
|
+
const graphs = await managementApi.listGraphs();
|
|
142
|
+
if (graphs.length > 0) {
|
|
143
|
+
console.log(chalk.yellow('\nAvailable graphs:'));
|
|
144
|
+
graphs.forEach((g) => {
|
|
145
|
+
console.log(chalk.gray(` • ${g.id} - ${g.name || 'Unnamed'}`));
|
|
146
|
+
});
|
|
147
|
+
console.log(chalk.gray('\nRun "inkeep chat" without arguments for interactive selection'));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
console.log(chalk.yellow('\nNo graphs found. Please create and push a graph first.'));
|
|
151
|
+
console.log(chalk.gray('Example: inkeep push ./my-graph.js'));
|
|
152
|
+
}
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
spinner.succeed(`Connected to graph: ${chalk.green(graph.name || graphId)}`);
|
|
156
|
+
// Display graph details
|
|
157
|
+
if (graph.description) {
|
|
158
|
+
console.log(chalk.gray(`Description: ${graph.description}`));
|
|
159
|
+
}
|
|
160
|
+
if (graph.defaultAgentId || graph.default_agent_id) {
|
|
161
|
+
console.log(chalk.gray(`Default Agent: ${graph.defaultAgentId || graph.default_agent_id}`));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
spinner.fail('Failed to connect to graph');
|
|
166
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
// Create readline interface for chat
|
|
170
|
+
const rl = readline.createInterface({
|
|
171
|
+
input: process.stdin,
|
|
172
|
+
output: process.stdout,
|
|
173
|
+
prompt: chalk.cyan('You> '),
|
|
174
|
+
});
|
|
175
|
+
// Generate a conversation ID for this session
|
|
176
|
+
const conversationId = `cli-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
177
|
+
const messages = [];
|
|
178
|
+
let debugMode = false;
|
|
179
|
+
console.log(chalk.gray('\n💬 Chat session started. Type "exit" or press Ctrl+C to quit.'));
|
|
180
|
+
console.log(chalk.gray('Commands: help, clear, history, reset, debug\n'));
|
|
181
|
+
// Function to handle streaming response
|
|
182
|
+
async function handleStreamingResponse(stream, showDebug = false) {
|
|
183
|
+
const decoder = new TextDecoder();
|
|
184
|
+
const reader = stream.getReader();
|
|
185
|
+
let buffer = '';
|
|
186
|
+
let responseContent = '';
|
|
187
|
+
const debugOperations = [];
|
|
188
|
+
let hasStartedResponse = false;
|
|
189
|
+
try {
|
|
190
|
+
while (true) {
|
|
191
|
+
const { done, value } = await reader.read();
|
|
192
|
+
if (done)
|
|
193
|
+
break;
|
|
194
|
+
buffer += decoder.decode(value, { stream: true });
|
|
195
|
+
const lines = buffer.split('\n');
|
|
196
|
+
buffer = lines.pop() || '';
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
if (line.startsWith('data: ')) {
|
|
199
|
+
const data = line.slice(6);
|
|
200
|
+
if (data === '[DONE]')
|
|
201
|
+
continue;
|
|
202
|
+
try {
|
|
203
|
+
const parsed = JSON.parse(data);
|
|
204
|
+
// Handle OpenAI-style streaming chunks
|
|
205
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
206
|
+
if (content) {
|
|
207
|
+
// Process content character by character to extract JSON objects
|
|
208
|
+
let currentPos = 0;
|
|
209
|
+
while (currentPos < content.length) {
|
|
210
|
+
// Check if we're at the start of a JSON data operation
|
|
211
|
+
if (content.substring(currentPos).startsWith('{"type":"data-operation"')) {
|
|
212
|
+
// Find the matching closing brace
|
|
213
|
+
let braceCount = 0;
|
|
214
|
+
let jsonEnd = currentPos;
|
|
215
|
+
for (let i = currentPos; i < content.length; i++) {
|
|
216
|
+
if (content[i] === '{')
|
|
217
|
+
braceCount++;
|
|
218
|
+
if (content[i] === '}') {
|
|
219
|
+
braceCount--;
|
|
220
|
+
if (braceCount === 0) {
|
|
221
|
+
jsonEnd = i + 1;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Extract and parse the JSON object
|
|
227
|
+
const jsonStr = content.substring(currentPos, jsonEnd);
|
|
228
|
+
try {
|
|
229
|
+
const dataOp = JSON.parse(jsonStr);
|
|
230
|
+
debugOperations.push(dataOp);
|
|
231
|
+
// Show debug info if enabled
|
|
232
|
+
if (showDebug && dataOp.type === 'data-operation') {
|
|
233
|
+
const opType = dataOp.data?.type || 'unknown';
|
|
234
|
+
const ctx = dataOp.data?.ctx || {};
|
|
235
|
+
// Format context based on operation type
|
|
236
|
+
let ctxDisplay = '';
|
|
237
|
+
if (opType === 'agent_thinking' || opType === 'iteration_start') {
|
|
238
|
+
ctxDisplay = ctx.agent || JSON.stringify(ctx);
|
|
239
|
+
}
|
|
240
|
+
else if (opType === 'task_creation') {
|
|
241
|
+
ctxDisplay = `agent: ${ctx.agent}`;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
ctxDisplay = JSON.stringify(ctx);
|
|
245
|
+
}
|
|
246
|
+
// Add newline before completion operations that come after text
|
|
247
|
+
if (opType === 'completion' && hasStartedResponse) {
|
|
248
|
+
console.log(''); // Add newline before completion
|
|
249
|
+
}
|
|
250
|
+
console.log(chalk.gray(` [${opType}] ${ctxDisplay}`));
|
|
251
|
+
}
|
|
252
|
+
currentPos = jsonEnd;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// Failed to parse, treat as regular content
|
|
256
|
+
if (!hasStartedResponse) {
|
|
257
|
+
process.stdout.write(chalk.green('Assistant> '));
|
|
258
|
+
hasStartedResponse = true;
|
|
259
|
+
}
|
|
260
|
+
process.stdout.write(content[currentPos]);
|
|
261
|
+
responseContent += content[currentPos];
|
|
262
|
+
currentPos++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// Regular text content
|
|
267
|
+
if (!hasStartedResponse) {
|
|
268
|
+
process.stdout.write(chalk.green('Assistant> '));
|
|
269
|
+
hasStartedResponse = true;
|
|
270
|
+
}
|
|
271
|
+
process.stdout.write(content[currentPos]);
|
|
272
|
+
responseContent += content[currentPos];
|
|
273
|
+
currentPos++;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Ignore parse errors
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
reader.releaseLock();
|
|
287
|
+
}
|
|
288
|
+
// Add final newline if we had content
|
|
289
|
+
if (hasStartedResponse) {
|
|
290
|
+
console.log('\n');
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
console.log(chalk.green('Assistant> ') + chalk.gray('(no response)') + '\n');
|
|
294
|
+
}
|
|
295
|
+
return responseContent;
|
|
296
|
+
}
|
|
297
|
+
// Handle user input
|
|
298
|
+
rl.on('line', async (input) => {
|
|
299
|
+
const trimmedInput = input.trim();
|
|
300
|
+
const command = trimmedInput.toLowerCase().replace(/^\//, '');
|
|
301
|
+
if (command === 'exit') {
|
|
302
|
+
console.log(chalk.gray('Goodbye! 👋'));
|
|
303
|
+
rl.close();
|
|
304
|
+
process.exit(0);
|
|
305
|
+
}
|
|
306
|
+
if (command === 'clear') {
|
|
307
|
+
console.clear();
|
|
308
|
+
console.log(chalk.gray('Screen cleared. Conversation context preserved.\n'));
|
|
309
|
+
rl.prompt();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (command === 'help') {
|
|
313
|
+
console.log(chalk.cyan('\n📚 Available commands:'));
|
|
314
|
+
console.log(chalk.gray(' • exit - End the chat session'));
|
|
315
|
+
console.log(chalk.gray(' • clear - Clear the screen (preserves context)'));
|
|
316
|
+
console.log(chalk.gray(' • history - Show conversation history'));
|
|
317
|
+
console.log(chalk.gray(' • reset - Reset conversation context'));
|
|
318
|
+
console.log(chalk.gray(' • debug - Toggle debug mode (show/hide data operations)'));
|
|
319
|
+
console.log(chalk.gray(' • help - Show this help message'));
|
|
320
|
+
console.log(chalk.gray('\n Commands can be prefixed with / (e.g., /help)\n'));
|
|
321
|
+
rl.prompt();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (command === 'debug') {
|
|
325
|
+
debugMode = !debugMode;
|
|
326
|
+
console.log(chalk.yellow(`\n🔧 Debug mode: ${debugMode ? 'ON' : 'OFF'}`));
|
|
327
|
+
if (debugMode) {
|
|
328
|
+
console.log(chalk.gray('Data operations will be shown during responses.\n'));
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
console.log(chalk.gray('Data operations are hidden.\n'));
|
|
332
|
+
}
|
|
333
|
+
rl.prompt();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (command === 'history') {
|
|
337
|
+
console.log(chalk.cyan('\n📜 Conversation History:'));
|
|
338
|
+
if (messages.length === 0) {
|
|
339
|
+
console.log(chalk.gray(' (No messages yet)\n'));
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
messages.forEach((msg, idx) => {
|
|
343
|
+
const role = msg.role === 'user' ? chalk.blue('You') : chalk.green('Assistant');
|
|
344
|
+
const preview = msg.content.substring(0, 100);
|
|
345
|
+
const suffix = msg.content.length > 100 ? '...' : '';
|
|
346
|
+
console.log(` ${idx + 1}. ${role}: ${preview}${suffix}`);
|
|
347
|
+
});
|
|
348
|
+
console.log();
|
|
349
|
+
}
|
|
350
|
+
rl.prompt();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (command === 'reset') {
|
|
354
|
+
messages.length = 0;
|
|
355
|
+
console.log(chalk.yellow('⚠️ Conversation context has been reset.\n'));
|
|
356
|
+
rl.prompt();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!trimmedInput) {
|
|
360
|
+
rl.prompt();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// Add user message to history
|
|
364
|
+
messages.push({ role: 'user', content: trimmedInput });
|
|
365
|
+
try {
|
|
366
|
+
// Send message to API using execution API
|
|
367
|
+
const response = await executionApi.chatCompletion(graphId, messages, conversationId);
|
|
368
|
+
let assistantResponse;
|
|
369
|
+
if (typeof response === 'string') {
|
|
370
|
+
// Non-streaming response
|
|
371
|
+
console.log(chalk.green('Assistant>'), response);
|
|
372
|
+
assistantResponse = response;
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
// Streaming response
|
|
376
|
+
assistantResponse = await handleStreamingResponse(response, debugMode);
|
|
377
|
+
}
|
|
378
|
+
// Add assistant response to history
|
|
379
|
+
messages.push({ role: 'assistant', content: assistantResponse });
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
|
|
383
|
+
}
|
|
384
|
+
rl.prompt();
|
|
385
|
+
});
|
|
386
|
+
rl.on('close', () => {
|
|
387
|
+
console.log(chalk.gray('\n📊 Session Summary:'));
|
|
388
|
+
console.log(chalk.gray(` • Graph: ${graphId}`));
|
|
389
|
+
console.log(chalk.gray(` • Messages: ${messages.length}`));
|
|
390
|
+
console.log(chalk.gray(` • Duration: ${new Date().toLocaleTimeString()}`));
|
|
391
|
+
console.log(chalk.gray('\nChat session ended.'));
|
|
392
|
+
process.exit(0);
|
|
393
|
+
});
|
|
394
|
+
// Initial prompt
|
|
395
|
+
rl.prompt();
|
|
396
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as readline from 'node:readline';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { ManagementApiClient, ExecutionApiClient } from '../api.js';
|
|
5
|
+
export async function chatCommand(graphId, options) {
|
|
6
|
+
const managementApi = await ManagementApiClient.create(options.url, options.config);
|
|
7
|
+
const executionApi = await ExecutionApiClient.create(options.url, options.config);
|
|
8
|
+
// Check if graph exists using management API
|
|
9
|
+
const spinner = ora('Connecting to graph...').start();
|
|
10
|
+
try {
|
|
11
|
+
const graph = await managementApi.getGraph(graphId);
|
|
12
|
+
if (!graph) {
|
|
13
|
+
spinner.fail(`Graph "${graphId}" not found`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
spinner.succeed(`Connected to graph: ${graph.name || graphId}`);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
spinner.fail('Failed to connect to graph');
|
|
20
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
// Create readline interface
|
|
24
|
+
const rl = readline.createInterface({
|
|
25
|
+
input: process.stdin,
|
|
26
|
+
output: process.stdout,
|
|
27
|
+
prompt: chalk.cyan('You> '),
|
|
28
|
+
});
|
|
29
|
+
// Generate a conversation ID for this session
|
|
30
|
+
const conversationId = `cli-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
31
|
+
const messages = [];
|
|
32
|
+
console.log(chalk.gray('\n💬 Chat session started. Type "exit" or press Ctrl+C to quit.\n'));
|
|
33
|
+
// Function to handle streaming response
|
|
34
|
+
async function handleStreamingResponse(stream) {
|
|
35
|
+
const decoder = new TextDecoder();
|
|
36
|
+
const reader = stream.getReader();
|
|
37
|
+
let buffer = '';
|
|
38
|
+
let responseContent = '';
|
|
39
|
+
process.stdout.write(chalk.green('Assistant> '));
|
|
40
|
+
try {
|
|
41
|
+
while (true) {
|
|
42
|
+
const { done, value } = await reader.read();
|
|
43
|
+
if (done)
|
|
44
|
+
break;
|
|
45
|
+
buffer += decoder.decode(value, { stream: true });
|
|
46
|
+
const lines = buffer.split('\n');
|
|
47
|
+
buffer = lines.pop() || '';
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
if (line.startsWith('data: ')) {
|
|
50
|
+
const data = line.slice(6);
|
|
51
|
+
if (data === '[DONE]')
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(data);
|
|
55
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
56
|
+
if (content) {
|
|
57
|
+
// Debug logging
|
|
58
|
+
if (process.env.DEBUG) {
|
|
59
|
+
console.error('Content received:', content);
|
|
60
|
+
}
|
|
61
|
+
// Filter out data operation JSON messages
|
|
62
|
+
if (!content.startsWith('{"type":"data-operation"')) {
|
|
63
|
+
process.stdout.write(content);
|
|
64
|
+
responseContent += content;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
// Log parse errors for debugging
|
|
70
|
+
if (process.env.DEBUG) {
|
|
71
|
+
console.error('Parse error:', err, 'Data:', data);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
reader.releaseLock();
|
|
80
|
+
}
|
|
81
|
+
console.log('\n');
|
|
82
|
+
return responseContent;
|
|
83
|
+
}
|
|
84
|
+
// Set up tab completion for graph IDs
|
|
85
|
+
rl.on('line', async (input) => {
|
|
86
|
+
const trimmedInput = input.trim();
|
|
87
|
+
if (trimmedInput.toLowerCase() === 'exit') {
|
|
88
|
+
console.log(chalk.gray('Goodbye! 👋'));
|
|
89
|
+
rl.close();
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
if (!trimmedInput) {
|
|
93
|
+
rl.prompt();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Add user message to history
|
|
97
|
+
messages.push({ role: 'user', content: trimmedInput });
|
|
98
|
+
try {
|
|
99
|
+
// Send message to API using execution API
|
|
100
|
+
const response = await executionApi.chatCompletion(graphId, messages, conversationId);
|
|
101
|
+
let assistantResponse;
|
|
102
|
+
if (typeof response === 'string') {
|
|
103
|
+
// Non-streaming response
|
|
104
|
+
console.log(chalk.green('Assistant>'), response);
|
|
105
|
+
assistantResponse = response;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Streaming response
|
|
109
|
+
assistantResponse = await handleStreamingResponse(response);
|
|
110
|
+
}
|
|
111
|
+
// Add assistant response to history
|
|
112
|
+
messages.push({ role: 'assistant', content: assistantResponse });
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
|
|
116
|
+
}
|
|
117
|
+
rl.prompt();
|
|
118
|
+
});
|
|
119
|
+
rl.on('close', () => {
|
|
120
|
+
console.log(chalk.gray('\nChat session ended.'));
|
|
121
|
+
process.exit(0);
|
|
122
|
+
});
|
|
123
|
+
// Initial prompt
|
|
124
|
+
rl.prompt();
|
|
125
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface ConfigOptions {
|
|
2
|
+
configFilePath?: string;
|
|
3
|
+
}
|
|
4
|
+
export declare function configGetCommand(key?: string, options?: ConfigOptions): Promise<void>;
|
|
5
|
+
export declare function configSetCommand(key: string, value: string, options?: ConfigOptions): Promise<void>;
|
|
6
|
+
export declare function configListCommand(options?: ConfigOptions): Promise<void>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
export async function configGetCommand(key, options) {
|
|
5
|
+
const configPath = options?.configFilePath || join(process.cwd(), 'inkeep.config.ts');
|
|
6
|
+
if (!existsSync(configPath)) {
|
|
7
|
+
console.error(chalk.red('No configuration file found.'));
|
|
8
|
+
console.log(chalk.gray('Run "inkeep init" to create one, or specify a config file with --config-file-path'));
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
13
|
+
// Parse the config file to extract values
|
|
14
|
+
const tenantIdMatch = content.match(/tenantId:\s*['"]([^'"]+)['"]/);
|
|
15
|
+
const apiUrlMatch = content.match(/apiUrl:\s*['"]([^'"]+)['"]/);
|
|
16
|
+
const config = {
|
|
17
|
+
tenantId: tenantIdMatch ? tenantIdMatch[1] : undefined,
|
|
18
|
+
apiUrl: apiUrlMatch ? apiUrlMatch[1] : undefined,
|
|
19
|
+
};
|
|
20
|
+
if (key) {
|
|
21
|
+
// Get specific key
|
|
22
|
+
const value = config[key];
|
|
23
|
+
if (value !== undefined) {
|
|
24
|
+
console.log(value);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.error(chalk.red(`Unknown configuration key: ${key}`));
|
|
28
|
+
console.log(chalk.gray('Available keys: tenantId, apiUrl'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// Display all config
|
|
34
|
+
console.log(chalk.cyan('Current configuration:'));
|
|
35
|
+
console.log(chalk.gray(' Config file:'), configPath);
|
|
36
|
+
console.log(chalk.gray(' Tenant ID:'), config.tenantId || chalk.yellow('(not set)'));
|
|
37
|
+
console.log(chalk.gray(' API URL:'), config.apiUrl || chalk.yellow('(not set)'));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error(chalk.red('Failed to read configuration:'), error);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function configSetCommand(key, value, options) {
|
|
46
|
+
const configPath = options?.configFilePath || join(process.cwd(), 'inkeep.config.ts');
|
|
47
|
+
// Validate the key
|
|
48
|
+
if (!['tenantId', 'apiUrl'].includes(key)) {
|
|
49
|
+
console.error(chalk.red(`Invalid configuration key: ${key}`));
|
|
50
|
+
console.log(chalk.gray('Available keys: tenantId, apiUrl'));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
// Validate apiUrl if setting it
|
|
54
|
+
if (key === 'apiUrl') {
|
|
55
|
+
try {
|
|
56
|
+
new URL(value);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
console.error(chalk.red('Invalid URL format'));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!existsSync(configPath)) {
|
|
64
|
+
// Create a new config file if it doesn't exist
|
|
65
|
+
const configContent = `import { defineConfig } from '@inkeep/agents-cli';
|
|
66
|
+
|
|
67
|
+
export default defineConfig({
|
|
68
|
+
tenantId: '${key === 'tenantId' ? value : ''}',
|
|
69
|
+
projectId: '${key === 'projectId' ? value : 'default'}',
|
|
70
|
+
apiUrl: '${key === 'apiUrl' ? value : 'http://localhost:3002'}',
|
|
71
|
+
});
|
|
72
|
+
`;
|
|
73
|
+
try {
|
|
74
|
+
writeFileSync(configPath, configContent);
|
|
75
|
+
console.log(chalk.green('✓'), `Created config file and set ${key} to:`, chalk.cyan(value));
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error(chalk.red('Failed to create config file:'), error);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Update existing config file
|
|
84
|
+
try {
|
|
85
|
+
let content = readFileSync(configPath, 'utf-8');
|
|
86
|
+
if (key === 'tenantId') {
|
|
87
|
+
// Update or add tenantId
|
|
88
|
+
if (content.includes('tenantId:')) {
|
|
89
|
+
content = content.replace(/tenantId:\s*['"][^'"]*['"]/, `tenantId: '${value}'`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Add tenantId to the config object
|
|
93
|
+
content = content.replace(/defineConfig\s*\(\s*{/, `defineConfig({\n tenantId: '${value}',`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (key === 'projectId') {
|
|
97
|
+
// Update or add projectId
|
|
98
|
+
if (content.includes('projectId:')) {
|
|
99
|
+
content = content.replace(/projectId:\s*['"][^'"]*['"]/, `projectId: '${value}'`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Add projectId after tenantId
|
|
103
|
+
content = content.replace(/(tenantId:\s*['"][^'"]*['"]),?/, `$1,\n projectId: '${value}',`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (key === 'apiUrl') {
|
|
107
|
+
// Update or add apiUrl
|
|
108
|
+
if (content.includes('apiUrl:')) {
|
|
109
|
+
content = content.replace(/apiUrl:\s*['"][^'"]*['"]/, `apiUrl: '${value}'`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Add apiUrl to the config object
|
|
113
|
+
content = content.replace(/defineConfig\s*\(\s*{/, `defineConfig({\n apiUrl: '${value}',`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
writeFileSync(configPath, content);
|
|
117
|
+
console.log(chalk.green('✓'), `Updated ${key} to:`, chalk.cyan(value));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.error(chalk.red('Failed to update config file:'), error);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export async function configListCommand(options) {
|
|
126
|
+
// Alias for configGetCommand without a key
|
|
127
|
+
await configGetCommand(undefined, options);
|
|
128
|
+
}
|