@agents-at-scale/ark 0.1.47 → 0.1.50
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/.arkrc.template.yaml +51 -0
- package/README.md +2 -0
- package/dist/arkServices.js +13 -8
- package/dist/commands/chat/index.js +1 -1
- package/dist/commands/completion/index.js +17 -1
- package/dist/commands/dashboard/index.d.ts +2 -2
- package/dist/commands/dashboard/index.js +5 -4
- package/dist/commands/export/index.d.ts +3 -0
- package/dist/commands/export/index.js +73 -0
- package/dist/commands/export/index.spec.d.ts +1 -0
- package/dist/commands/export/index.spec.js +145 -0
- package/dist/commands/import/index.d.ts +3 -0
- package/dist/commands/import/index.js +27 -0
- package/dist/commands/import/index.spec.d.ts +1 -0
- package/dist/commands/import/index.spec.js +46 -0
- package/dist/commands/install/index.js +12 -0
- package/dist/commands/memory/index.js +9 -4
- package/dist/commands/models/kubernetes/manifest-builder.js +1 -1
- package/dist/commands/queries/index.js +4 -4
- package/dist/commands/queries/index.spec.d.ts +1 -0
- package/dist/commands/queries/index.spec.js +167 -0
- package/dist/commands/query/index.js +2 -0
- package/dist/components/ChatUI.js +4 -4
- package/dist/index.js +4 -0
- package/dist/lib/arkApiProxy.d.ts +1 -1
- package/dist/lib/arkApiProxy.js +2 -2
- package/dist/lib/arkServiceProxy.d.ts +3 -1
- package/dist/lib/arkServiceProxy.js +34 -1
- package/dist/lib/arkServiceProxy.spec.d.ts +1 -0
- package/dist/lib/arkServiceProxy.spec.js +100 -0
- package/dist/lib/chatClient.d.ts +1 -0
- package/dist/lib/chatClient.js +7 -1
- package/dist/lib/config.d.ts +3 -1
- package/dist/lib/config.js +21 -7
- package/dist/lib/config.spec.js +10 -0
- package/dist/lib/executeQuery.d.ts +1 -0
- package/dist/lib/executeQuery.js +17 -8
- package/dist/lib/kubectl.d.ts +2 -0
- package/dist/lib/kubectl.js +6 -0
- package/dist/lib/kubectl.spec.js +16 -0
- package/dist/lib/types.d.ts +3 -2
- package/dist/types/arkService.d.ts +5 -0
- package/dist/ui/MainMenu.js +6 -2
- package/package.json +4 -2
- package/dist/ui/AgentSelector.d.ts +0 -8
- package/dist/ui/AgentSelector.js +0 -53
- package/dist/ui/ModelSelector.d.ts +0 -8
- package/dist/ui/ModelSelector.js +0 -53
- package/dist/ui/TeamSelector.d.ts +0 -8
- package/dist/ui/TeamSelector.js +0 -55
- package/dist/ui/ToolSelector.d.ts +0 -8
- package/dist/ui/ToolSelector.js +0 -53
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import output from '../../lib/output.js';
|
|
3
|
+
const mockExeca = jest.fn();
|
|
4
|
+
jest.unstable_mockModule('execa', () => ({
|
|
5
|
+
execa: mockExeca,
|
|
6
|
+
}));
|
|
7
|
+
const { createQueriesCommand } = await import('./index.js');
|
|
8
|
+
describe('queries get command', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
console.log = jest.fn();
|
|
12
|
+
jest.spyOn(output, 'warning').mockImplementation(() => { });
|
|
13
|
+
jest.spyOn(output, 'error').mockImplementation(() => { });
|
|
14
|
+
jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
15
|
+
});
|
|
16
|
+
it('should get query with response in JSON format', async () => {
|
|
17
|
+
const mockQuery = {
|
|
18
|
+
metadata: {
|
|
19
|
+
name: 'test-query',
|
|
20
|
+
},
|
|
21
|
+
spec: {
|
|
22
|
+
input: 'test input',
|
|
23
|
+
target: { type: 'agent', name: 'test-agent' },
|
|
24
|
+
},
|
|
25
|
+
status: {
|
|
26
|
+
phase: 'done',
|
|
27
|
+
response: {
|
|
28
|
+
content: 'This is the response',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
mockExeca.mockResolvedValue({
|
|
33
|
+
stdout: JSON.stringify(mockQuery),
|
|
34
|
+
});
|
|
35
|
+
const command = createQueriesCommand({});
|
|
36
|
+
await command.parseAsync(['node', 'test', 'get', 'test-query']);
|
|
37
|
+
expect(console.log).toHaveBeenCalledWith(JSON.stringify(mockQuery, null, 2));
|
|
38
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', 'test-query', '-o', 'json'], { stdio: 'pipe' });
|
|
39
|
+
});
|
|
40
|
+
it('should get query with response flag in JSON format', async () => {
|
|
41
|
+
const mockQuery = {
|
|
42
|
+
metadata: {
|
|
43
|
+
name: 'test-query',
|
|
44
|
+
},
|
|
45
|
+
spec: {
|
|
46
|
+
input: 'test input',
|
|
47
|
+
target: { type: 'agent', name: 'test-agent' },
|
|
48
|
+
},
|
|
49
|
+
status: {
|
|
50
|
+
phase: 'done',
|
|
51
|
+
response: {
|
|
52
|
+
content: 'This is the response content',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
mockExeca.mockResolvedValue({
|
|
57
|
+
stdout: JSON.stringify(mockQuery),
|
|
58
|
+
});
|
|
59
|
+
const command = createQueriesCommand({});
|
|
60
|
+
await command.parseAsync([
|
|
61
|
+
'node',
|
|
62
|
+
'test',
|
|
63
|
+
'get',
|
|
64
|
+
'test-query',
|
|
65
|
+
'--response',
|
|
66
|
+
]);
|
|
67
|
+
expect(console.log).toHaveBeenCalledWith(JSON.stringify(mockQuery.status.response, null, 2));
|
|
68
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', 'test-query', '-o', 'json'], { stdio: 'pipe' });
|
|
69
|
+
});
|
|
70
|
+
it('should get query with response flag in markdown format', async () => {
|
|
71
|
+
const mockQuery = {
|
|
72
|
+
metadata: {
|
|
73
|
+
name: 'test-query',
|
|
74
|
+
},
|
|
75
|
+
spec: {
|
|
76
|
+
input: 'test input',
|
|
77
|
+
target: { type: 'agent', name: 'test-agent' },
|
|
78
|
+
},
|
|
79
|
+
status: {
|
|
80
|
+
phase: 'done',
|
|
81
|
+
response: {
|
|
82
|
+
content: '# Heading\n\nThis is markdown content',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
mockExeca.mockResolvedValue({
|
|
87
|
+
stdout: JSON.stringify(mockQuery),
|
|
88
|
+
});
|
|
89
|
+
const command = createQueriesCommand({});
|
|
90
|
+
await command.parseAsync([
|
|
91
|
+
'node',
|
|
92
|
+
'test',
|
|
93
|
+
'get',
|
|
94
|
+
'test-query',
|
|
95
|
+
'--response',
|
|
96
|
+
'--output',
|
|
97
|
+
'markdown',
|
|
98
|
+
]);
|
|
99
|
+
expect(console.log).toHaveBeenCalled();
|
|
100
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', 'test-query', '-o', 'json'], { stdio: 'pipe' });
|
|
101
|
+
});
|
|
102
|
+
it('should get query in markdown format without response flag', async () => {
|
|
103
|
+
const mockQuery = {
|
|
104
|
+
metadata: {
|
|
105
|
+
name: 'test-query',
|
|
106
|
+
},
|
|
107
|
+
spec: {
|
|
108
|
+
input: 'test input',
|
|
109
|
+
target: { type: 'agent', name: 'test-agent' },
|
|
110
|
+
},
|
|
111
|
+
status: {
|
|
112
|
+
phase: 'done',
|
|
113
|
+
response: {
|
|
114
|
+
content: '# Response\n\nMarkdown response',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
mockExeca.mockResolvedValue({
|
|
119
|
+
stdout: JSON.stringify(mockQuery),
|
|
120
|
+
});
|
|
121
|
+
const command = createQueriesCommand({});
|
|
122
|
+
await command.parseAsync([
|
|
123
|
+
'node',
|
|
124
|
+
'test',
|
|
125
|
+
'get',
|
|
126
|
+
'test-query',
|
|
127
|
+
'--output',
|
|
128
|
+
'markdown',
|
|
129
|
+
]);
|
|
130
|
+
expect(console.log).toHaveBeenCalled();
|
|
131
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', 'test-query', '-o', 'json'], { stdio: 'pipe' });
|
|
132
|
+
});
|
|
133
|
+
it('should warn when query has no response with response flag', async () => {
|
|
134
|
+
const mockQuery = {
|
|
135
|
+
metadata: {
|
|
136
|
+
name: 'test-query',
|
|
137
|
+
},
|
|
138
|
+
spec: {
|
|
139
|
+
input: 'test input',
|
|
140
|
+
target: { type: 'agent', name: 'test-agent' },
|
|
141
|
+
},
|
|
142
|
+
status: {
|
|
143
|
+
phase: 'running',
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
mockExeca.mockResolvedValue({
|
|
147
|
+
stdout: JSON.stringify(mockQuery),
|
|
148
|
+
});
|
|
149
|
+
const command = createQueriesCommand({});
|
|
150
|
+
await command.parseAsync([
|
|
151
|
+
'node',
|
|
152
|
+
'test',
|
|
153
|
+
'get',
|
|
154
|
+
'test-query',
|
|
155
|
+
'--response',
|
|
156
|
+
]);
|
|
157
|
+
expect(output.warning).toHaveBeenCalledWith('No response available');
|
|
158
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', 'test-query', '-o', 'json'], { stdio: 'pipe' });
|
|
159
|
+
});
|
|
160
|
+
it('should handle errors when getting query', async () => {
|
|
161
|
+
mockExeca.mockRejectedValue(new Error('Query not found'));
|
|
162
|
+
const command = createQueriesCommand({});
|
|
163
|
+
await command.parseAsync(['node', 'test', 'get', 'nonexistent-query']);
|
|
164
|
+
expect(output.error).toHaveBeenCalledWith('fetching query:', 'Query not found');
|
|
165
|
+
expect(process.exit).toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -11,6 +11,7 @@ export function createQueryCommand(config) {
|
|
|
11
11
|
.option('-o, --output <format>', 'Output format: yaml, json, name or events (shows structured event data)')
|
|
12
12
|
.option('--timeout <timeout>', 'Query timeout (e.g., 30s, 5m, 1h)')
|
|
13
13
|
.option('--session-id <sessionId>', 'Session ID to associate with the query for conversation continuity')
|
|
14
|
+
.option('--conversation-id <conversationId>', 'Conversation ID to associate with the query for memory continuity')
|
|
14
15
|
.action(async (target, message, options) => {
|
|
15
16
|
const parsed = parseTarget(target);
|
|
16
17
|
if (!parsed) {
|
|
@@ -24,6 +25,7 @@ export function createQueryCommand(config) {
|
|
|
24
25
|
outputFormat: options.output,
|
|
25
26
|
timeout: options.timeout || config.queryTimeout,
|
|
26
27
|
sessionId: options.sessionId,
|
|
28
|
+
conversationId: options.conversationId,
|
|
27
29
|
});
|
|
28
30
|
});
|
|
29
31
|
return queryCommand;
|
|
@@ -467,11 +467,11 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
|
|
|
467
467
|
}
|
|
468
468
|
// Send message and get response with abort signal
|
|
469
469
|
const fullResponse = await chatClientRef.current.sendMessage(target.id, apiMessages, { ...chatConfig, a2aContextId: a2aContextIdRef.current }, (chunk, toolCalls, arkMetadata) => {
|
|
470
|
-
// Extract A2A context ID from
|
|
471
|
-
// Chat TUI always queries a single target, so contextId is in
|
|
472
|
-
if (arkMetadata?.completedQuery?.status?.
|
|
470
|
+
// Extract A2A context ID from response
|
|
471
|
+
// Chat TUI always queries a single target, so contextId is in response
|
|
472
|
+
if (arkMetadata?.completedQuery?.status?.response?.a2a?.contextId) {
|
|
473
473
|
a2aContextIdRef.current =
|
|
474
|
-
arkMetadata.completedQuery.status.
|
|
474
|
+
arkMetadata.completedQuery.status.response.a2a.contextId;
|
|
475
475
|
}
|
|
476
476
|
// Update message progressively as chunks arrive
|
|
477
477
|
setMessages((prev) => {
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,9 @@ import { createCompletionCommand } from './commands/completion/index.js';
|
|
|
14
14
|
import { createDashboardCommand } from './commands/dashboard/index.js';
|
|
15
15
|
import { createDocsCommand } from './commands/docs/index.js';
|
|
16
16
|
import { createEvaluationCommand } from './commands/evaluation/index.js';
|
|
17
|
+
import { createExportCommand } from './commands/export/index.js';
|
|
17
18
|
import { createGenerateCommand } from './commands/generate/index.js';
|
|
19
|
+
import { createImportCommand } from './commands/import/index.js';
|
|
18
20
|
import { createInstallCommand } from './commands/install/index.js';
|
|
19
21
|
import { createMarketplaceCommand } from './commands/marketplace/index.js';
|
|
20
22
|
import { createMemoryCommand } from './commands/memory/index.js';
|
|
@@ -48,7 +50,9 @@ async function main() {
|
|
|
48
50
|
program.addCommand(createDashboardCommand(config));
|
|
49
51
|
program.addCommand(createDocsCommand(config));
|
|
50
52
|
program.addCommand(createEvaluationCommand(config));
|
|
53
|
+
program.addCommand(createExportCommand(config));
|
|
51
54
|
program.addCommand(createGenerateCommand(config));
|
|
55
|
+
program.addCommand(createImportCommand(config));
|
|
52
56
|
program.addCommand(createInstallCommand(config));
|
|
53
57
|
program.addCommand(createMarketplaceCommand(config));
|
|
54
58
|
program.addCommand(createMemoryCommand(config));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ArkApiClient } from './arkApiClient.js';
|
|
2
2
|
export declare class ArkApiProxy {
|
|
3
3
|
private serviceProxy;
|
|
4
|
-
constructor(localPort?: number);
|
|
4
|
+
constructor(localPort?: number, reusePortForwards?: boolean);
|
|
5
5
|
start(): Promise<ArkApiClient>;
|
|
6
6
|
stop(): void;
|
|
7
7
|
isRunning(): boolean;
|
package/dist/lib/arkApiProxy.js
CHANGED
|
@@ -2,9 +2,9 @@ import { ArkApiClient } from './arkApiClient.js';
|
|
|
2
2
|
import { ArkServiceProxy } from './arkServiceProxy.js';
|
|
3
3
|
import { arkServices } from '../arkServices.js';
|
|
4
4
|
export class ArkApiProxy {
|
|
5
|
-
constructor(localPort) {
|
|
5
|
+
constructor(localPort, reusePortForwards = false) {
|
|
6
6
|
const arkApiService = arkServices['ark-api'];
|
|
7
|
-
this.serviceProxy = new ArkServiceProxy(arkApiService, localPort);
|
|
7
|
+
this.serviceProxy = new ArkServiceProxy(arkApiService, localPort, reusePortForwards);
|
|
8
8
|
}
|
|
9
9
|
async start() {
|
|
10
10
|
const arkApiUrl = await this.serviceProxy.start();
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { ArkService } from '../arkServices.js';
|
|
2
2
|
export declare class ArkServiceProxy {
|
|
3
|
+
private reusePortForwards;
|
|
3
4
|
private kubectlProcess?;
|
|
4
5
|
private localPort;
|
|
5
6
|
private isReady;
|
|
6
7
|
private service;
|
|
7
|
-
constructor(service: ArkService, localPort?: number);
|
|
8
|
+
constructor(service: ArkService, localPort?: number, reusePortForwards?: boolean);
|
|
8
9
|
private getRandomPort;
|
|
10
|
+
private checkExistingPortForward;
|
|
9
11
|
start(): Promise<string>;
|
|
10
12
|
stop(): void;
|
|
11
13
|
isRunning(): boolean;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import find from 'find-process';
|
|
3
|
+
import Debug from 'debug';
|
|
4
|
+
const debug = Debug('ark:service-proxy');
|
|
2
5
|
export class ArkServiceProxy {
|
|
3
|
-
constructor(service, localPort) {
|
|
6
|
+
constructor(service, localPort, reusePortForwards = false) {
|
|
7
|
+
this.reusePortForwards = reusePortForwards;
|
|
4
8
|
this.isReady = false;
|
|
5
9
|
this.service = service;
|
|
6
10
|
this.localPort =
|
|
@@ -9,10 +13,39 @@ export class ArkServiceProxy {
|
|
|
9
13
|
getRandomPort() {
|
|
10
14
|
return Math.floor(Math.random() * (65535 - 1024) + 1024);
|
|
11
15
|
}
|
|
16
|
+
async checkExistingPortForward() {
|
|
17
|
+
try {
|
|
18
|
+
const processes = await find('port', this.localPort);
|
|
19
|
+
if (processes.length === 0) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const kubectlProcess = processes.find((proc) => proc.cmd?.includes('kubectl') && proc.cmd?.includes('port-forward'));
|
|
23
|
+
if (kubectlProcess) {
|
|
24
|
+
debug(`Reusing existing kubectl port-forward on port ${this.localPort} (PID: ${kubectlProcess.pid})`);
|
|
25
|
+
this.isReady = true;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const processInfo = processes[0];
|
|
29
|
+
throw new Error(`${this.service.name} port forward failed: port ${this.localPort} is already in use by ${processInfo.name} (PID: ${processInfo.pid})`);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof Error && error.message.includes('already in use')) {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
debug(`Error checking for existing port-forward: ${error}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
12
39
|
async start() {
|
|
13
40
|
if (!this.service.k8sServiceName || !this.service.k8sServicePort) {
|
|
14
41
|
throw new Error(`${this.service.name} service configuration missing k8sServiceName or k8sServicePort`);
|
|
15
42
|
}
|
|
43
|
+
if (this.reusePortForwards) {
|
|
44
|
+
const isReused = await this.checkExistingPortForward();
|
|
45
|
+
if (isReused) {
|
|
46
|
+
return `http://localhost:${this.localPort}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
16
49
|
return new Promise((resolve, reject) => {
|
|
17
50
|
const args = [
|
|
18
51
|
'port-forward',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { Buffer } from 'buffer';
|
|
3
|
+
const mockFind = jest.fn();
|
|
4
|
+
jest.unstable_mockModule('find-process', () => ({
|
|
5
|
+
default: mockFind,
|
|
6
|
+
}));
|
|
7
|
+
const mockSpawn = jest.fn();
|
|
8
|
+
const mockChildProcess = {
|
|
9
|
+
spawn: mockSpawn,
|
|
10
|
+
};
|
|
11
|
+
jest.unstable_mockModule('child_process', () => ({
|
|
12
|
+
...mockChildProcess,
|
|
13
|
+
spawn: mockSpawn,
|
|
14
|
+
}));
|
|
15
|
+
const { ArkServiceProxy } = await import('./arkServiceProxy.js');
|
|
16
|
+
describe('ArkServiceProxy', () => {
|
|
17
|
+
const mockService = {
|
|
18
|
+
name: 'test-service',
|
|
19
|
+
helmReleaseName: 'test-service',
|
|
20
|
+
description: 'Test service',
|
|
21
|
+
k8sServiceName: 'test-service-k8s',
|
|
22
|
+
k8sServicePort: 8080,
|
|
23
|
+
namespace: 'default',
|
|
24
|
+
enabled: true,
|
|
25
|
+
category: 'test',
|
|
26
|
+
};
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
jest.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
describe('port-forward reuse', () => {
|
|
31
|
+
it('creates new port-forward when reuse is disabled', async () => {
|
|
32
|
+
const proxy = new ArkServiceProxy(mockService, 3000, false);
|
|
33
|
+
const mockProcess = {
|
|
34
|
+
stdout: { on: jest.fn() },
|
|
35
|
+
stderr: { on: jest.fn() },
|
|
36
|
+
on: jest.fn(),
|
|
37
|
+
kill: jest.fn(),
|
|
38
|
+
};
|
|
39
|
+
mockSpawn.mockReturnValue(mockProcess);
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
const stdoutCallback = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
42
|
+
if (stdoutCallback) {
|
|
43
|
+
stdoutCallback(Buffer.from('Forwarding from 127.0.0.1:3000'));
|
|
44
|
+
}
|
|
45
|
+
}, 10);
|
|
46
|
+
const url = await proxy.start();
|
|
47
|
+
expect(url).toBe('http://localhost:3000');
|
|
48
|
+
expect(mockFind).not.toHaveBeenCalled();
|
|
49
|
+
expect(mockSpawn).toHaveBeenCalledWith('kubectl', ['port-forward', 'service/test-service-k8s', '3000:8080', '--namespace', 'default'], expect.any(Object));
|
|
50
|
+
});
|
|
51
|
+
it('reuses existing kubectl port-forward when reuse is enabled', async () => {
|
|
52
|
+
mockFind.mockResolvedValue([
|
|
53
|
+
{
|
|
54
|
+
pid: 12345,
|
|
55
|
+
name: 'kubectl',
|
|
56
|
+
cmd: 'kubectl port-forward service/test-service-k8s 3000:8080',
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
const proxy = new ArkServiceProxy(mockService, 3000, true);
|
|
60
|
+
const url = await proxy.start();
|
|
61
|
+
expect(url).toBe('http://localhost:3000');
|
|
62
|
+
expect(mockFind).toHaveBeenCalledWith('port', 3000);
|
|
63
|
+
expect(mockSpawn).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
it('creates new port-forward when port is not in use', async () => {
|
|
66
|
+
mockFind.mockResolvedValue([]);
|
|
67
|
+
const proxy = new ArkServiceProxy(mockService, 3000, true);
|
|
68
|
+
const mockProcess = {
|
|
69
|
+
stdout: { on: jest.fn() },
|
|
70
|
+
stderr: { on: jest.fn() },
|
|
71
|
+
on: jest.fn(),
|
|
72
|
+
kill: jest.fn(),
|
|
73
|
+
};
|
|
74
|
+
mockSpawn.mockReturnValue(mockProcess);
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
const stdoutCallback = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
77
|
+
if (stdoutCallback) {
|
|
78
|
+
stdoutCallback(Buffer.from('Forwarding from 127.0.0.1:3000'));
|
|
79
|
+
}
|
|
80
|
+
}, 10);
|
|
81
|
+
const url = await proxy.start();
|
|
82
|
+
expect(url).toBe('http://localhost:3000');
|
|
83
|
+
expect(mockFind).toHaveBeenCalledWith('port', 3000);
|
|
84
|
+
expect(mockSpawn).toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
it('throws error when port is in use by non-kubectl process', async () => {
|
|
87
|
+
mockFind.mockResolvedValue([
|
|
88
|
+
{
|
|
89
|
+
pid: 54321,
|
|
90
|
+
name: 'node',
|
|
91
|
+
cmd: 'node server.js',
|
|
92
|
+
},
|
|
93
|
+
]);
|
|
94
|
+
const proxy = new ArkServiceProxy(mockService, 3000, true);
|
|
95
|
+
await expect(proxy.start()).rejects.toThrow('test-service port forward failed: port 3000 is already in use by node (PID: 54321)');
|
|
96
|
+
expect(mockFind).toHaveBeenCalledWith('port', 3000);
|
|
97
|
+
expect(mockSpawn).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
package/dist/lib/chatClient.d.ts
CHANGED
package/dist/lib/chatClient.js
CHANGED
|
@@ -17,11 +17,17 @@ export class ChatClient {
|
|
|
17
17
|
signal: signal,
|
|
18
18
|
};
|
|
19
19
|
// Build metadata object - only add if we have something to include
|
|
20
|
-
if (config.sessionId ||
|
|
20
|
+
if (config.sessionId ||
|
|
21
|
+
config.conversationId ||
|
|
22
|
+
config.a2aContextId ||
|
|
23
|
+
config.queryTimeout) {
|
|
21
24
|
params.metadata = {};
|
|
22
25
|
if (config.sessionId) {
|
|
23
26
|
params.metadata.sessionId = config.sessionId;
|
|
24
27
|
}
|
|
28
|
+
if (config.conversationId) {
|
|
29
|
+
params.metadata.conversationId = config.conversationId;
|
|
30
|
+
}
|
|
25
31
|
if (config.queryTimeout) {
|
|
26
32
|
params.metadata.timeout = config.queryTimeout;
|
|
27
33
|
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -12,9 +12,11 @@ export interface ArkConfig {
|
|
|
12
12
|
chat?: ChatConfig;
|
|
13
13
|
marketplace?: MarketplaceConfig;
|
|
14
14
|
services?: {
|
|
15
|
-
|
|
15
|
+
reusePortForwards?: boolean;
|
|
16
|
+
[serviceName: string]: Partial<ArkService> | boolean | undefined;
|
|
16
17
|
};
|
|
17
18
|
queryTimeout?: string;
|
|
19
|
+
defaultExportTypes?: string[];
|
|
18
20
|
clusterInfo?: ClusterInfo;
|
|
19
21
|
}
|
|
20
22
|
/**
|
package/dist/lib/config.js
CHANGED
|
@@ -20,6 +20,9 @@ export function loadConfig() {
|
|
|
20
20
|
repoUrl: 'https://github.com/mckinsey/agents-at-scale-marketplace',
|
|
21
21
|
registry: 'oci://ghcr.io/mckinsey/agents-at-scale-marketplace/charts',
|
|
22
22
|
},
|
|
23
|
+
services: {
|
|
24
|
+
reusePortForwards: false,
|
|
25
|
+
},
|
|
23
26
|
};
|
|
24
27
|
// Load user config from home directory
|
|
25
28
|
const userConfigPath = path.join(os.homedir(), '.arkrc.yaml');
|
|
@@ -48,9 +51,7 @@ export function loadConfig() {
|
|
|
48
51
|
// Apply environment variable overrides
|
|
49
52
|
if (process.env.ARK_CHAT_STREAMING !== undefined) {
|
|
50
53
|
config.chat = config.chat || {};
|
|
51
|
-
config.chat.streaming =
|
|
52
|
-
process.env.ARK_CHAT_STREAMING === '1' ||
|
|
53
|
-
process.env.ARK_CHAT_STREAMING === 'true';
|
|
54
|
+
config.chat.streaming = process.env.ARK_CHAT_STREAMING === '1';
|
|
54
55
|
}
|
|
55
56
|
if (process.env.ARK_CHAT_OUTPUT_FORMAT !== undefined) {
|
|
56
57
|
config.chat = config.chat || {};
|
|
@@ -70,6 +71,11 @@ export function loadConfig() {
|
|
|
70
71
|
config.marketplace = config.marketplace || {};
|
|
71
72
|
config.marketplace.registry = process.env.ARK_MARKETPLACE_REGISTRY;
|
|
72
73
|
}
|
|
74
|
+
if (process.env.ARK_SERVICES_REUSE_PORT_FORWARDS !== undefined) {
|
|
75
|
+
config.services = config.services || {};
|
|
76
|
+
config.services.reusePortForwards =
|
|
77
|
+
process.env.ARK_SERVICES_REUSE_PORT_FORWARDS === '1';
|
|
78
|
+
}
|
|
73
79
|
return config;
|
|
74
80
|
}
|
|
75
81
|
/**
|
|
@@ -96,16 +102,24 @@ function mergeConfig(target, source) {
|
|
|
96
102
|
}
|
|
97
103
|
if (source.services) {
|
|
98
104
|
target.services = target.services || {};
|
|
105
|
+
if (source.services.reusePortForwards !== undefined) {
|
|
106
|
+
target.services.reusePortForwards = source.services.reusePortForwards;
|
|
107
|
+
}
|
|
99
108
|
for (const [serviceName, overrides] of Object.entries(source.services)) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
if (serviceName !== 'reusePortForwards' && typeof overrides === 'object') {
|
|
110
|
+
target.services[serviceName] = {
|
|
111
|
+
...target.services[serviceName],
|
|
112
|
+
...overrides,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
104
115
|
}
|
|
105
116
|
}
|
|
106
117
|
if (source.queryTimeout !== undefined) {
|
|
107
118
|
target.queryTimeout = source.queryTimeout;
|
|
108
119
|
}
|
|
120
|
+
if (source.defaultExportTypes) {
|
|
121
|
+
target.defaultExportTypes = source.defaultExportTypes;
|
|
122
|
+
}
|
|
109
123
|
}
|
|
110
124
|
/**
|
|
111
125
|
* Get the paths checked for config files
|
package/dist/lib/config.spec.js
CHANGED
|
@@ -39,6 +39,9 @@ describe('config', () => {
|
|
|
39
39
|
repoUrl: 'https://github.com/mckinsey/agents-at-scale-marketplace',
|
|
40
40
|
registry: 'oci://ghcr.io/mckinsey/agents-at-scale-marketplace/charts',
|
|
41
41
|
},
|
|
42
|
+
services: {
|
|
43
|
+
reusePortForwards: false,
|
|
44
|
+
},
|
|
42
45
|
});
|
|
43
46
|
});
|
|
44
47
|
it('loads and merges configs in order: defaults, user, project', () => {
|
|
@@ -77,6 +80,13 @@ describe('config', () => {
|
|
|
77
80
|
const config = loadConfig();
|
|
78
81
|
expect(config.queryTimeout).toBe('30m');
|
|
79
82
|
});
|
|
83
|
+
it('loads defaultExportTypes from config file', () => {
|
|
84
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
85
|
+
mockFs.readFileSync.mockReturnValue('yaml');
|
|
86
|
+
mockYaml.parse.mockReturnValue({ defaultExportTypes: ['agents', 'teams'] });
|
|
87
|
+
const config = loadConfig();
|
|
88
|
+
expect(config.defaultExportTypes).toEqual(['agents', 'teams']);
|
|
89
|
+
});
|
|
80
90
|
it('ARK_QUERY_TIMEOUT environment variable overrides config', () => {
|
|
81
91
|
mockFs.existsSync.mockReturnValue(true);
|
|
82
92
|
mockFs.readFileSync.mockReturnValue('yaml');
|
package/dist/lib/executeQuery.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ExitCodes } from './errors.js';
|
|
|
8
8
|
import { ArkApiProxy } from './arkApiProxy.js';
|
|
9
9
|
import { ChatClient } from './chatClient.js';
|
|
10
10
|
import { watchEventsLive } from './kubectl.js';
|
|
11
|
+
import { loadConfig } from './config.js';
|
|
11
12
|
export async function executeQuery(options) {
|
|
12
13
|
if (options.outputFormat) {
|
|
13
14
|
return executeQueryWithFormat(options);
|
|
@@ -15,7 +16,8 @@ export async function executeQuery(options) {
|
|
|
15
16
|
let arkApiProxy;
|
|
16
17
|
const spinner = ora('Connecting to Ark API...').start();
|
|
17
18
|
try {
|
|
18
|
-
|
|
19
|
+
const config = loadConfig();
|
|
20
|
+
arkApiProxy = new ArkApiProxy(undefined, config.services?.reusePortForwards ?? false);
|
|
19
21
|
const arkApiClient = await arkApiProxy.start();
|
|
20
22
|
const chatClient = new ChatClient(arkApiClient);
|
|
21
23
|
spinner.text = 'Executing query...';
|
|
@@ -29,7 +31,13 @@ export async function executeQuery(options) {
|
|
|
29
31
|
let headerShown = false;
|
|
30
32
|
let firstOutput = true;
|
|
31
33
|
const sessionId = options.sessionId || process.env.ARK_SESSION_ID;
|
|
32
|
-
|
|
34
|
+
const conversationId = options.conversationId || process.env.ARK_CONVERSATION_ID;
|
|
35
|
+
await chatClient.sendMessage(targetId, messages, {
|
|
36
|
+
streamingEnabled: true,
|
|
37
|
+
sessionId,
|
|
38
|
+
conversationId,
|
|
39
|
+
queryTimeout: options.timeout,
|
|
40
|
+
}, (chunk, toolCalls, arkMetadata) => {
|
|
33
41
|
if (firstOutput) {
|
|
34
42
|
spinner.stop();
|
|
35
43
|
firstOutput = false;
|
|
@@ -105,12 +113,13 @@ async function executeQueryWithFormat(options) {
|
|
|
105
113
|
...((options.sessionId || process.env.ARK_SESSION_ID) && {
|
|
106
114
|
sessionId: options.sessionId || process.env.ARK_SESSION_ID,
|
|
107
115
|
}),
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
...((options.conversationId || process.env.ARK_CONVERSATION_ID) && {
|
|
117
|
+
conversationId: options.conversationId || process.env.ARK_CONVERSATION_ID,
|
|
118
|
+
}),
|
|
119
|
+
target: {
|
|
120
|
+
type: options.targetType,
|
|
121
|
+
name: options.targetName,
|
|
122
|
+
},
|
|
114
123
|
},
|
|
115
124
|
};
|
|
116
125
|
try {
|
package/dist/lib/kubectl.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ interface K8sResource {
|
|
|
6
6
|
}
|
|
7
7
|
export declare function getResource<T extends K8sResource>(resourceType: string, name: string): Promise<T>;
|
|
8
8
|
export declare function listResources<T extends K8sResource>(resourceType: string, options?: {
|
|
9
|
+
namespace?: string;
|
|
10
|
+
labels?: string;
|
|
9
11
|
sortBy?: string;
|
|
10
12
|
}): Promise<T[]>;
|
|
11
13
|
export declare function deleteResource(resourceType: string, name?: string, options?: {
|
package/dist/lib/kubectl.js
CHANGED
|
@@ -25,6 +25,12 @@ export async function listResources(resourceType, options) {
|
|
|
25
25
|
if (options?.sortBy) {
|
|
26
26
|
args.push(`--sort-by=${options.sortBy}`);
|
|
27
27
|
}
|
|
28
|
+
if (options?.namespace) {
|
|
29
|
+
args.push('-n', options.namespace);
|
|
30
|
+
}
|
|
31
|
+
if (options?.labels) {
|
|
32
|
+
args.push('-l', options.labels);
|
|
33
|
+
}
|
|
28
34
|
args.push('-o', 'json');
|
|
29
35
|
const result = await execa('kubectl', args, { stdio: 'pipe' });
|
|
30
36
|
const data = JSON.parse(result.stdout);
|
package/dist/lib/kubectl.spec.js
CHANGED
|
@@ -152,6 +152,22 @@ describe('kubectl', () => {
|
|
|
152
152
|
'json',
|
|
153
153
|
], { stdio: 'pipe' });
|
|
154
154
|
});
|
|
155
|
+
it('should pass namespace and label filters', async () => {
|
|
156
|
+
const result = await listResources('queries', {
|
|
157
|
+
labels: 'app=test',
|
|
158
|
+
namespace: 'foo'
|
|
159
|
+
});
|
|
160
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', [
|
|
161
|
+
'get',
|
|
162
|
+
'queries',
|
|
163
|
+
'-n',
|
|
164
|
+
'foo',
|
|
165
|
+
'-l',
|
|
166
|
+
'app=test',
|
|
167
|
+
'-o',
|
|
168
|
+
'json',
|
|
169
|
+
], { stdio: 'pipe' });
|
|
170
|
+
});
|
|
155
171
|
it('should handle kubectl errors when listing resources', async () => {
|
|
156
172
|
mockExeca.mockRejectedValue(new Error('kubectl connection error'));
|
|
157
173
|
await expect(listResources('queries')).rejects.toThrow('kubectl connection error');
|