@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.
Files changed (75) hide show
  1. package/LICENSE.md +51 -0
  2. package/README.md +512 -0
  3. package/dist/__tests__/api.test.d.ts +1 -0
  4. package/dist/__tests__/api.test.js +257 -0
  5. package/dist/__tests__/cli.test.d.ts +1 -0
  6. package/dist/__tests__/cli.test.js +153 -0
  7. package/dist/__tests__/commands/config.test.d.ts +1 -0
  8. package/dist/__tests__/commands/config.test.js +154 -0
  9. package/dist/__tests__/commands/init.test.d.ts +1 -0
  10. package/dist/__tests__/commands/init.test.js +186 -0
  11. package/dist/__tests__/commands/pull-retry.test.d.ts +1 -0
  12. package/dist/__tests__/commands/pull-retry.test.js +156 -0
  13. package/dist/__tests__/commands/pull.test.d.ts +1 -0
  14. package/dist/__tests__/commands/pull.test.js +54 -0
  15. package/dist/__tests__/commands/push-spinner.test.d.ts +1 -0
  16. package/dist/__tests__/commands/push-spinner.test.js +127 -0
  17. package/dist/__tests__/commands/push.test.d.ts +1 -0
  18. package/dist/__tests__/commands/push.test.js +265 -0
  19. package/dist/__tests__/config-validation.test.d.ts +1 -0
  20. package/dist/__tests__/config-validation.test.js +106 -0
  21. package/dist/__tests__/package.test.d.ts +1 -0
  22. package/dist/__tests__/package.test.js +82 -0
  23. package/dist/__tests__/utils/json-comparator.test.d.ts +1 -0
  24. package/dist/__tests__/utils/json-comparator.test.js +174 -0
  25. package/dist/__tests__/utils/port-manager.test.d.ts +1 -0
  26. package/dist/__tests__/utils/port-manager.test.js +144 -0
  27. package/dist/__tests__/utils/ts-loader.test.d.ts +1 -0
  28. package/dist/__tests__/utils/ts-loader.test.js +233 -0
  29. package/dist/api.d.ts +23 -0
  30. package/dist/api.js +140 -0
  31. package/dist/commands/chat-enhanced.d.ts +7 -0
  32. package/dist/commands/chat-enhanced.js +396 -0
  33. package/dist/commands/chat.d.ts +5 -0
  34. package/dist/commands/chat.js +125 -0
  35. package/dist/commands/config.d.ts +6 -0
  36. package/dist/commands/config.js +128 -0
  37. package/dist/commands/init.d.ts +5 -0
  38. package/dist/commands/init.js +171 -0
  39. package/dist/commands/list-graphs.d.ts +6 -0
  40. package/dist/commands/list-graphs.js +131 -0
  41. package/dist/commands/mcp-list.d.ts +4 -0
  42. package/dist/commands/mcp-list.js +156 -0
  43. package/dist/commands/mcp-start-simple.d.ts +5 -0
  44. package/dist/commands/mcp-start-simple.js +193 -0
  45. package/dist/commands/mcp-start.d.ts +5 -0
  46. package/dist/commands/mcp-start.js +217 -0
  47. package/dist/commands/mcp-status.d.ts +1 -0
  48. package/dist/commands/mcp-status.js +96 -0
  49. package/dist/commands/mcp-stop.d.ts +5 -0
  50. package/dist/commands/mcp-stop.js +160 -0
  51. package/dist/commands/pull.d.ts +15 -0
  52. package/dist/commands/pull.js +313 -0
  53. package/dist/commands/pull.llm-generate.d.ts +10 -0
  54. package/dist/commands/pull.llm-generate.js +184 -0
  55. package/dist/commands/push.d.ts +6 -0
  56. package/dist/commands/push.js +268 -0
  57. package/dist/config.d.ts +43 -0
  58. package/dist/config.js +292 -0
  59. package/dist/exports.d.ts +2 -0
  60. package/dist/exports.js +2 -0
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.js +98 -0
  63. package/dist/types/config.d.ts +9 -0
  64. package/dist/types/config.js +3 -0
  65. package/dist/types/graph.d.ts +10 -0
  66. package/dist/types/graph.js +1 -0
  67. package/dist/utils/json-comparator.d.ts +60 -0
  68. package/dist/utils/json-comparator.js +222 -0
  69. package/dist/utils/mcp-runner.d.ts +6 -0
  70. package/dist/utils/mcp-runner.js +147 -0
  71. package/dist/utils/port-manager.d.ts +43 -0
  72. package/dist/utils/port-manager.js +92 -0
  73. package/dist/utils/ts-loader.d.ts +5 -0
  74. package/dist/utils/ts-loader.js +146 -0
  75. 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,6 @@
1
+ export interface ListGraphsOptions {
2
+ tenantId?: string;
3
+ managementApiUrl?: string;
4
+ configFilePath?: string;
5
+ }
6
+ export declare function listGraphsCommand(options: ListGraphsOptions): Promise<void>;
@@ -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,4 @@
1
+ export declare function mcpListCommand(options?: {
2
+ verbose?: boolean;
3
+ format?: 'table' | 'tree';
4
+ }): Promise<void>;
@@ -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,5 @@
1
+ export interface McpStartOptions {
2
+ detached?: boolean;
3
+ verbose?: boolean;
4
+ }
5
+ export declare function mcpStartCommand(graphPath: string, options: McpStartOptions): Promise<void>;
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export interface McpStartOptions {
2
+ detached?: boolean;
3
+ verbose?: boolean;
4
+ }
5
+ export declare function mcpStartCommand(graphPath: string, options: McpStartOptions): Promise<void>;