@agents-at-scale/ark 0.1.39 → 0.1.41

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.
@@ -128,6 +128,18 @@ const defaultArkServices = {
128
128
  k8sDeploymentName: 'ark-mcp',
129
129
  k8sDevDeploymentName: 'ark-mcp-devspace',
130
130
  },
131
+ 'mcp-filesystem': {
132
+ name: 'mcp-filesystem',
133
+ helmReleaseName: 'mcp-filesystem',
134
+ description: 'Stateful filesystem MCP server with workspace isolation',
135
+ enabled: false,
136
+ category: 'service',
137
+ // namespace: undefined - uses current context namespace
138
+ chartPath: `${REGISTRY_BASE}/mcp-filesystem`,
139
+ installArgs: [],
140
+ k8sDeploymentName: 'mcp-filesystem',
141
+ k8sDevDeploymentName: 'mcp-filesystem-devspace',
142
+ },
131
143
  'agents-at-scale': {
132
144
  name: 'agents-at-scale',
133
145
  helmReleaseName: 'agents-at-scale',
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander';
2
- import output from '../../lib/output.js';
2
+ import chalk from 'chalk';
3
3
  import { executeQuery, parseTarget } from '../../lib/executeQuery.js';
4
+ import { ExitCodes } from '../../lib/errors.js';
4
5
  export function createQueryCommand(_) {
5
6
  const queryCommand = new Command('query');
6
7
  queryCommand
@@ -8,11 +9,10 @@ export function createQueryCommand(_) {
8
9
  .argument('<target>', 'Query target (e.g., model/default, agent/my-agent)')
9
10
  .argument('<message>', 'Message to send')
10
11
  .action(async (target, message) => {
11
- // Parse and validate target format
12
12
  const parsed = parseTarget(target);
13
13
  if (!parsed) {
14
- output.error('Invalid target format. Use: model/name or agent/name etc');
15
- process.exit(1);
14
+ console.error(chalk.red('Invalid target format. Use: model/name or agent/name etc'));
15
+ process.exit(ExitCodes.CliError);
16
16
  }
17
17
  await executeQuery({
18
18
  targetType: parsed.type,
@@ -15,6 +15,9 @@ jest.unstable_mockModule('../../lib/output.js', () => ({
15
15
  const mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {
16
16
  throw new Error('process.exit called');
17
17
  }));
18
+ const mockConsoleError = jest
19
+ .spyOn(console, 'error')
20
+ .mockImplementation(() => { });
18
21
  const { createQueryCommand } = await import('./index.js');
19
22
  describe('createQueryCommand', () => {
20
23
  beforeEach(() => {
@@ -47,7 +50,7 @@ describe('createQueryCommand', () => {
47
50
  await expect(command.parseAsync(['node', 'test', 'invalid-target', 'Hello'])).rejects.toThrow('process.exit called');
48
51
  expect(mockParseTarget).toHaveBeenCalledWith('invalid-target');
49
52
  expect(mockExecuteQuery).not.toHaveBeenCalled();
50
- expect(mockOutput.error).toHaveBeenCalledWith('Invalid target format. Use: model/name or agent/name etc');
53
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Invalid target format'));
51
54
  expect(mockExit).toHaveBeenCalledWith(1);
52
55
  });
53
56
  });
@@ -7,10 +7,7 @@ import { marked } from 'marked';
7
7
  // @ts-ignore - no types available
8
8
  import TerminalRenderer from 'marked-terminal';
9
9
  import { APIError } from 'openai';
10
- import { AgentSelector } from '../ui/AgentSelector.js';
11
- import { ModelSelector } from '../ui/ModelSelector.js';
12
- import { TeamSelector } from '../ui/TeamSelector.js';
13
- import { ToolSelector } from '../ui/ToolSelector.js';
10
+ import { TargetSelector } from '../ui/TargetSelector.js';
14
11
  import { useAsyncOperation, AsyncOperationStatus } from './AsyncOperation.js';
15
12
  import { createConnectingToArkOperation } from '../ui/asyncOperations/connectingToArk.js';
16
13
  // Generate a unique ID for messages
@@ -48,6 +45,12 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
48
45
  const [showModelSelector, setShowModelSelector] = React.useState(false);
49
46
  const [showTeamSelector, setShowTeamSelector] = React.useState(false);
50
47
  const [showToolSelector, setShowToolSelector] = React.useState(false);
48
+ const [agents, setAgents] = React.useState([]);
49
+ const [models, setModels] = React.useState([]);
50
+ const [teams, setTeams] = React.useState([]);
51
+ const [tools, setTools] = React.useState([]);
52
+ const [selectorLoading, setSelectorLoading] = React.useState(false);
53
+ const [selectorError, setSelectorError] = React.useState(undefined);
51
54
  // Message history navigation
52
55
  const [messageHistory, setMessageHistory] = React.useState([]);
53
56
  const [historyIndex, setHistoryIndex] = React.useState(-1);
@@ -56,6 +59,50 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
56
59
  streamingEnabled: config?.chat?.streaming ?? true,
57
60
  currentTarget: undefined,
58
61
  });
62
+ React.useEffect(() => {
63
+ if (showAgentSelector && agents.length === 0) {
64
+ setSelectorLoading(true);
65
+ setSelectorError(undefined);
66
+ arkApiClient
67
+ .getAgents()
68
+ .then(setAgents)
69
+ .catch((err) => setSelectorError(err.message))
70
+ .finally(() => setSelectorLoading(false));
71
+ }
72
+ }, [showAgentSelector, arkApiClient, agents.length]);
73
+ React.useEffect(() => {
74
+ if (showModelSelector && models.length === 0) {
75
+ setSelectorLoading(true);
76
+ setSelectorError(undefined);
77
+ arkApiClient
78
+ .getModels()
79
+ .then(setModels)
80
+ .catch((err) => setSelectorError(err.message))
81
+ .finally(() => setSelectorLoading(false));
82
+ }
83
+ }, [showModelSelector, arkApiClient, models.length]);
84
+ React.useEffect(() => {
85
+ if (showTeamSelector && teams.length === 0) {
86
+ setSelectorLoading(true);
87
+ setSelectorError(undefined);
88
+ arkApiClient
89
+ .getTeams()
90
+ .then(setTeams)
91
+ .catch((err) => setSelectorError(err.message))
92
+ .finally(() => setSelectorLoading(false));
93
+ }
94
+ }, [showTeamSelector, arkApiClient, teams.length]);
95
+ React.useEffect(() => {
96
+ if (showToolSelector && tools.length === 0) {
97
+ setSelectorLoading(true);
98
+ setSelectorError(undefined);
99
+ arkApiClient
100
+ .getTools()
101
+ .then(setTools)
102
+ .catch((err) => setSelectorError(err.message))
103
+ .finally(() => setSelectorLoading(false));
104
+ }
105
+ }, [showToolSelector, arkApiClient, tools.length]);
59
106
  const chatClientRef = React.useRef(undefined);
60
107
  // Configure markdown when output format changes
61
108
  React.useEffect(() => {
@@ -608,8 +655,10 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
608
655
  }
609
656
  // Show agent selector if requested
610
657
  if (showAgentSelector) {
611
- return (_jsx(AgentSelector, { arkApiClient: arkApiClient, onSelect: (agent) => {
612
- // Update the target to the selected agent
658
+ return (_jsx(TargetSelector, { targets: agents, title: "Select Agent", subtitle: "Choose an agent to start a conversation with", loading: selectorLoading, error: selectorError, formatInlineDetail: (t) => t.description, showDetailPanel: true, onSelect: (target) => {
659
+ const agent = agents.find((a) => a.name === target.name);
660
+ if (!agent)
661
+ return;
613
662
  const agentTarget = {
614
663
  id: `agent/${agent.name}`,
615
664
  name: agent.name,
@@ -620,7 +669,6 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
620
669
  setChatConfig((prev) => ({ ...prev, currentTarget: agentTarget }));
621
670
  setMessages([]);
622
671
  setShowAgentSelector(false);
623
- // Add system message about the selection
624
672
  const systemMessage = {
625
673
  id: generateMessageId(),
626
674
  type: 'system',
@@ -633,8 +681,18 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
633
681
  }
634
682
  // Show model selector if requested
635
683
  if (showModelSelector) {
636
- return (_jsx(ModelSelector, { arkApiClient: arkApiClient, onSelect: (model) => {
637
- // Update the target to the selected model
684
+ return (_jsx(TargetSelector, { targets: models, title: "Select Model", subtitle: "Choose a model to start a conversation with", loading: selectorLoading, error: selectorError, formatLabel: (t) => {
685
+ const model = models.find((m) => m.name === t.name);
686
+ return model
687
+ ? `${model.name}${model.type ? ` (${model.type})` : ''}`
688
+ : t.name;
689
+ }, formatInlineDetail: (t) => {
690
+ const model = models.find((m) => m.name === t.name);
691
+ return model?.model;
692
+ }, showDetailPanel: false, onSelect: (target) => {
693
+ const model = models.find((m) => m.name === target.name);
694
+ if (!model)
695
+ return;
638
696
  const modelTarget = {
639
697
  id: `model/${model.name}`,
640
698
  name: model.name,
@@ -645,7 +703,6 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
645
703
  setChatConfig((prev) => ({ ...prev, currentTarget: modelTarget }));
646
704
  setMessages([]);
647
705
  setShowModelSelector(false);
648
- // Add system message about the selection
649
706
  const systemMessage = {
650
707
  id: generateMessageId(),
651
708
  type: 'system',
@@ -658,8 +715,15 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
658
715
  }
659
716
  // Show team selector if requested
660
717
  if (showTeamSelector) {
661
- return (_jsx(TeamSelector, { arkApiClient: arkApiClient, onSelect: (team) => {
662
- // Update the target to the selected team
718
+ return (_jsx(TargetSelector, { targets: teams, title: "Select Team", subtitle: "Choose a team to start a conversation with", loading: selectorLoading, error: selectorError, formatLabel: (t) => {
719
+ const team = teams.find((tm) => tm.name === t.name);
720
+ return team
721
+ ? `${team.name}${team.strategy ? ` (${team.strategy})` : ''}`
722
+ : t.name;
723
+ }, formatInlineDetail: (t) => t.description, showDetailPanel: true, onSelect: (target) => {
724
+ const team = teams.find((tm) => tm.name === target.name);
725
+ if (!team)
726
+ return;
663
727
  const teamTarget = {
664
728
  id: `team/${team.name}`,
665
729
  name: team.name,
@@ -670,7 +734,6 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
670
734
  setChatConfig((prev) => ({ ...prev, currentTarget: teamTarget }));
671
735
  setMessages([]);
672
736
  setShowTeamSelector(false);
673
- // Add system message about the selection
674
737
  const systemMessage = {
675
738
  id: generateMessageId(),
676
739
  type: 'system',
@@ -683,8 +746,10 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
683
746
  }
684
747
  // Show tool selector if requested
685
748
  if (showToolSelector) {
686
- return (_jsx(ToolSelector, { arkApiClient: arkApiClient, onSelect: (tool) => {
687
- // Update the target to the selected tool
749
+ return (_jsx(TargetSelector, { targets: tools, title: "Select Tool", subtitle: "Choose a tool to start a conversation with", loading: selectorLoading, error: selectorError, formatInlineDetail: (t) => t.description, showDetailPanel: true, onSelect: (target) => {
750
+ const tool = tools.find((tl) => tl.name === target.name);
751
+ if (!tool)
752
+ return;
688
753
  const toolTarget = {
689
754
  id: `tool/${tool.name}`,
690
755
  name: tool.name,
@@ -695,7 +760,6 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
695
760
  setChatConfig((prev) => ({ ...prev, currentTarget: toolTarget }));
696
761
  setMessages([]);
697
762
  setShowToolSelector(false);
698
- // Add system message about the selection
699
763
  const systemMessage = {
700
764
  id: generateMessageId(),
701
765
  type: 'system',
@@ -6,6 +6,7 @@ export class ArkApiClient {
6
6
  baseURL: `${arkApiUrl}/openai/v1`,
7
7
  apiKey: 'dummy', // ark-api doesn't require an API key
8
8
  dangerouslyAllowBrowser: false,
9
+ maxRetries: 0, // Disable automatic retries for query errors
9
10
  });
10
11
  }
11
12
  getBaseUrl() {
@@ -0,0 +1 @@
1
+ export declare function parseDuration(duration: string): number;
@@ -0,0 +1,20 @@
1
+ export function parseDuration(duration) {
2
+ const match = duration.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)$/);
3
+ if (!match) {
4
+ throw new Error(`Invalid duration format: ${duration}`);
5
+ }
6
+ const value = parseFloat(match[1]);
7
+ const unit = match[2];
8
+ switch (unit) {
9
+ case 'ms':
10
+ return value;
11
+ case 's':
12
+ return value * 1000;
13
+ case 'm':
14
+ return value * 60 * 1000;
15
+ case 'h':
16
+ return value * 60 * 60 * 1000;
17
+ default:
18
+ throw new Error(`Unknown duration unit: ${unit}`);
19
+ }
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import { parseDuration } from './duration.js';
2
+ describe('parseDuration', () => {
3
+ it('should parse durations correctly', () => {
4
+ expect(parseDuration('100ms')).toBe(100);
5
+ expect(parseDuration('30s')).toBe(30000);
6
+ expect(parseDuration('5m')).toBe(300000);
7
+ expect(parseDuration('1h')).toBe(3600000);
8
+ });
9
+ it('should throw on invalid format', () => {
10
+ expect(() => parseDuration('invalid')).toThrow('Invalid duration format');
11
+ expect(() => parseDuration('10')).toThrow('Invalid duration format');
12
+ });
13
+ });
@@ -1,6 +1,12 @@
1
1
  /**
2
2
  * Centralized error handling for ARK CLI
3
3
  */
4
+ export declare const ExitCodes: {
5
+ readonly Success: 0;
6
+ readonly CliError: 1;
7
+ readonly OperationError: 2;
8
+ readonly Timeout: 3;
9
+ };
4
10
  export declare enum ErrorCode {
5
11
  INVALID_INPUT = "INVALID_INPUT",
6
12
  FILE_NOT_FOUND = "FILE_NOT_FOUND",
@@ -3,6 +3,12 @@
3
3
  */
4
4
  import chalk from 'chalk';
5
5
  import fs from 'fs';
6
+ export const ExitCodes = {
7
+ Success: 0,
8
+ CliError: 1,
9
+ OperationError: 2,
10
+ Timeout: 3,
11
+ };
6
12
  export var ErrorCode;
7
13
  (function (ErrorCode) {
8
14
  ErrorCode["INVALID_INPUT"] = "INVALID_INPUT";
@@ -6,6 +6,8 @@ export interface QueryOptions {
6
6
  targetType: string;
7
7
  targetName: string;
8
8
  message: string;
9
+ timeout?: string;
10
+ watchTimeout?: string;
9
11
  verbose?: boolean;
10
12
  }
11
13
  /**
@@ -3,17 +3,24 @@
3
3
  */
4
4
  import { execa } from 'execa';
5
5
  import ora from 'ora';
6
+ import chalk from 'chalk';
6
7
  import output from './output.js';
8
+ import { ExitCodes } from './errors.js';
9
+ import { parseDuration } from './duration.js';
7
10
  /**
8
11
  * Execute a query against any ARK target (model, agent, team)
9
12
  * This is the shared implementation used by all query commands
10
13
  */
11
14
  export async function executeQuery(options) {
12
15
  const spinner = ora('Creating query...').start();
13
- // Generate a unique query name
16
+ const queryTimeoutMs = options.timeout
17
+ ? parseDuration(options.timeout)
18
+ : parseDuration('5m');
19
+ const watchTimeoutMs = options.watchTimeout
20
+ ? parseDuration(options.watchTimeout)
21
+ : queryTimeoutMs + 60000;
14
22
  const timestamp = Date.now();
15
23
  const queryName = `cli-query-${timestamp}`;
16
- // Create the Query resource
17
24
  const queryManifest = {
18
25
  apiVersion: 'ark.mckinsey.com/v1alpha1',
19
26
  kind: 'Query',
@@ -22,6 +29,7 @@ export async function executeQuery(options) {
22
29
  },
23
30
  spec: {
24
31
  input: options.message,
32
+ ...(options.timeout && { timeout: options.timeout }),
25
33
  targets: [
26
34
  {
27
35
  type: options.targetType,
@@ -37,86 +45,67 @@ export async function executeQuery(options) {
37
45
  input: JSON.stringify(queryManifest),
38
46
  stdio: ['pipe', 'pipe', 'pipe'],
39
47
  });
40
- // Watch for query completion
41
- spinner.text = 'Query status: initializing';
42
- let queryComplete = false;
43
- let attempts = 0;
44
- const maxAttempts = 300; // 5 minutes with 1 second intervals
45
- while (!queryComplete && attempts < maxAttempts) {
46
- attempts++;
47
- try {
48
- const { stdout } = await execa('kubectl', ['get', 'query', queryName, '-o', 'json'], { stdio: 'pipe' });
49
- const query = JSON.parse(stdout);
50
- const phase = query.status?.phase;
51
- // Update spinner with current phase
52
- if (phase) {
53
- spinner.text = `Query status: ${phase}`;
54
- }
55
- // Check if query is complete based on phase
56
- if (phase === 'done') {
57
- queryComplete = true;
58
- spinner.succeed('Query completed');
59
- // Extract and display the response from responses array
60
- if (query.status?.responses && query.status.responses.length > 0) {
61
- const response = query.status.responses[0];
62
- console.log('\n' + (response.content || response));
63
- }
64
- else {
65
- output.warning('No response received');
66
- }
67
- }
68
- else if (phase === 'error') {
69
- queryComplete = true;
70
- spinner.fail('Query failed');
71
- // Try to get error message from conditions or status
72
- const errorCondition = query.status?.conditions?.find((c) => {
73
- return c.type === 'Complete' && c.status === 'False';
74
- });
75
- if (errorCondition?.message) {
76
- output.error(errorCondition.message);
77
- }
78
- else if (query.status?.error) {
79
- output.error(query.status.error);
80
- }
81
- else {
82
- output.error('Query failed with unknown error');
83
- }
48
+ // Watch for query completion using kubectl wait
49
+ spinner.text = 'Waiting for query completion...';
50
+ try {
51
+ await execa('kubectl', [
52
+ 'wait',
53
+ '--for=condition=Completed',
54
+ `query/${queryName}`,
55
+ `--timeout=${Math.floor(watchTimeoutMs / 1000)}s`,
56
+ ], { timeout: watchTimeoutMs });
57
+ }
58
+ catch (error) {
59
+ spinner.stop();
60
+ // Check if it's a timeout or other error
61
+ if (error instanceof Error &&
62
+ error.message.includes('timed out waiting')) {
63
+ console.error(chalk.red(`Query did not complete within ${options.watchTimeout ?? `${Math.floor(watchTimeoutMs / 1000)}s`}`));
64
+ process.exit(ExitCodes.Timeout);
65
+ }
66
+ // For other errors, fetch the query to check status
67
+ }
68
+ spinner.stop();
69
+ // Fetch final query state
70
+ try {
71
+ const { stdout } = await execa('kubectl', ['get', 'query', queryName, '-o', 'json'], { stdio: 'pipe' });
72
+ const query = JSON.parse(stdout);
73
+ const phase = query.status?.phase;
74
+ // Check if query completed successfully or with error
75
+ if (phase === 'done') {
76
+ // Extract and display the response from responses array
77
+ if (query.status?.responses && query.status.responses.length > 0) {
78
+ const response = query.status.responses[0];
79
+ console.log(response.content || response);
84
80
  }
85
- else if (phase === 'canceled') {
86
- queryComplete = true;
87
- spinner.warn('Query canceled');
88
- // Try to get cancellation reason if available
89
- if (query.status?.message) {
90
- output.warning(query.status.message);
91
- }
81
+ else {
82
+ output.warning('No response received');
92
83
  }
93
84
  }
94
- catch {
95
- // Query might not exist yet, continue waiting
96
- spinner.text = 'Query status: waiting for query to be created';
85
+ else if (phase === 'error') {
86
+ const response = query.status?.responses?.[0];
87
+ console.error(chalk.red(response?.content || 'Query failed with unknown error'));
88
+ process.exit(ExitCodes.OperationError);
97
89
  }
98
- if (!queryComplete) {
99
- await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
90
+ else if (phase === 'canceled') {
91
+ spinner.warn('Query canceled');
92
+ if (query.status?.message) {
93
+ output.warning(query.status.message);
94
+ }
95
+ process.exit(ExitCodes.OperationError);
100
96
  }
101
97
  }
102
- if (!queryComplete) {
103
- spinner.fail('Query timed out');
104
- output.error('Query did not complete within 5 minutes');
98
+ catch (error) {
99
+ console.error(chalk.red(error instanceof Error
100
+ ? error.message
101
+ : 'Failed to fetch query result'));
102
+ process.exit(ExitCodes.CliError);
105
103
  }
106
104
  }
107
105
  catch (error) {
108
- spinner.fail('Query failed');
109
- output.error(error instanceof Error ? error.message : 'Unknown error');
110
- process.exit(1);
111
- }
112
- finally {
113
- // Clean up the query resource
114
- try {
115
- await execa('kubectl', ['delete', 'query', queryName], { stdio: 'pipe' });
116
- }
117
- catch {
118
- // Ignore cleanup errors
119
- }
106
+ spinner.stop();
107
+ console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
108
+ process.exit(ExitCodes.CliError);
120
109
  }
121
110
  }
122
111
  /**
@@ -8,6 +8,7 @@ const mockSpinner = {
8
8
  succeed: jest.fn(),
9
9
  fail: jest.fn(),
10
10
  warn: jest.fn(),
11
+ stop: jest.fn(),
11
12
  text: '',
12
13
  };
13
14
  const mockOra = jest.fn(() => mockSpinner);
@@ -25,7 +26,11 @@ const mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {
25
26
  throw new Error('process.exit called');
26
27
  }));
27
28
  const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => { });
29
+ const mockConsoleError = jest
30
+ .spyOn(console, 'error')
31
+ .mockImplementation(() => { });
28
32
  const { executeQuery, parseTarget } = await import('./executeQuery.js');
33
+ const { ExitCodes } = await import('./errors.js');
29
34
  describe('executeQuery', () => {
30
35
  beforeEach(() => {
31
36
  jest.clearAllMocks();
@@ -71,9 +76,6 @@ describe('executeQuery', () => {
71
76
  exitCode: 0,
72
77
  };
73
78
  }
74
- if (args.includes('delete')) {
75
- return { stdout: '', stderr: '', exitCode: 0 };
76
- }
77
79
  return { stdout: '', stderr: '', exitCode: 0 };
78
80
  });
79
81
  await executeQuery({
@@ -82,14 +84,14 @@ describe('executeQuery', () => {
82
84
  message: 'Hello',
83
85
  });
84
86
  expect(mockSpinner.start).toHaveBeenCalled();
85
- expect(mockSpinner.succeed).toHaveBeenCalledWith('Query completed');
86
- expect(mockConsoleLog).toHaveBeenCalledWith('\nTest response');
87
+ expect(mockSpinner.stop).toHaveBeenCalled();
88
+ expect(mockConsoleLog).toHaveBeenCalledWith('Test response');
87
89
  });
88
- it('should handle query error phase', async () => {
90
+ it('should handle query error phase and exit with code 2', async () => {
89
91
  const mockQueryResponse = {
90
92
  status: {
91
93
  phase: 'error',
92
- error: 'Query failed with test error',
94
+ responses: [{ content: 'Query failed with test error' }],
93
95
  },
94
96
  };
95
97
  mockExeca.mockImplementation(async (command, args) => {
@@ -103,20 +105,23 @@ describe('executeQuery', () => {
103
105
  exitCode: 0,
104
106
  };
105
107
  }
106
- if (args.includes('delete')) {
107
- return { stdout: '', stderr: '', exitCode: 0 };
108
- }
109
108
  return { stdout: '', stderr: '', exitCode: 0 };
110
109
  });
111
- await executeQuery({
112
- targetType: 'model',
113
- targetName: 'default',
114
- message: 'Hello',
115
- });
116
- expect(mockSpinner.fail).toHaveBeenCalledWith('Query failed');
117
- expect(mockOutput.error).toHaveBeenCalledWith('Query failed with test error');
110
+ try {
111
+ await executeQuery({
112
+ targetType: 'model',
113
+ targetName: 'default',
114
+ message: 'Hello',
115
+ });
116
+ }
117
+ catch (error) {
118
+ expect(error.message).toBe('process.exit called');
119
+ }
120
+ expect(mockSpinner.stop).toHaveBeenCalled();
121
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Query failed with test error'));
122
+ expect(mockExit).toHaveBeenCalledWith(ExitCodes.OperationError);
118
123
  });
119
- it('should handle query canceled phase', async () => {
124
+ it('should handle query canceled phase and exit with code 2', async () => {
120
125
  const mockQueryResponse = {
121
126
  status: {
122
127
  phase: 'canceled',
@@ -134,27 +139,27 @@ describe('executeQuery', () => {
134
139
  exitCode: 0,
135
140
  };
136
141
  }
137
- if (args.includes('delete')) {
138
- return { stdout: '', stderr: '', exitCode: 0 };
139
- }
140
142
  return { stdout: '', stderr: '', exitCode: 0 };
141
143
  });
142
- await executeQuery({
143
- targetType: 'agent',
144
- targetName: 'test-agent',
145
- message: 'Hello',
146
- });
144
+ try {
145
+ await executeQuery({
146
+ targetType: 'agent',
147
+ targetName: 'test-agent',
148
+ message: 'Hello',
149
+ });
150
+ }
151
+ catch (error) {
152
+ expect(error.message).toBe('process.exit called');
153
+ }
147
154
  expect(mockSpinner.warn).toHaveBeenCalledWith('Query canceled');
148
155
  expect(mockOutput.warning).toHaveBeenCalledWith('Query was canceled');
156
+ expect(mockExit).toHaveBeenCalledWith(ExitCodes.OperationError);
149
157
  });
150
- it('should clean up query resource even on failure', async () => {
158
+ it('should handle kubectl apply failures with exit code 1', async () => {
151
159
  mockExeca.mockImplementation(async (command, args) => {
152
160
  if (args.includes('apply')) {
153
161
  throw new Error('Failed to apply');
154
162
  }
155
- if (args.includes('delete')) {
156
- return { stdout: '', stderr: '', exitCode: 0 };
157
- }
158
163
  return { stdout: '', stderr: '', exitCode: 0 };
159
164
  });
160
165
  await expect(executeQuery({
@@ -162,9 +167,37 @@ describe('executeQuery', () => {
162
167
  targetName: 'default',
163
168
  message: 'Hello',
164
169
  })).rejects.toThrow('process.exit called');
165
- expect(mockSpinner.fail).toHaveBeenCalledWith('Query failed');
166
- expect(mockOutput.error).toHaveBeenCalledWith('Failed to apply');
167
- expect(mockExit).toHaveBeenCalledWith(1);
170
+ expect(mockSpinner.stop).toHaveBeenCalled();
171
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Failed to apply'));
172
+ expect(mockExit).toHaveBeenCalledWith(ExitCodes.CliError);
173
+ });
174
+ it('should handle query timeout and exit with code 3', async () => {
175
+ mockExeca.mockImplementation(async (command, args) => {
176
+ if (args.includes('apply')) {
177
+ return { stdout: '', stderr: '', exitCode: 0 };
178
+ }
179
+ if (args.includes('wait')) {
180
+ // Simulate kubectl wait timeout
181
+ const error = new Error('timed out waiting for the condition');
182
+ throw error;
183
+ }
184
+ return { stdout: '', stderr: '', exitCode: 0 };
185
+ });
186
+ try {
187
+ await executeQuery({
188
+ targetType: 'model',
189
+ targetName: 'default',
190
+ message: 'Hello',
191
+ timeout: '100ms',
192
+ watchTimeout: '200ms',
193
+ });
194
+ }
195
+ catch (error) {
196
+ expect(error.message).toBe('process.exit called');
197
+ }
198
+ expect(mockSpinner.stop).toHaveBeenCalled();
199
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Query did not complete within 200ms'));
200
+ expect(mockExit).toHaveBeenCalledWith(ExitCodes.Timeout);
168
201
  });
169
202
  });
170
203
  });
@@ -0,0 +1,19 @@
1
+ export interface Target {
2
+ name: string;
3
+ namespace: string;
4
+ description?: string;
5
+ }
6
+ interface TargetSelectorProps {
7
+ targets: Target[];
8
+ title: string;
9
+ subtitle: string;
10
+ onSelect: (target: Target) => void;
11
+ onExit: () => void;
12
+ formatLabel?: (target: Target) => string;
13
+ formatInlineDetail?: (target: Target) => string | undefined;
14
+ showDetailPanel?: boolean;
15
+ loading?: boolean;
16
+ error?: string;
17
+ }
18
+ export declare function TargetSelector({ targets, title, subtitle, onSelect, onExit, formatLabel, formatInlineDetail, showDetailPanel, loading, error, }: TargetSelectorProps): import("react/jsx-runtime").JSX.Element;
19
+ export {};
@@ -0,0 +1,48 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ export function TargetSelector({ targets, title, subtitle, onSelect, onExit, formatLabel = (target) => target.name, formatInlineDetail, showDetailPanel = false, loading = false, error, }) {
5
+ const [selectedIndex, setSelectedIndex] = useState(0);
6
+ useInput((input, key) => {
7
+ if (key.escape) {
8
+ onExit();
9
+ }
10
+ else if (key.upArrow || input === 'k') {
11
+ setSelectedIndex((prev) => (prev === 0 ? targets.length - 1 : prev - 1));
12
+ }
13
+ else if (key.downArrow || input === 'j') {
14
+ setSelectedIndex((prev) => (prev === targets.length - 1 ? 0 : prev + 1));
15
+ }
16
+ else if (key.return) {
17
+ onSelect(targets[selectedIndex]);
18
+ }
19
+ else {
20
+ const num = parseInt(input, 10);
21
+ if (!isNaN(num) && num >= 1 && num <= targets.length) {
22
+ onSelect(targets[num - 1]);
23
+ }
24
+ }
25
+ });
26
+ const truncate = (text, maxLength) => {
27
+ if (text.length <= maxLength)
28
+ return text;
29
+ return text.substring(0, maxLength - 3) + '...';
30
+ };
31
+ if (loading) {
32
+ return (_jsx(Box, { children: _jsxs(Text, { children: ["Loading ", title.toLowerCase(), "..."] }) }));
33
+ }
34
+ if (error) {
35
+ return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) }));
36
+ }
37
+ if (targets.length === 0) {
38
+ return (_jsx(Box, { children: _jsxs(Text, { children: ["No ", title.toLowerCase(), " available"] }) }));
39
+ }
40
+ const selectedTarget = targets[selectedIndex];
41
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 2, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: title }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: subtitle }) }), _jsx(Box, { flexDirection: "column", children: targets.map((target, index) => {
42
+ const label = formatLabel(target);
43
+ const prefix = `${index === selectedIndex ? '❯ ' : ' '}${index + 1}. ${label}`;
44
+ const inlineDetail = formatInlineDetail?.(target);
45
+ const maxDetailLength = 80 - prefix.length;
46
+ return (_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { color: index === selectedIndex ? 'green' : undefined, children: prefix }), inlineDetail && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', truncate(inlineDetail, maxDetailLength)] }))] }, `${target.namespace}-${target.name}`));
47
+ }) }), showDetailPanel && selectedTarget.description && (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: selectedTarget.description }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Number to select \u00B7 Esc to exit" }) })] }));
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-at-scale/ark",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "Ark CLI - Interactive terminal interface for ARK agents",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -62,6 +62,7 @@ kubectl get queries
62
62
  | `image.repository` | string | `"{{ .Values.mcpServerName }}"` | Image repository |
63
63
  | `image.tag` | string | `"latest"` | Image tag |
64
64
  | `service.port` | int | `8080` | Service port |
65
+ | `mcpServer.timeout` | string | `"30s"` | Timeout for MCP tool calls (e.g., "5m", "10m") |
65
66
 
66
67
  {{- if .Values.requiresAuth }}
67
68
  | `auth.token` | string | `""` | Authentication token |
@@ -15,6 +15,9 @@ spec:
15
15
  path: {{ .Values.mcpServer.path }}
16
16
  {{- end }}
17
17
  transport: sse
18
+ {{- if .Values.mcpServer.timeout }}
19
+ timeout: {{ .Values.mcpServer.timeout | quote }}
20
+ {{- end }}
18
21
  {{- if .Values.mcpServer.description }}
19
22
  description: {{ .Values.mcpServer.description | quote }}
20
23
  {{- end }}
@@ -81,4 +81,5 @@ mcpServer:
81
81
  create: true
82
82
  name: ""
83
83
  description: "{{ .Values.description }}"
84
- path: "/mcp"
84
+ path: "/mcp"
85
+ # timeout: "30s" # Uncomment and adjust for long-running operations (e.g., "5m", "10m")
@@ -1,8 +0,0 @@
1
- import { Agent, ArkApiClient } from '../lib/arkApiClient.js';
2
- interface AgentSelectorProps {
3
- arkApiClient: ArkApiClient;
4
- onSelect: (agent: Agent) => void;
5
- onExit: () => void;
6
- }
7
- export declare function AgentSelector({ arkApiClient, onSelect, onExit, }: AgentSelectorProps): import("react/jsx-runtime").JSX.Element;
8
- export {};
@@ -1,53 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- export function AgentSelector({ arkApiClient, onSelect, onExit, }) {
5
- const [selectedIndex, setSelectedIndex] = useState(0);
6
- const [agents, setAgents] = useState([]);
7
- const [loading, setLoading] = useState(true);
8
- const [error, setError] = useState(null);
9
- useEffect(() => {
10
- arkApiClient
11
- .getAgents()
12
- .then((fetchedAgents) => {
13
- setAgents(fetchedAgents);
14
- setLoading(false);
15
- })
16
- .catch((err) => {
17
- setError(err.message || 'Failed to fetch agents');
18
- setLoading(false);
19
- });
20
- }, [arkApiClient]);
21
- useInput((input, key) => {
22
- if (key.escape) {
23
- onExit();
24
- }
25
- else if (key.upArrow || input === 'k') {
26
- setSelectedIndex((prev) => (prev === 0 ? agents.length - 1 : prev - 1));
27
- }
28
- else if (key.downArrow || input === 'j') {
29
- setSelectedIndex((prev) => (prev === agents.length - 1 ? 0 : prev + 1));
30
- }
31
- else if (key.return) {
32
- onSelect(agents[selectedIndex]);
33
- }
34
- else {
35
- // Handle number keys for quick selection
36
- const num = parseInt(input, 10);
37
- if (!isNaN(num) && num >= 1 && num <= agents.length) {
38
- onSelect(agents[num - 1]);
39
- }
40
- }
41
- });
42
- if (loading) {
43
- return (_jsx(Box, { children: _jsx(Text, { children: "Loading agents..." }) }));
44
- }
45
- if (error) {
46
- return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) }));
47
- }
48
- if (agents.length === 0) {
49
- return (_jsx(Box, { children: _jsx(Text, { children: "No agents available" }) }));
50
- }
51
- const selectedAgent = agents[selectedIndex];
52
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 2, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Select Agent" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Choose an agent to start a conversation with" }) }), _jsx(Box, { flexDirection: "column", children: agents.map((agent, index) => (_jsx(Box, { marginBottom: 0, children: _jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, children: [index === selectedIndex ? '❯ ' : ' ', index + 1, ". ", agent.name] }) }, agent.name))) }), selectedAgent.description && (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: selectedAgent.description }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Number to select \u00B7 Esc to exit" }) })] }));
53
- }
@@ -1,8 +0,0 @@
1
- import { Model, ArkApiClient } from '../lib/arkApiClient.js';
2
- interface ModelSelectorProps {
3
- arkApiClient: ArkApiClient;
4
- onSelect: (model: Model) => void;
5
- onExit: () => void;
6
- }
7
- export declare function ModelSelector({ arkApiClient, onSelect, onExit, }: ModelSelectorProps): import("react/jsx-runtime").JSX.Element;
8
- export {};
@@ -1,53 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- export function ModelSelector({ arkApiClient, onSelect, onExit, }) {
5
- const [selectedIndex, setSelectedIndex] = useState(0);
6
- const [models, setModels] = useState([]);
7
- const [loading, setLoading] = useState(true);
8
- const [error, setError] = useState(null);
9
- useEffect(() => {
10
- arkApiClient
11
- .getModels()
12
- .then((fetchedModels) => {
13
- setModels(fetchedModels);
14
- setLoading(false);
15
- })
16
- .catch((err) => {
17
- setError(err.message || 'Failed to fetch models');
18
- setLoading(false);
19
- });
20
- }, [arkApiClient]);
21
- useInput((input, key) => {
22
- if (key.escape) {
23
- onExit();
24
- }
25
- else if (key.upArrow || input === 'k') {
26
- setSelectedIndex((prev) => (prev === 0 ? models.length - 1 : prev - 1));
27
- }
28
- else if (key.downArrow || input === 'j') {
29
- setSelectedIndex((prev) => (prev === models.length - 1 ? 0 : prev + 1));
30
- }
31
- else if (key.return) {
32
- onSelect(models[selectedIndex]);
33
- }
34
- else {
35
- // Handle number keys for quick selection
36
- const num = parseInt(input, 10);
37
- if (!isNaN(num) && num >= 1 && num <= models.length) {
38
- onSelect(models[num - 1]);
39
- }
40
- }
41
- });
42
- if (loading) {
43
- return (_jsx(Box, { children: _jsx(Text, { children: "Loading models..." }) }));
44
- }
45
- if (error) {
46
- return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) }));
47
- }
48
- if (models.length === 0) {
49
- return (_jsx(Box, { children: _jsx(Text, { children: "No models available" }) }));
50
- }
51
- const selectedModel = models[selectedIndex];
52
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 2, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Select Model" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Choose a model to start a conversation with" }) }), _jsx(Box, { flexDirection: "column", children: models.map((model, index) => (_jsx(Box, { marginBottom: 0, children: _jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, children: [index === selectedIndex ? '❯ ' : ' ', index + 1, ". ", model.name, model.type ? ` (${model.type})` : ''] }) }, model.name))) }), selectedModel && selectedModel.model && (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsxs(Text, { dimColor: true, wrap: "wrap", children: ["Model: ", selectedModel.model] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Number to select \u00B7 Esc to exit" }) })] }));
53
- }
@@ -1,8 +0,0 @@
1
- import { Team, ArkApiClient } from '../lib/arkApiClient.js';
2
- interface TeamSelectorProps {
3
- arkApiClient: ArkApiClient;
4
- onSelect: (team: Team) => void;
5
- onExit: () => void;
6
- }
7
- export declare function TeamSelector({ arkApiClient, onSelect, onExit, }: TeamSelectorProps): import("react/jsx-runtime").JSX.Element;
8
- export {};
@@ -1,55 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- export function TeamSelector({ arkApiClient, onSelect, onExit, }) {
5
- const [selectedIndex, setSelectedIndex] = useState(0);
6
- const [teams, setTeams] = useState([]);
7
- const [loading, setLoading] = useState(true);
8
- const [error, setError] = useState(null);
9
- useEffect(() => {
10
- arkApiClient
11
- .getTeams()
12
- .then((fetchedTeams) => {
13
- setTeams(fetchedTeams);
14
- setLoading(false);
15
- })
16
- .catch((err) => {
17
- setError(err.message || 'Failed to fetch teams');
18
- setLoading(false);
19
- });
20
- }, [arkApiClient]);
21
- useInput((input, key) => {
22
- if (key.escape) {
23
- onExit();
24
- }
25
- else if (key.upArrow || input === 'k') {
26
- setSelectedIndex((prev) => (prev === 0 ? teams.length - 1 : prev - 1));
27
- }
28
- else if (key.downArrow || input === 'j') {
29
- setSelectedIndex((prev) => (prev === teams.length - 1 ? 0 : prev + 1));
30
- }
31
- else if (key.return) {
32
- onSelect(teams[selectedIndex]);
33
- }
34
- else {
35
- // Handle number keys for quick selection
36
- const num = parseInt(input, 10);
37
- if (!isNaN(num) && num >= 1 && num <= teams.length) {
38
- onSelect(teams[num - 1]);
39
- }
40
- }
41
- });
42
- if (loading) {
43
- return (_jsx(Box, { children: _jsx(Text, { children: "Loading teams..." }) }));
44
- }
45
- if (error) {
46
- return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) }));
47
- }
48
- if (teams.length === 0) {
49
- return (_jsx(Box, { children: _jsx(Text, { children: "No teams available" }) }));
50
- }
51
- const selectedTeam = teams[selectedIndex];
52
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 2, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Select Team" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Choose a team to start a conversation with" }) }), _jsx(Box, { flexDirection: "column", children: teams.map((team, index) => (_jsx(Box, { marginBottom: 0, children: _jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, children: [index === selectedIndex ? '❯ ' : ' ', index + 1, ". ", team.name, team.strategy ? ` (${team.strategy})` : ''] }) }, team.name))) }), selectedTeam &&
53
- (selectedTeam.description || selectedTeam.members_count) && (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: selectedTeam.description ||
54
- `Members: ${selectedTeam.members_count}` }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Number to select \u00B7 Esc to exit" }) })] }));
55
- }
@@ -1,8 +0,0 @@
1
- import { Tool, ArkApiClient } from '../lib/arkApiClient.js';
2
- interface ToolSelectorProps {
3
- arkApiClient: ArkApiClient;
4
- onSelect: (tool: Tool) => void;
5
- onExit: () => void;
6
- }
7
- export declare function ToolSelector({ arkApiClient, onSelect, onExit, }: ToolSelectorProps): import("react/jsx-runtime").JSX.Element;
8
- export {};
@@ -1,53 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- export function ToolSelector({ arkApiClient, onSelect, onExit, }) {
5
- const [selectedIndex, setSelectedIndex] = useState(0);
6
- const [tools, setTools] = useState([]);
7
- const [loading, setLoading] = useState(true);
8
- const [error, setError] = useState(null);
9
- useEffect(() => {
10
- arkApiClient
11
- .getTools()
12
- .then((fetchedTools) => {
13
- setTools(fetchedTools);
14
- setLoading(false);
15
- })
16
- .catch((err) => {
17
- setError(err.message || 'Failed to fetch tools');
18
- setLoading(false);
19
- });
20
- }, [arkApiClient]);
21
- useInput((input, key) => {
22
- if (key.escape) {
23
- onExit();
24
- }
25
- else if (key.upArrow || input === 'k') {
26
- setSelectedIndex((prev) => (prev === 0 ? tools.length - 1 : prev - 1));
27
- }
28
- else if (key.downArrow || input === 'j') {
29
- setSelectedIndex((prev) => (prev === tools.length - 1 ? 0 : prev + 1));
30
- }
31
- else if (key.return) {
32
- onSelect(tools[selectedIndex]);
33
- }
34
- else {
35
- // Handle number keys for quick selection
36
- const num = parseInt(input, 10);
37
- if (!isNaN(num) && num >= 1 && num <= tools.length) {
38
- onSelect(tools[num - 1]);
39
- }
40
- }
41
- });
42
- if (loading) {
43
- return (_jsx(Box, { children: _jsx(Text, { children: "Loading tools..." }) }));
44
- }
45
- if (error) {
46
- return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) }));
47
- }
48
- if (tools.length === 0) {
49
- return (_jsx(Box, { children: _jsx(Text, { children: "No tools available" }) }));
50
- }
51
- const selectedTool = tools[selectedIndex];
52
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 2, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Select Tool" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Choose a tool to start a conversation with" }) }), _jsx(Box, { flexDirection: "column", children: tools.map((tool, index) => (_jsx(Box, { marginBottom: 0, children: _jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, children: [index === selectedIndex ? '❯ ' : ' ', index + 1, ". ", tool.name] }) }, tool.name))) }), selectedTool && selectedTool.description && (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: selectedTool.description }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Number to select \u00B7 Esc to exit" }) })] }));
53
- }