@agents-at-scale/ark 0.1.37 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/arkServices.js +9 -0
- package/dist/commands/chat/index.js +1 -2
- package/dist/commands/generate/generators/project.js +33 -26
- package/dist/commands/generate/index.js +2 -2
- package/dist/commands/generate/templateDiscovery.js +13 -4
- package/dist/components/AsyncOperation.d.ts +54 -0
- package/dist/components/AsyncOperation.js +110 -0
- package/dist/components/ChatUI.js +21 -72
- package/dist/components/SelectMenu.d.ts +17 -0
- package/dist/components/SelectMenu.js +21 -0
- package/dist/components/StatusMessage.d.ts +20 -0
- package/dist/components/StatusMessage.js +13 -0
- package/dist/ui/asyncOperations/connectingToArk.d.ts +15 -0
- package/dist/ui/asyncOperations/connectingToArk.js +63 -0
- package/package.json +5 -3
- package/templates/agent/agent.template.yaml +27 -0
- package/templates/marketplace/.editorconfig +24 -0
- package/templates/marketplace/.github/.keep +11 -0
- package/templates/marketplace/.github/workflows/.keep +16 -0
- package/templates/marketplace/.helmignore +23 -0
- package/templates/marketplace/.prettierrc.json +20 -0
- package/templates/marketplace/.yamllint.yml +53 -0
- package/templates/marketplace/README.md +197 -0
- package/templates/marketplace/agents/.keep +29 -0
- package/templates/marketplace/docs/.keep +19 -0
- package/templates/marketplace/mcp-servers/.keep +32 -0
- package/templates/marketplace/models/.keep +23 -0
- package/templates/marketplace/projects/.keep +43 -0
- package/templates/marketplace/queries/.keep +25 -0
- package/templates/marketplace/teams/.keep +29 -0
- package/templates/marketplace/tools/.keep +32 -0
- package/templates/marketplace/tools/examples/.keep +17 -0
- package/templates/mcp-server/Dockerfile +133 -0
- package/templates/mcp-server/Makefile +186 -0
- package/templates/mcp-server/README.md +178 -0
- package/templates/mcp-server/build.sh +76 -0
- package/templates/mcp-server/chart/Chart.yaml +22 -0
- package/templates/mcp-server/chart/templates/_helpers.tpl +62 -0
- package/templates/mcp-server/chart/templates/deployment.yaml +80 -0
- package/templates/mcp-server/chart/templates/hpa.yaml +32 -0
- package/templates/mcp-server/chart/templates/mcpserver.yaml +21 -0
- package/templates/mcp-server/chart/templates/secret.yaml +11 -0
- package/templates/mcp-server/chart/templates/service.yaml +15 -0
- package/templates/mcp-server/chart/templates/serviceaccount.yaml +13 -0
- package/templates/mcp-server/chart/values.yaml +84 -0
- package/templates/mcp-server/example-values.yaml +74 -0
- package/templates/mcp-server/examples/{{ .Values.mcpServerName }}-agent.yaml +33 -0
- package/templates/mcp-server/examples/{{ .Values.mcpServerName }}-query.yaml +24 -0
- package/templates/models/azure.yaml +33 -0
- package/templates/models/claude.yaml +28 -0
- package/templates/models/gemini.yaml +28 -0
- package/templates/models/openai.yaml +39 -0
- package/templates/project/.editorconfig +24 -0
- package/templates/project/.helmignore +24 -0
- package/templates/project/.prettierrc.json +16 -0
- package/templates/project/.yamllint.yml +50 -0
- package/templates/project/Chart.yaml +19 -0
- package/templates/project/Makefile +360 -0
- package/templates/project/README.md +377 -0
- package/templates/project/agents/.keep +11 -0
- package/templates/project/docs/.keep +14 -0
- package/templates/project/mcp-servers/.keep +34 -0
- package/templates/project/models/.keep +17 -0
- package/templates/project/queries/.keep +11 -0
- package/templates/project/scripts/setup.sh +108 -0
- package/templates/project/teams/.keep +11 -0
- package/templates/project/templates/00-rbac.yaml +168 -0
- package/templates/project/templates/01-models.yaml +11 -0
- package/templates/project/templates/02-mcp-servers.yaml +22 -0
- package/templates/project/templates/03-tools.yaml +12 -0
- package/templates/project/templates/04-agents.yaml +12 -0
- package/templates/project/templates/05-teams.yaml +11 -0
- package/templates/project/templates/06-queries.yaml +11 -0
- package/templates/project/templates/_helpers.tpl +91 -0
- package/templates/project/tests/e2e/.keep +10 -0
- package/templates/project/tests/unit/.keep +10 -0
- package/templates/project/tools/.keep +25 -0
- package/templates/project/tools/example-tool.yaml.disabled +94 -0
- package/templates/project/tools/examples/data-tool/Dockerfile +32 -0
- package/templates/project/values.yaml +141 -0
- package/templates/query/query.template.yaml +13 -0
- package/templates/team/team.template.yaml +17 -0
- package/templates/tool/.python-version +1 -0
- package/templates/tool/Dockerfile +23 -0
- package/templates/tool/README.md +238 -0
- package/templates/tool/agent.yaml +19 -0
- package/templates/tool/deploy.sh +10 -0
- package/templates/tool/deployment/deployment.yaml +31 -0
- package/templates/tool/deployment/kustomization.yaml +7 -0
- package/templates/tool/deployment/mcpserver.yaml +12 -0
- package/templates/tool/deployment/service.yaml +12 -0
- package/templates/tool/deployment/serviceaccount.yaml +8 -0
- package/templates/tool/deployment/values.yaml +3 -0
- package/templates/tool/pyproject.toml +9 -0
- package/templates/tool/src/main.py +36 -0
- package/templates/tool/uv.lock +498 -0
package/dist/arkServices.js
CHANGED
|
@@ -69,6 +69,15 @@ const defaultArkServices = {
|
|
|
69
69
|
k8sDeploymentName: 'ark-controller',
|
|
70
70
|
k8sDevDeploymentName: 'ark-controller-devspace',
|
|
71
71
|
},
|
|
72
|
+
'ark-tenant': {
|
|
73
|
+
name: 'ark-tenant',
|
|
74
|
+
helmReleaseName: 'ark-tenant',
|
|
75
|
+
description: 'Tenant provisioning with RBAC and resource quotas',
|
|
76
|
+
enabled: true,
|
|
77
|
+
category: 'core',
|
|
78
|
+
chartPath: `${REGISTRY_BASE}/ark-tenant`,
|
|
79
|
+
installArgs: [],
|
|
80
|
+
},
|
|
72
81
|
'ark-api': {
|
|
73
82
|
name: 'ark-api',
|
|
74
83
|
helmReleaseName: 'ark-api',
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
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
|
-
//
|
|
169
|
+
// Use defaults for missing options
|
|
165
170
|
if (!projectType) {
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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('-
|
|
217
|
+
.option('--no-interactive', type === 'marketplace'
|
|
218
218
|
? 'Not supported for marketplace'
|
|
219
|
-
: '
|
|
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
|
|
7
|
-
//
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
653
|
-
if (
|
|
654
|
-
return
|
|
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
|
+
}
|