@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.
- package/dist/arkServices.js +21 -13
- package/dist/commands/generate/generators/project.js +2 -2
- package/dist/commands/install/index.js +34 -0
- package/dist/commands/install/index.spec.js +143 -0
- package/dist/commands/models/create.spec.js +20 -1
- package/dist/commands/models/kubernetes/manifest-builder.js +1 -1
- package/dist/commands/models/kubernetes/secret-manager.d.ts +3 -0
- package/dist/commands/models/kubernetes/secret-manager.js +40 -0
- package/dist/commands/queries/index.js +4 -4
- package/dist/commands/queries/index.spec.d.ts +1 -0
- package/dist/commands/queries/index.spec.js +167 -0
- package/dist/components/ChatUI.js +4 -4
- package/dist/lib/executeQuery.js +4 -6
- package/dist/lib/types.d.ts +2 -2
- package/dist/types/arkService.d.ts +5 -0
- package/package.json +2 -2
- package/templates/mcp-server/Dockerfile +2 -2
- package/templates/mcp-server/chart/values.yaml +1 -1
- package/templates/project/values.yaml +2 -2
package/dist/arkServices.js
CHANGED
|
@@ -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
|
-
'
|
|
135
|
-
name: '
|
|
136
|
-
helmReleaseName: '
|
|
137
|
-
description: '
|
|
138
|
-
enabled:
|
|
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
|
-
|
|
141
|
-
chartPath: `${
|
|
142
|
-
installArgs: [],
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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'
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
});
|
|
@@ -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?.
|
|
30
|
-
const response = query.status.
|
|
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?.
|
|
44
|
-
console.log(renderMarkdown(query.status.
|
|
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
|
|
471
|
-
// Chat TUI always queries a single target, so contextId is in
|
|
472
|
-
if (arkMetadata?.completedQuery?.status?.
|
|
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.
|
|
474
|
+
arkMetadata.completedQuery.status.response.a2a.contextId;
|
|
475
475
|
}
|
|
476
476
|
// Update message progressively as chunks arrive
|
|
477
477
|
setMessages((prev) => {
|
package/dist/lib/executeQuery.js
CHANGED
|
@@ -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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
},
|
|
124
|
-
],
|
|
119
|
+
target: {
|
|
120
|
+
type: options.targetType,
|
|
121
|
+
name: options.targetName,
|
|
122
|
+
},
|
|
125
123
|
},
|
|
126
124
|
};
|
|
127
125
|
try {
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 }}
|
|
@@ -49,7 +49,7 @@ security:
|
|
|
49
49
|
# Pod security context
|
|
50
50
|
podSecurityContext:
|
|
51
51
|
runAsNonRoot: true
|
|
52
|
-
runAsUser:
|
|
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:
|
|
60
|
+
runAsUser: 1001
|
|
61
61
|
capabilities:
|
|
62
62
|
drop:
|
|
63
63
|
- ALL
|