@agents-at-scale/ark 0.1.64 → 0.1.67

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.
@@ -5,7 +5,7 @@ import { StatusChecker } from '../../components/statusChecker.js';
5
5
  import { StatusFormatter, } from '../../ui/statusFormatter.js';
6
6
  import { fetchVersionInfo } from '../../lib/versions.js';
7
7
  import { waitForServicesReady, } from '../../lib/waitForReady.js';
8
- import { runReadinessChecks, detectStorageBackend, } from '../../lib/readinessChecks.js';
8
+ import { runReadinessChecks, describeStorageBackend, } from '../../lib/readinessChecks.js';
9
9
  import { arkServices } from '../../arkServices.js';
10
10
  import output from '../../lib/output.js';
11
11
  import { parseTimeoutToSeconds } from '../../lib/timeout.js';
@@ -49,7 +49,26 @@ function enrichServiceDetails(service) {
49
49
  details: details.join(', '),
50
50
  };
51
51
  }
52
- function buildStatusSections(data, versionInfo) {
52
+ function backendStatusLine(detection) {
53
+ const display = {
54
+ etcd: { color: 'green', details: 'etcd' },
55
+ postgresql: { color: 'green', details: 'postgresql' },
56
+ 'not-installed': { color: 'yellow', details: 'not installed' },
57
+ unreachable: { color: 'yellow', details: 'cluster unreachable' },
58
+ forbidden: { color: 'yellow', details: 'access denied' },
59
+ undetermined: { color: 'yellow', details: 'undetermined' },
60
+ };
61
+ const { color, details } = display[detection.status];
62
+ return {
63
+ icon: '●',
64
+ iconColor: color,
65
+ status: 'storage backend',
66
+ statusColor: color,
67
+ name: '',
68
+ details,
69
+ };
70
+ }
71
+ function buildStatusSections(data, versionInfo, backend) {
53
72
  const sections = [];
54
73
  // Dependencies section
55
74
  sections.push({
@@ -251,6 +270,9 @@ function buildStatusSections(data, versionInfo) {
251
270
  }
252
271
  }
253
272
  }
273
+ if (backend) {
274
+ arkStatusLines.push(backendStatusLine(backend));
275
+ }
254
276
  sections.push({ title: 'ark status:', lines: arkStatusLines });
255
277
  return sections;
256
278
  }
@@ -266,12 +288,24 @@ export async function checkStatus(serviceNames, options) {
266
288
  statusChecker.checkAll(),
267
289
  fetchVersionInfo(),
268
290
  ]);
291
+ // Only probe for the storage backend if the cluster is reachable; probing an
292
+ // unreachable cluster would just retry to its timeout.
293
+ const detection = statusData.clusterAccess
294
+ ? await describeStorageBackend()
295
+ : {
296
+ backend: 'unknown',
297
+ status: 'unreachable',
298
+ message: 'Cluster is not reachable — cannot determine the storage backend.',
299
+ };
269
300
  spinner.stop();
270
- const sections = buildStatusSections(statusData, versionInfo);
301
+ const sections = buildStatusSections(statusData, versionInfo, detection);
271
302
  StatusFormatter.printSections(sections);
272
303
  if (options?.waitForReady) {
273
304
  const timeoutSeconds = parseTimeoutToSeconds(options.waitForReady);
274
- const backend = await detectStorageBackend();
305
+ const backend = detection.backend;
306
+ if (backend === 'unknown') {
307
+ output.warning(`${detection.message} Skipping backend-specific readiness checks.`);
308
+ }
275
309
  let servicesToWait = [];
276
310
  if (serviceNames && serviceNames.length > 0) {
277
311
  servicesToWait = serviceNames
@@ -316,7 +350,7 @@ export async function checkStatus(serviceNames, options) {
316
350
  waitSpinner.succeed('All services are ready');
317
351
  const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
318
352
  const remainingSeconds = Math.max(1, timeoutSeconds - elapsedSeconds);
319
- const deepResults = await runReadinessChecks(remainingSeconds, (r) => {
353
+ const deepResults = await runReadinessChecks(remainingSeconds, backend, (r) => {
320
354
  const icon = r.passed ? chalk.green('✓') : chalk.red('✗');
321
355
  const dur = `${(r.durationMs / 1000).toFixed(1)}s`;
322
356
  const suffix = r.message ? ` — ${r.message}` : '';
@@ -58,6 +58,10 @@ export declare class ArkApiClient {
58
58
  sessionId?: string;
59
59
  conversationId?: string;
60
60
  timeout?: string;
61
+ parameters?: Array<{
62
+ name: string;
63
+ value?: string;
64
+ }>;
61
65
  metadata?: {
62
66
  annotations?: Record<string, string>;
63
67
  };
@@ -159,6 +159,9 @@ export class ArkApiClient {
159
159
  sessionId: params.sessionId,
160
160
  conversationId: params.conversationId,
161
161
  timeout: params.timeout,
162
+ ...(params.parameters && params.parameters.length > 0
163
+ ? { parameters: params.parameters }
164
+ : {}),
162
165
  ...(params.metadata ? { metadata: params.metadata } : {}),
163
166
  }),
164
167
  });
@@ -1,4 +1,5 @@
1
1
  import { ArkApiClient, QueryTarget } from './arkApiClient.js';
2
+ import type { QueryParameter } from './types.js';
2
3
  export { QueryTarget };
3
4
  export interface ChatConfig {
4
5
  streamingEnabled: boolean;
@@ -7,6 +8,7 @@ export interface ChatConfig {
7
8
  conversationId?: string;
8
9
  queryTimeout?: string;
9
10
  a2aContextId?: string;
11
+ parameters?: QueryParameter[];
10
12
  }
11
13
  export interface ToolCall {
12
14
  id: string;
@@ -1,4 +1,19 @@
1
1
  import { QUERY_ANNOTATIONS } from './constants.js';
2
+ /** Best-effort human-readable error from a failed query's status. */
3
+ function extractQueryError(status) {
4
+ if (!status)
5
+ return 'Query failed';
6
+ if (status.error)
7
+ return status.error;
8
+ if (status.message)
9
+ return status.message;
10
+ const condition = status.conditions?.find((c) => c.message);
11
+ if (condition?.message)
12
+ return condition.message.trim();
13
+ if (status.response?.content)
14
+ return status.response.content;
15
+ return 'Query failed';
16
+ }
2
17
  export class ChatClient {
3
18
  arkApiClient;
4
19
  constructor(arkApiClient) {
@@ -26,6 +41,7 @@ export class ChatClient {
26
41
  sessionId: config.sessionId,
27
42
  conversationId: config.conversationId,
28
43
  timeout: config.queryTimeout,
44
+ parameters: config.parameters,
29
45
  ...(Object.keys(annotations).length > 0
30
46
  ? { metadata: { annotations } }
31
47
  : {}),
@@ -72,7 +88,7 @@ export class ChatClient {
72
88
  onChunk(content, undefined, undefined);
73
89
  }
74
90
  if (phase === 'error') {
75
- throw new Error(content || 'Query failed');
91
+ throw new Error(extractQueryError(query.status));
76
92
  }
77
93
  return content;
78
94
  }
@@ -155,6 +171,14 @@ export class ChatClient {
155
171
  finally {
156
172
  reader.releaseLock();
157
173
  }
174
+ // Empty stream (no content, no tool calls) can mean the query errored — surface it.
175
+ if (!fullResponse && toolCallsById.size === 0 && !signal?.aborted) {
176
+ const query = (await this.arkApiClient.getQuery(queryName));
177
+ const phase = query?.status?.phase;
178
+ if (phase === 'error' || phase === 'canceled') {
179
+ throw new Error(extractQueryError(query?.status));
180
+ }
181
+ }
158
182
  return fullResponse;
159
183
  }
160
184
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared query execution logic for both universal and resource-specific query commands
3
3
  */
4
- import type { QueryTarget } from './types.js';
4
+ import type { QueryTarget, QueryParameter } from './types.js';
5
5
  export interface QueryOptions {
6
6
  targetType: string;
7
7
  targetName: string;
@@ -12,6 +12,7 @@ export interface QueryOptions {
12
12
  outputFormat?: string;
13
13
  sessionId?: string;
14
14
  conversationId?: string;
15
+ parameters?: QueryParameter[];
15
16
  }
16
17
  export declare function executeQuery(options: QueryOptions): Promise<void>;
17
18
  /**
@@ -19,3 +20,5 @@ export declare function executeQuery(options: QueryOptions): Promise<void>;
19
20
  * Returns QueryTarget or null if invalid
20
21
  */
21
22
  export declare function parseTarget(target: string): QueryTarget | null;
23
+ /** Parse repeated `name=value` flags into QueryParameters (mirrors the fark CLI). */
24
+ export declare function parseParameters(parameters: string[]): QueryParameter[];
@@ -37,6 +37,7 @@ export async function executeQuery(options) {
37
37
  sessionId,
38
38
  conversationId,
39
39
  queryTimeout: options.timeout,
40
+ parameters: options.parameters,
40
41
  }, (chunk, toolCalls, arkMetadata) => {
41
42
  if (firstOutput) {
42
43
  spinner.stop();
@@ -120,6 +121,9 @@ async function executeQueryWithFormat(options) {
120
121
  type: options.targetType,
121
122
  name: options.targetName,
122
123
  },
124
+ ...(options.parameters && options.parameters.length > 0
125
+ ? { parameters: options.parameters }
126
+ : {}),
123
127
  },
124
128
  };
125
129
  try {
@@ -133,6 +137,10 @@ async function executeQueryWithFormat(options) {
133
137
  await watchEventsLive(queryName);
134
138
  return;
135
139
  }
140
+ if (options.outputFormat === 'events-pretty') {
141
+ await watchEventsLive(queryName, true);
142
+ return;
143
+ }
136
144
  const timeoutSeconds = 300;
137
145
  await execa('kubectl', [
138
146
  'wait',
@@ -149,7 +157,7 @@ async function executeQueryWithFormat(options) {
149
157
  console.log(stdout);
150
158
  }
151
159
  else {
152
- console.error(chalk.red(`Invalid output format: ${options.outputFormat}. Use: yaml, json, name, or events`));
160
+ console.error(chalk.red(`Invalid output format: ${options.outputFormat}. Use: yaml, json, name, events, or events-pretty`));
153
161
  process.exit(ExitCodes.CliError);
154
162
  }
155
163
  }
@@ -172,3 +180,18 @@ export function parseTarget(target) {
172
180
  name: parts[1],
173
181
  };
174
182
  }
183
+ /** Parse repeated `name=value` flags into QueryParameters (mirrors the fark CLI). */
184
+ export function parseParameters(parameters) {
185
+ return parameters.map((param) => {
186
+ const idx = param.indexOf('=');
187
+ if (idx === -1) {
188
+ throw new Error(`parameter must be in name=value format, got: ${param}`);
189
+ }
190
+ const name = param.slice(0, idx).trim();
191
+ const value = param.slice(idx + 1).trim();
192
+ if (name === '') {
193
+ throw new Error(`parameter name cannot be empty in: ${param}`);
194
+ }
195
+ return { name, value };
196
+ });
197
+ }
@@ -0,0 +1,29 @@
1
+ export interface K8sEvent {
2
+ type?: string;
3
+ reason?: string;
4
+ eventTime?: string;
5
+ lastTimestamp?: string;
6
+ firstTimestamp?: string;
7
+ metadata?: {
8
+ uid?: string;
9
+ creationTimestamp?: string;
10
+ annotations?: Record<string, string>;
11
+ };
12
+ }
13
+ export declare function pad(value: number, width: number): string;
14
+ /**
15
+ * Derive an HH:MM:SS.mmm timestamp, preferring the event-data payload timestamp
16
+ * and falling back through the native Event time fields to wall-clock now.
17
+ */
18
+ export declare function formatEventTimestamp(event: K8sEvent): string;
19
+ /**
20
+ * Render the structured event-data payload as `message > key=value ...`.
21
+ * The message is clipped and colored red for Warning/Error events; trailing
22
+ * fields keep their full value. Non-object payloads are echoed as-is.
23
+ */
24
+ export declare function formatEventData(eventData: string, eventType?: string): string;
25
+ /**
26
+ * Format a Kubernetes Event carrying an ark event-data annotation into a
27
+ * single human-readable line. Returns null when the event has no payload.
28
+ */
29
+ export declare function formatEvent(event: K8sEvent): string | null;
@@ -0,0 +1,138 @@
1
+ import chalk from 'chalk';
2
+ import { EVENT_ANNOTATIONS } from './constants.js';
3
+ /**
4
+ * Shape of the JSON carried in the `ark.mckinsey.com/event-data` annotation:
5
+ *
6
+ * {
7
+ * "durationMs": "7084.30",
8
+ * "input": "ok?",
9
+ * "message": "Query execution completed",
10
+ * "queryId": "0b61250b-...",
11
+ * "queryName": "cli-query-1784110137509",
12
+ * "queryNamespace": "ark-system",
13
+ * "sessionId": "0b61250b-...",
14
+ * "targetType": "model",
15
+ * "timestamp": "2026-07-15T10:09:04.602422162Z"
16
+ * }
17
+ */
18
+ const NUMERIC = /^-?\d+(\.\d+)?$/;
19
+ const MAX_MESSAGE_LENGTH = 120;
20
+ // Fields rendered first (in this order); everything else is sorted alphabetically.
21
+ const FIELD_PRIORITY = { input: 0, durationMs: 1 };
22
+ // Keys pulled out of the trailing field list because they are rendered elsewhere.
23
+ const RESERVED_KEYS = new Set(['message', 'timestamp']);
24
+ export function pad(value, width) {
25
+ return value.toString().padStart(width, '0');
26
+ }
27
+ function parseEventData(raw) {
28
+ if (!raw) {
29
+ return null;
30
+ }
31
+ try {
32
+ const parsed = JSON.parse(raw);
33
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
34
+ ? parsed
35
+ : null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ function firstValidDate(candidates) {
42
+ for (const candidate of candidates) {
43
+ if (!candidate) {
44
+ continue;
45
+ }
46
+ const date = new Date(candidate);
47
+ if (!Number.isNaN(date.getTime())) {
48
+ return date;
49
+ }
50
+ }
51
+ return new Date();
52
+ }
53
+ function truncate(value, max = MAX_MESSAGE_LENGTH) {
54
+ return value.length > max ? `${value.slice(0, max)}…` : value;
55
+ }
56
+ /**
57
+ * Derive an HH:MM:SS.mmm timestamp, preferring the event-data payload timestamp
58
+ * and falling back through the native Event time fields to wall-clock now.
59
+ */
60
+ export function formatEventTimestamp(event) {
61
+ const data = parseEventData(event.metadata?.annotations?.[EVENT_ANNOTATIONS.EVENT_DATA]);
62
+ const dataTimestamp = typeof data?.timestamp === 'string' ? data.timestamp : undefined;
63
+ const date = firstValidDate([
64
+ dataTimestamp,
65
+ event.eventTime,
66
+ event.lastTimestamp,
67
+ event.firstTimestamp,
68
+ event.metadata?.creationTimestamp,
69
+ ]);
70
+ return `${pad(date.getHours(), 2)}:${pad(date.getMinutes(), 2)}:${pad(date.getSeconds(), 2)}.${pad(date.getMilliseconds(), 3)}`;
71
+ }
72
+ function colorReason(reason, eventType) {
73
+ if (eventType === 'Warning') {
74
+ return chalk.yellow(reason);
75
+ }
76
+ if (eventType === 'Normal') {
77
+ return chalk.green(reason);
78
+ }
79
+ return chalk.red(reason);
80
+ }
81
+ function isErrorType(eventType) {
82
+ return eventType === 'Warning' || eventType === 'Error';
83
+ }
84
+ function compareKeys(a, b) {
85
+ const rank = (key) => FIELD_PRIORITY[key] ?? Number.MAX_SAFE_INTEGER;
86
+ return rank(a) - rank(b) || a.localeCompare(b);
87
+ }
88
+ function renderFieldValue(key, value) {
89
+ if (typeof value === 'number') {
90
+ return chalk.green(String(value));
91
+ }
92
+ if (value !== null && typeof value === 'object') {
93
+ return chalk.white(JSON.stringify(value));
94
+ }
95
+ const str = String(value);
96
+ if (NUMERIC.test(str)) {
97
+ return chalk.green(str);
98
+ }
99
+ return chalk.white(key === 'input' ? `"${str}"` : str);
100
+ }
101
+ /**
102
+ * Render the structured event-data payload as `message > key=value ...`.
103
+ * The message is clipped and colored red for Warning/Error events; trailing
104
+ * fields keep their full value. Non-object payloads are echoed as-is.
105
+ */
106
+ export function formatEventData(eventData, eventType = 'Normal') {
107
+ const data = parseEventData(eventData);
108
+ if (!data) {
109
+ return ` ${eventData}`;
110
+ }
111
+ const messageColor = isErrorType(eventType) ? chalk.red : chalk.white;
112
+ const messagePart = typeof data.message === 'string'
113
+ ? ` ${messageColor(truncate(data.message))}`
114
+ : '';
115
+ const fields = Object.keys(data)
116
+ .filter((key) => !RESERVED_KEYS.has(key))
117
+ .sort(compareKeys)
118
+ .map((key) => `${chalk.blue(key)}${chalk.dim('=')}${renderFieldValue(key, data[key])}`)
119
+ .join(' ');
120
+ if (!fields) {
121
+ return messagePart;
122
+ }
123
+ return `${messagePart} ${chalk.dim('>')} ${fields}`;
124
+ }
125
+ /**
126
+ * Format a Kubernetes Event carrying an ark event-data annotation into a
127
+ * single human-readable line. Returns null when the event has no payload.
128
+ */
129
+ export function formatEvent(event) {
130
+ const eventData = event.metadata?.annotations?.[EVENT_ANNOTATIONS.EVENT_DATA];
131
+ if (!eventData) {
132
+ return null;
133
+ }
134
+ const timestamp = formatEventTimestamp(event);
135
+ const reason = event.reason || 'Unknown';
136
+ const eventType = event.type || 'Normal';
137
+ return `${chalk.dim(timestamp)} [${colorReason(reason, eventType)}]${formatEventData(eventData, eventType)}`;
138
+ }
@@ -14,5 +14,5 @@ export declare function deleteResource(resourceType: string, name?: string, opti
14
14
  all?: boolean;
15
15
  }): Promise<void>;
16
16
  export declare function replaceResource<T extends K8sResource>(resource: T): Promise<T>;
17
- export declare function watchEventsLive(queryName: string): Promise<void>;
17
+ export declare function watchEventsLive(queryName: string, pretty?: boolean): Promise<void>;
18
18
  export {};
@@ -1,6 +1,7 @@
1
1
  import { execa } from 'execa';
2
2
  import chalk from 'chalk';
3
3
  import { EVENT_ANNOTATIONS } from './constants.js';
4
+ import { formatEvent } from './formatEvent.js';
4
5
  export async function getResource(resourceType, name) {
5
6
  if (name === '@latest') {
6
7
  const result = await execa('kubectl', [
@@ -53,7 +54,7 @@ export async function replaceResource(resource) {
53
54
  });
54
55
  return JSON.parse(result.stdout);
55
56
  }
56
- export async function watchEventsLive(queryName) {
57
+ export async function watchEventsLive(queryName, pretty = false) {
57
58
  const seenEvents = new Set();
58
59
  const pollEvents = async () => {
59
60
  try {
@@ -73,16 +74,24 @@ export async function watchEventsLive(queryName) {
73
74
  const annotations = event.metadata?.annotations || {};
74
75
  const eventData = annotations[EVENT_ANNOTATIONS.EVENT_DATA];
75
76
  if (eventData) {
76
- const now = new Date();
77
- const hours = now.getHours().toString().padStart(2, '0');
78
- const minutes = now.getMinutes().toString().padStart(2, '0');
79
- const seconds = now.getSeconds().toString().padStart(2, '0');
80
- const millis = now.getMilliseconds().toString().padStart(3, '0');
81
- const timestamp = `${hours}:${minutes}:${seconds}.${millis}`;
82
- const reason = event.reason || 'Unknown';
83
- const eventType = event.type || 'Normal';
84
- const colorCode = eventType === 'Normal' ? 32 : eventType === 'Warning' ? 33 : 31;
85
- console.log(`${timestamp} \x1b[${colorCode}m${reason}\x1b[0m ${eventData}`);
77
+ if (pretty) {
78
+ const line = formatEvent(event);
79
+ if (line !== null) {
80
+ console.log(line);
81
+ }
82
+ }
83
+ else {
84
+ const now = new Date();
85
+ const hours = now.getHours().toString().padStart(2, '0');
86
+ const minutes = now.getMinutes().toString().padStart(2, '0');
87
+ const seconds = now.getSeconds().toString().padStart(2, '0');
88
+ const millis = now.getMilliseconds().toString().padStart(3, '0');
89
+ const timestamp = `${hours}:${minutes}:${seconds}.${millis}`;
90
+ const reason = event.reason || 'Unknown';
91
+ const eventType = event.type || 'Normal';
92
+ const colorCode = eventType === 'Normal' ? 32 : eventType === 'Warning' ? 33 : 31;
93
+ console.log(`${timestamp} \x1b[${colorCode}m${reason}\x1b[0m ${eventData}`);
94
+ }
86
95
  }
87
96
  }
88
97
  }
@@ -1,4 +1,11 @@
1
1
  export type StorageBackend = 'etcd' | 'postgresql';
2
+ export type DetectedBackend = StorageBackend | 'unknown';
3
+ export type BackendStatus = 'etcd' | 'postgresql' | 'not-installed' | 'unreachable' | 'forbidden' | 'undetermined';
4
+ export interface BackendDetection {
5
+ backend: DetectedBackend;
6
+ status: BackendStatus;
7
+ message: string;
8
+ }
2
9
  export interface ReadinessCheckResult {
3
10
  name: string;
4
11
  passed: boolean;
@@ -6,5 +13,6 @@ export interface ReadinessCheckResult {
6
13
  message?: string;
7
14
  }
8
15
  export type ReadinessProgress = (result: ReadinessCheckResult) => void;
9
- export declare function detectStorageBackend(): Promise<StorageBackend>;
10
- export declare function runReadinessChecks(timeoutSeconds: number, onProgress?: ReadinessProgress): Promise<ReadinessCheckResult[]>;
16
+ export declare function describeStorageBackend(): Promise<BackendDetection>;
17
+ export declare function detectStorageBackend(): Promise<DetectedBackend>;
18
+ export declare function runReadinessChecks(timeoutSeconds: number, backend: DetectedBackend, onProgress?: ReadinessProgress): Promise<ReadinessCheckResult[]>;
@@ -14,9 +14,89 @@ async function runKubectl(args, timeoutMs) {
14
14
  stderr: result.stderr ?? '',
15
15
  };
16
16
  }
17
+ function classifyFailure(stderr) {
18
+ if (/not\s*found/i.test(stderr)) {
19
+ return 'not-found';
20
+ }
21
+ if (/forbidden|unauthorized/i.test(stderr)) {
22
+ return 'forbidden';
23
+ }
24
+ if (/connection refused|was refused|no such host|i\/o timeout|timed out|dial tcp|unable to connect|did you specify the right host/i.test(stderr)) {
25
+ return 'unreachable';
26
+ }
27
+ return 'undetermined';
28
+ }
29
+ const DETECT_RETRY_DELAY_MS = 250;
30
+ const DETECT_MAX_RETRIES = 2;
31
+ function isAuthoritativeResult(result) {
32
+ if (result.exitCode === 0) {
33
+ return true;
34
+ }
35
+ return /not\s*found|forbidden/i.test(result.stderr);
36
+ }
37
+ async function probeKubectl(args, timeoutMs, maxRetries = DETECT_MAX_RETRIES) {
38
+ let result = await runKubectl(args, timeoutMs);
39
+ for (let attempt = 0; attempt < maxRetries && !isAuthoritativeResult(result); attempt++) {
40
+ await sleep(DETECT_RETRY_DELAY_MS);
41
+ result = await runKubectl(args, timeoutMs);
42
+ }
43
+ return result;
44
+ }
45
+ function unknownFrom(reason) {
46
+ switch (reason) {
47
+ case 'forbidden':
48
+ return {
49
+ backend: 'unknown',
50
+ status: 'forbidden',
51
+ message: 'Access denied reading cluster resources (RBAC) — cannot determine the storage backend.',
52
+ };
53
+ case 'unreachable':
54
+ return {
55
+ backend: 'unknown',
56
+ status: 'unreachable',
57
+ message: 'Cluster is not reachable (connection failed or timed out) — cannot determine the storage backend.',
58
+ };
59
+ default:
60
+ return {
61
+ backend: 'unknown',
62
+ status: 'undetermined',
63
+ message: 'Could not determine the storage backend (unrecognized kubectl error).',
64
+ };
65
+ }
66
+ }
67
+ export async function describeStorageBackend() {
68
+ const crd = await probeKubectl(['get', 'crd', 'agents.ark.mckinsey.com'], 10000);
69
+ if (crd.exitCode === 0) {
70
+ return {
71
+ backend: 'etcd',
72
+ status: 'etcd',
73
+ message: 'ARK is running the etcd (Kubernetes-native) backend; agents are stored as CRDs.',
74
+ };
75
+ }
76
+ const crdReason = classifyFailure(crd.stderr);
77
+ if (crdReason !== 'not-found') {
78
+ return unknownFrom(crdReason);
79
+ }
80
+ const api = await probeKubectl(['get', 'apiservice', 'v1alpha1.ark.mckinsey.com', '-o', 'name'], 10000);
81
+ if (api.exitCode === 0) {
82
+ return {
83
+ backend: 'postgresql',
84
+ status: 'postgresql',
85
+ message: 'ARK is running the PostgreSQL backend; agents are served by the aggregated API server.',
86
+ };
87
+ }
88
+ const apiReason = classifyFailure(api.stderr);
89
+ if (apiReason === 'not-found') {
90
+ return {
91
+ backend: 'unknown',
92
+ status: 'not-installed',
93
+ message: 'ARK is not installed on this cluster (no agents CRD and no aggregated APIService).',
94
+ };
95
+ }
96
+ return unknownFrom(apiReason);
97
+ }
17
98
  export async function detectStorageBackend() {
18
- const { exitCode } = await runKubectl(['get', 'crd', 'agents.ark.mckinsey.com'], 10000);
19
- return exitCode === 0 ? 'etcd' : 'postgresql';
99
+ return (await describeStorageBackend()).backend;
20
100
  }
21
101
  async function waitForApiServices(timeoutSeconds) {
22
102
  const start = Date.now();
@@ -64,11 +144,20 @@ async function waitForApiGroup(timeoutSeconds) {
64
144
  message: 'timed out waiting for ark.mckinsey.com API group',
65
145
  };
66
146
  }
67
- export async function runReadinessChecks(timeoutSeconds, onProgress) {
68
- const backend = await detectStorageBackend();
147
+ export async function runReadinessChecks(timeoutSeconds, backend, onProgress) {
69
148
  if (backend === 'etcd') {
70
149
  return [];
71
150
  }
151
+ if (backend === 'unknown') {
152
+ const result = {
153
+ name: 'Storage backend',
154
+ passed: false,
155
+ durationMs: 0,
156
+ message: 'could not determine storage backend (ARK not installed, cluster unreachable, or access denied)',
157
+ };
158
+ onProgress?.(result);
159
+ return [result];
160
+ }
72
161
  const overallStart = Date.now();
73
162
  const remaining = () => Math.max(1, timeoutSeconds - Math.floor((Date.now() - overallStart) / 1000));
74
163
  const checks = [
@@ -92,6 +92,10 @@ export interface QueryTarget {
92
92
  type: string;
93
93
  name: string;
94
94
  }
95
+ export interface QueryParameter {
96
+ name: string;
97
+ value?: string;
98
+ }
95
99
  export interface QueryResponse {
96
100
  content?: string;
97
101
  a2a?: {
@@ -124,6 +128,7 @@ export interface Query {
124
128
  sessionId?: string;
125
129
  conversationId?: string;
126
130
  timeout?: string;
131
+ parameters?: QueryParameter[];
127
132
  };
128
133
  status?: QueryStatus;
129
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-at-scale/ark",
3
- "version": "0.1.64",
3
+ "version": "0.1.67",
4
4
  "description": "Ark CLI - Interactive terminal interface for ARK agents",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -85,15 +85,17 @@
85
85
  "node": ">=18.0.0"
86
86
  },
87
87
  "overrides": {
88
- "brace-expansion": ">=5.0.6",
88
+ "brace-expansion": ">=5.0.7",
89
89
  "minimatch": "^10.2.3",
90
90
  "rollup": "4.59.0",
91
- "hono": "^4.12.18",
91
+ "hono": "^4.12.25",
92
92
  "@hono/node-server": "^1.19.13",
93
93
  "flatted": "^3.4.2",
94
94
  "tar": "^7.5.11",
95
95
  "express-rate-limit": "^8.3.0",
96
- "fast-uri": "^3.1.1",
97
- "ws": "^8.20.1"
96
+ "fast-uri": "^3.1.4",
97
+ "ws": "^8.21.0",
98
+ "esbuild": "^0.28.1",
99
+ "form-data": "^4.0.6"
98
100
  }
99
101
  }
@@ -63,7 +63,7 @@ ENTRYPOINT [ "/bin/bash", "-c", "mcp-proxy --debug --port 8080 --host 0.0.0.0 --
63
63
  {{- end }}
64
64
 
65
65
  {{- else if eq .Values.technology "go" }}
66
- FROM golang:1.26.3-alpine AS go-builder
66
+ FROM golang:1.26.5-alpine AS go-builder
67
67
  {{- if .Values.packageSource }}
68
68
  {{- if eq .Values.packageSource "go-install" }}
69
69
  RUN go install {{ .Values.packageName }}@latest