@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,171 @@
|
|
|
1
|
+
import { existsSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
/**
|
|
6
|
+
* Find the most appropriate directory for the config file by looking for
|
|
7
|
+
* common project root indicators
|
|
8
|
+
*/
|
|
9
|
+
function findProjectRoot(startPath) {
|
|
10
|
+
let currentPath = resolve(startPath);
|
|
11
|
+
const root = dirname(currentPath);
|
|
12
|
+
// Look for common project root indicators
|
|
13
|
+
const rootIndicators = [
|
|
14
|
+
'package.json',
|
|
15
|
+
'.git',
|
|
16
|
+
'.gitignore',
|
|
17
|
+
'tsconfig.json',
|
|
18
|
+
'package-lock.json',
|
|
19
|
+
'yarn.lock',
|
|
20
|
+
'pnpm-lock.yaml',
|
|
21
|
+
];
|
|
22
|
+
while (currentPath !== root) {
|
|
23
|
+
const files = readdirSync(currentPath);
|
|
24
|
+
// Check if any root indicators exist at this level
|
|
25
|
+
if (rootIndicators.some((indicator) => files.includes(indicator))) {
|
|
26
|
+
return currentPath;
|
|
27
|
+
}
|
|
28
|
+
const parentPath = dirname(currentPath);
|
|
29
|
+
if (parentPath === currentPath) {
|
|
30
|
+
break; // Reached filesystem root
|
|
31
|
+
}
|
|
32
|
+
currentPath = parentPath;
|
|
33
|
+
}
|
|
34
|
+
// If no project root found, use the original path
|
|
35
|
+
return startPath;
|
|
36
|
+
}
|
|
37
|
+
export async function initCommand(options) {
|
|
38
|
+
let configPath;
|
|
39
|
+
if (options?.path) {
|
|
40
|
+
// User specified a path
|
|
41
|
+
const resolvedPath = resolve(process.cwd(), options.path);
|
|
42
|
+
// Check if it's a directory or a file path
|
|
43
|
+
if (options.path.endsWith('.ts') || options.path.endsWith('.js')) {
|
|
44
|
+
// It's a file path
|
|
45
|
+
configPath = resolvedPath;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// It's a directory path
|
|
49
|
+
configPath = join(resolvedPath, 'inkeep.config.ts');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Auto-detect project root
|
|
54
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
55
|
+
const suggestedPath = join(projectRoot, 'inkeep.config.ts');
|
|
56
|
+
if (options?.interactive === false) {
|
|
57
|
+
// Non-interactive mode: use the detected project root
|
|
58
|
+
configPath = suggestedPath;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Ask user to confirm or change the location
|
|
62
|
+
const { confirmedPath } = await inquirer.prompt([
|
|
63
|
+
{
|
|
64
|
+
type: 'input',
|
|
65
|
+
name: 'confirmedPath',
|
|
66
|
+
message: 'Where should the config file be created?',
|
|
67
|
+
default: suggestedPath,
|
|
68
|
+
validate: (input) => {
|
|
69
|
+
if (!input || input.trim() === '') {
|
|
70
|
+
return 'Path is required';
|
|
71
|
+
}
|
|
72
|
+
// Check if the directory exists
|
|
73
|
+
const dir = input.endsWith('.ts') || input.endsWith('.js') ? dirname(input) : input;
|
|
74
|
+
const resolvedDir = resolve(process.cwd(), dir);
|
|
75
|
+
if (!existsSync(resolvedDir)) {
|
|
76
|
+
return `Directory does not exist: ${resolvedDir}`;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
]);
|
|
82
|
+
const resolvedPath = resolve(process.cwd(), confirmedPath);
|
|
83
|
+
configPath =
|
|
84
|
+
confirmedPath.endsWith('.ts') || confirmedPath.endsWith('.js')
|
|
85
|
+
? resolvedPath
|
|
86
|
+
: join(resolvedPath, 'inkeep.config.ts');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Check if config file already exists
|
|
90
|
+
if (existsSync(configPath)) {
|
|
91
|
+
const { overwrite } = await inquirer.prompt([
|
|
92
|
+
{
|
|
93
|
+
type: 'confirm',
|
|
94
|
+
name: 'overwrite',
|
|
95
|
+
message: `${basename(configPath)} already exists at this location. Do you want to overwrite it?`,
|
|
96
|
+
default: false,
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
if (!overwrite) {
|
|
100
|
+
console.log(chalk.yellow('Init cancelled.'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Prompt for configuration values
|
|
105
|
+
const answers = await inquirer.prompt([
|
|
106
|
+
{
|
|
107
|
+
type: 'input',
|
|
108
|
+
name: 'tenantId',
|
|
109
|
+
message: 'Enter your tenant ID:',
|
|
110
|
+
validate: (input) => {
|
|
111
|
+
if (!input || input.trim() === '') {
|
|
112
|
+
return 'Tenant ID is required';
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: 'input',
|
|
119
|
+
name: 'projectId',
|
|
120
|
+
message: 'Enter your project ID:',
|
|
121
|
+
default: 'default',
|
|
122
|
+
validate: (input) => {
|
|
123
|
+
if (!input || input.trim() === '') {
|
|
124
|
+
return 'Project ID is required';
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: 'input',
|
|
131
|
+
name: 'apiUrl',
|
|
132
|
+
message: 'Enter the API URL:',
|
|
133
|
+
default: 'http://localhost:3002',
|
|
134
|
+
validate: (input) => {
|
|
135
|
+
try {
|
|
136
|
+
new URL(input);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return 'Please enter a valid URL';
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
// Generate the config file content
|
|
146
|
+
const configContent = `import { defineConfig } from '@inkeep/agents-cli';
|
|
147
|
+
|
|
148
|
+
export default defineConfig({
|
|
149
|
+
tenantId: '${answers.tenantId}',
|
|
150
|
+
projectId: '${answers.projectId}',
|
|
151
|
+
apiUrl: '${answers.apiUrl}',
|
|
152
|
+
});
|
|
153
|
+
`;
|
|
154
|
+
// Write the config file
|
|
155
|
+
try {
|
|
156
|
+
writeFileSync(configPath, configContent);
|
|
157
|
+
console.log(chalk.green('✓'), `Created ${chalk.cyan(configPath)}`);
|
|
158
|
+
console.log(chalk.gray('\nYou can now use the Inkeep CLI commands.'));
|
|
159
|
+
console.log(chalk.gray('For example: inkeep list-graphs'));
|
|
160
|
+
// If the config is not in the current directory, provide a hint
|
|
161
|
+
const configDir = dirname(configPath);
|
|
162
|
+
if (configDir !== process.cwd()) {
|
|
163
|
+
console.log(chalk.gray(`\nNote: Config file created in ${configDir}`));
|
|
164
|
+
console.log(chalk.gray(`Use --config ${configPath} with commands, or run commands from that directory.`));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error(chalk.red('Failed to create config file:'), error);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, extname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import Table from 'cli-table3';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { ManagementApiClient } from '../api.js';
|
|
9
|
+
import { validateConfiguration } from '../config.js';
|
|
10
|
+
export async function listGraphsCommand(options) {
|
|
11
|
+
// Check if we need to re-run with tsx for TypeScript config files
|
|
12
|
+
if (!process.env.TSX_RUNNING) {
|
|
13
|
+
// Helper function to find config file
|
|
14
|
+
function findConfigFile(startPath = process.cwd()) {
|
|
15
|
+
let currentPath = resolve(startPath);
|
|
16
|
+
const root = '/';
|
|
17
|
+
const configNames = ['inkeep.config.ts', 'inkeep.config.js', '.inkeeprc.ts', '.inkeeprc.js'];
|
|
18
|
+
while (currentPath !== root) {
|
|
19
|
+
// Check for config files at this level
|
|
20
|
+
for (const configName of configNames) {
|
|
21
|
+
const configPath = resolve(currentPath, configName);
|
|
22
|
+
if (existsSync(configPath)) {
|
|
23
|
+
return configPath;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const parentPath = dirname(currentPath);
|
|
27
|
+
if (parentPath === currentPath) {
|
|
28
|
+
break; // Reached filesystem root
|
|
29
|
+
}
|
|
30
|
+
currentPath = parentPath;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
// Determine if we have a TypeScript config that needs tsx
|
|
35
|
+
let configPath = null;
|
|
36
|
+
if (options?.configFilePath) {
|
|
37
|
+
// User specified a config path
|
|
38
|
+
configPath = resolve(process.cwd(), options.configFilePath);
|
|
39
|
+
if (!existsSync(configPath)) {
|
|
40
|
+
// Config file doesn't exist, let the normal flow handle the error
|
|
41
|
+
configPath = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// Search for config file
|
|
46
|
+
configPath = findConfigFile();
|
|
47
|
+
}
|
|
48
|
+
// If we found a TypeScript config file, re-run with tsx
|
|
49
|
+
if (configPath && extname(configPath) === '.ts') {
|
|
50
|
+
// Re-run this command with tsx
|
|
51
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
52
|
+
const __dirname = dirname(__filename);
|
|
53
|
+
const cliPath = resolve(__dirname, '../index.js');
|
|
54
|
+
const args = [cliPath, 'list-graphs'];
|
|
55
|
+
if (options?.tenantId)
|
|
56
|
+
args.push('--tenant-id', options.tenantId);
|
|
57
|
+
if (options?.managementApiUrl)
|
|
58
|
+
args.push('--management-api-url', options.managementApiUrl);
|
|
59
|
+
if (options?.configFilePath)
|
|
60
|
+
args.push('--config-file-path', options.configFilePath);
|
|
61
|
+
const child = spawn('npx', ['tsx', ...args], {
|
|
62
|
+
cwd: process.cwd(),
|
|
63
|
+
stdio: 'inherit',
|
|
64
|
+
env: { ...process.env, TSX_RUNNING: '1' },
|
|
65
|
+
});
|
|
66
|
+
child.on('error', (error) => {
|
|
67
|
+
console.error(chalk.red('Failed to load TypeScript configuration:'), error.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|
|
70
|
+
child.on('exit', (code) => {
|
|
71
|
+
process.exit(code || 0);
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Validate configuration
|
|
77
|
+
let config;
|
|
78
|
+
try {
|
|
79
|
+
config = await validateConfiguration(options.tenantId, options.managementApiUrl, undefined, // executionApiUrl not needed for list-graphs
|
|
80
|
+
options.configFilePath);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error(chalk.red(error.message));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
// Log configuration sources for debugging
|
|
87
|
+
console.log(chalk.gray('Using configuration:'));
|
|
88
|
+
console.log(chalk.gray(` • Tenant ID: ${config.sources.tenantId}`));
|
|
89
|
+
console.log(chalk.gray(` • API URL: ${config.sources.managementApiUrl}`));
|
|
90
|
+
console.log();
|
|
91
|
+
const api = await ManagementApiClient.create(config.managementApiUrl, options.configFilePath, config.tenantId);
|
|
92
|
+
const spinner = ora('Fetching graphs...').start();
|
|
93
|
+
try {
|
|
94
|
+
const graphs = await api.listGraphs();
|
|
95
|
+
spinner.succeed(`Found ${graphs.length} graph(s)`);
|
|
96
|
+
if (graphs.length === 0) {
|
|
97
|
+
console.log(chalk.gray('No graphs found. Push a graph with: inkeep push <graph-path>'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Create a table to display graphs
|
|
101
|
+
const table = new Table({
|
|
102
|
+
head: [
|
|
103
|
+
chalk.cyan('Graph ID'),
|
|
104
|
+
chalk.cyan('Name'),
|
|
105
|
+
chalk.cyan('Default Agent'),
|
|
106
|
+
chalk.cyan('Created'),
|
|
107
|
+
],
|
|
108
|
+
style: {
|
|
109
|
+
head: [],
|
|
110
|
+
border: [],
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
for (const graph of graphs) {
|
|
114
|
+
const createdDate = graph.createdAt
|
|
115
|
+
? new Date(graph.createdAt).toLocaleDateString()
|
|
116
|
+
: 'Unknown';
|
|
117
|
+
table.push([
|
|
118
|
+
graph.id || '',
|
|
119
|
+
graph.name || graph.id || '',
|
|
120
|
+
graph.defaultAgentId || chalk.gray('None'),
|
|
121
|
+
createdDate,
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
console.log('\n' + table.toString());
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
spinner.fail('Failed to fetch graphs');
|
|
128
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import Table from 'cli-table3';
|
|
6
|
+
const MCP_DIR = join(homedir(), '.inkeep', 'mcp');
|
|
7
|
+
const REGISTRY_FILE = join(MCP_DIR, 'servers.json');
|
|
8
|
+
function loadRegistry() {
|
|
9
|
+
if (!existsSync(REGISTRY_FILE)) {
|
|
10
|
+
return { servers: [] };
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(REGISTRY_FILE, 'utf-8'));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { servers: [] };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function isProcessRunning(pid) {
|
|
20
|
+
try {
|
|
21
|
+
process.kill(pid, 0);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function formatUptime(ms) {
|
|
29
|
+
const seconds = Math.floor(ms / 1000);
|
|
30
|
+
const minutes = Math.floor(seconds / 60);
|
|
31
|
+
const hours = Math.floor(minutes / 60);
|
|
32
|
+
const days = Math.floor(hours / 24);
|
|
33
|
+
if (days > 0)
|
|
34
|
+
return `${days}d ${hours % 24}h`;
|
|
35
|
+
if (hours > 0)
|
|
36
|
+
return `${hours}h ${minutes % 60}m`;
|
|
37
|
+
if (minutes > 0)
|
|
38
|
+
return `${minutes}m`;
|
|
39
|
+
return `${seconds}s`;
|
|
40
|
+
}
|
|
41
|
+
function getStatusIcon(isRunning, deployment) {
|
|
42
|
+
if (deployment === 'remote') {
|
|
43
|
+
return '🔵'; // Remote servers are always "connected"
|
|
44
|
+
}
|
|
45
|
+
return isRunning ? '🟢' : '🔴';
|
|
46
|
+
}
|
|
47
|
+
function getDeploymentIcon(deployment) {
|
|
48
|
+
return deployment === 'local' ? '🏠' : '☁️';
|
|
49
|
+
}
|
|
50
|
+
export async function mcpListCommand(options) {
|
|
51
|
+
const registry = loadRegistry();
|
|
52
|
+
if (registry.servers.length === 0) {
|
|
53
|
+
console.log(chalk.gray('No MCP servers registered'));
|
|
54
|
+
console.log(chalk.gray('\nStart servers with: inkeep mcp start <graph-file>'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Group servers by graph
|
|
58
|
+
const serversByGraph = new Map();
|
|
59
|
+
for (const server of registry.servers) {
|
|
60
|
+
const servers = serversByGraph.get(server.graphId) || [];
|
|
61
|
+
servers.push(server);
|
|
62
|
+
serversByGraph.set(server.graphId, servers);
|
|
63
|
+
}
|
|
64
|
+
if (options?.format === 'tree' || !options?.format) {
|
|
65
|
+
// Tree format (default)
|
|
66
|
+
console.log(chalk.bold('\n📡 MCP Servers Overview'));
|
|
67
|
+
console.log(chalk.gray('━'.repeat(70)) + '\n');
|
|
68
|
+
let totalRunning = 0;
|
|
69
|
+
let totalStopped = 0;
|
|
70
|
+
let totalRemote = 0;
|
|
71
|
+
for (const [graphId, servers] of serversByGraph) {
|
|
72
|
+
console.log(chalk.bold.cyan(`Graph: ${graphId}`));
|
|
73
|
+
for (const server of servers) {
|
|
74
|
+
const isRunning = server.deployment === 'local' ? isProcessRunning(server.pid) : true;
|
|
75
|
+
const uptime = isRunning
|
|
76
|
+
? formatUptime(new Date().getTime() - new Date(server.startedAt).getTime())
|
|
77
|
+
: 'stopped';
|
|
78
|
+
if (server.deployment === 'remote') {
|
|
79
|
+
totalRemote++;
|
|
80
|
+
}
|
|
81
|
+
else if (isRunning) {
|
|
82
|
+
totalRunning++;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
totalStopped++;
|
|
86
|
+
}
|
|
87
|
+
const statusIcon = getStatusIcon(isRunning, server.deployment);
|
|
88
|
+
const deploymentIcon = getDeploymentIcon(server.deployment);
|
|
89
|
+
let line = `├─ ${statusIcon} ${chalk.white(server.name.padEnd(16))} | ${deploymentIcon} ${server.deployment.padEnd(6)} | `;
|
|
90
|
+
if (server.deployment === 'local') {
|
|
91
|
+
line += chalk.cyan(`:${server.port}`.padEnd(6));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
line += chalk.blue(server.serverUrl?.substring(0, 25) + '...');
|
|
95
|
+
}
|
|
96
|
+
line += ` | ${server.transport?.toUpperCase().padEnd(4) || 'IPC '.padEnd(4)}`;
|
|
97
|
+
line += ` | ${isRunning ? chalk.green(uptime.padEnd(8)) : chalk.red('Stopped '.padEnd(8))}`;
|
|
98
|
+
console.log(line);
|
|
99
|
+
if (options?.verbose && server.description) {
|
|
100
|
+
console.log(chalk.gray(`│ └─ ${server.description}`));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
console.log();
|
|
104
|
+
}
|
|
105
|
+
// Summary
|
|
106
|
+
console.log(chalk.gray('Summary: ') +
|
|
107
|
+
chalk.green(`${totalRunning} running`) +
|
|
108
|
+
', ' +
|
|
109
|
+
chalk.red(`${totalStopped} stopped`) +
|
|
110
|
+
', ' +
|
|
111
|
+
chalk.blue(`${totalRemote} remote`));
|
|
112
|
+
}
|
|
113
|
+
else if (options.format === 'table') {
|
|
114
|
+
// Table format
|
|
115
|
+
const table = new Table({
|
|
116
|
+
head: [
|
|
117
|
+
chalk.cyan('Status'),
|
|
118
|
+
chalk.cyan('Name'),
|
|
119
|
+
chalk.cyan('Graph'),
|
|
120
|
+
chalk.cyan('Type'),
|
|
121
|
+
chalk.cyan('Location'),
|
|
122
|
+
chalk.cyan('Transport'),
|
|
123
|
+
chalk.cyan('Uptime'),
|
|
124
|
+
],
|
|
125
|
+
style: {
|
|
126
|
+
head: [],
|
|
127
|
+
border: ['gray'],
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
for (const server of registry.servers) {
|
|
131
|
+
const isRunning = server.deployment === 'local' ? isProcessRunning(server.pid) : true;
|
|
132
|
+
const uptime = isRunning
|
|
133
|
+
? formatUptime(new Date().getTime() - new Date(server.startedAt).getTime())
|
|
134
|
+
: 'N/A';
|
|
135
|
+
const statusIcon = getStatusIcon(isRunning, server.deployment);
|
|
136
|
+
const location = server.deployment === 'local'
|
|
137
|
+
? chalk.cyan(`:${server.port}`)
|
|
138
|
+
: chalk.blue(server.serverUrl?.substring(0, 30) || 'N/A');
|
|
139
|
+
table.push([
|
|
140
|
+
statusIcon,
|
|
141
|
+
server.name,
|
|
142
|
+
server.graphId,
|
|
143
|
+
server.deployment,
|
|
144
|
+
location,
|
|
145
|
+
server.transport?.toUpperCase() || 'IPC',
|
|
146
|
+
isRunning ? chalk.green(uptime) : chalk.red('Stopped'),
|
|
147
|
+
]);
|
|
148
|
+
}
|
|
149
|
+
console.log(chalk.bold('\n📡 MCP Servers\n'));
|
|
150
|
+
console.log(table.toString());
|
|
151
|
+
}
|
|
152
|
+
console.log(chalk.gray('\nCommands:'));
|
|
153
|
+
console.log(chalk.gray(' • Start servers: inkeep mcp start <graph-file>'));
|
|
154
|
+
console.log(chalk.gray(' • Stop servers: inkeep mcp stop'));
|
|
155
|
+
console.log(chalk.gray(' • Server status: inkeep mcp status'));
|
|
156
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { execSync, spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
export async function mcpStartCommand(graphPath, options) {
|
|
8
|
+
let spinner = ora('Loading graph configuration...').start();
|
|
9
|
+
try {
|
|
10
|
+
// Resolve the graph path
|
|
11
|
+
const fullPath = resolve(graphPath);
|
|
12
|
+
if (!existsSync(fullPath)) {
|
|
13
|
+
spinner.fail(`Graph file not found: ${graphPath}`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
spinner.text = 'Starting MCP servers...';
|
|
17
|
+
// Get the directory of the graph file for proper module resolution
|
|
18
|
+
const graphDir = dirname(fullPath);
|
|
19
|
+
// Check if it's a TypeScript file
|
|
20
|
+
if (fullPath.endsWith('.ts')) {
|
|
21
|
+
spinner.text = 'Checking TypeScript runtime...';
|
|
22
|
+
// Check if tsx is available in the graph directory
|
|
23
|
+
try {
|
|
24
|
+
execSync('npx tsx --version', {
|
|
25
|
+
stdio: 'pipe',
|
|
26
|
+
cwd: graphDir,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
spinner.fail('TypeScript runtime not found');
|
|
31
|
+
console.log(chalk.yellow('\nTo use TypeScript graph files:'));
|
|
32
|
+
console.log(chalk.gray(' Option 1: Compile your graph first'));
|
|
33
|
+
console.log(chalk.gray(' cd inkeep-chat && pnpm build'));
|
|
34
|
+
console.log(chalk.gray(' inkeep mcp start inkeep-chat/dist/examples/graph.graph.js'));
|
|
35
|
+
console.log(chalk.gray('\n Option 2: Ensure tsx is installed in the graph directory'));
|
|
36
|
+
console.log(chalk.gray(' cd inkeep-chat && pnpm add -D tsx'));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Convert to file URL for import
|
|
41
|
+
const fileUrl = pathToFileURL(fullPath).href;
|
|
42
|
+
// Spawn tsx process to run the runner with the graph file
|
|
43
|
+
const child = spawn('npx', [
|
|
44
|
+
'tsx',
|
|
45
|
+
'--eval',
|
|
46
|
+
`
|
|
47
|
+
import('${fileUrl}').then(async module => {
|
|
48
|
+
const servers = module.servers || module.tools || [];
|
|
49
|
+
const graph = module.graph;
|
|
50
|
+
|
|
51
|
+
let graphId = 'unknown';
|
|
52
|
+
if (graph && typeof graph.getId === 'function') {
|
|
53
|
+
graphId = graph.getId();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const startedServers = [];
|
|
57
|
+
let nextPort = 3100;
|
|
58
|
+
|
|
59
|
+
for (const server of servers) {
|
|
60
|
+
if (!server) continue;
|
|
61
|
+
|
|
62
|
+
// Try to get name using getName() method or direct property access
|
|
63
|
+
let name = 'unnamed';
|
|
64
|
+
if (typeof server.getName === 'function') {
|
|
65
|
+
name = server.getName();
|
|
66
|
+
} else if (server.config && server.config.name) {
|
|
67
|
+
name = server.config.name;
|
|
68
|
+
} else if (server.name) {
|
|
69
|
+
name = server.name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Try to get server URL
|
|
73
|
+
let serverUrl = undefined;
|
|
74
|
+
if (typeof server.getServerUrl === 'function') {
|
|
75
|
+
serverUrl = server.getServerUrl();
|
|
76
|
+
} else if (server.config && server.config.serverUrl) {
|
|
77
|
+
serverUrl = server.config.serverUrl;
|
|
78
|
+
} else if (server.serverUrl) {
|
|
79
|
+
serverUrl = server.serverUrl;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isLocal = !serverUrl;
|
|
83
|
+
const port = isLocal ? (server.port || server.config?.port || nextPort++) : undefined;
|
|
84
|
+
|
|
85
|
+
console.log(JSON.stringify({
|
|
86
|
+
type: 'server_started',
|
|
87
|
+
name,
|
|
88
|
+
port,
|
|
89
|
+
serverUrl,
|
|
90
|
+
deployment: isLocal ? 'local' : 'remote'
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
startedServers.push(name);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(JSON.stringify({
|
|
97
|
+
type: 'all_started',
|
|
98
|
+
count: startedServers.length,
|
|
99
|
+
graphId
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
if (!${options.detached || false}) {
|
|
103
|
+
// Keep process alive
|
|
104
|
+
process.stdin.resume();
|
|
105
|
+
|
|
106
|
+
process.on('SIGINT', () => {
|
|
107
|
+
console.log(JSON.stringify({ type: 'shutting_down' }));
|
|
108
|
+
process.exit(0);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}).catch(error => {
|
|
112
|
+
console.error(JSON.stringify({
|
|
113
|
+
type: 'error',
|
|
114
|
+
message: error.message
|
|
115
|
+
}));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
});
|
|
118
|
+
`,
|
|
119
|
+
], {
|
|
120
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
121
|
+
cwd: graphDir, // Set working directory to graph file's directory
|
|
122
|
+
env: {
|
|
123
|
+
...process.env,
|
|
124
|
+
ENVIRONMENT: process.env.ENVIRONMENT || 'test',
|
|
125
|
+
DB_FILE_NAME: process.env.DB_FILE_NAME || ':memory:',
|
|
126
|
+
INKEEP_TENANT_ID: process.env.INKEEP_TENANT_ID || 'test-tenant',
|
|
127
|
+
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || 'test-key',
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
let serverCount = 0;
|
|
131
|
+
let graphId = 'unknown';
|
|
132
|
+
// Handle stdout
|
|
133
|
+
child.stdout?.on('data', (data) => {
|
|
134
|
+
const lines = data.toString().split('\n').filter(Boolean);
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
try {
|
|
137
|
+
const msg = JSON.parse(line);
|
|
138
|
+
if (msg.type === 'server_started') {
|
|
139
|
+
const icon = msg.deployment === 'local' ? '🏠' : '☁️';
|
|
140
|
+
const location = msg.deployment === 'local'
|
|
141
|
+
? `on port ${chalk.cyan(msg.port)}`
|
|
142
|
+
: `at ${chalk.cyan(msg.serverUrl)}`;
|
|
143
|
+
spinner.succeed(`${icon} Started ${chalk.green(msg.name)} ${location}`);
|
|
144
|
+
spinner = ora().start(); // Create new spinner for next server
|
|
145
|
+
serverCount++;
|
|
146
|
+
}
|
|
147
|
+
else if (msg.type === 'all_started') {
|
|
148
|
+
spinner.stop();
|
|
149
|
+
console.log(chalk.green(`\n✅ Started ${msg.count} MCP server(s)`));
|
|
150
|
+
graphId = msg.graphId || 'unknown';
|
|
151
|
+
if (!options.detached) {
|
|
152
|
+
console.log(chalk.gray('\nServers are running. Press Ctrl+C to stop all servers.\n'));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (msg.type === 'shutting_down') {
|
|
156
|
+
console.log(chalk.yellow('\n\nShutting down MCP servers...'));
|
|
157
|
+
}
|
|
158
|
+
else if (msg.type === 'error') {
|
|
159
|
+
spinner.fail(`Error: ${msg.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Not JSON, just print it
|
|
164
|
+
if (options.verbose) {
|
|
165
|
+
console.log(chalk.gray(line));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// Handle stderr
|
|
171
|
+
child.stderr?.on('data', (data) => {
|
|
172
|
+
if (options.verbose) {
|
|
173
|
+
console.error(chalk.red(data.toString()));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
// Handle exit
|
|
177
|
+
child.on('exit', (code) => {
|
|
178
|
+
if (code !== 0 && code !== null) {
|
|
179
|
+
spinner.fail('Failed to start MCP servers');
|
|
180
|
+
process.exit(code);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
// Forward SIGINT to child
|
|
184
|
+
process.on('SIGINT', () => {
|
|
185
|
+
child.kill('SIGINT');
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
spinner.fail('Failed to start MCP servers');
|
|
190
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|