@zhive/cli 0.6.0 → 0.6.2

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.
@@ -1,40 +1,17 @@
1
1
  import { Command } from 'commander';
2
2
  import { z } from 'zod';
3
- import { HiveClient } from '@zhive/sdk';
3
+ import { HiveClient, TIMEFRAME_DURATION_MS } from '@zhive/sdk';
4
4
  import { styled, symbols } from '../../shared/theme.js';
5
5
  import { HIVE_API_URL } from '../../../shared/config/constant.js';
6
6
  import { findAgentByName, loadAgentCredentials, scanAgents } from '../../../shared/config/agent.js';
7
+ import { printZodError } from '../../shared/ validation.js';
7
8
  const CreateCommentOptionsSchema = z.object({
8
9
  agent: z.string().min(1),
9
10
  round: z.string().min(1),
10
- conviction: z.string().transform((val, ctx) => {
11
- const num = parseFloat(val);
12
- if (isNaN(num)) {
13
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Must be a number. Got: ${val}` });
14
- return z.NEVER;
15
- }
16
- if (num < -100 || num > 100) {
17
- ctx.addIssue({
18
- code: 'custom',
19
- message: `Must be between -100 and 100. Got: ${val}`,
20
- });
21
- return z.NEVER;
22
- }
23
- return num;
24
- }),
11
+ conviction: z.coerce.number().min(-100).max(100),
25
12
  text: z.string().min(1),
26
13
  token: z.string().min(1),
27
- duration: z.string().transform((val, ctx) => {
28
- const num = parseInt(val, 10);
29
- if (isNaN(num) || num <= 0) {
30
- ctx.addIssue({
31
- code: 'custom',
32
- message: `Must be a positive number. Got: ${val}`,
33
- });
34
- return z.NEVER;
35
- }
36
- return num;
37
- }),
14
+ timeframe: z.enum(['1h', '4h', '24h']),
38
15
  });
39
16
  export function createMegathreadCreateCommentCommand() {
40
17
  return new Command('create-comment')
@@ -44,17 +21,14 @@ export function createMegathreadCreateCommentCommand() {
44
21
  .requiredOption('--conviction <number>', 'Conviction score (-100 to 100)')
45
22
  .requiredOption('--text <text>', 'Comment text')
46
23
  .requiredOption('--token <tokenId>', 'Token/project ID')
47
- .requiredOption('--duration <ms>', 'Round duration in milliseconds')
24
+ .requiredOption('--timeframe <tf>', 'Timeframe (1h, 4h, 24h)')
48
25
  .action(async (options) => {
49
26
  const parseResult = CreateCommentOptionsSchema.safeParse(options);
50
27
  if (!parseResult.success) {
51
- const errors = parseResult.error.issues
52
- .map((e) => `${e.path.join('.')}: ${e.message}`)
53
- .join(', ');
54
- console.error(styled.red(`${symbols.cross} Validation error: ${errors}`));
28
+ printZodError(parseResult);
55
29
  process.exit(1);
56
30
  }
57
- const { agent: agentName, round: roundId, conviction, text, token, duration, } = parseResult.data;
31
+ const { agent: agentName, round: roundId, conviction, text, token, timeframe, } = parseResult.data;
58
32
  const agentConfig = await findAgentByName(agentName);
59
33
  if (!agentConfig) {
60
34
  const agents = await scanAgents();
@@ -73,6 +47,7 @@ export function createMegathreadCreateCommentCommand() {
73
47
  process.exit(1);
74
48
  }
75
49
  const client = new HiveClient(HIVE_API_URL, credentials.apiKey);
50
+ const duration = TIMEFRAME_DURATION_MS[timeframe];
76
51
  const payload = {
77
52
  text,
78
53
  conviction,
@@ -83,11 +83,11 @@ describe('createMegathreadCreateCommentCommand', () => {
83
83
  'Test comment',
84
84
  '--token',
85
85
  'bitcoin',
86
- '--duration',
87
- '3600000',
86
+ '--timeframe',
87
+ '1h',
88
88
  ], { from: 'user' })).rejects.toThrow('process.exit(1)');
89
- expect(consoleErrorOutput.join('\n')).toContain('conviction: Must be between -100 and 100');
90
- expect(consoleErrorOutput.join('\n')).toContain('Got: 150');
89
+ expect(consoleErrorOutput.join('\n')).toContain('conviction');
90
+ expect(consoleErrorOutput.join('\n')).toContain('100');
91
91
  });
92
92
  it('shows error when conviction is too low', async () => {
93
93
  const command = createMegathreadCreateCommentCommand();
@@ -102,11 +102,11 @@ describe('createMegathreadCreateCommentCommand', () => {
102
102
  'Test comment',
103
103
  '--token',
104
104
  'bitcoin',
105
- '--duration',
106
- '3600000',
105
+ '--timeframe',
106
+ '1h',
107
107
  ], { from: 'user' })).rejects.toThrow('process.exit(1)');
108
- expect(consoleErrorOutput.join('\n')).toContain('conviction: Must be between -100 and 100');
109
- expect(consoleErrorOutput.join('\n')).toContain('Got: -150');
108
+ expect(consoleErrorOutput.join('\n')).toContain('conviction');
109
+ expect(consoleErrorOutput.join('\n')).toContain('-100');
110
110
  });
111
111
  it('shows error when conviction is not a number', async () => {
112
112
  const command = createMegathreadCreateCommentCommand();
@@ -121,11 +121,11 @@ describe('createMegathreadCreateCommentCommand', () => {
121
121
  'Test comment',
122
122
  '--token',
123
123
  'bitcoin',
124
- '--duration',
125
- '3600000',
124
+ '--timeframe',
125
+ '1h',
126
126
  ], { from: 'user' })).rejects.toThrow('process.exit(1)');
127
- expect(consoleErrorOutput.join('\n')).toContain('conviction: Must be a number');
128
- expect(consoleErrorOutput.join('\n')).toContain('Got: abc');
127
+ expect(consoleErrorOutput.join('\n')).toContain('conviction');
128
+ expect(consoleErrorOutput.join('\n')).toContain('number');
129
129
  });
130
130
  it('accepts valid conviction at upper boundary', async () => {
131
131
  mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
@@ -142,8 +142,8 @@ describe('createMegathreadCreateCommentCommand', () => {
142
142
  'Test comment',
143
143
  '--token',
144
144
  'bitcoin',
145
- '--duration',
146
- '3600000',
145
+ '--timeframe',
146
+ '1h',
147
147
  ], { from: 'user' });
148
148
  expect(mockPostMegathreadComment).toHaveBeenCalledWith('round-123', {
149
149
  text: 'Test comment',
@@ -167,8 +167,8 @@ describe('createMegathreadCreateCommentCommand', () => {
167
167
  'Test comment',
168
168
  '--token',
169
169
  'bitcoin',
170
- '--duration',
171
- '3600000',
170
+ '--timeframe',
171
+ '1h',
172
172
  ], { from: 'user' });
173
173
  expect(mockPostMegathreadComment).toHaveBeenCalledWith('round-123', {
174
174
  text: 'Test comment',
@@ -192,8 +192,8 @@ describe('createMegathreadCreateCommentCommand', () => {
192
192
  'Test comment',
193
193
  '--token',
194
194
  'bitcoin',
195
- '--duration',
196
- '3600000',
195
+ '--timeframe',
196
+ '1h',
197
197
  ], { from: 'user' });
198
198
  expect(mockPostMegathreadComment).toHaveBeenCalledWith('round-123', {
199
199
  text: 'Test comment',
@@ -203,8 +203,8 @@ describe('createMegathreadCreateCommentCommand', () => {
203
203
  });
204
204
  });
205
205
  });
206
- describe('duration validation', () => {
207
- it('shows error when duration is negative', async () => {
206
+ describe('timeframe validation', () => {
207
+ it('shows error for invalid timeframe value', async () => {
208
208
  const command = createMegathreadCreateCommentCommand();
209
209
  await expect(command.parseAsync([
210
210
  '--agent',
@@ -217,15 +217,15 @@ describe('createMegathreadCreateCommentCommand', () => {
217
217
  'Test comment',
218
218
  '--token',
219
219
  'bitcoin',
220
- '--duration',
221
- '-1',
220
+ '--timeframe',
221
+ '2h',
222
222
  ], { from: 'user' })).rejects.toThrow('process.exit(1)');
223
- expect(consoleErrorOutput.join('\n')).toContain('duration: Must be a positive number');
224
- expect(consoleErrorOutput.join('\n')).toContain('Got: -1');
225
223
  });
226
- it('shows error when duration is zero', async () => {
224
+ it('accepts 1h timeframe', async () => {
225
+ mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
226
+ mockPostMegathreadComment.mockResolvedValue(undefined);
227
227
  const command = createMegathreadCreateCommentCommand();
228
- await expect(command.parseAsync([
228
+ await command.parseAsync([
229
229
  '--agent',
230
230
  'test-agent',
231
231
  '--round',
@@ -236,15 +236,21 @@ describe('createMegathreadCreateCommentCommand', () => {
236
236
  'Test comment',
237
237
  '--token',
238
238
  'bitcoin',
239
- '--duration',
240
- '0',
241
- ], { from: 'user' })).rejects.toThrow('process.exit(1)');
242
- expect(consoleErrorOutput.join('\n')).toContain('duration: Must be a positive number');
243
- expect(consoleErrorOutput.join('\n')).toContain('Got: 0');
239
+ '--timeframe',
240
+ '1h',
241
+ ], { from: 'user' });
242
+ expect(mockPostMegathreadComment).toHaveBeenCalledWith('round-123', {
243
+ text: 'Test comment',
244
+ conviction: 50,
245
+ tokenId: 'bitcoin',
246
+ roundDuration: 3600000,
247
+ });
244
248
  });
245
- it('shows error when duration is not a number', async () => {
249
+ it('accepts 4h timeframe', async () => {
250
+ mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
251
+ mockPostMegathreadComment.mockResolvedValue(undefined);
246
252
  const command = createMegathreadCreateCommentCommand();
247
- await expect(command.parseAsync([
253
+ await command.parseAsync([
248
254
  '--agent',
249
255
  'test-agent',
250
256
  '--round',
@@ -255,11 +261,40 @@ describe('createMegathreadCreateCommentCommand', () => {
255
261
  'Test comment',
256
262
  '--token',
257
263
  'bitcoin',
258
- '--duration',
259
- 'abc',
260
- ], { from: 'user' })).rejects.toThrow('process.exit(1)');
261
- expect(consoleErrorOutput.join('\n')).toContain('duration: Must be a positive number');
262
- expect(consoleErrorOutput.join('\n')).toContain('Got: abc');
264
+ '--timeframe',
265
+ '4h',
266
+ ], { from: 'user' });
267
+ expect(mockPostMegathreadComment).toHaveBeenCalledWith('round-123', {
268
+ text: 'Test comment',
269
+ conviction: 50,
270
+ tokenId: 'bitcoin',
271
+ roundDuration: 14400000,
272
+ });
273
+ });
274
+ it('accepts 24h timeframe', async () => {
275
+ mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
276
+ mockPostMegathreadComment.mockResolvedValue(undefined);
277
+ const command = createMegathreadCreateCommentCommand();
278
+ await command.parseAsync([
279
+ '--agent',
280
+ 'test-agent',
281
+ '--round',
282
+ 'round-123',
283
+ '--conviction',
284
+ '50',
285
+ '--text',
286
+ 'Test comment',
287
+ '--token',
288
+ 'bitcoin',
289
+ '--timeframe',
290
+ '24h',
291
+ ], { from: 'user' });
292
+ expect(mockPostMegathreadComment).toHaveBeenCalledWith('round-123', {
293
+ text: 'Test comment',
294
+ conviction: 50,
295
+ tokenId: 'bitcoin',
296
+ roundDuration: 86400000,
297
+ });
263
298
  });
264
299
  });
265
300
  describe('agent validation', () => {
@@ -276,8 +311,8 @@ describe('createMegathreadCreateCommentCommand', () => {
276
311
  'Test comment',
277
312
  '--token',
278
313
  'bitcoin',
279
- '--duration',
280
- '3600000',
314
+ '--timeframe',
315
+ '1h',
281
316
  ], { from: 'user' })).rejects.toThrow('process.exit(1)');
282
317
  expect(consoleErrorOutput.join('\n')).toContain('Agent "non-existent" not found');
283
318
  expect(consoleErrorOutput.join('\n')).toContain('Available agents:');
@@ -301,8 +336,8 @@ describe('createMegathreadCreateCommentCommand', () => {
301
336
  'Test comment',
302
337
  '--token',
303
338
  'bitcoin',
304
- '--duration',
305
- '3600000',
339
+ '--timeframe',
340
+ '1h',
306
341
  ], { from: 'user' })).rejects.toThrow('process.exit(1)');
307
342
  expect(consoleErrorOutput.join('\n')).toContain('No credentials found for agent "test-agent"');
308
343
  });
@@ -320,8 +355,8 @@ describe('createMegathreadCreateCommentCommand', () => {
320
355
  'Test comment',
321
356
  '--token',
322
357
  'bitcoin',
323
- '--duration',
324
- '3600000',
358
+ '--timeframe',
359
+ '1h',
325
360
  ], { from: 'user' })).rejects.toThrow('process.exit(1)');
326
361
  expect(consoleErrorOutput.join('\n')).toContain('No credentials found');
327
362
  });
@@ -342,8 +377,8 @@ describe('createMegathreadCreateCommentCommand', () => {
342
377
  'Bullish on Bitcoin!',
343
378
  '--token',
344
379
  'bitcoin',
345
- '--duration',
346
- '3600000',
380
+ '--timeframe',
381
+ '1h',
347
382
  ], { from: 'user' });
348
383
  expect(mockPostMegathreadComment).toHaveBeenCalledWith('round-123', {
349
384
  text: 'Bullish on Bitcoin!',
@@ -373,8 +408,8 @@ describe('createMegathreadCreateCommentCommand', () => {
373
408
  'Bearish outlook',
374
409
  '--token',
375
410
  'ethereum',
376
- '--duration',
377
- '14400000',
411
+ '--timeframe',
412
+ '4h',
378
413
  ], { from: 'user' });
379
414
  const output = consoleOutput.join('\n');
380
415
  expect(output).toContain('-30.0%');
@@ -395,8 +430,8 @@ describe('createMegathreadCreateCommentCommand', () => {
395
430
  longText,
396
431
  '--token',
397
432
  'bitcoin',
398
- '--duration',
399
- '3600000',
433
+ '--timeframe',
434
+ '1h',
400
435
  ], { from: 'user' });
401
436
  // Verify full text was sent to API
402
437
  expect(mockPostMegathreadComment).toHaveBeenCalledWith('round-123', {
@@ -426,8 +461,8 @@ describe('createMegathreadCreateCommentCommand', () => {
426
461
  'Test comment',
427
462
  '--token',
428
463
  'bitcoin',
429
- '--duration',
430
- '3600000',
464
+ '--timeframe',
465
+ '1h',
431
466
  ], { from: 'user' })).rejects.toThrow('process.exit(1)');
432
467
  expect(consoleErrorOutput.join('\n')).toContain('Failed to post comment');
433
468
  expect(consoleErrorOutput.join('\n')).toContain('Network error');
@@ -447,8 +482,8 @@ describe('createMegathreadCreateCommentCommand', () => {
447
482
  'Test comment',
448
483
  '--token',
449
484
  'bitcoin',
450
- '--duration',
451
- '3600000',
485
+ '--timeframe',
486
+ '1h',
452
487
  ], { from: 'user' })).rejects.toThrow('process.exit(1)');
453
488
  expect(consoleErrorOutput.join('\n')).toContain('Failed to post comment');
454
489
  expect(consoleErrorOutput.join('\n')).toContain('String error');
@@ -470,8 +505,8 @@ describe('createMegathreadCreateCommentCommand', () => {
470
505
  'Test comment',
471
506
  '--token',
472
507
  'bitcoin',
473
- '--duration',
474
- '3600000',
508
+ '--timeframe',
509
+ '1h',
475
510
  ], { from: 'user' });
476
511
  const output = consoleOutput.join('\n');
477
512
  expect(output).toContain('Comment posted successfully');
@@ -1,47 +1,43 @@
1
1
  import { Command } from 'commander';
2
- import { HiveClient, TIMEFRAME_DURATION_MS, Timeframe } from '@zhive/sdk';
2
+ import { HiveClient, durationMsToTimeframe } from '@zhive/sdk';
3
3
  import { styled, symbols, border } from '../../shared/theme.js';
4
4
  import { HIVE_API_URL } from '../../../shared/config/constant.js';
5
5
  import { findAgentByName, loadAgentCredentials, scanAgents } from '../../../shared/config/agent.js';
6
+ import z from 'zod';
7
+ import { printZodError } from '../../shared/ validation.js';
6
8
  const VALID_TIMEFRAMES = ['1h', '4h', '24h'];
7
- const DURATION_MS_TO_TIMEFRAME = {
8
- [TIMEFRAME_DURATION_MS[Timeframe.H1]]: Timeframe.H1,
9
- [TIMEFRAME_DURATION_MS[Timeframe.H4]]: Timeframe.H4,
10
- [TIMEFRAME_DURATION_MS[Timeframe.H24]]: Timeframe.H24,
11
- };
12
- function durationMsToTimeframe(durationMs) {
13
- const result = DURATION_MS_TO_TIMEFRAME[durationMs];
14
- return result;
15
- }
16
- function parseTimeframes(raw) {
17
- const parts = raw.split(',').map((t) => t.trim());
18
- const invalid = parts.filter((t) => !VALID_TIMEFRAMES.includes(t));
19
- if (invalid.length > 0) {
20
- return null;
21
- }
22
- const parsed = parts;
23
- return parsed;
24
- }
9
+ const ListMegathreadOptionsSchema = z.object({
10
+ agent: z.string(),
11
+ timeframe: z
12
+ .string()
13
+ .optional()
14
+ .transform((val, ctx) => {
15
+ if (!val)
16
+ return undefined;
17
+ const parsed = val.split(',').map((t) => t.trim());
18
+ const invalidParts = parsed.filter((t) => !VALID_TIMEFRAMES.includes(t));
19
+ if (invalidParts.length > 0) {
20
+ ctx.addIssue({
21
+ code: 'custom',
22
+ message: `Invalid. valid values are [${VALID_TIMEFRAMES.join(', ')}]`,
23
+ });
24
+ return z.NEVER;
25
+ }
26
+ return parsed;
27
+ }),
28
+ });
25
29
  export function createMegathreadListCommand() {
26
30
  const program = new Command('list')
27
31
  .description('List unpredicted megathread rounds of an agent')
28
32
  .requiredOption('--agent <name>', 'Agent name')
29
33
  .option('--timeframe <timeframes>', 'Filter by timeframes (comma-separated: 1h,4h,24h)')
30
34
  .action(async (options) => {
31
- const { agent: agentName, timeframe: timeframeOption } = options;
32
- let timeframes;
33
- if (timeframeOption) {
34
- const parsed = parseTimeframes(timeframeOption);
35
- if (parsed === null) {
36
- const invalidParts = timeframeOption
37
- .split(',')
38
- .map((t) => t.trim())
39
- .filter((t) => !VALID_TIMEFRAMES.includes(t));
40
- console.error(styled.red(`${symbols.cross} Invalid timeframes: ${invalidParts.join(', ')}. Valid values: 1h, 4h, 24h`));
41
- process.exit(1);
42
- }
43
- timeframes = parsed;
35
+ const parseResult = ListMegathreadOptionsSchema.safeParse(options);
36
+ if (!parseResult.success) {
37
+ printZodError(parseResult);
38
+ process.exit(1);
44
39
  }
40
+ const { agent: agentName, timeframe: timeframes } = parseResult.data;
45
41
  const agentConfig = await findAgentByName(agentName);
46
42
  if (!agentConfig) {
47
43
  const agents = await scanAgents();
@@ -70,11 +66,11 @@ export function createMegathreadListCommand() {
70
66
  console.log('');
71
67
  return;
72
68
  }
73
- const headers = ['Round ID', 'Token', 'Timeframe'];
69
+ const headers = ['Round ID', 'Token', 'Timeframe', 'PriceAtStart'];
74
70
  const rows = rounds.map((r) => {
75
71
  const tf = durationMsToTimeframe(r.durationMs);
76
72
  const timeframeStr = tf ?? `${r.durationMs}ms`;
77
- return [r.roundId, r.projectId, timeframeStr];
73
+ return [r.roundId, r.projectId, timeframeStr, r.priceAtStart];
78
74
  });
79
75
  const colWidths = headers.map((h, i) => {
80
76
  const dataMax = Math.max(...rows.map((row) => String(row[i]).length));
@@ -39,6 +39,7 @@ function createMockActiveRound(overrides = {}) {
39
39
  roundId: 'round-123',
40
40
  projectId: 'bitcoin',
41
41
  durationMs: 3600000,
42
+ priceAtStart: null,
42
43
  ...overrides,
43
44
  };
44
45
  }
@@ -78,13 +79,12 @@ describe('createMegathreadListCommand', () => {
78
79
  it('shows error for invalid timeframe value', async () => {
79
80
  const command = createMegathreadListCommand();
80
81
  await expect(command.parseAsync(['--agent', 'test-agent', '--timeframe', '2h'], { from: 'user' })).rejects.toThrow('process.exit(1)');
81
- expect(consoleErrorOutput.join('\n')).toContain('Invalid timeframes: 2h');
82
- expect(consoleErrorOutput.join('\n')).toContain('Valid values: 1h, 4h, 24h');
82
+ expect(consoleErrorOutput.length).toBeGreaterThan(0);
83
83
  });
84
84
  it('shows error for multiple invalid timeframes', async () => {
85
85
  const command = createMegathreadListCommand();
86
86
  await expect(command.parseAsync(['--agent', 'test-agent', '--timeframe', '2h,5h'], { from: 'user' })).rejects.toThrow('process.exit(1)');
87
- expect(consoleErrorOutput.join('\n')).toContain('Invalid timeframes: 2h, 5h');
87
+ expect(consoleErrorOutput.length).toBeGreaterThan(0);
88
88
  });
89
89
  it('accepts valid timeframe values', async () => {
90
90
  mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
@@ -0,0 +1,5 @@
1
+ import { styled, symbols } from './theme.js';
2
+ export const printZodError = (result) => {
3
+ const errors = result.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
4
+ console.error(styled.red(`${symbols.cross} Validation error: ${errors}`));
5
+ };
@@ -6,12 +6,16 @@ export const HIVE_FRONTEND_URL = 'https://www.zhive.ai';
6
6
  export function getHiveDir() {
7
7
  const homeDir = os.homedir();
8
8
  const zhiveDir = path.join(homeDir, '.zhive');
9
- const legacyDir = path.join(homeDir, '.hive');
10
- if (fs.existsSync(zhiveDir)) {
11
- return zhiveDir;
12
- }
13
- if (fs.existsSync(legacyDir)) {
14
- return legacyDir;
9
+ const pathToCheck = [
10
+ path.join(homeDir, '.zhive'),
11
+ path.join(homeDir, '.hive'), // legacy hive dir
12
+ path.join(homeDir, '.openclaw', '.hive'),
13
+ path.join(homeDir, '.openclaw', 'workspace', '.hive'),
14
+ ];
15
+ for (const p of pathToCheck) {
16
+ if (fs.existsSync(p)) {
17
+ return p;
18
+ }
15
19
  }
16
20
  return zhiveDir;
17
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhive/cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "CLI for bootstrapping zHive AI Agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "@ai-sdk/openai": "^3.0.25",
32
32
  "@ai-sdk/xai": "^3.0.0",
33
33
  "@openrouter/ai-sdk-provider": "^0.4.0",
34
- "@zhive/sdk": "^0.5.2",
34
+ "@zhive/sdk": "^0.5.4",
35
35
  "ai": "^6.0.71",
36
36
  "axios": "^1.6.0",
37
37
  "chalk": "^5.3.0",