@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.
- package/dist/commands/agents/index.js +13 -1
- package/dist/commands/import/index.js +47 -12
- package/dist/commands/install/index.js +33 -15
- package/dist/commands/mcp/login.js +24 -0
- package/dist/commands/models/kubernetes/manifest-builder.js +30 -18
- package/dist/commands/models/kubernetes/secret-manager.js +18 -8
- package/dist/commands/models/providers/bedrock.d.ts +7 -2
- package/dist/commands/models/providers/bedrock.js +91 -42
- package/dist/commands/query/index.js +12 -2
- package/dist/commands/status/index.js +39 -5
- package/dist/lib/arkApiClient.d.ts +4 -0
- package/dist/lib/arkApiClient.js +3 -0
- package/dist/lib/chatClient.d.ts +2 -0
- package/dist/lib/chatClient.js +25 -1
- package/dist/lib/executeQuery.d.ts +4 -1
- package/dist/lib/executeQuery.js +24 -1
- package/dist/lib/formatEvent.d.ts +29 -0
- package/dist/lib/formatEvent.js +138 -0
- package/dist/lib/kubectl.d.ts +1 -1
- package/dist/lib/kubectl.js +20 -11
- package/dist/lib/readinessChecks.d.ts +10 -2
- package/dist/lib/readinessChecks.js +93 -4
- package/dist/lib/types.d.ts +5 -0
- package/package.json +7 -5
- package/templates/mcp-server/Dockerfile +1 -1
- package/templates/models/azure.yaml +7 -6
- package/templates/models/claude.yaml +1 -1
- package/templates/models/gemini.yaml +1 -1
- package/templates/models/openai.yaml +1 -1
- package/templates/query/query.template.yaml +3 -3
- package/templates/tool/uv.lock +215 -72
|
@@ -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,
|
|
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
|
|
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 =
|
|
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}` : '';
|
package/dist/lib/arkApiClient.js
CHANGED
|
@@ -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
|
});
|
package/dist/lib/chatClient.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/chatClient.js
CHANGED
|
@@ -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(
|
|
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[];
|
package/dist/lib/executeQuery.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/lib/kubectl.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/lib/kubectl.js
CHANGED
|
@@ -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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
10
|
-
export declare function
|
|
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
|
-
|
|
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 = [
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
88
|
+
"brace-expansion": ">=5.0.7",
|
|
89
89
|
"minimatch": "^10.2.3",
|
|
90
90
|
"rollup": "4.59.0",
|
|
91
|
-
"hono": "^4.12.
|
|
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.
|
|
97
|
-
"ws": "^8.
|
|
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.
|
|
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
|