@agents-at-scale/ark 0.1.36-rc1 → 0.1.37

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 (132) 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/completion/index.js +0 -2
  6. package/dist/commands/install/index.js +49 -58
  7. package/dist/commands/models/create.d.ts +9 -1
  8. package/dist/commands/models/create.js +97 -90
  9. package/dist/commands/models/create.spec.js +9 -37
  10. package/dist/commands/models/index.js +8 -2
  11. package/dist/commands/models/index.spec.js +1 -1
  12. package/dist/commands/status/index.d.ts +3 -1
  13. package/dist/commands/status/index.js +54 -2
  14. package/dist/components/ChatUI.js +19 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/lib/arkApiClient.d.ts +1 -2
  18. package/dist/lib/arkApiClient.js +5 -6
  19. package/dist/lib/config.d.ts +4 -0
  20. package/dist/lib/config.js +9 -0
  21. package/dist/lib/nextSteps.js +1 -1
  22. package/dist/lib/nextSteps.spec.js +1 -1
  23. package/dist/lib/security.js +4 -0
  24. package/dist/lib/startup.js +6 -2
  25. package/dist/lib/startup.spec.js +1 -1
  26. package/dist/lib/timeout.d.ts +1 -0
  27. package/dist/lib/timeout.js +20 -0
  28. package/dist/lib/timeout.spec.js +14 -0
  29. package/dist/lib/waitForReady.d.ts +8 -0
  30. package/dist/lib/waitForReady.js +32 -0
  31. package/dist/lib/waitForReady.spec.js +104 -0
  32. package/dist/types/arkService.d.ts +27 -0
  33. package/package.json +3 -3
  34. package/dist/charts/charts.d.ts +0 -5
  35. package/dist/charts/charts.js +0 -6
  36. package/dist/charts/dependencies.d.ts +0 -6
  37. package/dist/charts/dependencies.js +0 -50
  38. package/dist/charts/types.d.ts +0 -40
  39. package/dist/commands/agents/selector.d.ts +0 -8
  40. package/dist/commands/agents/selector.js +0 -53
  41. package/dist/commands/agents.d.ts +0 -2
  42. package/dist/commands/agents.js +0 -53
  43. package/dist/commands/chat.d.ts +0 -2
  44. package/dist/commands/chat.js +0 -45
  45. package/dist/commands/cluster/get-ip.d.ts +0 -2
  46. package/dist/commands/cluster/get-ip.js +0 -32
  47. package/dist/commands/cluster/get-type.d.ts +0 -2
  48. package/dist/commands/cluster/get-type.js +0 -26
  49. package/dist/commands/completion.d.ts +0 -2
  50. package/dist/commands/completion.js +0 -265
  51. package/dist/commands/config.d.ts +0 -2
  52. package/dist/commands/config.js +0 -44
  53. package/dist/commands/dashboard.d.ts +0 -3
  54. package/dist/commands/dashboard.js +0 -39
  55. package/dist/commands/dev/index.d.ts +0 -3
  56. package/dist/commands/dev/index.js +0 -9
  57. package/dist/commands/dev/tool/check.d.ts +0 -2
  58. package/dist/commands/dev/tool/check.js +0 -142
  59. package/dist/commands/dev/tool/clean.d.ts +0 -2
  60. package/dist/commands/dev/tool/clean.js +0 -153
  61. package/dist/commands/dev/tool/generate.d.ts +0 -2
  62. package/dist/commands/dev/tool/generate.js +0 -28
  63. package/dist/commands/dev/tool/index.d.ts +0 -2
  64. package/dist/commands/dev/tool/index.js +0 -14
  65. package/dist/commands/dev/tool/init.d.ts +0 -2
  66. package/dist/commands/dev/tool/init.js +0 -320
  67. package/dist/commands/dev/tool/shared.d.ts +0 -5
  68. package/dist/commands/dev/tool/shared.js +0 -258
  69. package/dist/commands/dev/tool/status.d.ts +0 -2
  70. package/dist/commands/dev/tool/status.js +0 -136
  71. package/dist/commands/dev/tool-generate.spec.js +0 -163
  72. package/dist/commands/dev/tool.d.ts +0 -2
  73. package/dist/commands/dev/tool.js +0 -559
  74. package/dist/commands/dev/tool.spec.js +0 -48
  75. package/dist/commands/install.d.ts +0 -3
  76. package/dist/commands/install.js +0 -147
  77. package/dist/commands/models/selector.d.ts +0 -8
  78. package/dist/commands/models/selector.js +0 -53
  79. package/dist/commands/routes.d.ts +0 -2
  80. package/dist/commands/routes.js +0 -101
  81. package/dist/commands/status.d.ts +0 -3
  82. package/dist/commands/status.js +0 -33
  83. package/dist/commands/targets.d.ts +0 -2
  84. package/dist/commands/targets.js +0 -65
  85. package/dist/commands/teams/selector.d.ts +0 -8
  86. package/dist/commands/teams/selector.js +0 -55
  87. package/dist/commands/tools/selector.d.ts +0 -8
  88. package/dist/commands/tools/selector.js +0 -53
  89. package/dist/commands/uninstall.d.ts +0 -2
  90. package/dist/commands/uninstall.js +0 -83
  91. package/dist/components/DashboardCLI.d.ts +0 -3
  92. package/dist/components/DashboardCLI.js +0 -149
  93. package/dist/components/StatusView.d.ts +0 -10
  94. package/dist/components/StatusView.js +0 -39
  95. package/dist/config.d.ts +0 -23
  96. package/dist/config.js +0 -92
  97. package/dist/lib/arkClient.d.ts +0 -32
  98. package/dist/lib/arkClient.js +0 -43
  99. package/dist/lib/commandUtils.d.ts +0 -4
  100. package/dist/lib/commandUtils.js +0 -18
  101. package/dist/lib/commandUtils.test.d.ts +0 -1
  102. package/dist/lib/commandUtils.test.js +0 -44
  103. package/dist/lib/config.test.d.ts +0 -1
  104. package/dist/lib/config.test.js +0 -93
  105. package/dist/lib/consts.d.ts +0 -9
  106. package/dist/lib/consts.js +0 -13
  107. package/dist/lib/consts.spec.d.ts +0 -1
  108. package/dist/lib/consts.spec.js +0 -15
  109. package/dist/lib/dev/tools/analyzer.d.ts +0 -30
  110. package/dist/lib/dev/tools/analyzer.js +0 -190
  111. package/dist/lib/dev/tools/discover_tools.py +0 -392
  112. package/dist/lib/dev/tools/mcp-types.d.ts +0 -28
  113. package/dist/lib/dev/tools/mcp-types.js +0 -86
  114. package/dist/lib/dev/tools/types.d.ts +0 -50
  115. package/dist/lib/dev/tools/types.js +0 -1
  116. package/dist/lib/exec.d.ts +0 -1
  117. package/dist/lib/exec.js +0 -9
  118. package/dist/lib/gatewayManager.d.ts +0 -24
  119. package/dist/lib/gatewayManager.js +0 -85
  120. package/dist/lib/kubernetes.d.ts +0 -28
  121. package/dist/lib/kubernetes.js +0 -122
  122. package/dist/lib/portUtils.d.ts +0 -8
  123. package/dist/lib/portUtils.js +0 -39
  124. package/dist/lib/progress.d.ts +0 -128
  125. package/dist/lib/progress.js +0 -273
  126. package/dist/lib/queryRunner.d.ts +0 -22
  127. package/dist/lib/queryRunner.js +0 -142
  128. package/dist/lib/wrappers/git.d.ts +0 -2
  129. package/dist/lib/wrappers/git.js +0 -43
  130. /package/dist/{charts/types.js → lib/timeout.spec.d.ts} +0 -0
  131. /package/dist/{commands/dev/tool-generate.spec.d.ts → lib/waitForReady.spec.d.ts} +0 -0
  132. /package/dist/{commands/dev/tool.spec.d.ts → types/arkService.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { execa } from 'execa';
2
2
  import inquirer from 'inquirer';
3
3
  import output from '../../lib/output.js';
4
- export async function createModel(modelName) {
4
+ export async function createModel(modelName, options = {}) {
5
5
  // Step 1: Get model name if not provided
6
6
  if (!modelName) {
7
7
  const nameAnswer = await inquirer.prompt([
@@ -27,66 +27,87 @@ export async function createModel(modelName) {
27
27
  try {
28
28
  await execa('kubectl', ['get', 'model', modelName], { stdio: 'pipe' });
29
29
  output.warning(`model ${modelName} already exists`);
30
- const { overwrite } = await inquirer.prompt([
31
- {
32
- type: 'confirm',
33
- name: 'overwrite',
34
- message: `overwrite existing model ${modelName}?`,
35
- default: false,
36
- },
37
- ]);
38
- if (!overwrite) {
39
- output.info('model creation cancelled');
40
- return false;
30
+ if (!options.yes) {
31
+ const { overwrite } = await inquirer.prompt([
32
+ {
33
+ type: 'confirm',
34
+ name: 'overwrite',
35
+ message: `overwrite existing model ${modelName}?`,
36
+ default: false,
37
+ },
38
+ ]);
39
+ if (!overwrite) {
40
+ output.info('model creation cancelled');
41
+ return false;
42
+ }
41
43
  }
42
44
  }
43
45
  catch {
44
46
  // Model doesn't exist, continue
45
47
  }
46
- // Step 2: Choose model type
47
- const { modelType } = await inquirer.prompt([
48
- {
49
- type: 'list',
50
- name: 'modelType',
51
- message: 'select model provider:',
52
- choices: [
53
- { name: 'Azure OpenAI', value: 'azure' },
54
- { name: 'OpenAI', value: 'openai' },
55
- ],
56
- default: 'azure',
57
- },
58
- ]);
59
- // Step 3: Get common parameters
60
- const commonAnswers = await inquirer.prompt([
61
- {
62
- type: 'input',
63
- name: 'modelVersion',
64
- message: 'model version:',
65
- default: 'gpt-4o-mini',
66
- },
67
- {
68
- type: 'input',
69
- name: 'baseUrl',
70
- message: 'base URL:',
71
- validate: (input) => {
72
- if (!input)
73
- return 'base URL is required';
74
- try {
75
- new URL(input);
76
- return true;
77
- }
78
- catch {
79
- return 'please enter a valid URL';
80
- }
48
+ // Step 2: Get model type
49
+ let modelType = options.type;
50
+ if (!modelType) {
51
+ const answer = await inquirer.prompt([
52
+ {
53
+ type: 'list',
54
+ name: 'modelType',
55
+ message: 'select model provider:',
56
+ choices: [
57
+ { name: 'Azure OpenAI', value: 'azure' },
58
+ { name: 'OpenAI', value: 'openai' },
59
+ ],
60
+ default: 'azure',
81
61
  },
82
- },
83
- ]);
84
- // Remove trailing slash from base URL
85
- const baseUrl = commonAnswers.baseUrl.replace(/\/$/, '');
86
- // Step 4: Get provider-specific parameters
87
- let apiVersion = '';
88
- if (modelType === 'azure') {
89
- const azureAnswers = await inquirer.prompt([
62
+ ]);
63
+ modelType = answer.modelType;
64
+ }
65
+ // Step 3: Get model name
66
+ let model = options.model;
67
+ if (!model) {
68
+ const answer = await inquirer.prompt([
69
+ {
70
+ type: 'input',
71
+ name: 'model',
72
+ message: 'model:',
73
+ default: 'gpt-4o-mini',
74
+ },
75
+ ]);
76
+ model = answer.model;
77
+ }
78
+ // Step 4: Get base URL
79
+ let baseUrl = options.baseUrl;
80
+ if (!baseUrl) {
81
+ const answer = await inquirer.prompt([
82
+ {
83
+ type: 'input',
84
+ name: 'baseUrl',
85
+ message: 'base URL:',
86
+ validate: (input) => {
87
+ if (!input)
88
+ return 'base URL is required';
89
+ try {
90
+ new URL(input);
91
+ return true;
92
+ }
93
+ catch {
94
+ return 'please enter a valid URL';
95
+ }
96
+ },
97
+ },
98
+ ]);
99
+ baseUrl = answer.baseUrl;
100
+ }
101
+ // Validate and clean base URL
102
+ if (!baseUrl) {
103
+ output.error('base URL is required');
104
+ return false;
105
+ }
106
+ baseUrl = baseUrl.replace(/\/$/, '');
107
+ // Step 5: Get API version (Azure only)
108
+ let apiVersion = options.apiVersion || '';
109
+ if (modelType === 'azure' && !options.apiVersion) {
110
+ const answer = await inquirer.prompt([
90
111
  {
91
112
  type: 'input',
92
113
  name: 'apiVersion',
@@ -94,33 +115,29 @@ export async function createModel(modelName) {
94
115
  default: '2024-12-01-preview',
95
116
  },
96
117
  ]);
97
- apiVersion = azureAnswers.apiVersion;
118
+ apiVersion = answer.apiVersion;
98
119
  }
99
- // Step 5: Get API key (password input)
100
- const { apiKey } = await inquirer.prompt([
101
- {
102
- type: 'password',
103
- name: 'apiKey',
104
- message: 'API key:',
105
- mask: '*',
106
- validate: (input) => {
107
- if (!input)
108
- return 'API key is required';
109
- return true;
120
+ // Step 6: Get API key
121
+ let apiKey = options.apiKey;
122
+ if (!apiKey) {
123
+ const answer = await inquirer.prompt([
124
+ {
125
+ type: 'password',
126
+ name: 'apiKey',
127
+ message: 'API key:',
128
+ mask: '*',
129
+ validate: (input) => {
130
+ if (!input)
131
+ return 'API key is required';
132
+ return true;
133
+ },
110
134
  },
111
- },
112
- ]);
135
+ ]);
136
+ apiKey = answer.apiKey;
137
+ }
113
138
  // Step 6: Create the Kubernetes secret
114
139
  const secretName = `${modelName}-model-api-key`;
115
- output.info(`creating secret ${secretName}...`);
116
140
  try {
117
- // Delete existing secret if it exists (update scenario)
118
- await execa('kubectl', ['delete', 'secret', secretName], {
119
- stdio: 'pipe',
120
- }).catch(() => {
121
- // Ignore error if secret doesn't exist
122
- });
123
- // Create the secret
124
141
  await execa('kubectl', [
125
142
  'create',
126
143
  'secret',
@@ -128,7 +145,7 @@ export async function createModel(modelName) {
128
145
  secretName,
129
146
  `--from-literal=api-key=${apiKey}`,
130
147
  ], { stdio: 'pipe' });
131
- output.success(`secret ${secretName} created`);
148
+ output.success(`created secret ${secretName}`);
132
149
  }
133
150
  catch (error) {
134
151
  output.error('failed to create secret');
@@ -146,7 +163,7 @@ export async function createModel(modelName) {
146
163
  spec: {
147
164
  type: modelType,
148
165
  model: {
149
- value: commonAnswers.modelVersion,
166
+ value: model,
150
167
  },
151
168
  config: {},
152
169
  },
@@ -192,22 +209,12 @@ export async function createModel(modelName) {
192
209
  input: manifestJson,
193
210
  stdio: ['pipe', 'pipe', 'pipe'],
194
211
  });
195
- output.success(`model ${modelName} created successfully`);
196
- console.log();
197
- output.info('you can now use this model with ARK agents and queries');
212
+ output.success(`model ${modelName} created`);
198
213
  return true;
199
214
  }
200
215
  catch (error) {
201
216
  output.error('failed to create model');
202
217
  console.error(error);
203
- // Try to clean up the secret if model creation failed
204
- try {
205
- await execa('kubectl', ['delete', 'secret', secretName], { stdio: 'pipe' });
206
- output.info(`cleaned up secret ${secretName}`);
207
- }
208
- catch {
209
- // Ignore cleanup errors
210
- }
211
218
  return false;
212
219
  }
213
220
  }
@@ -31,28 +31,23 @@ describe('createModel', () => {
31
31
  // Prompts for model details
32
32
  mockInquirer.prompt
33
33
  .mockResolvedValueOnce({ modelType: 'openai' })
34
- .mockResolvedValueOnce({
35
- modelVersion: 'gpt-4',
36
- baseUrl: 'https://api.openai.com/',
37
- })
34
+ .mockResolvedValueOnce({ model: 'gpt-4' })
35
+ .mockResolvedValueOnce({ baseUrl: 'https://api.openai.com/' })
38
36
  .mockResolvedValueOnce({ apiKey: 'secret-key' });
39
37
  // Secret operations succeed
40
- mockExeca.mockResolvedValueOnce({}); // delete secret (may not exist)
41
38
  mockExeca.mockResolvedValueOnce({}); // create secret
42
39
  mockExeca.mockResolvedValueOnce({}); // apply model
43
40
  const result = await createModel('test-model');
44
41
  expect(result).toBe(true);
45
42
  expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'model', 'test-model'], { stdio: 'pipe' });
46
- expect(mockOutput.success).toHaveBeenCalledWith('model test-model created successfully');
43
+ expect(mockOutput.success).toHaveBeenCalledWith('model test-model created');
47
44
  });
48
45
  it('prompts for name when not provided', async () => {
49
46
  mockInquirer.prompt
50
47
  .mockResolvedValueOnce({ modelName: 'prompted-model' })
51
48
  .mockResolvedValueOnce({ modelType: 'azure' })
52
- .mockResolvedValueOnce({
53
- modelVersion: 'gpt-4',
54
- baseUrl: 'https://azure.com',
55
- })
49
+ .mockResolvedValueOnce({ model: 'gpt-4' })
50
+ .mockResolvedValueOnce({ baseUrl: 'https://azure.com' })
56
51
  .mockResolvedValueOnce({ apiVersion: '2024-12-01' })
57
52
  .mockResolvedValueOnce({ apiKey: 'secret' });
58
53
  mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
@@ -72,10 +67,8 @@ describe('createModel', () => {
72
67
  mockInquirer.prompt
73
68
  .mockResolvedValueOnce({ overwrite: true })
74
69
  .mockResolvedValueOnce({ modelType: 'openai' })
75
- .mockResolvedValueOnce({
76
- modelVersion: 'gpt-4',
77
- baseUrl: 'https://api.openai.com',
78
- })
70
+ .mockResolvedValueOnce({ model: 'gpt-4' })
71
+ .mockResolvedValueOnce({ baseUrl: 'https://api.openai.com' })
79
72
  .mockResolvedValueOnce({ apiKey: 'secret' });
80
73
  mockExeca.mockResolvedValue({}); // remaining kubectl ops
81
74
  const result = await createModel('existing-model');
@@ -93,33 +86,12 @@ describe('createModel', () => {
93
86
  mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
94
87
  mockInquirer.prompt
95
88
  .mockResolvedValueOnce({ modelType: 'openai' })
96
- .mockResolvedValueOnce({
97
- modelVersion: 'gpt-4',
98
- baseUrl: 'https://api.openai.com',
99
- })
89
+ .mockResolvedValueOnce({ model: 'gpt-4' })
90
+ .mockResolvedValueOnce({ baseUrl: 'https://api.openai.com' })
100
91
  .mockResolvedValueOnce({ apiKey: 'secret' });
101
- mockExeca.mockRejectedValueOnce(new Error('delete failed')); // delete secret may fail
102
92
  mockExeca.mockRejectedValueOnce(new Error('secret creation failed')); // create secret fails
103
93
  const result = await createModel('test-model');
104
94
  expect(result).toBe(false);
105
95
  expect(mockOutput.error).toHaveBeenCalledWith('failed to create secret');
106
96
  });
107
- it('cleans up secret if model creation fails', async () => {
108
- mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
109
- mockInquirer.prompt
110
- .mockResolvedValueOnce({ modelType: 'openai' })
111
- .mockResolvedValueOnce({
112
- modelVersion: 'gpt-4',
113
- baseUrl: 'https://api.openai.com',
114
- })
115
- .mockResolvedValueOnce({ apiKey: 'secret' });
116
- mockExeca.mockResolvedValueOnce({}); // delete secret
117
- mockExeca.mockResolvedValueOnce({}); // create secret
118
- mockExeca.mockRejectedValueOnce(new Error('apply failed')); // apply model fails
119
- mockExeca.mockResolvedValueOnce({}); // cleanup secret
120
- const result = await createModel('test-model');
121
- expect(result).toBe(false);
122
- expect(mockOutput.error).toHaveBeenCalledWith('failed to create model');
123
- expect(mockExeca).toHaveBeenCalledWith('kubectl', ['delete', 'secret', 'test-model-model-api-key'], { stdio: 'pipe' });
124
- });
125
97
  });
@@ -53,8 +53,14 @@ export function createModelsCommand(_) {
53
53
  createCommand
54
54
  .description('Create a new model')
55
55
  .argument('[name]', 'Model name (optional)')
56
- .action(async (name) => {
57
- await createModel(name);
56
+ .option('--type <type>', 'Model provider type (azure, openai, bedrock)')
57
+ .option('--model <model>', 'Model name (e.g., gpt-4o-mini)')
58
+ .option('--base-url <url>', 'Base URL for the model API')
59
+ .option('--api-key <key>', 'API key for authentication')
60
+ .option('--api-version <version>', 'API version (Azure only)')
61
+ .option('--yes', 'Skip confirmation prompts')
62
+ .action(async (name, options) => {
63
+ await createModel(name, options);
58
64
  });
59
65
  modelsCommand.addCommand(createCommand);
60
66
  // Add query command
@@ -76,7 +76,7 @@ describe('models command', () => {
76
76
  it('create subcommand works', async () => {
77
77
  const command = createModelsCommand({});
78
78
  await command.parseAsync(['node', 'test', 'create', 'my-model']);
79
- expect(mockCreateModel).toHaveBeenCalledWith('my-model');
79
+ expect(mockCreateModel).toHaveBeenCalledWith('my-model', expect.objectContaining({}));
80
80
  });
81
81
  it('query subcommand works', async () => {
82
82
  const command = createModelsCommand({});
@@ -1,3 +1,5 @@
1
1
  import { Command } from 'commander';
2
- export declare function checkStatus(): Promise<void>;
2
+ export declare function checkStatus(serviceNames?: string[], options?: {
3
+ waitForReady?: string;
4
+ }): Promise<void>;
3
5
  export declare function createStatusCommand(): Command;
@@ -4,6 +4,10 @@ import ora from 'ora';
4
4
  import { StatusChecker } from '../../components/statusChecker.js';
5
5
  import { StatusFormatter, } from '../../ui/statusFormatter.js';
6
6
  import { fetchVersionInfo } from '../../lib/versions.js';
7
+ import { waitForServicesReady, } from '../../lib/waitForReady.js';
8
+ import { arkServices } from '../../arkServices.js';
9
+ import output from '../../lib/output.js';
10
+ import { parseTimeoutToSeconds } from '../../lib/timeout.js';
7
11
  /**
8
12
  * Enrich service with formatted details including version/revision
9
13
  */
@@ -249,7 +253,7 @@ function buildStatusSections(data, versionInfo) {
249
253
  sections.push({ title: 'ark status:', lines: arkStatusLines });
250
254
  return sections;
251
255
  }
252
- export async function checkStatus() {
256
+ export async function checkStatus(serviceNames, options) {
253
257
  const spinner = ora('Checking system status').start();
254
258
  try {
255
259
  spinner.text = 'Checking system dependencies';
@@ -264,6 +268,52 @@ export async function checkStatus() {
264
268
  spinner.stop();
265
269
  const sections = buildStatusSections(statusData, versionInfo);
266
270
  StatusFormatter.printSections(sections);
271
+ if (options?.waitForReady) {
272
+ const timeoutSeconds = parseTimeoutToSeconds(options.waitForReady);
273
+ let servicesToWait = [];
274
+ if (serviceNames && serviceNames.length > 0) {
275
+ servicesToWait = serviceNames
276
+ .map((name) => Object.values(arkServices).find((s) => s.name === name))
277
+ .filter((s) => s !== undefined &&
278
+ s.k8sDeploymentName !== undefined &&
279
+ s.namespace !== undefined);
280
+ if (servicesToWait.length === 0) {
281
+ output.error(`No valid services found matching: ${serviceNames.join(', ')}`);
282
+ process.exit(1);
283
+ }
284
+ }
285
+ else {
286
+ servicesToWait = Object.values(arkServices).filter((s) => s.enabled &&
287
+ s.category === 'core' &&
288
+ s.k8sDeploymentName &&
289
+ s.namespace);
290
+ }
291
+ console.log();
292
+ const waitSpinner = ora(`Waiting for services to be ready (timeout: ${timeoutSeconds}s)...`).start();
293
+ const statusMap = new Map();
294
+ servicesToWait.forEach((s) => statusMap.set(s.name, false));
295
+ const startTime = Date.now();
296
+ const result = await waitForServicesReady(servicesToWait, timeoutSeconds, (progress) => {
297
+ statusMap.set(progress.serviceName, progress.ready);
298
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
299
+ const lines = servicesToWait.map((s) => {
300
+ const ready = statusMap.get(s.name);
301
+ const icon = ready ? '✓' : '⋯';
302
+ const status = ready ? 'ready' : 'waiting...';
303
+ const color = ready ? chalk.green : chalk.yellow;
304
+ return ` ${color(icon)} ${chalk.bold(s.name)} ${chalk.blue(`(${s.namespace})`)} - ${status}`;
305
+ });
306
+ waitSpinner.text = `Waiting for services to be ready (${elapsed}/${timeoutSeconds}s)...\n${lines.join('\n')}`;
307
+ });
308
+ if (result) {
309
+ waitSpinner.succeed('All services are ready');
310
+ process.exit(0);
311
+ }
312
+ else {
313
+ waitSpinner.fail(`Services did not become ready within ${timeoutSeconds} seconds`);
314
+ process.exit(1);
315
+ }
316
+ }
267
317
  process.exit(0);
268
318
  }
269
319
  catch (error) {
@@ -276,6 +326,8 @@ export function createStatusCommand() {
276
326
  const statusCommand = new Command('status');
277
327
  statusCommand
278
328
  .description('Check ARK system status')
279
- .action(() => checkStatus());
329
+ .argument('[services...]', 'specific services to check (optional)')
330
+ .option('--wait-for-ready <timeout>', 'wait for services to be ready (e.g., 30s, 2m, 1h)')
331
+ .action((services, options) => checkStatus(services, options));
280
332
  return statusCommand;
281
333
  }
@@ -7,6 +7,7 @@ import * as React from 'react';
7
7
  import { marked } from 'marked';
8
8
  // @ts-ignore - no types available
9
9
  import TerminalRenderer from 'marked-terminal';
10
+ import { APIError } from 'openai';
10
11
  import { ChatClient, } from '../lib/chatClient.js';
11
12
  import { AgentSelector } from '../ui/AgentSelector.js';
12
13
  import { ModelSelector } from '../ui/ModelSelector.js';
@@ -548,7 +549,24 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
548
549
  // Request was cancelled, message already updated by Esc handler
549
550
  return;
550
551
  }
551
- const errorMessage = err instanceof Error ? err.message : 'Failed to send message';
552
+ let errorMessage = 'Failed to send message';
553
+ // OpenAI SDK errors include response body in .error property
554
+ if (err instanceof APIError) {
555
+ if (err.error && typeof err.error === 'object') {
556
+ errorMessage = JSON.stringify(err.error, null, 2);
557
+ }
558
+ else {
559
+ errorMessage = err.message;
560
+ }
561
+ }
562
+ // Standard JavaScript errors
563
+ else if (err instanceof Error) {
564
+ errorMessage = err.message;
565
+ }
566
+ // String errors from throw statements
567
+ else if (typeof err === 'string') {
568
+ errorMessage = err;
569
+ }
552
570
  setError(errorMessage);
553
571
  setIsTyping(false);
554
572
  setAbortController(null);
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;