@agents-at-scale/ark 0.1.37 → 0.1.38

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 (95) hide show
  1. package/dist/commands/chat/index.js +1 -2
  2. package/dist/commands/generate/generators/project.js +33 -26
  3. package/dist/commands/generate/index.js +2 -2
  4. package/dist/commands/generate/templateDiscovery.js +13 -4
  5. package/dist/components/AsyncOperation.d.ts +54 -0
  6. package/dist/components/AsyncOperation.js +110 -0
  7. package/dist/components/ChatUI.js +21 -72
  8. package/dist/components/SelectMenu.d.ts +17 -0
  9. package/dist/components/SelectMenu.js +21 -0
  10. package/dist/components/StatusMessage.d.ts +20 -0
  11. package/dist/components/StatusMessage.js +13 -0
  12. package/dist/ui/asyncOperations/connectingToArk.d.ts +15 -0
  13. package/dist/ui/asyncOperations/connectingToArk.js +63 -0
  14. package/package.json +5 -3
  15. package/templates/agent/agent.template.yaml +27 -0
  16. package/templates/marketplace/.editorconfig +24 -0
  17. package/templates/marketplace/.github/.keep +11 -0
  18. package/templates/marketplace/.github/workflows/.keep +16 -0
  19. package/templates/marketplace/.helmignore +23 -0
  20. package/templates/marketplace/.prettierrc.json +20 -0
  21. package/templates/marketplace/.yamllint.yml +53 -0
  22. package/templates/marketplace/README.md +197 -0
  23. package/templates/marketplace/agents/.keep +29 -0
  24. package/templates/marketplace/docs/.keep +19 -0
  25. package/templates/marketplace/mcp-servers/.keep +32 -0
  26. package/templates/marketplace/models/.keep +23 -0
  27. package/templates/marketplace/projects/.keep +43 -0
  28. package/templates/marketplace/queries/.keep +25 -0
  29. package/templates/marketplace/teams/.keep +29 -0
  30. package/templates/marketplace/tools/.keep +32 -0
  31. package/templates/marketplace/tools/examples/.keep +17 -0
  32. package/templates/mcp-server/Dockerfile +133 -0
  33. package/templates/mcp-server/Makefile +186 -0
  34. package/templates/mcp-server/README.md +178 -0
  35. package/templates/mcp-server/build.sh +76 -0
  36. package/templates/mcp-server/chart/Chart.yaml +22 -0
  37. package/templates/mcp-server/chart/templates/_helpers.tpl +62 -0
  38. package/templates/mcp-server/chart/templates/deployment.yaml +80 -0
  39. package/templates/mcp-server/chart/templates/hpa.yaml +32 -0
  40. package/templates/mcp-server/chart/templates/mcpserver.yaml +21 -0
  41. package/templates/mcp-server/chart/templates/secret.yaml +11 -0
  42. package/templates/mcp-server/chart/templates/service.yaml +15 -0
  43. package/templates/mcp-server/chart/templates/serviceaccount.yaml +13 -0
  44. package/templates/mcp-server/chart/values.yaml +84 -0
  45. package/templates/mcp-server/example-values.yaml +74 -0
  46. package/templates/mcp-server/examples/{{ .Values.mcpServerName }}-agent.yaml +33 -0
  47. package/templates/mcp-server/examples/{{ .Values.mcpServerName }}-query.yaml +24 -0
  48. package/templates/models/azure.yaml +33 -0
  49. package/templates/models/claude.yaml +28 -0
  50. package/templates/models/gemini.yaml +28 -0
  51. package/templates/models/openai.yaml +39 -0
  52. package/templates/project/.editorconfig +24 -0
  53. package/templates/project/.helmignore +24 -0
  54. package/templates/project/.prettierrc.json +16 -0
  55. package/templates/project/.yamllint.yml +50 -0
  56. package/templates/project/Chart.yaml +19 -0
  57. package/templates/project/Makefile +360 -0
  58. package/templates/project/README.md +377 -0
  59. package/templates/project/agents/.keep +11 -0
  60. package/templates/project/docs/.keep +14 -0
  61. package/templates/project/mcp-servers/.keep +34 -0
  62. package/templates/project/models/.keep +17 -0
  63. package/templates/project/queries/.keep +11 -0
  64. package/templates/project/scripts/setup.sh +108 -0
  65. package/templates/project/teams/.keep +11 -0
  66. package/templates/project/templates/00-rbac.yaml +168 -0
  67. package/templates/project/templates/01-models.yaml +11 -0
  68. package/templates/project/templates/02-mcp-servers.yaml +22 -0
  69. package/templates/project/templates/03-tools.yaml +12 -0
  70. package/templates/project/templates/04-agents.yaml +12 -0
  71. package/templates/project/templates/05-teams.yaml +11 -0
  72. package/templates/project/templates/06-queries.yaml +11 -0
  73. package/templates/project/templates/_helpers.tpl +91 -0
  74. package/templates/project/tests/e2e/.keep +10 -0
  75. package/templates/project/tests/unit/.keep +10 -0
  76. package/templates/project/tools/.keep +25 -0
  77. package/templates/project/tools/example-tool.yaml.disabled +94 -0
  78. package/templates/project/tools/examples/data-tool/Dockerfile +32 -0
  79. package/templates/project/values.yaml +141 -0
  80. package/templates/query/query.template.yaml +13 -0
  81. package/templates/team/team.template.yaml +17 -0
  82. package/templates/tool/.python-version +1 -0
  83. package/templates/tool/Dockerfile +23 -0
  84. package/templates/tool/README.md +238 -0
  85. package/templates/tool/agent.yaml +19 -0
  86. package/templates/tool/deploy.sh +10 -0
  87. package/templates/tool/deployment/deployment.yaml +31 -0
  88. package/templates/tool/deployment/kustomization.yaml +7 -0
  89. package/templates/tool/deployment/mcpserver.yaml +12 -0
  90. package/templates/tool/deployment/service.yaml +12 -0
  91. package/templates/tool/deployment/serviceaccount.yaml +8 -0
  92. package/templates/tool/deployment/values.yaml +3 -0
  93. package/templates/tool/pyproject.toml +9 -0
  94. package/templates/tool/src/main.py +36 -0
  95. package/templates/tool/uv.lock +498 -0
@@ -13,14 +13,13 @@ export function createChatCommand(config) {
13
13
  // Direct target argument (e.g., "agent/sample-agent")
14
14
  const initialTargetId = targetArg;
15
15
  // Config is passed from main
16
- // Initialize proxy first - no spinner, just let ChatUI handle loading state
17
16
  try {
18
17
  const proxy = new ArkApiProxy();
19
18
  const arkApiClient = await proxy.start();
20
- // Pass the initialized client and config to ChatUI
21
19
  render(_jsx(ChatUI, { initialTargetId: initialTargetId, arkApiClient: arkApiClient, arkApiProxy: proxy, config: config }));
22
20
  }
23
21
  catch (error) {
22
+ // Handle proxy startup failure or other errors
24
23
  output.error(error instanceof Error ? error.message : 'ARK API connection failed');
25
24
  process.exit(1);
26
25
  }
@@ -46,7 +46,7 @@ class ProjectGenerator {
46
46
  spinner.succeed('Prerequisites validated');
47
47
  // Get project configuration
48
48
  spinner.start('Gathering project configuration');
49
- const config = await this.getProjectConfig(name, destination, options);
49
+ const config = await this.getProjectConfig(name, destination, options, spinner);
50
50
  spinner.succeed(`Project "${config.name}" configured`);
51
51
  // Discover and configure models (only if not skipped)
52
52
  if (config.configureModels) {
@@ -104,25 +104,28 @@ class ProjectGenerator {
104
104
  console.log(chalk.cyan('💡 Tip: Install kubectl and helm later to deploy your project to a cluster'));
105
105
  }
106
106
  }
107
- async getProjectConfig(name, destination, options) {
108
- console.log(chalk.gray(`\n${'─'.repeat(50)}`));
109
- console.log(chalk.cyan('Project Configuration'));
110
- console.log(chalk.gray(`${'─'.repeat(50)}\n`));
111
- // Use command line options if provided, otherwise prompt
107
+ async getProjectConfig(name, destination, options, spinner) {
108
+ // Use command line options if provided
112
109
  let projectType = options.projectType;
113
110
  let parentDir = destination;
114
- let namespace = options.namespace || name;
111
+ let namespace = options.namespace;
115
112
  // Validate project type if provided
116
113
  if (projectType &&
117
114
  projectType !== 'empty' &&
118
115
  projectType !== 'with-samples') {
119
116
  throw new Error(`Invalid project type: ${projectType}. Must be 'empty' or 'with-samples'`);
120
117
  }
121
- // Validate and normalize namespace
122
- namespace = toKebabCase(namespace);
123
- validateNameStrict(namespace, 'namespace');
124
- // Only prompt if in interactive mode and missing required options
125
- if (options.interactive || !options.projectType || !options.namespace) {
118
+ // Default to interactive mode unless all required options are provided
119
+ // or explicitly set to non-interactive
120
+ const shouldPrompt = options.interactive !== false &&
121
+ (!options.projectType || !options.namespace);
122
+ if (shouldPrompt) {
123
+ // Stop spinner before showing prompts
124
+ spinner.stop();
125
+ // Show configuration header only when prompting
126
+ console.log(chalk.gray(`\n${'─'.repeat(50)}`));
127
+ console.log(chalk.cyan('Project Configuration'));
128
+ console.log(chalk.gray(`${'─'.repeat(50)}\n`));
126
129
  const prompts = [];
127
130
  if (!options.projectType) {
128
131
  prompts.push({
@@ -160,11 +163,19 @@ class ProjectGenerator {
160
163
  parentDir = answers.parentDir || parentDir;
161
164
  namespace = answers.namespace || namespace;
162
165
  }
166
+ // Restart spinner after prompts
167
+ spinner.start('Finalizing configuration');
163
168
  }
164
- // Ensure projectType has a value
169
+ // Use defaults for missing options
165
170
  if (!projectType) {
166
- throw new Error('Project type is required. Use --project-type <empty|with-samples> or run in interactive mode.');
171
+ projectType = GENERATOR_DEFAULTS.projectType; // 'with-samples'
167
172
  }
173
+ if (!namespace) {
174
+ namespace = name; // Default namespace to project name
175
+ }
176
+ // Validate and normalize namespace
177
+ namespace = toKebabCase(namespace);
178
+ validateNameStrict(namespace, 'namespace');
168
179
  const projectPath = path.join(parentDir, name);
169
180
  // Check if directory exists
170
181
  if (fs.existsSync(projectPath)) {
@@ -329,7 +340,10 @@ class ProjectGenerator {
329
340
  return descriptions[name] || `Model: ${name}`;
330
341
  }
331
342
  async configureGit(config) {
332
- console.log(chalk.cyan('📋 Git Repository Configuration\n'));
343
+ // If git setup is explicitly skipped, don't do anything
344
+ if (!config.initGit) {
345
+ return;
346
+ }
333
347
  // Check if git is available
334
348
  const gitAvailable = await this.isGitAvailable();
335
349
  if (!gitAvailable) {
@@ -337,7 +351,7 @@ class ProjectGenerator {
337
351
  config.initGit = false;
338
352
  return;
339
353
  }
340
- // Check if git is configured
354
+ // Check if git is configured (only if we're initializing git)
341
355
  try {
342
356
  await execa('git', ['config', 'user.name'], { stdio: 'pipe' });
343
357
  await execa('git', ['config', 'user.email'], { stdio: 'pipe' });
@@ -345,16 +359,9 @@ class ProjectGenerator {
345
359
  catch {
346
360
  console.log(chalk.yellow('⚠️ Git user not configured. Run: git config --global user.name "Your Name" && git config --global user.email "your.email@example.com"'));
347
361
  }
348
- const gitAnswers = await inquirer.prompt([
349
- {
350
- type: 'confirm',
351
- name: 'initGit',
352
- message: 'Initialize git repository with initial commit?',
353
- default: true,
354
- },
355
- ]);
356
- config.initGit = gitAnswers.initGit;
357
- config.createCommit = gitAnswers.initGit; // Always create commit if initializing git
362
+ // Since initGit is already true from command line options or defaults,
363
+ // we can proceed without prompting
364
+ config.createCommit = true; // Always create commit if initializing git
358
365
  }
359
366
  async generateProject(config) {
360
367
  console.log(chalk.cyan(CLI_CONFIG.messages.generatingProject));
@@ -214,9 +214,9 @@ ${chalk.cyan('Use Cases:')}
214
214
  : 'Working directory (default: current directory)', type === 'project' || type === 'marketplace'
215
215
  ? getDefaultDestination()
216
216
  : undefined)
217
- .option('-i, --interactive', type === 'marketplace'
217
+ .option('--no-interactive', type === 'marketplace'
218
218
  ? 'Not supported for marketplace'
219
- : 'Force additional configuration prompts (generators prompt by default when info is missing)', false);
219
+ : 'Skip interactive prompts and use defaults (prompts by default)');
220
220
  if (helpText?.examples) {
221
221
  subCommand.addHelpText('after', helpText.examples);
222
222
  }
@@ -3,11 +3,20 @@ import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  export class TemplateDiscovery {
5
5
  constructor() {
6
- // Get the path to the templates directory relative to the ark CLI
7
- // Navigate from tools/ark-cli/src/commands/generate/templateDiscovery.js to agents-at-scale/templates
6
+ // Get the path to the templates directory
7
+ // This handles both development and production scenarios
8
8
  const currentFile = fileURLToPath(import.meta.url);
9
- const arkRoot = path.resolve(path.dirname(currentFile), '../../../../../');
10
- this.templatesPath = path.join(arkRoot, 'templates');
9
+ // Try production path first (templates included in npm package)
10
+ const packageRoot = path.resolve(path.dirname(currentFile), '../../../');
11
+ const productionTemplatesPath = path.join(packageRoot, 'templates');
12
+ if (fs.existsSync(productionTemplatesPath)) {
13
+ this.templatesPath = productionTemplatesPath;
14
+ }
15
+ else {
16
+ // Fall back to development path (relative to ARK project root)
17
+ const arkRoot = path.resolve(path.dirname(currentFile), '../../../../../');
18
+ this.templatesPath = path.join(arkRoot, 'templates');
19
+ }
11
20
  }
12
21
  /**
13
22
  * Discover all available templates in the templates directory
@@ -0,0 +1,54 @@
1
+ import * as React from 'react';
2
+ export interface AsyncOperationConfig {
3
+ /** Message to display during operation */
4
+ message: string;
5
+ /** Async function to execute */
6
+ operation: (signal: AbortSignal) => Promise<void>;
7
+ /** Optional tip displayed below message */
8
+ tip?: string;
9
+ /** Show "(esc to interrupt)" hint */
10
+ showInterrupt?: boolean;
11
+ /** Hide UI on success */
12
+ hideOnSuccess?: boolean;
13
+ /** Called when operation fails */
14
+ onError?: (error: Error) => void;
15
+ /** Error menu options with callbacks */
16
+ errorOptions?: Array<{
17
+ label: string;
18
+ onSelect: () => void;
19
+ }>;
20
+ }
21
+ interface AsyncOperationState {
22
+ /** Current operation state - idle: not running/cleared, loading: executing, success: completed, error: failed */
23
+ status: 'idle' | 'loading' | 'success' | 'error';
24
+ /** Message shown during loading and success */
25
+ message: string;
26
+ /** Optional tip text shown below message during loading */
27
+ tip?: string;
28
+ /** Brief error message when status is error */
29
+ error?: string;
30
+ /** Full error details/stack trace when status is error */
31
+ errorDetails?: string;
32
+ /** Whether to show interrupt hint during loading */
33
+ showInterrupt: boolean;
34
+ /** Whether to hide UI after success */
35
+ hideOnSuccess: boolean;
36
+ /** Menu options shown when status is error */
37
+ errorOptions: Array<{
38
+ label: string;
39
+ onSelect: () => void;
40
+ }>;
41
+ }
42
+ export declare function useAsyncOperation(): {
43
+ state: AsyncOperationState;
44
+ run: (config: AsyncOperationConfig) => Promise<void>;
45
+ interrupt: () => void;
46
+ retry: () => void;
47
+ clear: () => void;
48
+ };
49
+ export type AsyncOperation = ReturnType<typeof useAsyncOperation>;
50
+ interface AsyncOperationStatusProps {
51
+ operation: AsyncOperation;
52
+ }
53
+ export declare const AsyncOperationStatus: React.FC<AsyncOperationStatusProps>;
54
+ export {};
@@ -0,0 +1,110 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useInput } from 'ink';
3
+ import * as React from 'react';
4
+ import { StatusMessage } from './StatusMessage.js';
5
+ import { SelectMenu } from './SelectMenu.js';
6
+ export function useAsyncOperation() {
7
+ const [state, setState] = React.useState({
8
+ status: 'idle',
9
+ message: '',
10
+ showInterrupt: false,
11
+ hideOnSuccess: false,
12
+ errorOptions: [],
13
+ });
14
+ const abortControllerRef = React.useRef(null);
15
+ const configRef = React.useRef(null);
16
+ const run = React.useCallback(async (config) => {
17
+ configRef.current = config;
18
+ const retry = () => {
19
+ if (configRef.current) {
20
+ run(configRef.current);
21
+ }
22
+ };
23
+ const errorOptions = config.errorOptions || [
24
+ { label: 'Try again', onSelect: retry },
25
+ { label: 'Quit', onSelect: () => process.exit(0) },
26
+ ];
27
+ setState({
28
+ status: 'loading',
29
+ message: config.message,
30
+ tip: config.tip,
31
+ showInterrupt: config.showInterrupt ?? false,
32
+ hideOnSuccess: config.hideOnSuccess ?? false,
33
+ errorOptions,
34
+ });
35
+ const controller = new AbortController();
36
+ abortControllerRef.current = controller;
37
+ try {
38
+ await config.operation(controller.signal);
39
+ setState((prev) => ({ ...prev, status: 'success' }));
40
+ }
41
+ catch (err) {
42
+ if (err instanceof Error && err.name === 'AbortError') {
43
+ setState((prev) => ({ ...prev, status: 'idle' }));
44
+ return;
45
+ }
46
+ const error = err instanceof Error ? err : new Error(String(err));
47
+ setState((prev) => ({
48
+ ...prev,
49
+ status: 'error',
50
+ error: error.message,
51
+ errorDetails: error.stack,
52
+ }));
53
+ config.onError?.(error);
54
+ }
55
+ finally {
56
+ abortControllerRef.current = null;
57
+ }
58
+ }, []);
59
+ const interrupt = React.useCallback(() => {
60
+ abortControllerRef.current?.abort();
61
+ abortControllerRef.current = null;
62
+ setState((prev) => ({ ...prev, status: 'idle' }));
63
+ }, []);
64
+ const retry = React.useCallback(() => {
65
+ if (configRef.current) {
66
+ run(configRef.current);
67
+ }
68
+ }, [run]);
69
+ const clear = React.useCallback(() => {
70
+ setState({
71
+ status: 'idle',
72
+ message: '',
73
+ showInterrupt: false,
74
+ hideOnSuccess: false,
75
+ errorOptions: [],
76
+ });
77
+ }, []);
78
+ return {
79
+ state,
80
+ run,
81
+ interrupt,
82
+ retry,
83
+ clear,
84
+ };
85
+ }
86
+ export const AsyncOperationStatus = ({ operation, }) => {
87
+ const { state } = operation;
88
+ useInput((input, key) => {
89
+ if (state.status === 'loading' && state.showInterrupt && key.escape) {
90
+ operation.interrupt();
91
+ return;
92
+ }
93
+ });
94
+ if (state.status === 'idle') {
95
+ return null;
96
+ }
97
+ if (state.status === 'success' && state.hideOnSuccess) {
98
+ return null;
99
+ }
100
+ if (state.status === 'loading') {
101
+ return (_jsx(StatusMessage, { status: "loading", message: state.message, hint: state.showInterrupt ? '(esc to interrupt)' : undefined, tip: state.tip }));
102
+ }
103
+ if (state.status === 'success') {
104
+ return _jsx(StatusMessage, { status: "success", message: state.message });
105
+ }
106
+ if (state.status === 'error') {
107
+ return (_jsx(StatusMessage, { status: "error", message: state.message, details: state.error, errorMessage: state.errorDetails, children: _jsx(SelectMenu, { items: state.errorOptions }) }));
108
+ }
109
+ return null;
110
+ };
@@ -2,17 +2,17 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Box, Text, useInput, useApp } from 'ink';
3
3
  import TextInput from 'ink-text-input';
4
4
  import Spinner from 'ink-spinner';
5
- import chalk from 'chalk';
6
5
  import * as React from 'react';
7
6
  import { marked } from 'marked';
8
7
  // @ts-ignore - no types available
9
8
  import TerminalRenderer from 'marked-terminal';
10
9
  import { APIError } from 'openai';
11
- import { ChatClient, } from '../lib/chatClient.js';
12
10
  import { AgentSelector } from '../ui/AgentSelector.js';
13
11
  import { ModelSelector } from '../ui/ModelSelector.js';
14
12
  import { TeamSelector } from '../ui/TeamSelector.js';
15
13
  import { ToolSelector } from '../ui/ToolSelector.js';
14
+ import { useAsyncOperation, AsyncOperationStatus } from './AsyncOperation.js';
15
+ import { createConnectingToArkOperation } from '../ui/asyncOperations/connectingToArk.js';
16
16
  // Generate a unique ID for messages
17
17
  let messageIdCounter = 0;
18
18
  const generateMessageId = () => {
@@ -31,13 +31,13 @@ const configureMarkdown = () => {
31
31
  };
32
32
  const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
33
33
  const { exit } = useApp();
34
+ const asyncOp = useAsyncOperation();
34
35
  const [messages, setMessages] = React.useState([]);
35
36
  const [input, setInput] = React.useState('');
36
37
  const [isTyping, setIsTyping] = React.useState(false);
37
38
  const [target, setTarget] = React.useState(null);
38
39
  const [availableTargets, setAvailableTargets] = React.useState([]);
39
40
  const [error, setError] = React.useState(null);
40
- const [isLoading, setIsLoading] = React.useState(true);
41
41
  const [targetIndex, setTargetIndex] = React.useState(0);
42
42
  const [abortController, setAbortController] = React.useState(null);
43
43
  const [showCommands, setShowCommands] = React.useState(false);
@@ -65,78 +65,26 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
65
65
  }, [outputFormat]);
66
66
  // Initialize chat client and fetch targets on mount
67
67
  React.useEffect(() => {
68
- const initializeChat = async () => {
69
- try {
70
- // Use the provided ArkApiClient to create ChatClient
71
- const client = new ChatClient(arkApiClient);
68
+ asyncOp.run(createConnectingToArkOperation({
69
+ arkApiClient,
70
+ initialTargetId,
71
+ onSuccess: ({ client, targets, selectedTarget, selectedIndex }) => {
72
72
  chatClientRef.current = client;
73
- const targets = await client.getQueryTargets();
74
73
  setAvailableTargets(targets);
75
- if (initialTargetId) {
76
- // If initialTargetId is provided, find and set the target
77
- const matchedTarget = targets.find((t) => t.id === initialTargetId);
78
- const matchedIndex = targets.findIndex((t) => t.id === initialTargetId);
79
- if (matchedTarget) {
80
- setTarget(matchedTarget);
81
- setTargetIndex(matchedIndex >= 0 ? matchedIndex : 0);
82
- setChatConfig((prev) => ({ ...prev, currentTarget: matchedTarget }));
83
- setMessages([]);
84
- }
85
- else {
86
- // If target not found, show error and exit
87
- console.error(chalk.red('Error:'), `Target "${initialTargetId}" not found`);
88
- console.error(chalk.gray('Use "ark targets list" to see available targets'));
89
- if (arkApiProxy) {
90
- arkApiProxy.stop();
91
- }
92
- exit();
93
- }
94
- }
95
- else if (targets.length > 0) {
96
- // No initial target specified - auto-select first available
97
- // Priority: agents > models > tools
98
- const agents = targets.filter((t) => t.type === 'agent');
99
- const models = targets.filter((t) => t.type === 'model');
100
- const tools = targets.filter((t) => t.type === 'tool');
101
- let selectedTarget = null;
102
- let selectedIndex = 0;
103
- if (agents.length > 0) {
104
- selectedTarget = agents[0];
105
- selectedIndex = targets.findIndex((t) => t.id === agents[0].id);
106
- }
107
- else if (models.length > 0) {
108
- selectedTarget = models[0];
109
- selectedIndex = targets.findIndex((t) => t.id === models[0].id);
110
- }
111
- else if (tools.length > 0) {
112
- selectedTarget = tools[0];
113
- selectedIndex = targets.findIndex((t) => t.id === tools[0].id);
114
- }
115
- if (selectedTarget) {
116
- setTarget(selectedTarget);
117
- setTargetIndex(selectedIndex);
118
- setChatConfig((prev) => ({ ...prev, currentTarget: selectedTarget }));
119
- setMessages([]);
120
- }
121
- else {
122
- setError('No targets available');
123
- }
124
- }
125
- else {
126
- setError('No agents, models, or tools available');
74
+ if (selectedTarget) {
75
+ setTarget(selectedTarget);
76
+ setTargetIndex(selectedIndex);
77
+ setChatConfig((prev) => ({ ...prev, currentTarget: selectedTarget }));
78
+ setMessages([]);
127
79
  }
128
- setIsLoading(false);
129
- }
130
- catch (err) {
131
- const errorMessage = err instanceof Error ? err.message : 'Failed to initialize chat';
132
- console.error(chalk.red('Error:'), errorMessage);
80
+ },
81
+ onQuit: () => {
133
82
  if (arkApiProxy) {
134
83
  arkApiProxy.stop();
135
84
  }
136
85
  exit();
137
- }
138
- };
139
- initializeChat();
86
+ },
87
+ }));
140
88
  // Cleanup function to close port forward when component unmounts
141
89
  return () => {
142
90
  if (arkApiProxy) {
@@ -553,7 +501,8 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
553
501
  // OpenAI SDK errors include response body in .error property
554
502
  if (err instanceof APIError) {
555
503
  if (err.error && typeof err.error === 'object') {
556
- errorMessage = JSON.stringify(err.error, null, 2);
504
+ const errorObj = err.error;
505
+ errorMessage = errorObj.message || JSON.stringify(err.error, null, 2);
557
506
  }
558
507
  else {
559
508
  errorMessage = err.message;
@@ -649,9 +598,9 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
649
598
  }
650
599
  })() }) }))] }, toolIndex)))] }));
651
600
  };
652
- // Show loading state
653
- if (isLoading) {
654
- return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " Loading available targets..."] }) }));
601
+ // Show async operation status (connection, etc.)
602
+ if (asyncOp.state.status === 'loading' || asyncOp.state.status === 'error') {
603
+ return _jsx(AsyncOperationStatus, { operation: asyncOp });
655
604
  }
656
605
  // Show error if no targets available
657
606
  if (!target && error) {
@@ -0,0 +1,17 @@
1
+ import * as React from 'react';
2
+ export interface SelectMenuItem {
3
+ /** Menu item label */
4
+ label: string;
5
+ /** Optional description shown in gray */
6
+ description?: string;
7
+ /** Called when item is selected */
8
+ onSelect: () => void;
9
+ }
10
+ interface SelectMenuProps {
11
+ /** Menu items to display */
12
+ items: SelectMenuItem[];
13
+ /** Initial selected index (default: 0) */
14
+ initialIndex?: number;
15
+ }
16
+ export declare const SelectMenu: React.FC<SelectMenuProps>;
17
+ export {};
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import * as React from 'react';
4
+ export const SelectMenu = ({ items, initialIndex = 0, }) => {
5
+ const [selectedIndex, setSelectedIndex] = React.useState(initialIndex);
6
+ useInput((input, key) => {
7
+ if (key.upArrow || input === 'k') {
8
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
9
+ }
10
+ else if (key.downArrow || input === 'j') {
11
+ setSelectedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
12
+ }
13
+ else if (key.return) {
14
+ items[selectedIndex].onSelect();
15
+ }
16
+ });
17
+ return (_jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
18
+ const isSelected = index === selectedIndex;
19
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: isSelected ? 'green' : 'gray', children: isSelected ? '❯ ' : ' ' }), _jsx(Text, { color: isSelected ? 'green' : 'white', children: item.label }), item.description && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: item.description })] }))] }, index));
20
+ }) }));
21
+ };
@@ -0,0 +1,20 @@
1
+ import * as React from 'react';
2
+ export type StatusType = 'loading' | 'success' | 'error' | 'info';
3
+ interface StatusMessageProps {
4
+ /** Status type determines icon and color */
5
+ status: StatusType;
6
+ /** Main message text (bold) */
7
+ message: string;
8
+ /** Optional hint shown in gray next to message */
9
+ hint?: string;
10
+ /** Optional details shown indented with ⎿ prefix */
11
+ details?: string;
12
+ /** Optional full error message shown below details */
13
+ errorMessage?: string;
14
+ /** Optional tip shown indented with ⎿ prefix */
15
+ tip?: string;
16
+ /** Optional content rendered below message */
17
+ children?: React.ReactNode;
18
+ }
19
+ export declare const StatusMessage: React.FC<StatusMessageProps>;
20
+ export {};
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ export const StatusMessage = ({ status, message, hint, details, errorMessage, tip, children, }) => {
5
+ const statusConfig = {
6
+ loading: { icon: _jsx(Spinner, { type: "dots" }), color: 'yellow' },
7
+ success: { icon: '✓', color: 'green' },
8
+ error: { icon: '✗', color: 'red' },
9
+ info: { icon: '●', color: 'cyan' },
10
+ };
11
+ const config = statusConfig[status];
12
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: config.color, children: typeof config.icon === 'string' ? config.icon : config.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: config.color, bold: true, children: message }), hint && _jsxs(Text, { color: "gray", children: [" ", hint] })] }), details && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "gray", children: ["\u23BF ", details] }) })), errorMessage && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "gray", children: errorMessage }) })), tip && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "gray", children: ["\u23BF ", tip] }) })), children && _jsx(Box, { marginLeft: 2, children: children })] }));
13
+ };
@@ -0,0 +1,15 @@
1
+ import { AsyncOperationConfig } from '../../components/AsyncOperation.js';
2
+ import { ArkApiClient } from '../../lib/arkApiClient.js';
3
+ import { ChatClient, QueryTarget } from '../../lib/chatClient.js';
4
+ export interface ConnectingToArkParams {
5
+ arkApiClient: ArkApiClient;
6
+ initialTargetId?: string;
7
+ onSuccess: (data: {
8
+ client: ChatClient;
9
+ targets: QueryTarget[];
10
+ selectedTarget: QueryTarget | null;
11
+ selectedIndex: number;
12
+ }) => void;
13
+ onQuit: () => void;
14
+ }
15
+ export declare function createConnectingToArkOperation(params: ConnectingToArkParams): AsyncOperationConfig;
@@ -0,0 +1,63 @@
1
+ import { ChatClient } from '../../lib/chatClient.js';
2
+ export function createConnectingToArkOperation(params) {
3
+ return {
4
+ message: 'Connecting to Ark...',
5
+ operation: async (_signal) => {
6
+ const client = new ChatClient(params.arkApiClient);
7
+ const targets = await client.getQueryTargets();
8
+ let selectedTarget = null;
9
+ let selectedIndex = 0;
10
+ if (params.initialTargetId) {
11
+ const matchedTarget = targets.find((t) => t.id === params.initialTargetId);
12
+ const matchedIndex = targets.findIndex((t) => t.id === params.initialTargetId);
13
+ if (matchedTarget) {
14
+ selectedTarget = matchedTarget;
15
+ selectedIndex = matchedIndex >= 0 ? matchedIndex : 0;
16
+ }
17
+ else {
18
+ throw new Error(`Target "${params.initialTargetId}" not found. Use "ark targets list" to see available targets.`);
19
+ }
20
+ }
21
+ else if (targets.length > 0) {
22
+ const agents = targets.filter((t) => t.type === 'agent');
23
+ const models = targets.filter((t) => t.type === 'model');
24
+ const tools = targets.filter((t) => t.type === 'tool');
25
+ if (agents.length > 0) {
26
+ selectedTarget = agents[0];
27
+ selectedIndex = targets.findIndex((t) => t.id === agents[0].id);
28
+ }
29
+ else if (models.length > 0) {
30
+ selectedTarget = models[0];
31
+ selectedIndex = targets.findIndex((t) => t.id === models[0].id);
32
+ }
33
+ else if (tools.length > 0) {
34
+ selectedTarget = tools[0];
35
+ selectedIndex = targets.findIndex((t) => t.id === tools[0].id);
36
+ }
37
+ else {
38
+ throw new Error('No targets available');
39
+ }
40
+ }
41
+ else {
42
+ throw new Error('No agents, models, or tools available');
43
+ }
44
+ params.onSuccess({
45
+ client,
46
+ targets,
47
+ selectedTarget,
48
+ selectedIndex,
49
+ });
50
+ },
51
+ hideOnSuccess: true,
52
+ errorOptions: [
53
+ { label: 'Try again', onSelect: () => { } },
54
+ {
55
+ label: 'Check status',
56
+ onSelect: () => {
57
+ console.log('Status command not yet implemented');
58
+ },
59
+ },
60
+ { label: 'Quit', onSelect: params.onQuit },
61
+ ],
62
+ };
63
+ }