@agents-at-scale/ark 0.1.49 → 0.1.51

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.
@@ -119,29 +119,35 @@ const defaultArkServices = {
119
119
  k8sDeploymentName: 'ark-mcp',
120
120
  k8sDevDeploymentName: 'ark-mcp-devspace',
121
121
  },
122
+ // ark-broker replaces ark-cluster-memory (renamed in v0.1.49). The old release
123
+ // must be uninstalled first to avoid Helm ownership conflicts on shared
124
+ // resources like the ark-config-streaming ConfigMap.
122
125
  'ark-broker': {
123
126
  name: 'ark-broker',
124
127
  helmReleaseName: 'ark-broker',
125
128
  description: 'In-memory storage service with streaming support for Ark queries',
126
129
  enabled: true,
127
130
  category: 'service',
128
- // namespace: undefined - uses current context namespace
129
131
  chartPath: `${REGISTRY_BASE}/ark-broker`,
130
132
  installArgs: [],
133
+ prerequisiteUninstalls: [{ releaseName: 'ark-cluster-memory' }],
131
134
  k8sDeploymentName: 'ark-broker',
132
135
  k8sDevDeploymentName: 'ark-broker-devspace',
133
136
  },
134
- 'mcp-filesystem': {
135
- name: 'mcp-filesystem',
136
- helmReleaseName: 'mcp-filesystem',
137
- description: 'Stateful filesystem MCP server with workspace isolation',
138
- enabled: false,
137
+ 'file-gateway': {
138
+ name: 'file-gateway',
139
+ helmReleaseName: 'file-gateway',
140
+ description: 'S3-compatible file storage gateway with REST API for shared storage access',
141
+ enabled: true,
139
142
  category: 'service',
140
- // namespace: undefined - uses current context namespace
141
- chartPath: `${REGISTRY_BASE}/mcp-filesystem`,
142
- installArgs: [],
143
- k8sDeploymentName: 'mcp-filesystem',
144
- k8sDevDeploymentName: 'mcp-filesystem-devspace',
143
+ namespace: 'default',
144
+ chartPath: `${getMarketplaceRegistry()}/file-gateway`,
145
+ installArgs: ['--create-namespace'],
146
+ k8sServiceName: 'file-gateway-file-api',
147
+ k8sServicePort: 8080,
148
+ k8sPortForwardLocalPort: undefined,
149
+ k8sDeploymentName: 'file-gateway-file-api',
150
+ k8sDevDeploymentName: undefined,
145
151
  },
146
152
  'localhost-gateway': {
147
153
  name: 'localhost-gateway',
@@ -153,7 +159,7 @@ const defaultArkServices = {
153
159
  chartPath: `${REGISTRY_BASE}/localhost-gateway`,
154
160
  installArgs: [],
155
161
  },
156
- 'noah': {
162
+ noah: {
157
163
  name: 'noah',
158
164
  helmReleaseName: 'noah',
159
165
  description: 'Runtime administration agent with cluster privileges',
@@ -173,7 +179,9 @@ function applyConfigOverrides(defaults) {
173
179
  for (const [key, service] of Object.entries(defaults)) {
174
180
  const override = overrides[key];
175
181
  result[key] =
176
- override && typeof override === 'object' ? { ...service, ...override } : service;
182
+ override && typeof override === 'object'
183
+ ? { ...service, ...override }
184
+ : service;
177
185
  }
178
186
  return result;
179
187
  }
@@ -727,7 +727,7 @@ Generated with ARK CLI generator`;
727
727
  },
728
728
  ];
729
729
  if (config.projectType === 'empty') {
730
- steps.push({ desc: 'Add YAML files to agents/, teams/, queries/ directories' }, { desc: 'Copy model configurations from samples/models/' }, { desc: 'Edit .env file to set your API keys' }, { desc: 'Deploy your project', cmd: 'devspace dev' });
730
+ steps.push({ desc: 'Add YAML files to agents/, teams/, queries/ directories' }, { desc: 'Use either the default model already in models/ or a configuration template from samples/models/ of ARK repository' }, { desc: 'Edit .env file to set your API keys' }, { desc: 'Deploy your project', cmd: 'devspace dev' });
731
731
  }
732
732
  else if (config.selectedModels && config.selectedModels !== 'none') {
733
733
  steps.push({ desc: 'Edit .env file to set your API keys' }, { desc: 'Load environment variables', cmd: 'source .env' }, { desc: 'Deploy your project', cmd: 'devspace dev' }, {
@@ -736,7 +736,7 @@ Generated with ARK CLI generator`;
736
736
  });
737
737
  }
738
738
  else {
739
- steps.push({ desc: 'Copy model configurations from samples/models/' }, { desc: 'Edit .env file to set your API keys' }, { desc: 'Deploy your project', cmd: 'devspace dev' });
739
+ steps.push({ desc: 'Use either the default model already in models/ or a configuration template from samples/models/ of ARK repository' }, { desc: 'Edit .env file to set your API keys' }, { desc: 'Deploy your project', cmd: 'devspace dev' });
740
740
  }
741
741
  console.log(chalk.magenta.bold('🚀 NEXT STEPS:\n'));
742
742
  let stepNumber = 1;
@@ -10,7 +10,41 @@ import { printNextSteps } from '../../lib/nextSteps.js';
10
10
  import ora from 'ora';
11
11
  import { waitForServicesReady, } from '../../lib/waitForReady.js';
12
12
  import { parseTimeoutToSeconds } from '../../lib/timeout.js';
13
+ async function uninstallPrerequisites(service, verbose = false) {
14
+ if (!service.prerequisiteUninstalls?.length)
15
+ return;
16
+ for (const prereq of service.prerequisiteUninstalls) {
17
+ const helmArgs = ['uninstall', prereq.releaseName, '--ignore-not-found'];
18
+ if (prereq.namespace) {
19
+ helmArgs.push('--namespace', prereq.namespace);
20
+ }
21
+ await execute('helm', helmArgs, { stdio: 'inherit' }, { verbose });
22
+ }
23
+ }
24
+ async function checkAndCleanFailedRelease(releaseName, namespace, verbose = false) {
25
+ const statusArgs = ['status', releaseName];
26
+ if (namespace) {
27
+ statusArgs.push('--namespace', namespace);
28
+ }
29
+ try {
30
+ const result = await execute('helm', statusArgs, {}, { verbose: false });
31
+ const stdout = String(result.stdout || '');
32
+ if (stdout.includes('STATUS: pending-install') ||
33
+ stdout.includes('STATUS: failed') ||
34
+ stdout.includes('STATUS: uninstalling')) {
35
+ const uninstallArgs = ['uninstall', releaseName];
36
+ if (namespace) {
37
+ uninstallArgs.push('--namespace', namespace);
38
+ }
39
+ await execute('helm', uninstallArgs, { stdio: 'inherit' }, { verbose });
40
+ }
41
+ }
42
+ catch {
43
+ }
44
+ }
13
45
  async function installService(service, verbose = false) {
46
+ await uninstallPrerequisites(service, verbose);
47
+ await checkAndCleanFailedRelease(service.helmReleaseName, service.namespace, verbose);
14
48
  const helmArgs = [
15
49
  'upgrade',
16
50
  '--install',
@@ -140,4 +140,147 @@ describe('install command', () => {
140
140
  await expect(command.parseAsync(['node', 'test', 'ark-api'])).rejects.toThrow('process.exit called');
141
141
  expect(mockExit).toHaveBeenCalledWith(1);
142
142
  });
143
+ describe('checkAndCleanFailedRelease', () => {
144
+ it('uninstalls release in pending-install state', async () => {
145
+ const mockService = {
146
+ name: 'ark-api',
147
+ helmReleaseName: 'ark-api',
148
+ chartPath: './charts/ark-api',
149
+ namespace: 'ark-system',
150
+ };
151
+ mockGetInstallableServices.mockReturnValue({
152
+ 'ark-api': mockService,
153
+ });
154
+ mockExeca
155
+ .mockResolvedValueOnce({
156
+ stdout: 'NAME: ark-api\nSTATUS: pending-install\n',
157
+ })
158
+ .mockResolvedValueOnce({})
159
+ .mockResolvedValueOnce({});
160
+ const command = createInstallCommand(mockConfig);
161
+ await command.parseAsync(['node', 'test', 'ark-api']);
162
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['status', 'ark-api', '--namespace', 'ark-system'], {});
163
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['uninstall', 'ark-api', '--namespace', 'ark-system'], { stdio: 'inherit' });
164
+ expect(mockExeca).toHaveBeenCalledWith('helm', [
165
+ 'upgrade',
166
+ '--install',
167
+ 'ark-api',
168
+ './charts/ark-api',
169
+ '--namespace',
170
+ 'ark-system',
171
+ ], { stdio: 'inherit' });
172
+ });
173
+ it('uninstalls release in failed state', async () => {
174
+ const mockService = {
175
+ name: 'ark-api',
176
+ helmReleaseName: 'ark-api',
177
+ chartPath: './charts/ark-api',
178
+ namespace: 'ark-system',
179
+ };
180
+ mockGetInstallableServices.mockReturnValue({
181
+ 'ark-api': mockService,
182
+ });
183
+ mockExeca
184
+ .mockResolvedValueOnce({
185
+ stdout: 'NAME: ark-api\nSTATUS: failed\n',
186
+ })
187
+ .mockResolvedValueOnce({})
188
+ .mockResolvedValueOnce({});
189
+ const command = createInstallCommand(mockConfig);
190
+ await command.parseAsync(['node', 'test', 'ark-api']);
191
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['uninstall', 'ark-api', '--namespace', 'ark-system'], { stdio: 'inherit' });
192
+ });
193
+ it('uninstalls release in uninstalling state', async () => {
194
+ const mockService = {
195
+ name: 'ark-dashboard',
196
+ helmReleaseName: 'ark-dashboard',
197
+ chartPath: './charts/ark-dashboard',
198
+ namespace: 'default',
199
+ };
200
+ mockGetInstallableServices.mockReturnValue({
201
+ 'ark-dashboard': mockService,
202
+ });
203
+ mockExeca
204
+ .mockResolvedValueOnce({
205
+ stdout: 'NAME: ark-dashboard\nSTATUS: uninstalling\nREVISION: 2\n',
206
+ })
207
+ .mockResolvedValueOnce({})
208
+ .mockResolvedValueOnce({});
209
+ const command = createInstallCommand(mockConfig);
210
+ await command.parseAsync(['node', 'test', 'ark-dashboard']);
211
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['uninstall', 'ark-dashboard', '--namespace', 'default'], { stdio: 'inherit' });
212
+ });
213
+ it('does not uninstall release in deployed state', async () => {
214
+ const mockService = {
215
+ name: 'ark-api',
216
+ helmReleaseName: 'ark-api',
217
+ chartPath: './charts/ark-api',
218
+ namespace: 'ark-system',
219
+ };
220
+ mockGetInstallableServices.mockReturnValue({
221
+ 'ark-api': mockService,
222
+ });
223
+ mockExeca
224
+ .mockResolvedValueOnce({
225
+ stdout: 'NAME: ark-api\nSTATUS: deployed\n',
226
+ })
227
+ .mockResolvedValueOnce({});
228
+ const command = createInstallCommand(mockConfig);
229
+ await command.parseAsync(['node', 'test', 'ark-api']);
230
+ const uninstallCalls = mockExeca.mock.calls.filter((call) => call[0] === 'helm' && call[1][0] === 'uninstall');
231
+ expect(uninstallCalls).toHaveLength(0);
232
+ expect(mockExeca).toHaveBeenCalledWith('helm', [
233
+ 'upgrade',
234
+ '--install',
235
+ 'ark-api',
236
+ './charts/ark-api',
237
+ '--namespace',
238
+ 'ark-system',
239
+ ], { stdio: 'inherit' });
240
+ });
241
+ it('handles helm status errors gracefully', async () => {
242
+ const mockService = {
243
+ name: 'ark-api',
244
+ helmReleaseName: 'ark-api',
245
+ chartPath: './charts/ark-api',
246
+ namespace: 'ark-system',
247
+ };
248
+ mockGetInstallableServices.mockReturnValue({
249
+ 'ark-api': mockService,
250
+ });
251
+ mockExeca
252
+ .mockRejectedValueOnce(new Error('release not found'))
253
+ .mockResolvedValueOnce({});
254
+ const command = createInstallCommand(mockConfig);
255
+ await command.parseAsync(['node', 'test', 'ark-api']);
256
+ expect(mockExeca).toHaveBeenCalledWith('helm', [
257
+ 'upgrade',
258
+ '--install',
259
+ 'ark-api',
260
+ './charts/ark-api',
261
+ '--namespace',
262
+ 'ark-system',
263
+ ], { stdio: 'inherit' });
264
+ });
265
+ it('handles service without namespace', async () => {
266
+ const mockService = {
267
+ name: 'ark-dashboard',
268
+ helmReleaseName: 'ark-dashboard',
269
+ chartPath: './charts/ark-dashboard',
270
+ };
271
+ mockGetInstallableServices.mockReturnValue({
272
+ 'ark-dashboard': mockService,
273
+ });
274
+ mockExeca
275
+ .mockResolvedValueOnce({
276
+ stdout: 'NAME: ark-dashboard\nSTATUS: failed\n',
277
+ })
278
+ .mockResolvedValueOnce({})
279
+ .mockResolvedValueOnce({});
280
+ const command = createInstallCommand(mockConfig);
281
+ await command.parseAsync(['node', 'test', 'ark-dashboard']);
282
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['status', 'ark-dashboard'], {});
283
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['uninstall', 'ark-dashboard'], { stdio: 'inherit' });
284
+ });
285
+ });
143
286
  });
@@ -134,15 +134,34 @@ describe('createModel', () => {
134
134
  expect(mockOutput.info).toHaveBeenCalledWith('model creation cancelled');
135
135
  });
136
136
  it('handles secret creation failure', async () => {
137
- mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
137
+ mockExeca
138
+ .mockRejectedValueOnce(new Error('not found')); // model doesn't exist
138
139
  mockInquirer.prompt
139
140
  .mockResolvedValueOnce({ modelType: 'openai' })
140
141
  .mockResolvedValueOnce({ model: 'gpt-4' })
141
142
  .mockResolvedValueOnce({ baseUrl: 'https://api.openai.com' })
142
143
  .mockResolvedValueOnce({ apiKey: 'secret' });
144
+ mockExeca
145
+ .mockRejectedValueOnce(new Error('not found')); // secret doesn't exist check
143
146
  mockExeca.mockRejectedValueOnce(new Error('secret creation failed')); // create secret fails
144
147
  const result = await createModel('test-model');
145
148
  expect(result).toBe(false);
146
149
  expect(mockOutput.error).toHaveBeenCalledWith('failed to create secret');
147
150
  });
151
+ it('updates existing secret when secret already exists', async () => {
152
+ mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
153
+ mockInquirer.prompt
154
+ .mockResolvedValueOnce({ modelType: 'openai' })
155
+ .mockResolvedValueOnce({ model: 'gpt-4' })
156
+ .mockResolvedValueOnce({ baseUrl: 'https://api.openai.com' })
157
+ .mockResolvedValueOnce({ apiKey: 'new-secret-key' });
158
+ mockExeca.mockResolvedValueOnce({}); // secret exists check
159
+ mockExeca.mockResolvedValueOnce({ stdout: 'secret yaml' }); // dry-run output
160
+ mockExeca.mockResolvedValueOnce({}); // kubectl apply
161
+ mockExeca.mockResolvedValueOnce({}); // apply model
162
+ const result = await createModel('test-model');
163
+ expect(result).toBe(true);
164
+ expect(mockOutput.success).toHaveBeenCalledWith('updated secret test-model-model-secret');
165
+ expect(mockOutput.success).toHaveBeenCalledWith('model test-model created');
166
+ });
148
167
  });
@@ -11,7 +11,7 @@ export class KubernetesModelManifestBuilder {
11
11
  name: this.modelName,
12
12
  },
13
13
  spec: {
14
- type: config.type,
14
+ provider: config.type, // Use provider field (required as of v0.50.0)
15
15
  model: {
16
16
  value: config.modelValue,
17
17
  },
@@ -4,4 +4,7 @@ export interface SecretManager {
4
4
  }
5
5
  export declare class KubernetesSecretManager implements SecretManager {
6
6
  createSecret(config: ProviderConfig): Promise<void>;
7
+ private secretExists;
8
+ private createNewSecret;
9
+ private updateSecret;
7
10
  }
@@ -3,6 +3,24 @@ import output from '../../../lib/output.js';
3
3
  // Kubernetes secret manager implementation
4
4
  export class KubernetesSecretManager {
5
5
  async createSecret(config) {
6
+ const secretExists = await this.secretExists(config.secretName);
7
+ if (secretExists) {
8
+ await this.updateSecret(config);
9
+ }
10
+ else {
11
+ await this.createNewSecret(config);
12
+ }
13
+ }
14
+ async secretExists(secretName) {
15
+ try {
16
+ await execa('kubectl', ['get', 'secret', secretName], { stdio: 'pipe' });
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ async createNewSecret(config) {
6
24
  const secretArgs = ['create', 'secret', 'generic', config.secretName];
7
25
  if (config.type === 'bedrock') {
8
26
  secretArgs.push(`--from-literal=access-key-id=${config.accessKeyId}`);
@@ -13,8 +31,30 @@ export class KubernetesSecretManager {
13
31
  }
14
32
  else {
15
33
  secretArgs.push(`--from-literal=api-key=${config.apiKey}`);
34
+ secretArgs.push(`--from-literal=token=${config.apiKey}`);
16
35
  }
17
36
  await execa('kubectl', secretArgs, { stdio: 'pipe' });
18
37
  output.success(`created secret ${config.secretName}`);
19
38
  }
39
+ async updateSecret(config) {
40
+ const secretArgs = ['create', 'secret', 'generic', config.secretName];
41
+ if (config.type === 'bedrock') {
42
+ secretArgs.push(`--from-literal=access-key-id=${config.accessKeyId}`);
43
+ secretArgs.push(`--from-literal=secret-access-key=${config.secretAccessKey}`);
44
+ if (config.sessionToken) {
45
+ secretArgs.push(`--from-literal=session-token=${config.sessionToken}`);
46
+ }
47
+ }
48
+ else {
49
+ secretArgs.push(`--from-literal=api-key=${config.apiKey}`);
50
+ secretArgs.push(`--from-literal=token=${config.apiKey}`);
51
+ }
52
+ secretArgs.push('--dry-run=client', '-o', 'yaml');
53
+ const { stdout } = await execa('kubectl', secretArgs, { stdio: 'pipe' });
54
+ await execa('kubectl', ['apply', '-f', '-'], {
55
+ input: stdout,
56
+ stdio: ['pipe', 'pipe', 'pipe'],
57
+ });
58
+ output.success(`updated secret ${config.secretName}`);
59
+ }
20
60
  }
@@ -26,8 +26,8 @@ async function getQuery(name, options) {
26
26
  try {
27
27
  const query = await getResource('queries', name);
28
28
  if (options.response) {
29
- if (query.status?.responses && query.status.responses.length > 0) {
30
- const response = query.status.responses[0];
29
+ if (query.status?.response) {
30
+ const response = query.status.response;
31
31
  if (options.output === 'markdown') {
32
32
  console.log(renderMarkdown(response.content || ''));
33
33
  }
@@ -40,8 +40,8 @@ async function getQuery(name, options) {
40
40
  }
41
41
  }
42
42
  else if (options.output === 'markdown') {
43
- if (query.status?.responses && query.status.responses.length > 0) {
44
- console.log(renderMarkdown(query.status.responses[0].content || ''));
43
+ if (query.status?.response) {
44
+ console.log(renderMarkdown(query.status.response.content || ''));
45
45
  }
46
46
  else {
47
47
  output.warning('No response available');
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,167 @@
1
+ import { jest } from '@jest/globals';
2
+ import output from '../../lib/output.js';
3
+ const mockExeca = jest.fn();
4
+ jest.unstable_mockModule('execa', () => ({
5
+ execa: mockExeca,
6
+ }));
7
+ const { createQueriesCommand } = await import('./index.js');
8
+ describe('queries get command', () => {
9
+ beforeEach(() => {
10
+ jest.clearAllMocks();
11
+ console.log = jest.fn();
12
+ jest.spyOn(output, 'warning').mockImplementation(() => { });
13
+ jest.spyOn(output, 'error').mockImplementation(() => { });
14
+ jest.spyOn(process, 'exit').mockImplementation(() => undefined);
15
+ });
16
+ it('should get query with response in JSON format', async () => {
17
+ const mockQuery = {
18
+ metadata: {
19
+ name: 'test-query',
20
+ },
21
+ spec: {
22
+ input: 'test input',
23
+ target: { type: 'agent', name: 'test-agent' },
24
+ },
25
+ status: {
26
+ phase: 'done',
27
+ response: {
28
+ content: 'This is the response',
29
+ },
30
+ },
31
+ };
32
+ mockExeca.mockResolvedValue({
33
+ stdout: JSON.stringify(mockQuery),
34
+ });
35
+ const command = createQueriesCommand({});
36
+ await command.parseAsync(['node', 'test', 'get', 'test-query']);
37
+ expect(console.log).toHaveBeenCalledWith(JSON.stringify(mockQuery, null, 2));
38
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', 'test-query', '-o', 'json'], { stdio: 'pipe' });
39
+ });
40
+ it('should get query with response flag in JSON format', async () => {
41
+ const mockQuery = {
42
+ metadata: {
43
+ name: 'test-query',
44
+ },
45
+ spec: {
46
+ input: 'test input',
47
+ target: { type: 'agent', name: 'test-agent' },
48
+ },
49
+ status: {
50
+ phase: 'done',
51
+ response: {
52
+ content: 'This is the response content',
53
+ },
54
+ },
55
+ };
56
+ mockExeca.mockResolvedValue({
57
+ stdout: JSON.stringify(mockQuery),
58
+ });
59
+ const command = createQueriesCommand({});
60
+ await command.parseAsync([
61
+ 'node',
62
+ 'test',
63
+ 'get',
64
+ 'test-query',
65
+ '--response',
66
+ ]);
67
+ expect(console.log).toHaveBeenCalledWith(JSON.stringify(mockQuery.status.response, null, 2));
68
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', 'test-query', '-o', 'json'], { stdio: 'pipe' });
69
+ });
70
+ it('should get query with response flag in markdown format', async () => {
71
+ const mockQuery = {
72
+ metadata: {
73
+ name: 'test-query',
74
+ },
75
+ spec: {
76
+ input: 'test input',
77
+ target: { type: 'agent', name: 'test-agent' },
78
+ },
79
+ status: {
80
+ phase: 'done',
81
+ response: {
82
+ content: '# Heading\n\nThis is markdown content',
83
+ },
84
+ },
85
+ };
86
+ mockExeca.mockResolvedValue({
87
+ stdout: JSON.stringify(mockQuery),
88
+ });
89
+ const command = createQueriesCommand({});
90
+ await command.parseAsync([
91
+ 'node',
92
+ 'test',
93
+ 'get',
94
+ 'test-query',
95
+ '--response',
96
+ '--output',
97
+ 'markdown',
98
+ ]);
99
+ expect(console.log).toHaveBeenCalled();
100
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', 'test-query', '-o', 'json'], { stdio: 'pipe' });
101
+ });
102
+ it('should get query in markdown format without response flag', async () => {
103
+ const mockQuery = {
104
+ metadata: {
105
+ name: 'test-query',
106
+ },
107
+ spec: {
108
+ input: 'test input',
109
+ target: { type: 'agent', name: 'test-agent' },
110
+ },
111
+ status: {
112
+ phase: 'done',
113
+ response: {
114
+ content: '# Response\n\nMarkdown response',
115
+ },
116
+ },
117
+ };
118
+ mockExeca.mockResolvedValue({
119
+ stdout: JSON.stringify(mockQuery),
120
+ });
121
+ const command = createQueriesCommand({});
122
+ await command.parseAsync([
123
+ 'node',
124
+ 'test',
125
+ 'get',
126
+ 'test-query',
127
+ '--output',
128
+ 'markdown',
129
+ ]);
130
+ expect(console.log).toHaveBeenCalled();
131
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', 'test-query', '-o', 'json'], { stdio: 'pipe' });
132
+ });
133
+ it('should warn when query has no response with response flag', async () => {
134
+ const mockQuery = {
135
+ metadata: {
136
+ name: 'test-query',
137
+ },
138
+ spec: {
139
+ input: 'test input',
140
+ target: { type: 'agent', name: 'test-agent' },
141
+ },
142
+ status: {
143
+ phase: 'running',
144
+ },
145
+ };
146
+ mockExeca.mockResolvedValue({
147
+ stdout: JSON.stringify(mockQuery),
148
+ });
149
+ const command = createQueriesCommand({});
150
+ await command.parseAsync([
151
+ 'node',
152
+ 'test',
153
+ 'get',
154
+ 'test-query',
155
+ '--response',
156
+ ]);
157
+ expect(output.warning).toHaveBeenCalledWith('No response available');
158
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', 'test-query', '-o', 'json'], { stdio: 'pipe' });
159
+ });
160
+ it('should handle errors when getting query', async () => {
161
+ mockExeca.mockRejectedValue(new Error('Query not found'));
162
+ const command = createQueriesCommand({});
163
+ await command.parseAsync(['node', 'test', 'get', 'nonexistent-query']);
164
+ expect(output.error).toHaveBeenCalledWith('fetching query:', 'Query not found');
165
+ expect(process.exit).toHaveBeenCalled();
166
+ });
167
+ });
@@ -467,11 +467,11 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
467
467
  }
468
468
  // Send message and get response with abort signal
469
469
  const fullResponse = await chatClientRef.current.sendMessage(target.id, apiMessages, { ...chatConfig, a2aContextId: a2aContextIdRef.current }, (chunk, toolCalls, arkMetadata) => {
470
- // Extract A2A context ID from first response
471
- // Chat TUI always queries a single target, so contextId is in responses[0]
472
- if (arkMetadata?.completedQuery?.status?.responses?.[0]?.a2a?.contextId) {
470
+ // Extract A2A context ID from response
471
+ // Chat TUI always queries a single target, so contextId is in response
472
+ if (arkMetadata?.completedQuery?.status?.response?.a2a?.contextId) {
473
473
  a2aContextIdRef.current =
474
- arkMetadata.completedQuery.status.responses[0].a2a.contextId;
474
+ arkMetadata.completedQuery.status.response.a2a.contextId;
475
475
  }
476
476
  // Update message progressively as chunks arrive
477
477
  setMessages((prev) => {
@@ -116,12 +116,10 @@ async function executeQueryWithFormat(options) {
116
116
  ...((options.conversationId || process.env.ARK_CONVERSATION_ID) && {
117
117
  conversationId: options.conversationId || process.env.ARK_CONVERSATION_ID,
118
118
  }),
119
- targets: [
120
- {
121
- type: options.targetType,
122
- name: options.targetName,
123
- },
124
- ],
119
+ target: {
120
+ type: options.targetType,
121
+ name: options.targetName,
122
+ },
125
123
  },
126
124
  };
127
125
  try {
@@ -101,7 +101,7 @@ export interface QueryResponse {
101
101
  export interface QueryStatus {
102
102
  phase?: 'initializing' | 'running' | 'done' | 'error' | 'canceled';
103
103
  conditions?: K8sCondition[];
104
- responses?: QueryResponse[];
104
+ response?: QueryResponse;
105
105
  message?: string;
106
106
  error?: string;
107
107
  tokenUsage?: {
@@ -120,7 +120,7 @@ export interface Query {
120
120
  metadata: K8sMetadata;
121
121
  spec?: {
122
122
  input: string;
123
- targets: QueryTarget[];
123
+ target: QueryTarget;
124
124
  sessionId?: string;
125
125
  conversationId?: string;
126
126
  timeout?: string;
@@ -1,3 +1,7 @@
1
+ export interface PrerequisiteUninstall {
2
+ releaseName: string;
3
+ namespace?: string;
4
+ }
1
5
  export interface ArkService {
2
6
  name: string;
3
7
  helmReleaseName: string;
@@ -7,6 +11,7 @@ export interface ArkService {
7
11
  namespace?: string;
8
12
  chartPath?: string;
9
13
  installArgs?: string[];
14
+ prerequisiteUninstalls?: PrerequisiteUninstall[];
10
15
  k8sServiceName?: string;
11
16
  k8sServicePort?: number;
12
17
  k8sPortForwardLocalPort?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-at-scale/ark",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
4
4
  "description": "Ark CLI - Interactive terminal interface for ARK agents",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -60,7 +60,7 @@
60
60
  "open": "^10.2.0",
61
61
  "openai": "^5.19.1",
62
62
  "ora": "^8.2.0",
63
- "react": "^19.1.0",
63
+ "react": "^19.1.5",
64
64
  "yaml": "^2.6.1"
65
65
  },
66
66
  "devDependencies": {
@@ -1,5 +1,5 @@
1
1
  {{- if eq .Values.technology "node" }}
2
- FROM node:22
2
+ FROM node:22.22.0-alpine
3
3
 
4
4
  # Install the MCP server package
5
5
  {{- if .Values.packageSource }}
@@ -78,7 +78,7 @@ WORKDIR /app
78
78
  RUN go build -o {{ .Values.mcpServerName }} .
79
79
  {{- end }}
80
80
 
81
- FROM node:22
81
+ FROM node:22.22.0-alpine
82
82
  {{- if eq .Values.packageSource "go-install" }}
83
83
  COPY --from=go-builder /go/bin/{{ .Values.packageName }} /usr/local/bin/
84
84
  {{- else }}
@@ -27,7 +27,7 @@ securityContext:
27
27
  - ALL
28
28
  readOnlyRootFilesystem: true
29
29
  runAsNonRoot: true
30
- runAsUser: 1000
30
+ runAsUser: 1001
31
31
 
32
32
  service:
33
33
  type: ClusterIP
@@ -49,7 +49,7 @@ security:
49
49
  # Pod security context
50
50
  podSecurityContext:
51
51
  runAsNonRoot: true
52
- runAsUser: 1000
52
+ runAsUser: 1001
53
53
  fsGroup: 2000
54
54
 
55
55
  # Container security context
@@ -57,7 +57,7 @@ security:
57
57
  allowPrivilegeEscalation: false
58
58
  readOnlyRootFilesystem: true
59
59
  runAsNonRoot: true
60
- runAsUser: 1000
60
+ runAsUser: 1001
61
61
  capabilities:
62
62
  drop:
63
63
  - ALL