@agents-at-scale/ark 0.1.45 → 0.1.47

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.
Files changed (49) hide show
  1. package/README.md +2 -0
  2. package/dist/arkServices.js +13 -1
  3. package/dist/arkServices.spec.js +6 -0
  4. package/dist/commands/agents/index.d.ts +1 -1
  5. package/dist/commands/agents/index.js +4 -2
  6. package/dist/commands/completion/index.js +2 -4
  7. package/dist/commands/install/index.js +20 -10
  8. package/dist/commands/marketplace/index.js +51 -23
  9. package/dist/commands/marketplace/index.spec.d.ts +1 -0
  10. package/dist/commands/marketplace/index.spec.js +88 -0
  11. package/dist/commands/models/index.d.ts +1 -1
  12. package/dist/commands/models/index.js +4 -2
  13. package/dist/commands/query/index.d.ts +1 -1
  14. package/dist/commands/query/index.js +4 -2
  15. package/dist/commands/status/index.js +7 -2
  16. package/dist/commands/teams/index.d.ts +1 -1
  17. package/dist/commands/teams/index.js +4 -2
  18. package/dist/commands/uninstall/index.js +20 -10
  19. package/dist/lib/chatClient.d.ts +1 -0
  20. package/dist/lib/chatClient.js +4 -2
  21. package/dist/lib/config.d.ts +14 -0
  22. package/dist/lib/config.js +41 -0
  23. package/dist/lib/config.spec.js +93 -0
  24. package/dist/lib/constants.d.ts +3 -0
  25. package/dist/lib/constants.js +5 -0
  26. package/dist/lib/executeQuery.js +9 -3
  27. package/dist/lib/executeQuery.spec.js +4 -1
  28. package/dist/lib/kubectl.d.ts +1 -0
  29. package/dist/lib/kubectl.js +62 -0
  30. package/dist/lib/marketplaceFetcher.d.ts +6 -0
  31. package/dist/lib/marketplaceFetcher.js +80 -0
  32. package/dist/lib/marketplaceFetcher.spec.d.ts +1 -0
  33. package/dist/lib/marketplaceFetcher.spec.js +225 -0
  34. package/dist/marketplaceServices.d.ts +15 -6
  35. package/dist/marketplaceServices.js +38 -40
  36. package/dist/marketplaceServices.spec.d.ts +1 -0
  37. package/dist/marketplaceServices.spec.js +74 -0
  38. package/dist/types/marketplace.d.ts +37 -0
  39. package/dist/types/marketplace.js +1 -0
  40. package/dist/ui/AgentSelector.d.ts +8 -0
  41. package/dist/ui/AgentSelector.js +53 -0
  42. package/dist/ui/ModelSelector.d.ts +8 -0
  43. package/dist/ui/ModelSelector.js +53 -0
  44. package/dist/ui/TeamSelector.d.ts +8 -0
  45. package/dist/ui/TeamSelector.js +55 -0
  46. package/dist/ui/ToolSelector.d.ts +8 -0
  47. package/dist/ui/ToolSelector.js +53 -0
  48. package/package.json +1 -1
  49. package/templates/marketplace/marketplace.json.example +59 -0
@@ -35,6 +35,10 @@ describe('config', () => {
35
35
  streaming: true,
36
36
  outputFormat: 'text',
37
37
  },
38
+ marketplace: {
39
+ repoUrl: 'https://github.com/mckinsey/agents-at-scale-marketplace',
40
+ registry: 'oci://ghcr.io/mckinsey/agents-at-scale-marketplace/charts',
41
+ },
38
42
  });
39
43
  });
40
44
  it('loads and merges configs in order: defaults, user, project', () => {
@@ -66,6 +70,21 @@ describe('config', () => {
66
70
  expect(config.chat?.streaming).toBe(true);
67
71
  expect(config.chat?.outputFormat).toBe('markdown');
68
72
  });
73
+ it('loads queryTimeout from config file', () => {
74
+ mockFs.existsSync.mockReturnValue(true);
75
+ mockFs.readFileSync.mockReturnValue('yaml');
76
+ mockYaml.parse.mockReturnValue({ queryTimeout: '30m' });
77
+ const config = loadConfig();
78
+ expect(config.queryTimeout).toBe('30m');
79
+ });
80
+ it('ARK_QUERY_TIMEOUT environment variable overrides config', () => {
81
+ mockFs.existsSync.mockReturnValue(true);
82
+ mockFs.readFileSync.mockReturnValue('yaml');
83
+ mockYaml.parse.mockReturnValue({ queryTimeout: '5m' });
84
+ process.env.ARK_QUERY_TIMEOUT = '1h';
85
+ const config = loadConfig();
86
+ expect(config.queryTimeout).toBe('1h');
87
+ });
69
88
  it('throws error for invalid YAML', () => {
70
89
  const userConfigPath = path.join(os.homedir(), '.arkrc.yaml');
71
90
  mockFs.existsSync.mockImplementation((path) => path === userConfigPath);
@@ -96,4 +115,78 @@ describe('config', () => {
96
115
  expect(mockYaml.stringify).toHaveBeenCalledWith(config);
97
116
  expect(result).toBe('formatted');
98
117
  });
118
+ it('loads marketplace config from config file', () => {
119
+ mockFs.existsSync.mockReturnValue(true);
120
+ mockFs.readFileSync.mockReturnValue('yaml');
121
+ mockYaml.parse.mockReturnValue({
122
+ marketplace: {
123
+ repoUrl: 'https://example.com/my-marketplace',
124
+ registry: 'oci://example.com/charts',
125
+ },
126
+ });
127
+ const config = loadConfig();
128
+ expect(config.marketplace?.repoUrl).toBe('https://example.com/my-marketplace');
129
+ expect(config.marketplace?.registry).toBe('oci://example.com/charts');
130
+ });
131
+ it('marketplace environment variables override config', () => {
132
+ mockFs.existsSync.mockReturnValue(true);
133
+ mockFs.readFileSync.mockReturnValue('yaml');
134
+ mockYaml.parse.mockReturnValue({
135
+ marketplace: {
136
+ repoUrl: 'https://example.com/my-marketplace',
137
+ registry: 'oci://example.com/charts',
138
+ },
139
+ });
140
+ process.env.ARK_MARKETPLACE_REPO_URL = 'https://custom.com/marketplace';
141
+ process.env.ARK_MARKETPLACE_REGISTRY = 'oci://custom.com/charts';
142
+ const config = loadConfig();
143
+ expect(config.marketplace?.repoUrl).toBe('https://custom.com/marketplace');
144
+ expect(config.marketplace?.registry).toBe('oci://custom.com/charts');
145
+ });
146
+ });
147
+ describe('marketplace helpers', () => {
148
+ const originalEnv = process.env;
149
+ beforeEach(() => {
150
+ jest.clearAllMocks();
151
+ process.env = { ...originalEnv };
152
+ });
153
+ afterEach(() => {
154
+ process.env = originalEnv;
155
+ });
156
+ it('getMarketplaceRepoUrl returns custom config value', async () => {
157
+ mockFs.existsSync.mockReturnValue(true);
158
+ mockFs.readFileSync.mockReturnValue('yaml');
159
+ mockYaml.parse.mockReturnValue({
160
+ marketplace: {
161
+ repoUrl: 'https://custom-repo.com/marketplace',
162
+ },
163
+ });
164
+ jest.resetModules();
165
+ const { getMarketplaceRepoUrl } = await import('./config.js');
166
+ expect(getMarketplaceRepoUrl()).toBe('https://custom-repo.com/marketplace');
167
+ });
168
+ it('getMarketplaceRepoUrl returns default value', async () => {
169
+ mockFs.existsSync.mockReturnValue(false);
170
+ jest.resetModules();
171
+ const { getMarketplaceRepoUrl } = await import('./config.js');
172
+ expect(getMarketplaceRepoUrl()).toBe('https://github.com/mckinsey/agents-at-scale-marketplace');
173
+ });
174
+ it('getMarketplaceRegistry returns custom config value', async () => {
175
+ mockFs.existsSync.mockReturnValue(true);
176
+ mockFs.readFileSync.mockReturnValue('yaml');
177
+ mockYaml.parse.mockReturnValue({
178
+ marketplace: {
179
+ registry: 'oci://custom-registry.com/charts',
180
+ },
181
+ });
182
+ jest.resetModules();
183
+ const { getMarketplaceRegistry } = await import('./config.js');
184
+ expect(getMarketplaceRegistry()).toBe('oci://custom-registry.com/charts');
185
+ });
186
+ it('getMarketplaceRegistry returns default value', async () => {
187
+ mockFs.existsSync.mockReturnValue(false);
188
+ jest.resetModules();
189
+ const { getMarketplaceRegistry } = await import('./config.js');
190
+ expect(getMarketplaceRegistry()).toBe('oci://ghcr.io/mckinsey/agents-at-scale-marketplace/charts');
191
+ });
99
192
  });
@@ -1,3 +1,6 @@
1
1
  export declare const QUERY_ANNOTATIONS: {
2
2
  readonly A2A_CONTEXT_ID: "ark.mckinsey.com/a2a-context-id";
3
3
  };
4
+ export declare const EVENT_ANNOTATIONS: {
5
+ readonly EVENT_DATA: "ark.mckinsey.com/event-data";
6
+ };
@@ -6,3 +6,8 @@ export const QUERY_ANNOTATIONS = {
6
6
  // A2A context ID annotation (goes to K8s annotations)
7
7
  A2A_CONTEXT_ID: `${ARK_PREFIX}a2a-context-id`,
8
8
  };
9
+ // Event annotation constants
10
+ export const EVENT_ANNOTATIONS = {
11
+ // Event data annotation for structured event data
12
+ EVENT_DATA: `${ARK_PREFIX}event-data`,
13
+ };
@@ -7,6 +7,7 @@ import chalk from 'chalk';
7
7
  import { ExitCodes } from './errors.js';
8
8
  import { ArkApiProxy } from './arkApiProxy.js';
9
9
  import { ChatClient } from './chatClient.js';
10
+ import { watchEventsLive } from './kubectl.js';
10
11
  export async function executeQuery(options) {
11
12
  if (options.outputFormat) {
12
13
  return executeQueryWithFormat(options);
@@ -27,9 +28,8 @@ export async function executeQuery(options) {
27
28
  let lastAgentName;
28
29
  let headerShown = false;
29
30
  let firstOutput = true;
30
- // Get sessionId from option or environment variable
31
31
  const sessionId = options.sessionId || process.env.ARK_SESSION_ID;
32
- await chatClient.sendMessage(targetId, messages, { streamingEnabled: true, sessionId }, (chunk, toolCalls, arkMetadata) => {
32
+ await chatClient.sendMessage(targetId, messages, { streamingEnabled: true, sessionId, queryTimeout: options.timeout }, (chunk, toolCalls, arkMetadata) => {
33
33
  if (firstOutput) {
34
34
  spinner.stop();
35
35
  firstOutput = false;
@@ -118,6 +118,12 @@ async function executeQueryWithFormat(options) {
118
118
  input: JSON.stringify(queryManifest),
119
119
  stdio: ['pipe', 'pipe', 'pipe'],
120
120
  });
121
+ // Give Kubernetes a moment to process the resource before watching
122
+ await new Promise((resolve) => setTimeout(resolve, 100));
123
+ if (options.outputFormat === 'events') {
124
+ await watchEventsLive(queryName);
125
+ return;
126
+ }
121
127
  const timeoutSeconds = 300;
122
128
  await execa('kubectl', [
123
129
  'wait',
@@ -134,7 +140,7 @@ async function executeQueryWithFormat(options) {
134
140
  console.log(stdout);
135
141
  }
136
142
  else {
137
- console.error(chalk.red(`Invalid output format: ${options.outputFormat}. Use: yaml, json, or name`));
143
+ console.error(chalk.red(`Invalid output format: ${options.outputFormat}. Use: yaml, json, name, or events`));
138
144
  process.exit(ExitCodes.CliError);
139
145
  }
140
146
  }
@@ -206,9 +206,12 @@ describe('executeQuery', () => {
206
206
  expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringMatching(/cli-query-\d+/));
207
207
  });
208
208
  it('should include sessionId in query manifest when outputFormat is specified', async () => {
209
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
209
210
  let appliedManifest = '';
210
211
  mockExeca.mockImplementation(async (command, args) => {
211
- if (args.includes('apply') && args.includes('-f') && args.includes('-')) {
212
+ if (args.includes('apply') &&
213
+ args.includes('-f') &&
214
+ args.includes('-')) {
212
215
  // Capture the stdin input
213
216
  const stdinIndex = args.indexOf('-');
214
217
  if (stdinIndex >= 0 && args[stdinIndex + 1]) {
@@ -12,4 +12,5 @@ export declare function deleteResource(resourceType: string, name?: string, opti
12
12
  all?: boolean;
13
13
  }): Promise<void>;
14
14
  export declare function replaceResource<T extends K8sResource>(resource: T): Promise<T>;
15
+ export declare function watchEventsLive(queryName: string): Promise<void>;
15
16
  export {};
@@ -1,4 +1,6 @@
1
1
  import { execa } from 'execa';
2
+ import chalk from 'chalk';
3
+ import { EVENT_ANNOTATIONS } from './constants.js';
2
4
  export async function getResource(resourceType, name) {
3
5
  if (name === '@latest') {
4
6
  const result = await execa('kubectl', [
@@ -45,3 +47,63 @@ export async function replaceResource(resource) {
45
47
  });
46
48
  return JSON.parse(result.stdout);
47
49
  }
50
+ export async function watchEventsLive(queryName) {
51
+ const seenEvents = new Set();
52
+ const pollEvents = async () => {
53
+ try {
54
+ const { stdout } = await execa('kubectl', [
55
+ 'get',
56
+ 'events',
57
+ '--field-selector',
58
+ `involvedObject.name=${queryName}`,
59
+ '-o',
60
+ 'json',
61
+ ]);
62
+ const eventsData = JSON.parse(stdout);
63
+ for (const event of eventsData.items || []) {
64
+ const eventId = event.metadata?.uid;
65
+ if (eventId && !seenEvents.has(eventId)) {
66
+ seenEvents.add(eventId);
67
+ const annotations = event.metadata?.annotations || {};
68
+ const eventData = annotations[EVENT_ANNOTATIONS.EVENT_DATA];
69
+ if (eventData) {
70
+ const now = new Date();
71
+ const hours = now.getHours().toString().padStart(2, '0');
72
+ const minutes = now.getMinutes().toString().padStart(2, '0');
73
+ const seconds = now.getSeconds().toString().padStart(2, '0');
74
+ const millis = now.getMilliseconds().toString().padStart(3, '0');
75
+ const timestamp = `${hours}:${minutes}:${seconds}.${millis}`;
76
+ const reason = event.reason || 'Unknown';
77
+ const eventType = event.type || 'Normal';
78
+ const colorCode = eventType === 'Normal' ? 32 : eventType === 'Warning' ? 33 : 31;
79
+ console.log(`${timestamp} \x1b[${colorCode}m${reason}\x1b[0m ${eventData}`);
80
+ }
81
+ }
82
+ }
83
+ // eslint-disable-next-line no-empty, @typescript-eslint/no-unused-vars
84
+ }
85
+ catch (error) { }
86
+ };
87
+ const pollInterval = setInterval(pollEvents, 200);
88
+ const timeoutSeconds = 300;
89
+ const waitProcess = execa('kubectl', [
90
+ 'wait',
91
+ '--for=condition=Completed',
92
+ `query/${queryName}`,
93
+ `--timeout=${timeoutSeconds}s`,
94
+ ], {
95
+ timeout: timeoutSeconds * 1000,
96
+ });
97
+ try {
98
+ await waitProcess;
99
+ await pollEvents();
100
+ await new Promise((resolve) => setTimeout(resolve, 200));
101
+ await pollEvents();
102
+ }
103
+ catch (error) {
104
+ console.error(chalk.red('Query wait failed:', error instanceof Error ? error.message : 'Unknown error'));
105
+ }
106
+ finally {
107
+ clearInterval(pollInterval);
108
+ }
109
+ }
@@ -0,0 +1,6 @@
1
+ import type { ArkService, ServiceCollection } from '../types/arkService.js';
2
+ import type { AnthropicMarketplaceManifest, AnthropicMarketplaceItem } from '../types/marketplace.js';
3
+ export declare function fetchMarketplaceManifest(): Promise<AnthropicMarketplaceManifest | null>;
4
+ export declare function mapMarketplaceItemToArkService(item: AnthropicMarketplaceItem, registry?: string): ArkService;
5
+ export declare function getMarketplaceServicesFromManifest(): Promise<ServiceCollection | null>;
6
+ export declare function getMarketplaceAgentsFromManifest(): Promise<ServiceCollection | null>;
@@ -0,0 +1,80 @@
1
+ import axios from 'axios';
2
+ import { getMarketplaceRepoUrl, getMarketplaceRegistry } from './config.js';
3
+ export async function fetchMarketplaceManifest() {
4
+ const repoUrl = getMarketplaceRepoUrl();
5
+ const manifestUrl = `${repoUrl}/raw/main/marketplace.json`;
6
+ try {
7
+ const response = await axios.get(manifestUrl, {
8
+ timeout: 10000,
9
+ headers: {
10
+ Accept: 'application/json',
11
+ },
12
+ });
13
+ return response.data;
14
+ }
15
+ catch (error) {
16
+ if (axios.isAxiosError(error)) {
17
+ if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
18
+ return null;
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ }
24
+ export function mapMarketplaceItemToArkService(item, registry) {
25
+ const defaultRegistry = registry || getMarketplaceRegistry();
26
+ const serviceName = item.name
27
+ .toLowerCase()
28
+ .replace(/[^a-z0-9-]/g, '-')
29
+ .replace(/^-+|-+$/g, '');
30
+ const chartPath = item.ark?.chartPath || `${defaultRegistry}/${serviceName}`;
31
+ return {
32
+ name: serviceName,
33
+ helmReleaseName: item.ark?.helmReleaseName || serviceName,
34
+ description: item.description,
35
+ enabled: true,
36
+ category: 'marketplace',
37
+ namespace: item.ark?.namespace || serviceName,
38
+ chartPath,
39
+ installArgs: item.ark?.installArgs || ['--create-namespace'],
40
+ k8sServiceName: item.ark?.k8sServiceName || serviceName,
41
+ k8sServicePort: item.ark?.k8sServicePort,
42
+ k8sPortForwardLocalPort: item.ark?.k8sPortForwardLocalPort,
43
+ k8sDeploymentName: item.ark?.k8sDeploymentName || serviceName,
44
+ k8sDevDeploymentName: item.ark?.k8sDevDeploymentName,
45
+ };
46
+ }
47
+ export async function getMarketplaceServicesFromManifest() {
48
+ const manifest = await fetchMarketplaceManifest();
49
+ if (!manifest || !manifest.items) {
50
+ return null;
51
+ }
52
+ const services = {};
53
+ for (const item of manifest.items) {
54
+ if (item.ark && item.type === 'service') {
55
+ const serviceName = item.name
56
+ .toLowerCase()
57
+ .replace(/[^a-z0-9-]/g, '-')
58
+ .replace(/^-+|-+$/g, '');
59
+ services[serviceName] = mapMarketplaceItemToArkService(item);
60
+ }
61
+ }
62
+ return Object.keys(services).length > 0 ? services : null;
63
+ }
64
+ export async function getMarketplaceAgentsFromManifest() {
65
+ const manifest = await fetchMarketplaceManifest();
66
+ if (!manifest || !manifest.items) {
67
+ return null;
68
+ }
69
+ const agents = {};
70
+ for (const item of manifest.items) {
71
+ if (item.ark && item.type === 'agent') {
72
+ const agentName = item.name
73
+ .toLowerCase()
74
+ .replace(/[^a-z0-9-]/g, '-')
75
+ .replace(/^-+|-+$/g, '');
76
+ agents[agentName] = mapMarketplaceItemToArkService(item);
77
+ }
78
+ }
79
+ return Object.keys(agents).length > 0 ? agents : null;
80
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,225 @@
1
+ import { jest } from '@jest/globals';
2
+ const mockAxiosGet = jest.fn();
3
+ jest.unstable_mockModule('axios', () => ({
4
+ default: {
5
+ get: mockAxiosGet,
6
+ isAxiosError: (error) => {
7
+ return (typeof error === 'object' &&
8
+ error !== null &&
9
+ 'isAxiosError' in error &&
10
+ error.isAxiosError === true);
11
+ },
12
+ },
13
+ }));
14
+ const mockGetMarketplaceRepoUrl = jest.fn();
15
+ const mockGetMarketplaceRegistry = jest.fn();
16
+ jest.unstable_mockModule('./config.js', () => ({
17
+ getMarketplaceRepoUrl: mockGetMarketplaceRepoUrl,
18
+ getMarketplaceRegistry: mockGetMarketplaceRegistry,
19
+ }));
20
+ const { fetchMarketplaceManifest, mapMarketplaceItemToArkService, getMarketplaceServicesFromManifest, } = await import('./marketplaceFetcher.js');
21
+ describe('marketplaceFetcher', () => {
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+ mockAxiosGet.mockClear();
25
+ mockGetMarketplaceRepoUrl.mockReturnValue('https://test-repo.example.com/marketplace');
26
+ mockGetMarketplaceRegistry.mockReturnValue('oci://test-registry.example.com/charts');
27
+ });
28
+ describe('fetchMarketplaceManifest', () => {
29
+ it('fetches and returns manifest successfully', async () => {
30
+ const mockManifest = {
31
+ version: '1.0.0',
32
+ marketplace: 'ARK Marketplace',
33
+ items: [
34
+ {
35
+ name: 'test-service',
36
+ description: 'Test service',
37
+ ark: {
38
+ chartPath: 'oci://registry/test-service',
39
+ namespace: 'test',
40
+ },
41
+ },
42
+ ],
43
+ };
44
+ mockAxiosGet.mockResolvedValue({
45
+ data: mockManifest,
46
+ status: 200,
47
+ statusText: 'OK',
48
+ headers: {},
49
+ config: {},
50
+ });
51
+ const result = await fetchMarketplaceManifest();
52
+ expect(result).toEqual(mockManifest);
53
+ expect(mockAxiosGet).toHaveBeenCalledWith(expect.stringContaining('marketplace.json'), expect.objectContaining({
54
+ timeout: 10000,
55
+ headers: { Accept: 'application/json' },
56
+ }));
57
+ });
58
+ it('returns null on network error', async () => {
59
+ const networkError = new Error('Network error');
60
+ networkError.code = 'ENOTFOUND';
61
+ networkError.isAxiosError = true;
62
+ mockAxiosGet.mockRejectedValue(networkError);
63
+ const result = await fetchMarketplaceManifest();
64
+ expect(result).toBeNull();
65
+ });
66
+ it('returns null on connection refused', async () => {
67
+ const connectionError = Object.assign(new Error('Connection refused'), {
68
+ code: 'ECONNREFUSED',
69
+ isAxiosError: true,
70
+ });
71
+ mockAxiosGet.mockRejectedValue(connectionError);
72
+ const result = await fetchMarketplaceManifest();
73
+ expect(result).toBeNull();
74
+ });
75
+ });
76
+ describe('mapMarketplaceItemToArkService', () => {
77
+ it('maps marketplace item to ARK service correctly', () => {
78
+ const item = {
79
+ name: 'test-service',
80
+ description: 'Test description',
81
+ ark: {
82
+ chartPath: 'oci://registry/test',
83
+ namespace: 'test-ns',
84
+ helmReleaseName: 'test-release',
85
+ installArgs: ['--create-namespace'],
86
+ k8sServiceName: 'test-svc',
87
+ k8sServicePort: 8080,
88
+ k8sDeploymentName: 'test-deploy',
89
+ },
90
+ };
91
+ const result = mapMarketplaceItemToArkService(item);
92
+ expect(result).toEqual({
93
+ name: 'test-service',
94
+ helmReleaseName: 'test-release',
95
+ description: 'Test description',
96
+ enabled: true,
97
+ category: 'marketplace',
98
+ namespace: 'test-ns',
99
+ chartPath: 'oci://registry/test',
100
+ installArgs: ['--create-namespace'],
101
+ k8sServiceName: 'test-svc',
102
+ k8sServicePort: 8080,
103
+ k8sDeploymentName: 'test-deploy',
104
+ });
105
+ });
106
+ it('uses defaults when ark fields are missing', () => {
107
+ const item = {
108
+ name: 'simple-service',
109
+ description: 'Simple service',
110
+ ark: {},
111
+ };
112
+ const result = mapMarketplaceItemToArkService(item);
113
+ expect(result.name).toBe('simple-service');
114
+ expect(result.helmReleaseName).toBe('simple-service');
115
+ expect(result.namespace).toBe('simple-service');
116
+ expect(result.chartPath).toContain('simple-service');
117
+ });
118
+ it('sanitizes service name', () => {
119
+ const item = {
120
+ name: 'Test Service 123!',
121
+ description: 'Test',
122
+ ark: {},
123
+ };
124
+ const result = mapMarketplaceItemToArkService(item);
125
+ expect(result.name).toBe('test-service-123');
126
+ });
127
+ });
128
+ describe('getMarketplaceServicesFromManifest', () => {
129
+ it('converts manifest items to service collection', async () => {
130
+ const mockManifest = {
131
+ version: '1.0.0',
132
+ marketplace: 'ARK Marketplace',
133
+ items: [
134
+ {
135
+ name: 'service1',
136
+ description: 'Service 1',
137
+ type: 'service',
138
+ ark: {
139
+ chartPath: 'oci://registry/service1',
140
+ namespace: 'ns1',
141
+ },
142
+ },
143
+ {
144
+ name: 'service2',
145
+ description: 'Service 2',
146
+ type: 'service',
147
+ ark: {
148
+ chartPath: 'oci://registry/service2',
149
+ namespace: 'ns2',
150
+ },
151
+ },
152
+ ],
153
+ };
154
+ mockAxiosGet.mockResolvedValue({
155
+ data: mockManifest,
156
+ status: 200,
157
+ statusText: 'OK',
158
+ headers: {},
159
+ config: {},
160
+ });
161
+ const result = await getMarketplaceServicesFromManifest();
162
+ expect(result).not.toBeNull();
163
+ expect(result?.['service1']).toBeDefined();
164
+ expect(result?.['service2']).toBeDefined();
165
+ expect(result?.['service1']?.description).toBe('Service 1');
166
+ expect(result?.['service2']?.description).toBe('Service 2');
167
+ });
168
+ it('filters out items without ark field', async () => {
169
+ const mockManifest = {
170
+ version: '1.0.0',
171
+ marketplace: 'ARK Marketplace',
172
+ items: [
173
+ {
174
+ name: 'service1',
175
+ description: 'Service 1',
176
+ type: 'service',
177
+ ark: {
178
+ chartPath: 'oci://registry/service1',
179
+ },
180
+ },
181
+ {
182
+ name: 'service2',
183
+ description: 'Service 2',
184
+ type: 'service',
185
+ },
186
+ ],
187
+ };
188
+ mockAxiosGet.mockResolvedValue({
189
+ data: mockManifest,
190
+ status: 200,
191
+ statusText: 'OK',
192
+ headers: {},
193
+ config: {},
194
+ });
195
+ const result = await getMarketplaceServicesFromManifest();
196
+ expect(result).not.toBeNull();
197
+ expect(result?.['service1']).toBeDefined();
198
+ expect(result?.['service2']).toBeUndefined();
199
+ });
200
+ it('returns null when manifest fetch fails', async () => {
201
+ const networkError = new Error('Network error');
202
+ networkError.code = 'ENOTFOUND';
203
+ networkError.isAxiosError = true;
204
+ mockAxiosGet.mockRejectedValue(networkError);
205
+ const result = await getMarketplaceServicesFromManifest();
206
+ expect(result).toBeNull();
207
+ });
208
+ it('returns null when manifest has no items', async () => {
209
+ const mockManifest = {
210
+ version: '1.0.0',
211
+ marketplace: 'ARK Marketplace',
212
+ items: [],
213
+ };
214
+ mockAxiosGet.mockResolvedValue({
215
+ data: mockManifest,
216
+ status: 200,
217
+ statusText: 'OK',
218
+ headers: {},
219
+ config: {},
220
+ });
221
+ const result = await getMarketplaceServicesFromManifest();
222
+ expect(result).toBeNull();
223
+ });
224
+ });
225
+ });
@@ -2,14 +2,23 @@
2
2
  * Marketplace service definitions for external ARK marketplace resources
3
3
  * Repository: https://github.com/mckinsey/agents-at-scale-marketplace
4
4
  * Charts are installed from the public OCI registry
5
+ *
6
+ * Supports Anthropic Marketplace JSON format for dynamic enumeration
5
7
  */
6
8
  import type { ArkService, ServiceCollection } from './types/arkService.js';
7
9
  /**
8
- * Available marketplace services
9
- * Charts are published to: oci://ghcr.io/mckinsey/agents-at-scale-marketplace/charts
10
+ * Get all marketplace services, fetching from marketplace.json
11
+ * Returns null if marketplace is unavailable
10
12
  */
11
- export declare const marketplaceServices: ServiceCollection;
12
- export declare function getMarketplaceService(name: string): ArkService | undefined;
13
- export declare function getAllMarketplaceServices(): ServiceCollection;
13
+ export declare function getAllMarketplaceServices(): Promise<ServiceCollection | null>;
14
+ /**
15
+ * Get all marketplace agents, fetching from marketplace.json
16
+ * Returns null if marketplace is unavailable
17
+ */
18
+ export declare function getAllMarketplaceAgents(): Promise<ServiceCollection | null>;
19
+ /**
20
+ * Get a marketplace item by path (supports both services and agents)
21
+ * Returns null if marketplace is unavailable
22
+ */
23
+ export declare function getMarketplaceItem(path: string): Promise<ArkService | undefined | null>;
14
24
  export declare function isMarketplaceService(name: string): boolean;
15
- export declare function extractMarketplaceServiceName(path: string): string;