@agents-at-scale/ark 0.1.36 → 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 (129) hide show
  1. package/README.md +53 -70
  2. package/dist/arkServices.d.ts +3 -27
  3. package/dist/arkServices.js +31 -3
  4. package/dist/arkServices.spec.js +118 -10
  5. package/dist/commands/chat/index.js +1 -2
  6. package/dist/commands/completion/index.js +0 -2
  7. package/dist/commands/generate/generators/project.js +33 -26
  8. package/dist/commands/generate/index.js +2 -2
  9. package/dist/commands/generate/templateDiscovery.js +13 -4
  10. package/dist/commands/install/index.js +49 -58
  11. package/dist/commands/models/create.d.ts +9 -1
  12. package/dist/commands/models/create.js +97 -90
  13. package/dist/commands/models/create.spec.js +9 -37
  14. package/dist/commands/models/index.js +8 -2
  15. package/dist/commands/models/index.spec.js +1 -1
  16. package/dist/commands/status/index.d.ts +3 -1
  17. package/dist/commands/status/index.js +54 -2
  18. package/dist/components/AsyncOperation.d.ts +54 -0
  19. package/dist/components/AsyncOperation.js +110 -0
  20. package/dist/components/ChatUI.js +39 -72
  21. package/dist/components/SelectMenu.d.ts +17 -0
  22. package/dist/components/SelectMenu.js +21 -0
  23. package/dist/components/StatusMessage.d.ts +20 -0
  24. package/dist/components/StatusMessage.js +13 -0
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/lib/arkApiClient.d.ts +1 -2
  28. package/dist/lib/arkApiClient.js +5 -6
  29. package/dist/lib/config.d.ts +4 -0
  30. package/dist/lib/config.js +9 -0
  31. package/dist/lib/nextSteps.js +1 -1
  32. package/dist/lib/nextSteps.spec.js +1 -1
  33. package/dist/lib/security.js +4 -0
  34. package/dist/lib/startup.js +6 -2
  35. package/dist/lib/startup.spec.js +1 -1
  36. package/dist/lib/timeout.d.ts +1 -0
  37. package/dist/lib/timeout.js +20 -0
  38. package/dist/lib/timeout.spec.d.ts +1 -0
  39. package/dist/lib/timeout.spec.js +14 -0
  40. package/dist/lib/waitForReady.d.ts +8 -0
  41. package/dist/lib/waitForReady.js +32 -0
  42. package/dist/lib/waitForReady.spec.d.ts +1 -0
  43. package/dist/lib/waitForReady.spec.js +104 -0
  44. package/dist/types/arkService.d.ts +27 -0
  45. package/dist/types/arkService.js +1 -0
  46. package/dist/ui/asyncOperations/connectingToArk.d.ts +15 -0
  47. package/dist/ui/asyncOperations/connectingToArk.js +63 -0
  48. package/package.json +7 -5
  49. package/templates/agent/agent.template.yaml +27 -0
  50. package/templates/marketplace/.editorconfig +24 -0
  51. package/templates/marketplace/.github/.keep +11 -0
  52. package/templates/marketplace/.github/workflows/.keep +16 -0
  53. package/templates/marketplace/.helmignore +23 -0
  54. package/templates/marketplace/.prettierrc.json +20 -0
  55. package/templates/marketplace/.yamllint.yml +53 -0
  56. package/templates/marketplace/README.md +197 -0
  57. package/templates/marketplace/agents/.keep +29 -0
  58. package/templates/marketplace/docs/.keep +19 -0
  59. package/templates/marketplace/mcp-servers/.keep +32 -0
  60. package/templates/marketplace/models/.keep +23 -0
  61. package/templates/marketplace/projects/.keep +43 -0
  62. package/templates/marketplace/queries/.keep +25 -0
  63. package/templates/marketplace/teams/.keep +29 -0
  64. package/templates/marketplace/tools/.keep +32 -0
  65. package/templates/marketplace/tools/examples/.keep +17 -0
  66. package/templates/mcp-server/Dockerfile +133 -0
  67. package/templates/mcp-server/Makefile +186 -0
  68. package/templates/mcp-server/README.md +178 -0
  69. package/templates/mcp-server/build.sh +76 -0
  70. package/templates/mcp-server/chart/Chart.yaml +22 -0
  71. package/templates/mcp-server/chart/templates/_helpers.tpl +62 -0
  72. package/templates/mcp-server/chart/templates/deployment.yaml +80 -0
  73. package/templates/mcp-server/chart/templates/hpa.yaml +32 -0
  74. package/templates/mcp-server/chart/templates/mcpserver.yaml +21 -0
  75. package/templates/mcp-server/chart/templates/secret.yaml +11 -0
  76. package/templates/mcp-server/chart/templates/service.yaml +15 -0
  77. package/templates/mcp-server/chart/templates/serviceaccount.yaml +13 -0
  78. package/templates/mcp-server/chart/values.yaml +84 -0
  79. package/templates/mcp-server/example-values.yaml +74 -0
  80. package/templates/mcp-server/examples/{{ .Values.mcpServerName }}-agent.yaml +33 -0
  81. package/templates/mcp-server/examples/{{ .Values.mcpServerName }}-query.yaml +24 -0
  82. package/templates/models/azure.yaml +33 -0
  83. package/templates/models/claude.yaml +28 -0
  84. package/templates/models/gemini.yaml +28 -0
  85. package/templates/models/openai.yaml +39 -0
  86. package/templates/project/.editorconfig +24 -0
  87. package/templates/project/.helmignore +24 -0
  88. package/templates/project/.prettierrc.json +16 -0
  89. package/templates/project/.yamllint.yml +50 -0
  90. package/templates/project/Chart.yaml +19 -0
  91. package/templates/project/Makefile +360 -0
  92. package/templates/project/README.md +377 -0
  93. package/templates/project/agents/.keep +11 -0
  94. package/templates/project/docs/.keep +14 -0
  95. package/templates/project/mcp-servers/.keep +34 -0
  96. package/templates/project/models/.keep +17 -0
  97. package/templates/project/queries/.keep +11 -0
  98. package/templates/project/scripts/setup.sh +108 -0
  99. package/templates/project/teams/.keep +11 -0
  100. package/templates/project/templates/00-rbac.yaml +168 -0
  101. package/templates/project/templates/01-models.yaml +11 -0
  102. package/templates/project/templates/02-mcp-servers.yaml +22 -0
  103. package/templates/project/templates/03-tools.yaml +12 -0
  104. package/templates/project/templates/04-agents.yaml +12 -0
  105. package/templates/project/templates/05-teams.yaml +11 -0
  106. package/templates/project/templates/06-queries.yaml +11 -0
  107. package/templates/project/templates/_helpers.tpl +91 -0
  108. package/templates/project/tests/e2e/.keep +10 -0
  109. package/templates/project/tests/unit/.keep +10 -0
  110. package/templates/project/tools/.keep +25 -0
  111. package/templates/project/tools/example-tool.yaml.disabled +94 -0
  112. package/templates/project/tools/examples/data-tool/Dockerfile +32 -0
  113. package/templates/project/values.yaml +141 -0
  114. package/templates/query/query.template.yaml +13 -0
  115. package/templates/team/team.template.yaml +17 -0
  116. package/templates/tool/.python-version +1 -0
  117. package/templates/tool/Dockerfile +23 -0
  118. package/templates/tool/README.md +238 -0
  119. package/templates/tool/agent.yaml +19 -0
  120. package/templates/tool/deploy.sh +10 -0
  121. package/templates/tool/deployment/deployment.yaml +31 -0
  122. package/templates/tool/deployment/kustomization.yaml +7 -0
  123. package/templates/tool/deployment/mcpserver.yaml +12 -0
  124. package/templates/tool/deployment/service.yaml +12 -0
  125. package/templates/tool/deployment/serviceaccount.yaml +8 -0
  126. package/templates/tool/deployment/values.yaml +3 -0
  127. package/templates/tool/pyproject.toml +9 -0
  128. package/templates/tool/src/main.py +36 -0
  129. package/templates/tool/uv.lock +498 -0
@@ -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,16 +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
- import { ChatClient, } from '../lib/chatClient.js';
9
+ import { APIError } from 'openai';
11
10
  import { AgentSelector } from '../ui/AgentSelector.js';
12
11
  import { ModelSelector } from '../ui/ModelSelector.js';
13
12
  import { TeamSelector } from '../ui/TeamSelector.js';
14
13
  import { ToolSelector } from '../ui/ToolSelector.js';
14
+ import { useAsyncOperation, AsyncOperationStatus } from './AsyncOperation.js';
15
+ import { createConnectingToArkOperation } from '../ui/asyncOperations/connectingToArk.js';
15
16
  // Generate a unique ID for messages
16
17
  let messageIdCounter = 0;
17
18
  const generateMessageId = () => {
@@ -30,13 +31,13 @@ const configureMarkdown = () => {
30
31
  };
31
32
  const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
32
33
  const { exit } = useApp();
34
+ const asyncOp = useAsyncOperation();
33
35
  const [messages, setMessages] = React.useState([]);
34
36
  const [input, setInput] = React.useState('');
35
37
  const [isTyping, setIsTyping] = React.useState(false);
36
38
  const [target, setTarget] = React.useState(null);
37
39
  const [availableTargets, setAvailableTargets] = React.useState([]);
38
40
  const [error, setError] = React.useState(null);
39
- const [isLoading, setIsLoading] = React.useState(true);
40
41
  const [targetIndex, setTargetIndex] = React.useState(0);
41
42
  const [abortController, setAbortController] = React.useState(null);
42
43
  const [showCommands, setShowCommands] = React.useState(false);
@@ -64,78 +65,26 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
64
65
  }, [outputFormat]);
65
66
  // Initialize chat client and fetch targets on mount
66
67
  React.useEffect(() => {
67
- const initializeChat = async () => {
68
- try {
69
- // Use the provided ArkApiClient to create ChatClient
70
- const client = new ChatClient(arkApiClient);
68
+ asyncOp.run(createConnectingToArkOperation({
69
+ arkApiClient,
70
+ initialTargetId,
71
+ onSuccess: ({ client, targets, selectedTarget, selectedIndex }) => {
71
72
  chatClientRef.current = client;
72
- const targets = await client.getQueryTargets();
73
73
  setAvailableTargets(targets);
74
- if (initialTargetId) {
75
- // If initialTargetId is provided, find and set the target
76
- const matchedTarget = targets.find((t) => t.id === initialTargetId);
77
- const matchedIndex = targets.findIndex((t) => t.id === initialTargetId);
78
- if (matchedTarget) {
79
- setTarget(matchedTarget);
80
- setTargetIndex(matchedIndex >= 0 ? matchedIndex : 0);
81
- setChatConfig((prev) => ({ ...prev, currentTarget: matchedTarget }));
82
- setMessages([]);
83
- }
84
- else {
85
- // If target not found, show error and exit
86
- console.error(chalk.red('Error:'), `Target "${initialTargetId}" not found`);
87
- console.error(chalk.gray('Use "ark targets list" to see available targets'));
88
- if (arkApiProxy) {
89
- arkApiProxy.stop();
90
- }
91
- exit();
92
- }
93
- }
94
- else if (targets.length > 0) {
95
- // No initial target specified - auto-select first available
96
- // Priority: agents > models > tools
97
- const agents = targets.filter((t) => t.type === 'agent');
98
- const models = targets.filter((t) => t.type === 'model');
99
- const tools = targets.filter((t) => t.type === 'tool');
100
- let selectedTarget = null;
101
- let selectedIndex = 0;
102
- if (agents.length > 0) {
103
- selectedTarget = agents[0];
104
- selectedIndex = targets.findIndex((t) => t.id === agents[0].id);
105
- }
106
- else if (models.length > 0) {
107
- selectedTarget = models[0];
108
- selectedIndex = targets.findIndex((t) => t.id === models[0].id);
109
- }
110
- else if (tools.length > 0) {
111
- selectedTarget = tools[0];
112
- selectedIndex = targets.findIndex((t) => t.id === tools[0].id);
113
- }
114
- if (selectedTarget) {
115
- setTarget(selectedTarget);
116
- setTargetIndex(selectedIndex);
117
- setChatConfig((prev) => ({ ...prev, currentTarget: selectedTarget }));
118
- setMessages([]);
119
- }
120
- else {
121
- setError('No targets available');
122
- }
74
+ if (selectedTarget) {
75
+ setTarget(selectedTarget);
76
+ setTargetIndex(selectedIndex);
77
+ setChatConfig((prev) => ({ ...prev, currentTarget: selectedTarget }));
78
+ setMessages([]);
123
79
  }
124
- else {
125
- setError('No agents, models, or tools available');
126
- }
127
- setIsLoading(false);
128
- }
129
- catch (err) {
130
- const errorMessage = err instanceof Error ? err.message : 'Failed to initialize chat';
131
- console.error(chalk.red('Error:'), errorMessage);
80
+ },
81
+ onQuit: () => {
132
82
  if (arkApiProxy) {
133
83
  arkApiProxy.stop();
134
84
  }
135
85
  exit();
136
- }
137
- };
138
- initializeChat();
86
+ },
87
+ }));
139
88
  // Cleanup function to close port forward when component unmounts
140
89
  return () => {
141
90
  if (arkApiProxy) {
@@ -548,7 +497,25 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
548
497
  // Request was cancelled, message already updated by Esc handler
549
498
  return;
550
499
  }
551
- const errorMessage = err instanceof Error ? err.message : 'Failed to send message';
500
+ let errorMessage = 'Failed to send message';
501
+ // OpenAI SDK errors include response body in .error property
502
+ if (err instanceof APIError) {
503
+ if (err.error && typeof err.error === 'object') {
504
+ const errorObj = err.error;
505
+ errorMessage = errorObj.message || JSON.stringify(err.error, null, 2);
506
+ }
507
+ else {
508
+ errorMessage = err.message;
509
+ }
510
+ }
511
+ // Standard JavaScript errors
512
+ else if (err instanceof Error) {
513
+ errorMessage = err.message;
514
+ }
515
+ // String errors from throw statements
516
+ else if (typeof err === 'string') {
517
+ errorMessage = err;
518
+ }
552
519
  setError(errorMessage);
553
520
  setIsTyping(false);
554
521
  setAbortController(null);
@@ -631,9 +598,9 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
631
598
  }
632
599
  })() }) }))] }, toolIndex)))] }));
633
600
  };
634
- // Show loading state
635
- if (isLoading) {
636
- 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 });
637
604
  }
638
605
  // Show error if no targets available
639
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
+ };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env NODE_NO_WARNINGS=1 node
1
+ #!/usr/bin/env node
2
2
  export {};
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env NODE_NO_WARNINGS=1 node
1
+ #!/usr/bin/env node
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { Command } from 'commander';
4
4
  import { render } from 'ink';
@@ -40,8 +40,7 @@ export interface Team {
40
40
  export declare class ArkApiClient {
41
41
  private openai;
42
42
  private baseUrl;
43
- private namespace;
44
- constructor(arkApiUrl: string, namespace?: string);
43
+ constructor(arkApiUrl: string);
45
44
  getBaseUrl(): string;
46
45
  getQueryTargets(): Promise<QueryTarget[]>;
47
46
  getAgents(): Promise<Agent[]>;
@@ -1,8 +1,7 @@
1
1
  import OpenAI from 'openai';
2
2
  export class ArkApiClient {
3
- constructor(arkApiUrl, namespace = 'default') {
3
+ constructor(arkApiUrl) {
4
4
  this.baseUrl = arkApiUrl;
5
- this.namespace = namespace;
6
5
  this.openai = new OpenAI({
7
6
  baseURL: `${arkApiUrl}/openai/v1`,
8
7
  apiKey: 'dummy', // ark-api doesn't require an API key
@@ -34,7 +33,7 @@ export class ArkApiClient {
34
33
  }
35
34
  async getAgents() {
36
35
  try {
37
- const response = await fetch(`${this.baseUrl}/v1/namespaces/${this.namespace}/agents`);
36
+ const response = await fetch(`${this.baseUrl}/v1/agents`);
38
37
  if (!response.ok) {
39
38
  throw new Error(`HTTP error! status: ${response.status}`);
40
39
  }
@@ -47,7 +46,7 @@ export class ArkApiClient {
47
46
  }
48
47
  async getModels() {
49
48
  try {
50
- const response = await fetch(`${this.baseUrl}/v1/namespaces/${this.namespace}/models`);
49
+ const response = await fetch(`${this.baseUrl}/v1/models`);
51
50
  if (!response.ok) {
52
51
  throw new Error(`HTTP error! status: ${response.status}`);
53
52
  }
@@ -60,7 +59,7 @@ export class ArkApiClient {
60
59
  }
61
60
  async getTools() {
62
61
  try {
63
- const response = await fetch(`${this.baseUrl}/v1/namespaces/${this.namespace}/tools`);
62
+ const response = await fetch(`${this.baseUrl}/v1/tools`);
64
63
  if (!response.ok) {
65
64
  throw new Error(`HTTP error! status: ${response.status}`);
66
65
  }
@@ -73,7 +72,7 @@ export class ArkApiClient {
73
72
  }
74
73
  async getTeams() {
75
74
  try {
76
- const response = await fetch(`${this.baseUrl}/v1/namespaces/${this.namespace}/teams`);
75
+ const response = await fetch(`${this.baseUrl}/v1/teams`);
77
76
  if (!response.ok) {
78
77
  throw new Error(`HTTP error! status: ${response.status}`);
79
78
  }
@@ -1,10 +1,14 @@
1
1
  import type { ClusterInfo } from './cluster.js';
2
+ import type { ArkService } from '../types/arkService.js';
2
3
  export interface ChatConfig {
3
4
  streaming?: boolean;
4
5
  outputFormat?: 'text' | 'markdown';
5
6
  }
6
7
  export interface ArkConfig {
7
8
  chat?: ChatConfig;
9
+ services?: {
10
+ [serviceName: string]: Partial<ArkService>;
11
+ };
8
12
  clusterInfo?: ClusterInfo;
9
13
  }
10
14
  /**
@@ -70,6 +70,15 @@ function mergeConfig(target, source) {
70
70
  target.chat.outputFormat = source.chat.outputFormat;
71
71
  }
72
72
  }
73
+ if (source.services) {
74
+ target.services = target.services || {};
75
+ for (const [serviceName, overrides] of Object.entries(source.services)) {
76
+ target.services[serviceName] = {
77
+ ...target.services[serviceName],
78
+ ...overrides,
79
+ };
80
+ }
81
+ }
73
82
  }
74
83
  /**
75
84
  * Get the paths checked for config files
@@ -4,7 +4,7 @@ import chalk from 'chalk';
4
4
  */
5
5
  export function printNextSteps() {
6
6
  console.log();
7
- console.log(chalk.green.bold('✓ ARK installed successfully!'));
7
+ console.log(chalk.green.bold('✓ Installation complete'));
8
8
  console.log();
9
9
  console.log(chalk.gray('Next steps:'));
10
10
  console.log();
@@ -15,7 +15,7 @@ describe('printNextSteps', () => {
15
15
  it('prints successful installation message', () => {
16
16
  printNextSteps();
17
17
  const fullOutput = output.join('\n');
18
- expect(fullOutput).toContain('ARK installed successfully!');
18
+ expect(fullOutput).toContain(' Installation complete');
19
19
  });
20
20
  it('includes all required commands', () => {
21
21
  printNextSteps();
@@ -9,6 +9,10 @@ export class SecurityUtils {
9
9
  * Validate that a path is safe and doesn't contain directory traversal attempts
10
10
  */
11
11
  static validatePath(filePath, context = 'path') {
12
+ // Skip validation for internal template paths - they're always safe
13
+ if (context === 'template path') {
14
+ return;
15
+ }
12
16
  if (!filePath || typeof filePath !== 'string') {
13
17
  throw new ValidationError(`Invalid ${context}: path must be a non-empty string`, 'path', ['Provide a valid file path']);
14
18
  }
@@ -52,7 +52,9 @@ export function showNoClusterError() {
52
52
  */
53
53
  async function hasKubernetesContext() {
54
54
  try {
55
- const { stdout } = await execa('kubectl', ['config', 'current-context']);
55
+ const { stdout } = await execa('kubectl', ['config', 'current-context'], {
56
+ timeout: 5000,
57
+ });
56
58
  return stdout.trim().length > 0;
57
59
  }
58
60
  catch {
@@ -72,7 +74,9 @@ export async function startup() {
72
74
  const hasContext = await hasKubernetesContext();
73
75
  if (hasContext) {
74
76
  try {
75
- const { stdout } = await execa('kubectl', ['config', 'current-context']);
77
+ const { stdout } = await execa('kubectl', ['config', 'current-context'], {
78
+ timeout: 5000,
79
+ });
76
80
  config.clusterInfo = {
77
81
  type: 'unknown', // We don't detect cluster type here - too slow
78
82
  context: stdout.trim(),
@@ -138,7 +138,7 @@ describe('startup', () => {
138
138
  type: 'unknown',
139
139
  context: 'minikube',
140
140
  });
141
- expect(mockExeca).toHaveBeenCalledWith('kubectl', ['config', 'current-context']);
141
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', ['config', 'current-context'], { timeout: 5000 });
142
142
  });
143
143
  it('handles missing kubectl context gracefully', async () => {
144
144
  mockCheckCommandExists.mockResolvedValue(true);
@@ -0,0 +1 @@
1
+ export declare function parseTimeoutToSeconds(value: string): number;
@@ -0,0 +1,20 @@
1
+ export function parseTimeoutToSeconds(value) {
2
+ const match = value.match(/^(\d+)([smh])?$/);
3
+ if (!match) {
4
+ throw new Error('Invalid timeout format. Use format like 30s, 2m, or 1h');
5
+ }
6
+ const num = parseInt(match[1], 10);
7
+ const unit = match[2];
8
+ if (!unit)
9
+ return num;
10
+ switch (unit) {
11
+ case 's':
12
+ return num;
13
+ case 'm':
14
+ return num * 60;
15
+ case 'h':
16
+ return num * 3600;
17
+ default:
18
+ return num;
19
+ }
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { parseTimeoutToSeconds } from './timeout.js';
3
+ describe('parseTimeoutToSeconds', () => {
4
+ it('should parse time units correctly', () => {
5
+ expect(parseTimeoutToSeconds('30s')).toBe(30);
6
+ expect(parseTimeoutToSeconds('2m')).toBe(120);
7
+ expect(parseTimeoutToSeconds('1h')).toBe(3600);
8
+ expect(parseTimeoutToSeconds('60')).toBe(60);
9
+ });
10
+ it('should throw error for invalid formats', () => {
11
+ expect(() => parseTimeoutToSeconds('abc')).toThrow('Invalid timeout format');
12
+ expect(() => parseTimeoutToSeconds('-5s')).toThrow('Invalid timeout format');
13
+ });
14
+ });
@@ -0,0 +1,8 @@
1
+ import type { ArkService } from '../types/arkService.js';
2
+ export interface WaitProgress {
3
+ serviceName: string;
4
+ ready: boolean;
5
+ error?: string;
6
+ }
7
+ export declare function waitForDeploymentReady(deploymentName: string, namespace: string, timeoutSeconds: number): Promise<boolean>;
8
+ export declare function waitForServicesReady(services: ArkService[], timeoutSeconds: number, onProgress?: (progress: WaitProgress) => void): Promise<boolean>;