@agents-at-scale/ark 0.1.35-rc1 → 0.1.35-rc2

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.
@@ -28,7 +28,7 @@ _ark_completion() {
28
28
 
29
29
  case \${COMP_CWORD} in
30
30
  1)
31
- opts="agents chat cluster completion config dashboard dev generate install models routes status targets teams tools uninstall help"
31
+ opts="agents chat cluster completion config dashboard dev docs generate install models query routes status targets teams tools uninstall help"
32
32
  COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
33
33
  return 0
34
34
  ;;
@@ -95,6 +95,17 @@ _ark_completion() {
95
95
  COMPREPLY=( $(compgen -W "\${targets}" -- \${cur}) )
96
96
  return 0
97
97
  ;;
98
+ query)
99
+ # Dynamically fetch available targets for query command
100
+ local targets
101
+ targets=$(ark targets list 2>/dev/null)
102
+ if [ -z "$targets" ]; then
103
+ # Fallback to common targets if API is not available
104
+ targets="model/default agent/sample-agent"
105
+ fi
106
+ COMPREPLY=( $(compgen -W "\${targets}" -- \${cur}) )
107
+ return 0
108
+ ;;
98
109
  esac
99
110
  ;;
100
111
  3)
@@ -159,9 +170,11 @@ _ark() {
159
170
  'config[Configuration management]' \\
160
171
  'dashboard[Open ARK dashboard]' \\
161
172
  'dev[Development tools for ARK]' \\
173
+ 'docs[Open ARK documentation]' \\
162
174
  'generate[Generate ARK resources]' \\
163
175
  'install[Install ARK services]' \\
164
176
  'models[List available models]' \\
177
+ 'query[Execute a single query]' \\
165
178
  'routes[List available routes]' \\
166
179
  'status[Check system status]' \\
167
180
  'targets[List available query targets]' \\
@@ -234,6 +247,15 @@ _ark() {
234
247
  fi
235
248
  _values 'available targets' \${targets[@]}
236
249
  ;;
250
+ query)
251
+ # Get available targets dynamically for query
252
+ local -a targets
253
+ targets=($(ark targets list 2>/dev/null))
254
+ if [ \${#targets[@]} -eq 0 ]; then
255
+ targets=('model/default' 'agent/sample-agent')
256
+ fi
257
+ _values 'available targets' \${targets[@]}
258
+ ;;
237
259
  esac
238
260
  ;;
239
261
  args)
@@ -0,0 +1,4 @@
1
+ import { Command } from 'commander';
2
+ import type { ArkConfig } from '../../lib/config.js';
3
+ export declare function openDocs(): Promise<void>;
4
+ export declare function createDocsCommand(_: ArkConfig): Command;
@@ -0,0 +1,18 @@
1
+ import chalk from 'chalk';
2
+ import { Command } from 'commander';
3
+ import open from 'open';
4
+ const DOCS_URL = 'https://mckinsey.github.io/agents-at-scale-ark/';
5
+ export async function openDocs() {
6
+ console.log(`Opening ARK documentation: ${chalk.blue(DOCS_URL)}`);
7
+ // Brief pause before opening browser
8
+ await new Promise((resolve) => setTimeout(resolve, 500));
9
+ // Open browser
10
+ await open(DOCS_URL);
11
+ }
12
+ export function createDocsCommand(_) {
13
+ const docsCommand = new Command('docs');
14
+ docsCommand
15
+ .description('Open the ARK documentation in your browser')
16
+ .action(openDocs);
17
+ return docsCommand;
18
+ }
@@ -1,8 +1,8 @@
1
1
  import { Command } from 'commander';
2
2
  import type { ArkConfig } from '../../lib/config.js';
3
- export declare function installArk(serviceName?: string, options?: {
3
+ export declare function installArk(config: ArkConfig, serviceName?: string, options?: {
4
4
  yes?: boolean;
5
5
  waitForReady?: string;
6
6
  verbose?: boolean;
7
7
  }): Promise<void>;
8
- export declare function createInstallCommand(_: ArkConfig): Command;
8
+ export declare function createInstallCommand(config: ArkConfig): Command;
@@ -2,10 +2,11 @@ import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { execute } from '../../lib/commands.js';
4
4
  import inquirer from 'inquirer';
5
- import { getClusterInfo } from '../../lib/cluster.js';
5
+ import { showNoClusterError } from '../../lib/startup.js';
6
6
  import output from '../../lib/output.js';
7
7
  import { getInstallableServices, arkDependencies } from '../../arkServices.js';
8
8
  import { isArkReady } from '../../lib/arkStatus.js';
9
+ import { printNextSteps } from '../../lib/nextSteps.js';
9
10
  import ora from 'ora';
10
11
  async function installService(service, verbose = false) {
11
12
  const helmArgs = [
@@ -22,25 +23,18 @@ async function installService(service, verbose = false) {
22
23
  helmArgs.push(...(service.installArgs || []));
23
24
  await execute('helm', helmArgs, { stdio: 'inherit' }, { verbose });
24
25
  }
25
- export async function installArk(serviceName, options = {}) {
26
+ export async function installArk(config, serviceName, options = {}) {
26
27
  // Validate that --wait-for-ready requires -y
27
28
  if (options.waitForReady && !options.yes) {
28
29
  output.error('--wait-for-ready requires -y flag for non-interactive mode');
29
30
  process.exit(1);
30
31
  }
31
- // Check cluster connectivity
32
- const clusterInfo = await getClusterInfo();
33
- if (clusterInfo.error) {
34
- output.error('no kubernetes cluster detected');
35
- output.info('please ensure you have a running cluster and kubectl is configured.');
36
- output.info('');
37
- output.info('for local development, we recommend minikube:');
38
- output.info('• install: https://minikube.sigs.k8s.io/docs/start');
39
- output.info('• start cluster: minikube start');
40
- output.info('');
41
- output.info('alternatively, you can use kind or docker desktop.');
32
+ // Check cluster connectivity from config
33
+ if (!config.clusterInfo) {
34
+ showNoClusterError();
42
35
  process.exit(1);
43
36
  }
37
+ const clusterInfo = config.clusterInfo;
44
38
  // Show cluster info
45
39
  output.success(`connected to cluster: ${chalk.bold(clusterInfo.context)}`);
46
40
  output.info(`type: ${clusterInfo.type}`);
@@ -250,6 +244,10 @@ export async function installArk(serviceName, options = {}) {
250
244
  }
251
245
  }
252
246
  }
247
+ // Show next steps after successful installation
248
+ if (!serviceName || serviceName === 'all') {
249
+ printNextSteps();
250
+ }
253
251
  // Wait for Ark to be ready if requested
254
252
  if (options.waitForReady) {
255
253
  // Parse timeout value (e.g., '30s', '2m', '60')
@@ -287,7 +285,7 @@ export async function installArk(serviceName, options = {}) {
287
285
  }
288
286
  }
289
287
  }
290
- export function createInstallCommand(_) {
288
+ export function createInstallCommand(config) {
291
289
  const command = new Command('install');
292
290
  command
293
291
  .description('Install ARK components using Helm')
@@ -296,7 +294,7 @@ export function createInstallCommand(_) {
296
294
  .option('--wait-for-ready <timeout>', 'wait for Ark to be ready after installation (e.g., 30s, 2m)')
297
295
  .option('-v, --verbose', 'show commands being executed')
298
296
  .action(async (service, options) => {
299
- await installArk(service, options);
297
+ await installArk(config, service, options);
300
298
  });
301
299
  return command;
302
300
  }
@@ -9,9 +9,11 @@ jest.unstable_mockModule('../../lib/cluster.js', () => ({
9
9
  getClusterInfo: mockGetClusterInfo,
10
10
  }));
11
11
  const mockGetInstallableServices = jest.fn();
12
+ const mockArkServices = {};
12
13
  const mockArkDependencies = {};
13
14
  jest.unstable_mockModule('../../arkServices.js', () => ({
14
15
  getInstallableServices: mockGetInstallableServices,
16
+ arkServices: mockArkServices,
15
17
  arkDependencies: mockArkDependencies,
16
18
  }));
17
19
  const mockOutput = {
@@ -30,6 +32,13 @@ jest.spyOn(console, 'log').mockImplementation(() => { });
30
32
  jest.spyOn(console, 'error').mockImplementation(() => { });
31
33
  const { createInstallCommand } = await import('./index.js');
32
34
  describe('install command', () => {
35
+ const mockConfig = {
36
+ clusterInfo: {
37
+ context: 'test-cluster',
38
+ type: 'minikube',
39
+ namespace: 'default',
40
+ },
41
+ };
33
42
  beforeEach(() => {
34
43
  jest.clearAllMocks();
35
44
  mockGetClusterInfo.mockResolvedValue({
@@ -39,7 +48,7 @@ describe('install command', () => {
39
48
  });
40
49
  });
41
50
  it('creates command with correct structure', () => {
42
- const command = createInstallCommand({});
51
+ const command = createInstallCommand(mockConfig);
43
52
  expect(command).toBeInstanceOf(Command);
44
53
  expect(command.name()).toBe('install');
45
54
  });
@@ -54,7 +63,7 @@ describe('install command', () => {
54
63
  mockGetInstallableServices.mockReturnValue({
55
64
  'ark-api': mockService,
56
65
  });
57
- const command = createInstallCommand({});
66
+ const command = createInstallCommand(mockConfig);
58
67
  await command.parseAsync(['node', 'test', 'ark-api']);
59
68
  expect(mockExeca).toHaveBeenCalledWith('helm', [
60
69
  'upgrade',
@@ -73,7 +82,7 @@ describe('install command', () => {
73
82
  'ark-api': { name: 'ark-api' },
74
83
  'ark-controller': { name: 'ark-controller' },
75
84
  });
76
- const command = createInstallCommand({});
85
+ const command = createInstallCommand(mockConfig);
77
86
  await expect(command.parseAsync(['node', 'test', 'invalid-service'])).rejects.toThrow('process.exit called');
78
87
  expect(mockOutput.error).toHaveBeenCalledWith("service 'invalid-service' not found");
79
88
  expect(mockOutput.info).toHaveBeenCalledWith('available services:');
@@ -92,7 +101,7 @@ describe('install command', () => {
92
101
  mockGetInstallableServices.mockReturnValue({
93
102
  'ark-dashboard': mockService,
94
103
  });
95
- const command = createInstallCommand({});
104
+ const command = createInstallCommand(mockConfig);
96
105
  await command.parseAsync(['node', 'test', 'ark-dashboard']);
97
106
  // Should NOT include --namespace flag
98
107
  expect(mockExeca).toHaveBeenCalledWith('helm', [
@@ -114,7 +123,7 @@ describe('install command', () => {
114
123
  mockGetInstallableServices.mockReturnValue({
115
124
  'simple-service': mockService,
116
125
  });
117
- const command = createInstallCommand({});
126
+ const command = createInstallCommand(mockConfig);
118
127
  await command.parseAsync(['node', 'test', 'simple-service']);
119
128
  expect(mockExeca).toHaveBeenCalledWith('helm', [
120
129
  'upgrade',
@@ -129,7 +138,6 @@ describe('install command', () => {
129
138
  mockGetClusterInfo.mockResolvedValue({ error: true });
130
139
  const command = createInstallCommand({});
131
140
  await expect(command.parseAsync(['node', 'test', 'ark-api'])).rejects.toThrow('process.exit called');
132
- expect(mockOutput.error).toHaveBeenCalledWith('no kubernetes cluster detected');
133
141
  expect(mockExit).toHaveBeenCalledWith(1);
134
142
  });
135
143
  });
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { ArkConfig } from '../../lib/config.js';
3
+ export declare function createQueryCommand(_: ArkConfig): Command;
@@ -0,0 +1,131 @@
1
+ import { Command } from 'commander';
2
+ import { execa } from 'execa';
3
+ import ora from 'ora';
4
+ import output from '../../lib/output.js';
5
+ async function runQuery(target, message) {
6
+ const spinner = ora('Creating query...').start();
7
+ // Generate a unique query name
8
+ const timestamp = Date.now();
9
+ const queryName = `cli-query-${timestamp}`;
10
+ // Parse the target format (e.g., model/default -> type: model, name: default)
11
+ const [targetType, targetName] = target.split('/');
12
+ // Create the Query resource
13
+ const queryManifest = {
14
+ apiVersion: 'ark.mckinsey.com/v1alpha1',
15
+ kind: 'Query',
16
+ metadata: {
17
+ name: queryName,
18
+ },
19
+ spec: {
20
+ input: message,
21
+ targets: [
22
+ {
23
+ type: targetType,
24
+ name: targetName,
25
+ },
26
+ ],
27
+ },
28
+ };
29
+ try {
30
+ // Apply the query
31
+ spinner.text = 'Submitting query...';
32
+ await execa('kubectl', ['apply', '-f', '-'], {
33
+ input: JSON.stringify(queryManifest),
34
+ stdio: ['pipe', 'pipe', 'pipe'],
35
+ });
36
+ // Watch for query completion
37
+ spinner.text = 'Query status: initializing';
38
+ let queryComplete = false;
39
+ let attempts = 0;
40
+ const maxAttempts = 300; // 5 minutes with 1 second intervals
41
+ while (!queryComplete && attempts < maxAttempts) {
42
+ attempts++;
43
+ try {
44
+ const { stdout } = await execa('kubectl', [
45
+ 'get',
46
+ 'query',
47
+ queryName,
48
+ '-o',
49
+ 'json',
50
+ ], { stdio: 'pipe' });
51
+ const query = JSON.parse(stdout);
52
+ const phase = query.status?.phase;
53
+ // Update spinner with current phase
54
+ if (phase) {
55
+ spinner.text = `Query status: ${phase}`;
56
+ }
57
+ // Check if query is complete based on phase
58
+ if (phase === 'done') {
59
+ queryComplete = true;
60
+ spinner.succeed('Query completed');
61
+ // Extract and display the response from responses array
62
+ if (query.status?.responses && query.status.responses.length > 0) {
63
+ const response = query.status.responses[0];
64
+ console.log('\n' + (response.content || response));
65
+ }
66
+ else {
67
+ output.warning('No response received');
68
+ }
69
+ }
70
+ else if (phase === 'error') {
71
+ queryComplete = true;
72
+ spinner.fail('Query failed');
73
+ // Try to get error message from conditions or status
74
+ const errorCondition = query.status?.conditions?.find((c) => {
75
+ const condition = c;
76
+ return condition.type === 'Complete' && condition.status === 'False';
77
+ });
78
+ if (errorCondition?.message) {
79
+ output.error(errorCondition.message);
80
+ }
81
+ else if (query.status?.error) {
82
+ output.error(query.status.error);
83
+ }
84
+ else {
85
+ output.error('Query failed with unknown error');
86
+ }
87
+ }
88
+ else if (phase === 'canceled') {
89
+ queryComplete = true;
90
+ spinner.warn('Query canceled');
91
+ // Try to get cancellation reason if available
92
+ if (query.status?.message) {
93
+ output.warning(query.status.message);
94
+ }
95
+ }
96
+ }
97
+ catch {
98
+ // Query might not exist yet, continue waiting
99
+ spinner.text = 'Running query: waiting for query to be created';
100
+ }
101
+ if (!queryComplete) {
102
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
103
+ }
104
+ }
105
+ if (!queryComplete) {
106
+ spinner.fail('Query timed out');
107
+ output.error('Query did not complete within 5 minutes');
108
+ }
109
+ }
110
+ catch (error) {
111
+ spinner.fail('Query failed');
112
+ output.error(error instanceof Error ? error.message : 'Unknown error');
113
+ process.exit(1);
114
+ }
115
+ }
116
+ export function createQueryCommand(_) {
117
+ const queryCommand = new Command('query');
118
+ queryCommand
119
+ .description('Execute a single query against a model or agent')
120
+ .argument('<target>', 'Query target (e.g., model/default, agent/my-agent)')
121
+ .argument('<message>', 'Message to send')
122
+ .action(async (target, message) => {
123
+ // Validate target format
124
+ if (!target.includes('/')) {
125
+ output.error('Invalid target format. Use: model/name or agent/name etc');
126
+ process.exit(1);
127
+ }
128
+ await runQuery(target, message);
129
+ });
130
+ return queryCommand;
131
+ }
@@ -158,9 +158,6 @@ function buildStatusSections(data, config) {
158
158
  statusColor: statusInfo.color,
159
159
  name: displayName,
160
160
  details: details,
161
- subtext: controller.status === 'healthy' && !data.defaultModelExists
162
- ? '(no default model configured)'
163
- : undefined,
164
161
  });
165
162
  // Add version update status as separate line
166
163
  if (controller.status === 'healthy' && controller.version && config) {
@@ -199,6 +196,39 @@ function buildStatusSections(data, config) {
199
196
  }
200
197
  }
201
198
  }
199
+ // Add default model status
200
+ if (data.defaultModel) {
201
+ if (!data.defaultModel.exists) {
202
+ arkStatusLines.push({
203
+ icon: '○',
204
+ iconColor: 'yellow',
205
+ status: 'default model',
206
+ statusColor: 'yellow',
207
+ name: '',
208
+ details: 'not configured',
209
+ });
210
+ }
211
+ else if (data.defaultModel.available) {
212
+ arkStatusLines.push({
213
+ icon: '●',
214
+ iconColor: 'green',
215
+ status: 'default model',
216
+ statusColor: 'green',
217
+ name: '',
218
+ details: data.defaultModel.provider || 'configured',
219
+ });
220
+ }
221
+ else {
222
+ arkStatusLines.push({
223
+ icon: '●',
224
+ iconColor: 'yellow',
225
+ status: 'default model',
226
+ statusColor: 'yellow',
227
+ name: '',
228
+ details: 'not available',
229
+ });
230
+ }
231
+ }
202
232
  }
203
233
  }
204
234
  sections.push({ title: 'ark status:', lines: arkStatusLines });
@@ -1,3 +1,3 @@
1
1
  import { Command } from 'commander';
2
2
  import type { ArkConfig } from '../../lib/config.js';
3
- export declare function createUninstallCommand(_: ArkConfig): Command;
3
+ export declare function createUninstallCommand(config: ArkConfig): Command;
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { execute } from '../../lib/commands.js';
4
4
  import inquirer from 'inquirer';
5
- import { getClusterInfo } from '../../lib/cluster.js';
5
+ import { showNoClusterError } from '../../lib/startup.js';
6
6
  import output from '../../lib/output.js';
7
7
  import { getInstallableServices } from '../../arkServices.js';
8
8
  async function uninstallService(service, verbose = false) {
@@ -13,14 +13,13 @@ async function uninstallService(service, verbose = false) {
13
13
  }
14
14
  await execute('helm', helmArgs, { stdio: 'inherit' }, { verbose });
15
15
  }
16
- async function uninstallArk(serviceName, options = {}) {
17
- // Check cluster connectivity
18
- const clusterInfo = await getClusterInfo();
19
- if (clusterInfo.error) {
20
- output.error('no kubernetes cluster detected');
21
- output.info('please ensure you have a running cluster and kubectl is configured.');
16
+ async function uninstallArk(config, serviceName, options = {}) {
17
+ // Check cluster connectivity from config
18
+ if (!config.clusterInfo) {
19
+ showNoClusterError();
22
20
  process.exit(1);
23
21
  }
22
+ const clusterInfo = config.clusterInfo;
24
23
  // Show cluster info
25
24
  output.success(`connected to cluster: ${chalk.bold(clusterInfo.context)}`);
26
25
  output.info(`type: ${clusterInfo.type}`);
@@ -93,7 +92,7 @@ async function uninstallArk(serviceName, options = {}) {
93
92
  }
94
93
  }
95
94
  }
96
- export function createUninstallCommand(_) {
95
+ export function createUninstallCommand(config) {
97
96
  const command = new Command('uninstall');
98
97
  command
99
98
  .description('Uninstall ARK components using Helm')
@@ -101,7 +100,7 @@ export function createUninstallCommand(_) {
101
100
  .option('-y, --yes', 'automatically confirm all uninstallations')
102
101
  .option('-v, --verbose', 'show commands being executed')
103
102
  .action(async (service, options) => {
104
- await uninstallArk(service, options);
103
+ await uninstallArk(config, service, options);
105
104
  });
106
105
  return command;
107
106
  }
@@ -9,8 +9,10 @@ jest.unstable_mockModule('../../lib/cluster.js', () => ({
9
9
  getClusterInfo: mockGetClusterInfo,
10
10
  }));
11
11
  const mockGetInstallableServices = jest.fn();
12
+ const mockArkServices = {};
12
13
  jest.unstable_mockModule('../../arkServices.js', () => ({
13
14
  getInstallableServices: mockGetInstallableServices,
15
+ arkServices: mockArkServices,
14
16
  }));
15
17
  const mockOutput = {
16
18
  error: jest.fn(),
@@ -28,6 +30,13 @@ jest.spyOn(console, 'log').mockImplementation(() => { });
28
30
  jest.spyOn(console, 'error').mockImplementation(() => { });
29
31
  const { createUninstallCommand } = await import('./index.js');
30
32
  describe('uninstall command', () => {
33
+ const mockConfig = {
34
+ clusterInfo: {
35
+ context: 'test-cluster',
36
+ type: 'minikube',
37
+ namespace: 'default',
38
+ },
39
+ };
31
40
  beforeEach(() => {
32
41
  jest.clearAllMocks();
33
42
  mockGetClusterInfo.mockResolvedValue({
@@ -37,7 +46,7 @@ describe('uninstall command', () => {
37
46
  });
38
47
  });
39
48
  it('creates command with correct structure', () => {
40
- const command = createUninstallCommand({});
49
+ const command = createUninstallCommand(mockConfig);
41
50
  expect(command).toBeInstanceOf(Command);
42
51
  expect(command.name()).toBe('uninstall');
43
52
  });
@@ -50,7 +59,7 @@ describe('uninstall command', () => {
50
59
  mockGetInstallableServices.mockReturnValue({
51
60
  'ark-api': mockService,
52
61
  });
53
- const command = createUninstallCommand({});
62
+ const command = createUninstallCommand(mockConfig);
54
63
  await command.parseAsync(['node', 'test', 'ark-api']);
55
64
  expect(mockExeca).toHaveBeenCalledWith('helm', [
56
65
  'uninstall',
@@ -68,7 +77,7 @@ describe('uninstall command', () => {
68
77
  'ark-api': { name: 'ark-api' },
69
78
  'ark-controller': { name: 'ark-controller' },
70
79
  });
71
- const command = createUninstallCommand({});
80
+ const command = createUninstallCommand(mockConfig);
72
81
  await expect(command.parseAsync(['node', 'test', 'invalid-service'])).rejects.toThrow('process.exit called');
73
82
  expect(mockOutput.error).toHaveBeenCalledWith("service 'invalid-service' not found");
74
83
  expect(mockOutput.info).toHaveBeenCalledWith('available services:');
@@ -85,7 +94,7 @@ describe('uninstall command', () => {
85
94
  mockGetInstallableServices.mockReturnValue({
86
95
  'ark-dashboard': mockService,
87
96
  });
88
- const command = createUninstallCommand({});
97
+ const command = createUninstallCommand(mockConfig);
89
98
  await command.parseAsync(['node', 'test', 'ark-dashboard']);
90
99
  // Should NOT include --namespace flag
91
100
  expect(mockExeca).toHaveBeenCalledWith('helm', ['uninstall', 'ark-dashboard', '--ignore-not-found'], {
@@ -102,7 +111,7 @@ describe('uninstall command', () => {
102
111
  'ark-api': mockService,
103
112
  });
104
113
  mockExeca.mockRejectedValue(new Error('helm failed'));
105
- const command = createUninstallCommand({});
114
+ const command = createUninstallCommand(mockConfig);
106
115
  await expect(command.parseAsync(['node', 'test', 'ark-api'])).rejects.toThrow('process.exit called');
107
116
  expect(mockOutput.error).toHaveBeenCalledWith('failed to uninstall ark-api');
108
117
  expect(mockExit).toHaveBeenCalledWith(1);
@@ -111,7 +120,6 @@ describe('uninstall command', () => {
111
120
  mockGetClusterInfo.mockResolvedValue({ error: true });
112
121
  const command = createUninstallCommand({});
113
122
  await expect(command.parseAsync(['node', 'test', 'ark-api'])).rejects.toThrow('process.exit called');
114
- expect(mockOutput.error).toHaveBeenCalledWith('no kubernetes cluster detected');
115
123
  expect(mockExit).toHaveBeenCalledWith(1);
116
124
  });
117
125
  });
@@ -372,16 +372,35 @@ export class StatusChecker {
372
372
  // Check if ARK is ready (controller is running)
373
373
  let arkReady = false;
374
374
  let defaultModelExists = false;
375
+ let defaultModel;
375
376
  if (clusterAccess) {
376
377
  arkReady = await isArkReady();
377
- // Check for default model
378
+ // Check for default model with detailed status
378
379
  if (arkReady) {
379
380
  try {
380
- await execa('kubectl', ['get', 'model', 'default', '-o', 'name']);
381
+ const { stdout } = await execa('kubectl', [
382
+ 'get',
383
+ 'model',
384
+ 'default',
385
+ '-o',
386
+ 'json',
387
+ ]);
388
+ const model = JSON.parse(stdout);
381
389
  defaultModelExists = true;
390
+ // Extract model details
391
+ const available = model.status?.conditions?.find((c) => c.type === 'Available')?.status === 'True';
392
+ defaultModel = {
393
+ exists: true,
394
+ available,
395
+ provider: model.spec?.provider,
396
+ details: model.spec?.model || model.spec?.apiEndpoint,
397
+ };
382
398
  }
383
399
  catch {
384
400
  defaultModelExists = false;
401
+ defaultModel = {
402
+ exists: false,
403
+ };
385
404
  }
386
405
  }
387
406
  }
@@ -392,6 +411,7 @@ export class StatusChecker {
392
411
  clusterInfo,
393
412
  arkReady,
394
413
  defaultModelExists,
414
+ defaultModel,
395
415
  };
396
416
  }
397
417
  }
package/dist/index.js CHANGED
@@ -12,10 +12,12 @@ import { createChatCommand } from './commands/chat/index.js';
12
12
  import { createClusterCommand } from './commands/cluster/index.js';
13
13
  import { createCompletionCommand } from './commands/completion/index.js';
14
14
  import { createDashboardCommand } from './commands/dashboard/index.js';
15
+ import { createDocsCommand } from './commands/docs/index.js';
15
16
  import { createDevCommand } from './commands/dev/index.js';
16
17
  import { createGenerateCommand } from './commands/generate/index.js';
17
18
  import { createInstallCommand } from './commands/install/index.js';
18
19
  import { createModelsCommand } from './commands/models/index.js';
20
+ import { createQueryCommand } from './commands/query/index.js';
19
21
  import { createUninstallCommand } from './commands/uninstall/index.js';
20
22
  import { createStatusCommand } from './commands/status/index.js';
21
23
  import { createConfigCommand } from './commands/config/index.js';
@@ -41,10 +43,12 @@ async function main() {
41
43
  program.addCommand(createClusterCommand(config));
42
44
  program.addCommand(createCompletionCommand(config));
43
45
  program.addCommand(createDashboardCommand(config));
46
+ program.addCommand(createDocsCommand(config));
44
47
  program.addCommand(createDevCommand(config));
45
48
  program.addCommand(createGenerateCommand(config));
46
49
  program.addCommand(createInstallCommand(config));
47
50
  program.addCommand(createModelsCommand(config));
51
+ program.addCommand(createQueryCommand(config));
48
52
  program.addCommand(createUninstallCommand(config));
49
53
  program.addCommand(createStatusCommand(config));
50
54
  program.addCommand(createConfigCommand(config));
@@ -1,3 +1,4 @@
1
+ import type { ClusterInfo } from './cluster.js';
1
2
  export interface ChatConfig {
2
3
  streaming?: boolean;
3
4
  outputFormat?: 'text' | 'markdown';
@@ -6,6 +7,7 @@ export interface ArkConfig {
6
7
  chat?: ChatConfig;
7
8
  latestVersion?: string;
8
9
  currentVersion?: string;
10
+ clusterInfo?: ClusterInfo;
9
11
  }
10
12
  /**
11
13
  * Load configuration from multiple sources with proper precedence:
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Print helpful next steps after successful ARK installation
3
+ */
4
+ export declare function printNextSteps(): void;
@@ -0,0 +1,20 @@
1
+ import chalk from 'chalk';
2
+ /**
3
+ * Print helpful next steps after successful ARK installation
4
+ */
5
+ export function printNextSteps() {
6
+ console.log();
7
+ console.log(chalk.green.bold('✓ ARK installed successfully!'));
8
+ console.log();
9
+ console.log(chalk.gray('Next steps:'));
10
+ console.log();
11
+ console.log(` ${chalk.gray('docs:')} ${chalk.blue('https://mckinsey.github.io/agents-at-scale-ark/')}`);
12
+ console.log(` ${chalk.gray('create model:')} ${chalk.white.bold('ark models create default')}`);
13
+ console.log(` ${chalk.gray('open dashboard:')} ${chalk.white.bold('ark dashboard')}`);
14
+ console.log(` ${chalk.gray('show agents:')} ${chalk.white.bold('kubectl get agents')}`);
15
+ console.log(` ${chalk.gray('run a query:')} ${chalk.white.bold('ark query model/default "What are large language models?"')}`);
16
+ console.log(` ${chalk.gray('interactive chat:')} ${chalk.white.bold('ark')} ${chalk.gray('# then choose \'Chat\'')}`);
17
+ console.log(` ${chalk.gray('new project:')} ${chalk.white.bold('ark generate project my-agents')}`);
18
+ console.log(` ${chalk.gray('install fark:')} ${chalk.blue('https://mckinsey.github.io/agents-at-scale-ark/developer-guide/cli-tools/')}`);
19
+ console.log();
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ import { jest } from '@jest/globals';
2
+ import { printNextSteps } from './nextSteps.js';
3
+ describe('printNextSteps', () => {
4
+ let consoleLogSpy;
5
+ let output = [];
6
+ beforeEach(() => {
7
+ output = [];
8
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation((...args) => {
9
+ output.push(args.join(' '));
10
+ });
11
+ });
12
+ afterEach(() => {
13
+ consoleLogSpy.mockRestore();
14
+ });
15
+ it('prints successful installation message', () => {
16
+ printNextSteps();
17
+ const fullOutput = output.join('\n');
18
+ expect(fullOutput).toContain('ARK installed successfully!');
19
+ });
20
+ it('includes all required commands', () => {
21
+ printNextSteps();
22
+ const fullOutput = output.join('\n');
23
+ // Check for each command
24
+ expect(fullOutput).toContain('ark models create default');
25
+ expect(fullOutput).toContain('ark dashboard');
26
+ expect(fullOutput).toContain('kubectl get agents');
27
+ expect(fullOutput).toContain('ark query model/default "What are large language models?"');
28
+ expect(fullOutput).toContain('ark');
29
+ expect(fullOutput).toContain("# then choose 'Chat'");
30
+ expect(fullOutput).toContain('ark generate project my-agents');
31
+ });
32
+ it('includes all required links', () => {
33
+ printNextSteps();
34
+ const fullOutput = output.join('\n');
35
+ // Check for documentation links
36
+ expect(fullOutput).toContain('https://mckinsey.github.io/agents-at-scale-ark/');
37
+ expect(fullOutput).toContain('https://mckinsey.github.io/agents-at-scale-ark/developer-guide/cli-tools/');
38
+ });
39
+ it('includes all section labels', () => {
40
+ printNextSteps();
41
+ const fullOutput = output.join('\n');
42
+ // Check for labels
43
+ expect(fullOutput).toContain('Next steps:');
44
+ expect(fullOutput).toContain('docs:');
45
+ expect(fullOutput).toContain('create model:');
46
+ expect(fullOutput).toContain('open dashboard:');
47
+ expect(fullOutput).toContain('show agents:');
48
+ expect(fullOutput).toContain('run a query:');
49
+ expect(fullOutput).toContain('interactive chat:');
50
+ expect(fullOutput).toContain('new project:');
51
+ expect(fullOutput).toContain('install fark:');
52
+ });
53
+ it('has correct structure with empty lines', () => {
54
+ printNextSteps();
55
+ // Should have empty lines for formatting
56
+ expect(output[0]).toBe('');
57
+ expect(output[output.length - 1]).toBe('');
58
+ });
59
+ });
@@ -1,4 +1,8 @@
1
1
  import type { ArkConfig } from './config.js';
2
+ /**
3
+ * Show error message when no cluster is detected
4
+ */
5
+ export declare function showNoClusterError(): void;
2
6
  /**
3
7
  * Initialize the CLI by checking requirements and loading config
4
8
  */
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import { checkCommandExists } from './commands.js';
3
3
  import { loadConfig } from './config.js';
4
4
  import { getArkVersion } from './arkStatus.js';
5
+ import { getClusterInfo } from './cluster.js';
5
6
  const REQUIRED_COMMANDS = [
6
7
  {
7
8
  name: 'kubectl',
@@ -32,6 +33,20 @@ async function checkRequirements() {
32
33
  process.exit(1);
33
34
  }
34
35
  }
36
+ /**
37
+ * Show error message when no cluster is detected
38
+ */
39
+ export function showNoClusterError() {
40
+ console.log(chalk.red.bold('\n✗ No Kubernetes cluster detected\n'));
41
+ console.log('Please ensure you have configured a connection to a Kubernetes cluster.');
42
+ console.log('For local development, you can use:');
43
+ console.log(` • Minikube: ${chalk.blue('https://minikube.sigs.k8s.io/docs/start')}`);
44
+ console.log(` • Docker Desktop: ${chalk.blue('https://docs.docker.com/desktop/kubernetes/')}`);
45
+ console.log(` • Kind: ${chalk.blue('https://kind.sigs.k8s.io/docs/user/quick-start/')}`);
46
+ console.log('');
47
+ console.log('And more. For help, check the Quickstart guide:');
48
+ console.log(chalk.blue(' https://mckinsey.github.io/agents-at-scale-ark/quickstart/'));
49
+ }
35
50
  /**
36
51
  * Fetch version information (non-blocking)
37
52
  */
@@ -67,6 +82,11 @@ export async function startup() {
67
82
  await checkRequirements();
68
83
  // Load config
69
84
  const config = loadConfig();
85
+ // Get cluster info - if no error, we have cluster access
86
+ const clusterInfo = await getClusterInfo();
87
+ if (!clusterInfo.error) {
88
+ config.clusterInfo = clusterInfo;
89
+ }
70
90
  // Fetch version info synchronously so it's available immediately
71
91
  await fetchVersionInfo(config);
72
92
  return config;
@@ -135,7 +135,7 @@ describe('startup', () => {
135
135
  expect(globalThis.fetch).toHaveBeenCalledWith('https://api.github.com/repos/mckinsey/agents-at-scale-ark/releases/latest');
136
136
  // Wait for async fetch to complete
137
137
  await new Promise((resolve) => setTimeout(resolve, 50));
138
- expect(config.latestVersion).toBe('v0.1.35');
138
+ expect(config.latestVersion).toBe('0.1.35');
139
139
  });
140
140
  it('handles GitHub API failure gracefully', async () => {
141
141
  globalThis.fetch.mockRejectedValue(new Error('Network error'));
@@ -31,11 +31,18 @@ export interface DependencyStatus {
31
31
  version?: string;
32
32
  details?: string;
33
33
  }
34
+ export interface ModelStatus {
35
+ exists: boolean;
36
+ available?: boolean;
37
+ provider?: string;
38
+ details?: string;
39
+ }
34
40
  export interface StatusData {
35
41
  services: ServiceStatus[];
36
42
  dependencies: DependencyStatus[];
37
43
  arkReady?: boolean;
38
44
  defaultModelExists?: boolean;
45
+ defaultModel?: ModelStatus;
39
46
  }
40
47
  export interface CommandVersionConfig {
41
48
  command: string;
@@ -239,7 +239,7 @@ const MainMenu = ({ config }) => {
239
239
  ║ Agents at Scale Platform ║
240
240
  ║ ║
241
241
  ╚═══════════════════════════════════════╝
242
- ` }), isChecking ? (_jsxs(Text, { color: "gray", children: [_jsx(Spinner, { type: "dots" }), " Checking Ark status..."] })) : arkReady ? (_jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: "\u25CF Ark is ready" }), config.currentVersion && (_jsxs(Text, { color: "gray", children: [" (", config.currentVersion, ")"] }))] })) : (_jsx(Text, { color: "yellow", bold: true, children: "\u25CF Ark is not installed" })), _jsx(Text, { color: "gray", children: "Interactive terminal interface for Ark agents" })] }), !isChecking && (_jsx(Box, { flexDirection: "column", paddingX: 4, marginTop: 1, children: choices.map((choice, index) => {
242
+ ` }), isChecking ? (_jsxs(Text, { color: "gray", children: [_jsx(Spinner, { type: "dots" }), " Checking Ark status..."] })) : arkReady ? (_jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: "\u25CF Ark is ready" }), config.currentVersion && (_jsxs(Text, { color: "gray", children: [" (", config.currentVersion, ")"] }))] })) : (_jsx(Text, { color: "yellow", bold: true, children: "\u25CF Ark is not installed" })), config.clusterInfo?.context ? (_jsxs(Text, { children: [_jsx(Text, { color: "gray", children: "Current context: " }), _jsx(Text, { color: "white", bold: true, children: config.clusterInfo.context })] })) : (_jsx(Text, { color: "gray", children: "No Kubernetes context configured" }))] }), !isChecking && (_jsx(Box, { flexDirection: "column", paddingX: 4, marginTop: 1, children: choices.map((choice, index) => {
243
243
  const isSelected = index === selectedIndex;
244
244
  return (_jsxs(Box, { flexDirection: "row", paddingY: 0, children: [_jsx(Text, { color: "gray", dimColor: true, children: isSelected ? '❯ ' : ' ' }), _jsxs(Text, { color: "gray", dimColor: true, children: [index + 1, "."] }), _jsx(Box, { marginLeft: 1, width: 20, children: _jsx(Text, { color: isSelected ? 'green' : 'white', bold: isSelected, children: choice.label }) }), _jsx(Text, { color: "gray", children: choice.description })] }, choice.value));
245
245
  }) }))] }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-at-scale/ark",
3
- "version": "0.1.35-rc1",
3
+ "version": "0.1.35-rc2",
4
4
  "description": "ARK CLI - Interactive terminal interface for ARK agents",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",