@zhive/cli 0.5.5 → 0.6.0
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/README.md +5 -5
- package/dist/CLAUDE.md +7 -0
- package/dist/backtest/CLAUDE.md +7 -0
- package/dist/cli.js +20 -0
- package/dist/commands/agent/commands/index.js +7 -0
- package/dist/commands/agent/commands/profile.js +40 -0
- package/dist/commands/agent/commands/profile.test.js +137 -0
- package/dist/commands/create/commands/index.js +10 -5
- package/dist/commands/list/commands/index.js +8 -3
- package/dist/commands/megathread/commands/create-comment.js +99 -0
- package/dist/commands/megathread/commands/create-comment.test.js +480 -0
- package/dist/commands/megathread/commands/index.js +9 -0
- package/dist/commands/megathread/commands/list.js +102 -0
- package/dist/commands/megathread/commands/list.test.js +206 -0
- package/dist/commands/migrate-templates/commands/index.js +9 -4
- package/dist/commands/run/commands/index.js +17 -12
- package/dist/commands/run/run-headless.js +2 -1
- package/dist/commands/start/commands/index.js +37 -32
- package/dist/commands/start/hooks/useAgent.js +2 -1
- package/dist/commands/start/services/backtest/runner.js +1 -1
- package/dist/commands/start-all/commands/index.js +22 -17
- package/dist/index.js +26 -57
- package/dist/{agent → services/agent}/analysis.js +5 -5
- package/dist/{load-agent-env.js → services/agent/env.js} +1 -1
- package/dist/{agent → services/agent/helpers}/model.js +2 -2
- package/dist/{agent → services/agent/prompts}/memory-prompt.js +20 -22
- package/dist/{agent → services/agent/prompts}/prompt.js +80 -54
- package/dist/{agent → services/agent}/tools/market/client.js +1 -1
- package/dist/{agent → services/agent}/tools/mindshare/client.js +1 -1
- package/dist/{agents.js → services/config/agent.js} +2 -2
- package/dist/{config.js → services/config/config.js} +1 -7
- package/dist/services/config/constant.js +8 -0
- package/dist/shared/agent/config.js +75 -0
- package/dist/shared/agent/env.js +30 -0
- package/dist/shared/agent/handler.js +129 -0
- package/dist/shared/agent/helpers/model.js +92 -0
- package/dist/shared/agent/runtime.js +15 -0
- package/dist/shared/ai-providers.js +66 -0
- package/dist/shared/config/agent.js +19 -0
- package/dist/shared/config/agent.test.js +115 -0
- package/package.json +4 -3
- package/dist/agent/app.js +0 -122
- package/dist/agent/commands/registry.js +0 -12
- package/dist/agent/components/AsciiTicker.js +0 -81
- package/dist/agent/components/CommandInput.js +0 -65
- package/dist/agent/components/HoneycombBoot.js +0 -291
- package/dist/agent/components/Spinner.js +0 -37
- package/dist/agent/hooks/useAgent.js +0 -480
- package/dist/agent/objects.js +0 -1
- package/dist/agent/process-lifecycle.js +0 -18
- package/dist/agent/run-headless.js +0 -189
- package/dist/agent/theme.js +0 -41
- package/dist/avatar.js +0 -34
- package/dist/backtest/default-backtest-data.js +0 -200
- package/dist/backtest/fetch.js +0 -41
- package/dist/backtest/import.js +0 -106
- package/dist/backtest/index.js +0 -10
- package/dist/backtest/results.js +0 -113
- package/dist/backtest/runner.js +0 -134
- package/dist/backtest/storage.js +0 -11
- package/dist/backtest/types.js +0 -1
- package/dist/commands/install.js +0 -50
- package/dist/commands/start/ui/PollText.js +0 -23
- package/dist/commands/start/ui/PredictionsPanel.js +0 -88
- package/dist/commands/start/ui/SpinnerContext.js +0 -20
- package/dist/components/InputGuard.js +0 -6
- package/dist/components/stdout-spinner.js +0 -48
- package/dist/create/CreateApp.js +0 -153
- package/dist/create/ai-generate.js +0 -147
- package/dist/create/generate.js +0 -73
- package/dist/create/steps/ApiKeyStep.js +0 -97
- package/dist/create/steps/AvatarStep.js +0 -16
- package/dist/create/steps/BioStep.js +0 -14
- package/dist/create/steps/DoneStep.js +0 -14
- package/dist/create/steps/IdentityStep.js +0 -163
- package/dist/create/steps/NameStep.js +0 -71
- package/dist/create/steps/ScaffoldStep.js +0 -58
- package/dist/create/steps/SoulStep.js +0 -58
- package/dist/create/steps/StrategyStep.js +0 -58
- package/dist/create/validate-api-key.js +0 -47
- package/dist/create/welcome.js +0 -304
- package/dist/list/ListApp.js +0 -79
- package/dist/migrate-templates/MigrateApp.js +0 -131
- package/dist/migrate-templates/migrate.js +0 -86
- package/dist/presets.js +0 -613
- package/dist/start/AgentProcessManager.js +0 -98
- package/dist/start/Dashboard.js +0 -92
- package/dist/start/SelectAgentApp.js +0 -81
- package/dist/start/StartApp.js +0 -189
- package/dist/start/patch-headless.js +0 -101
- package/dist/start/patch-managed-mode.js +0 -142
- package/dist/start/start-command.js +0 -24
- package/dist/theme.js +0 -54
- /package/dist/{agent → services/agent}/config.js +0 -0
- /package/dist/{agent → services/agent}/helpers.js +0 -0
- /package/dist/{agent → services/agent/prompts}/chat-prompt.js +0 -0
- /package/dist/{agent → services/agent}/skills/index.js +0 -0
- /package/dist/{agent → services/agent}/skills/skill-parser.js +0 -0
- /package/dist/{agent → services/agent}/skills/types.js +0 -0
- /package/dist/{agent → services/agent/tools}/edit-section.js +0 -0
- /package/dist/{agent → services/agent/tools}/fetch-rules.js +0 -0
- /package/dist/{agent → services/agent}/tools/index.js +0 -0
- /package/dist/{agent → services/agent}/tools/market/index.js +0 -0
- /package/dist/{agent → services/agent}/tools/market/tools.js +0 -0
- /package/dist/{agent → services/agent}/tools/mindshare/index.js +0 -0
- /package/dist/{agent → services/agent}/tools/mindshare/tools.js +0 -0
- /package/dist/{agent → services/agent}/tools/read-skill-tool.js +0 -0
- /package/dist/{agent → services/agent}/tools/ta/index.js +0 -0
- /package/dist/{agent → services/agent}/tools/ta/indicators.js +0 -0
- /package/dist/{agent → services/agent}/types.js +0 -0
- /package/dist/{ai-providers.js → services/ai-providers.js} +0 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const FIXTURES_DIR = path.join(__dirname, '../../../../__fixtures__/mock-hive');
|
|
6
|
+
vi.mock('../../../shared/config/constant.js', () => ({
|
|
7
|
+
getHiveDir: vi.fn(() => FIXTURES_DIR),
|
|
8
|
+
HIVE_API_URL: 'http://localhost:6969',
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('../../../shared/config/ai-providers.js', () => ({
|
|
11
|
+
AI_PROVIDERS: [
|
|
12
|
+
{ label: 'OpenAI', package: '@ai-sdk/openai', envVar: 'OPENAI_API_KEY' },
|
|
13
|
+
],
|
|
14
|
+
}));
|
|
15
|
+
vi.mock('@zhive/sdk', async () => {
|
|
16
|
+
const actual = await vi.importActual('@zhive/sdk');
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
HiveClient: vi.fn().mockImplementation(() => ({
|
|
20
|
+
postMegathreadComment: vi.fn(),
|
|
21
|
+
})),
|
|
22
|
+
loadCredentials: vi.fn(),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
vi.mock('../../shared/theme.js', () => ({
|
|
26
|
+
styled: {
|
|
27
|
+
red: (text) => text,
|
|
28
|
+
gray: (text) => text,
|
|
29
|
+
green: (text) => text,
|
|
30
|
+
},
|
|
31
|
+
symbols: {
|
|
32
|
+
cross: '✗',
|
|
33
|
+
check: '✓',
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
import { HiveClient, loadCredentials } from '@zhive/sdk';
|
|
37
|
+
import { createMegathreadCreateCommentCommand } from './create-comment.js';
|
|
38
|
+
const MockHiveClient = HiveClient;
|
|
39
|
+
const mockLoadCredentials = loadCredentials;
|
|
40
|
+
describe('createMegathreadCreateCommentCommand', () => {
|
|
41
|
+
let consoleLogSpy;
|
|
42
|
+
let consoleErrorSpy;
|
|
43
|
+
let processExitSpy;
|
|
44
|
+
let consoleOutput;
|
|
45
|
+
let consoleErrorOutput;
|
|
46
|
+
let mockPostMegathreadComment;
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.clearAllMocks();
|
|
49
|
+
consoleOutput = [];
|
|
50
|
+
consoleErrorOutput = [];
|
|
51
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation((...args) => {
|
|
52
|
+
consoleOutput.push(args.join(' '));
|
|
53
|
+
});
|
|
54
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
|
|
55
|
+
consoleErrorOutput.push(args.join(' '));
|
|
56
|
+
});
|
|
57
|
+
processExitSpy = vi
|
|
58
|
+
.spyOn(process, 'exit')
|
|
59
|
+
.mockImplementation((code) => {
|
|
60
|
+
throw new Error(`process.exit(${code})`);
|
|
61
|
+
});
|
|
62
|
+
mockPostMegathreadComment = vi.fn();
|
|
63
|
+
MockHiveClient.mockImplementation(() => ({
|
|
64
|
+
postMegathreadComment: mockPostMegathreadComment,
|
|
65
|
+
}));
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
consoleLogSpy.mockRestore();
|
|
69
|
+
consoleErrorSpy.mockRestore();
|
|
70
|
+
processExitSpy.mockRestore();
|
|
71
|
+
});
|
|
72
|
+
describe('conviction validation', () => {
|
|
73
|
+
it('shows error when conviction is too high', async () => {
|
|
74
|
+
const command = createMegathreadCreateCommentCommand();
|
|
75
|
+
await expect(command.parseAsync([
|
|
76
|
+
'--agent',
|
|
77
|
+
'test-agent',
|
|
78
|
+
'--round',
|
|
79
|
+
'round-123',
|
|
80
|
+
'--conviction',
|
|
81
|
+
'150',
|
|
82
|
+
'--text',
|
|
83
|
+
'Test comment',
|
|
84
|
+
'--token',
|
|
85
|
+
'bitcoin',
|
|
86
|
+
'--duration',
|
|
87
|
+
'3600000',
|
|
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');
|
|
91
|
+
});
|
|
92
|
+
it('shows error when conviction is too low', async () => {
|
|
93
|
+
const command = createMegathreadCreateCommentCommand();
|
|
94
|
+
await expect(command.parseAsync([
|
|
95
|
+
'--agent',
|
|
96
|
+
'test-agent',
|
|
97
|
+
'--round',
|
|
98
|
+
'round-123',
|
|
99
|
+
'--conviction',
|
|
100
|
+
'-150',
|
|
101
|
+
'--text',
|
|
102
|
+
'Test comment',
|
|
103
|
+
'--token',
|
|
104
|
+
'bitcoin',
|
|
105
|
+
'--duration',
|
|
106
|
+
'3600000',
|
|
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');
|
|
110
|
+
});
|
|
111
|
+
it('shows error when conviction is not a number', async () => {
|
|
112
|
+
const command = createMegathreadCreateCommentCommand();
|
|
113
|
+
await expect(command.parseAsync([
|
|
114
|
+
'--agent',
|
|
115
|
+
'test-agent',
|
|
116
|
+
'--round',
|
|
117
|
+
'round-123',
|
|
118
|
+
'--conviction',
|
|
119
|
+
'abc',
|
|
120
|
+
'--text',
|
|
121
|
+
'Test comment',
|
|
122
|
+
'--token',
|
|
123
|
+
'bitcoin',
|
|
124
|
+
'--duration',
|
|
125
|
+
'3600000',
|
|
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');
|
|
129
|
+
});
|
|
130
|
+
it('accepts valid conviction at upper boundary', async () => {
|
|
131
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
132
|
+
mockPostMegathreadComment.mockResolvedValue(undefined);
|
|
133
|
+
const command = createMegathreadCreateCommentCommand();
|
|
134
|
+
await command.parseAsync([
|
|
135
|
+
'--agent',
|
|
136
|
+
'test-agent',
|
|
137
|
+
'--round',
|
|
138
|
+
'round-123',
|
|
139
|
+
'--conviction',
|
|
140
|
+
'100',
|
|
141
|
+
'--text',
|
|
142
|
+
'Test comment',
|
|
143
|
+
'--token',
|
|
144
|
+
'bitcoin',
|
|
145
|
+
'--duration',
|
|
146
|
+
'3600000',
|
|
147
|
+
], { from: 'user' });
|
|
148
|
+
expect(mockPostMegathreadComment).toHaveBeenCalledWith('round-123', {
|
|
149
|
+
text: 'Test comment',
|
|
150
|
+
conviction: 100,
|
|
151
|
+
tokenId: 'bitcoin',
|
|
152
|
+
roundDuration: 3600000,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
it('accepts valid conviction at lower boundary', async () => {
|
|
156
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
157
|
+
mockPostMegathreadComment.mockResolvedValue(undefined);
|
|
158
|
+
const command = createMegathreadCreateCommentCommand();
|
|
159
|
+
await command.parseAsync([
|
|
160
|
+
'--agent',
|
|
161
|
+
'test-agent',
|
|
162
|
+
'--round',
|
|
163
|
+
'round-123',
|
|
164
|
+
'--conviction',
|
|
165
|
+
'-100',
|
|
166
|
+
'--text',
|
|
167
|
+
'Test comment',
|
|
168
|
+
'--token',
|
|
169
|
+
'bitcoin',
|
|
170
|
+
'--duration',
|
|
171
|
+
'3600000',
|
|
172
|
+
], { from: 'user' });
|
|
173
|
+
expect(mockPostMegathreadComment).toHaveBeenCalledWith('round-123', {
|
|
174
|
+
text: 'Test comment',
|
|
175
|
+
conviction: -100,
|
|
176
|
+
tokenId: 'bitcoin',
|
|
177
|
+
roundDuration: 3600000,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
it('accepts decimal conviction values', async () => {
|
|
181
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
182
|
+
mockPostMegathreadComment.mockResolvedValue(undefined);
|
|
183
|
+
const command = createMegathreadCreateCommentCommand();
|
|
184
|
+
await command.parseAsync([
|
|
185
|
+
'--agent',
|
|
186
|
+
'test-agent',
|
|
187
|
+
'--round',
|
|
188
|
+
'round-123',
|
|
189
|
+
'--conviction',
|
|
190
|
+
'25.5',
|
|
191
|
+
'--text',
|
|
192
|
+
'Test comment',
|
|
193
|
+
'--token',
|
|
194
|
+
'bitcoin',
|
|
195
|
+
'--duration',
|
|
196
|
+
'3600000',
|
|
197
|
+
], { from: 'user' });
|
|
198
|
+
expect(mockPostMegathreadComment).toHaveBeenCalledWith('round-123', {
|
|
199
|
+
text: 'Test comment',
|
|
200
|
+
conviction: 25.5,
|
|
201
|
+
tokenId: 'bitcoin',
|
|
202
|
+
roundDuration: 3600000,
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('duration validation', () => {
|
|
207
|
+
it('shows error when duration is negative', async () => {
|
|
208
|
+
const command = createMegathreadCreateCommentCommand();
|
|
209
|
+
await expect(command.parseAsync([
|
|
210
|
+
'--agent',
|
|
211
|
+
'test-agent',
|
|
212
|
+
'--round',
|
|
213
|
+
'round-123',
|
|
214
|
+
'--conviction',
|
|
215
|
+
'50',
|
|
216
|
+
'--text',
|
|
217
|
+
'Test comment',
|
|
218
|
+
'--token',
|
|
219
|
+
'bitcoin',
|
|
220
|
+
'--duration',
|
|
221
|
+
'-1',
|
|
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
|
+
});
|
|
226
|
+
it('shows error when duration is zero', async () => {
|
|
227
|
+
const command = createMegathreadCreateCommentCommand();
|
|
228
|
+
await expect(command.parseAsync([
|
|
229
|
+
'--agent',
|
|
230
|
+
'test-agent',
|
|
231
|
+
'--round',
|
|
232
|
+
'round-123',
|
|
233
|
+
'--conviction',
|
|
234
|
+
'50',
|
|
235
|
+
'--text',
|
|
236
|
+
'Test comment',
|
|
237
|
+
'--token',
|
|
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');
|
|
244
|
+
});
|
|
245
|
+
it('shows error when duration is not a number', async () => {
|
|
246
|
+
const command = createMegathreadCreateCommentCommand();
|
|
247
|
+
await expect(command.parseAsync([
|
|
248
|
+
'--agent',
|
|
249
|
+
'test-agent',
|
|
250
|
+
'--round',
|
|
251
|
+
'round-123',
|
|
252
|
+
'--conviction',
|
|
253
|
+
'50',
|
|
254
|
+
'--text',
|
|
255
|
+
'Test comment',
|
|
256
|
+
'--token',
|
|
257
|
+
'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');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
describe('agent validation', () => {
|
|
266
|
+
it('shows error when agent not found and lists available agents', async () => {
|
|
267
|
+
const command = createMegathreadCreateCommentCommand();
|
|
268
|
+
await expect(command.parseAsync([
|
|
269
|
+
'--agent',
|
|
270
|
+
'non-existent',
|
|
271
|
+
'--round',
|
|
272
|
+
'round-123',
|
|
273
|
+
'--conviction',
|
|
274
|
+
'50',
|
|
275
|
+
'--text',
|
|
276
|
+
'Test comment',
|
|
277
|
+
'--token',
|
|
278
|
+
'bitcoin',
|
|
279
|
+
'--duration',
|
|
280
|
+
'3600000',
|
|
281
|
+
], { from: 'user' })).rejects.toThrow('process.exit(1)');
|
|
282
|
+
expect(consoleErrorOutput.join('\n')).toContain('Agent "non-existent" not found');
|
|
283
|
+
expect(consoleErrorOutput.join('\n')).toContain('Available agents:');
|
|
284
|
+
expect(consoleErrorOutput.join('\n')).toContain('test-agent');
|
|
285
|
+
expect(consoleErrorOutput.join('\n')).toContain('empty-agent');
|
|
286
|
+
expect(consoleErrorOutput.join('\n')).toContain('agent-no-skills');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
describe('credentials validation', () => {
|
|
290
|
+
it('shows error when credentials are missing', async () => {
|
|
291
|
+
mockLoadCredentials.mockResolvedValue(null);
|
|
292
|
+
const command = createMegathreadCreateCommentCommand();
|
|
293
|
+
await expect(command.parseAsync([
|
|
294
|
+
'--agent',
|
|
295
|
+
'test-agent',
|
|
296
|
+
'--round',
|
|
297
|
+
'round-123',
|
|
298
|
+
'--conviction',
|
|
299
|
+
'50',
|
|
300
|
+
'--text',
|
|
301
|
+
'Test comment',
|
|
302
|
+
'--token',
|
|
303
|
+
'bitcoin',
|
|
304
|
+
'--duration',
|
|
305
|
+
'3600000',
|
|
306
|
+
], { from: 'user' })).rejects.toThrow('process.exit(1)');
|
|
307
|
+
expect(consoleErrorOutput.join('\n')).toContain('No credentials found for agent "test-agent"');
|
|
308
|
+
});
|
|
309
|
+
it('shows error when credentials have no API key', async () => {
|
|
310
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: null });
|
|
311
|
+
const command = createMegathreadCreateCommentCommand();
|
|
312
|
+
await expect(command.parseAsync([
|
|
313
|
+
'--agent',
|
|
314
|
+
'test-agent',
|
|
315
|
+
'--round',
|
|
316
|
+
'round-123',
|
|
317
|
+
'--conviction',
|
|
318
|
+
'50',
|
|
319
|
+
'--text',
|
|
320
|
+
'Test comment',
|
|
321
|
+
'--token',
|
|
322
|
+
'bitcoin',
|
|
323
|
+
'--duration',
|
|
324
|
+
'3600000',
|
|
325
|
+
], { from: 'user' })).rejects.toThrow('process.exit(1)');
|
|
326
|
+
expect(consoleErrorOutput.join('\n')).toContain('No credentials found');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
describe('successful comment posting', () => {
|
|
330
|
+
it('posts comment and shows success message', async () => {
|
|
331
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
332
|
+
mockPostMegathreadComment.mockResolvedValue(undefined);
|
|
333
|
+
const command = createMegathreadCreateCommentCommand();
|
|
334
|
+
await command.parseAsync([
|
|
335
|
+
'--agent',
|
|
336
|
+
'test-agent',
|
|
337
|
+
'--round',
|
|
338
|
+
'round-123',
|
|
339
|
+
'--conviction',
|
|
340
|
+
'50',
|
|
341
|
+
'--text',
|
|
342
|
+
'Bullish on Bitcoin!',
|
|
343
|
+
'--token',
|
|
344
|
+
'bitcoin',
|
|
345
|
+
'--duration',
|
|
346
|
+
'3600000',
|
|
347
|
+
], { from: 'user' });
|
|
348
|
+
expect(mockPostMegathreadComment).toHaveBeenCalledWith('round-123', {
|
|
349
|
+
text: 'Bullish on Bitcoin!',
|
|
350
|
+
conviction: 50,
|
|
351
|
+
tokenId: 'bitcoin',
|
|
352
|
+
roundDuration: 3600000,
|
|
353
|
+
});
|
|
354
|
+
const output = consoleOutput.join('\n');
|
|
355
|
+
expect(output).toContain('Comment posted successfully');
|
|
356
|
+
expect(output).toContain('round-123');
|
|
357
|
+
expect(output).toContain('bitcoin');
|
|
358
|
+
expect(output).toContain('+50.0%');
|
|
359
|
+
expect(output).toContain('Bullish on Bitcoin!');
|
|
360
|
+
});
|
|
361
|
+
it('formats negative conviction correctly', async () => {
|
|
362
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
363
|
+
mockPostMegathreadComment.mockResolvedValue(undefined);
|
|
364
|
+
const command = createMegathreadCreateCommentCommand();
|
|
365
|
+
await command.parseAsync([
|
|
366
|
+
'--agent',
|
|
367
|
+
'test-agent',
|
|
368
|
+
'--round',
|
|
369
|
+
'round-123',
|
|
370
|
+
'--conviction',
|
|
371
|
+
'-30',
|
|
372
|
+
'--text',
|
|
373
|
+
'Bearish outlook',
|
|
374
|
+
'--token',
|
|
375
|
+
'ethereum',
|
|
376
|
+
'--duration',
|
|
377
|
+
'14400000',
|
|
378
|
+
], { from: 'user' });
|
|
379
|
+
const output = consoleOutput.join('\n');
|
|
380
|
+
expect(output).toContain('-30.0%');
|
|
381
|
+
});
|
|
382
|
+
it('truncates long text in success message', async () => {
|
|
383
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
384
|
+
mockPostMegathreadComment.mockResolvedValue(undefined);
|
|
385
|
+
const longText = 'A'.repeat(100);
|
|
386
|
+
const command = createMegathreadCreateCommentCommand();
|
|
387
|
+
await command.parseAsync([
|
|
388
|
+
'--agent',
|
|
389
|
+
'test-agent',
|
|
390
|
+
'--round',
|
|
391
|
+
'round-123',
|
|
392
|
+
'--conviction',
|
|
393
|
+
'25',
|
|
394
|
+
'--text',
|
|
395
|
+
longText,
|
|
396
|
+
'--token',
|
|
397
|
+
'bitcoin',
|
|
398
|
+
'--duration',
|
|
399
|
+
'3600000',
|
|
400
|
+
], { from: 'user' });
|
|
401
|
+
// Verify full text was sent to API
|
|
402
|
+
expect(mockPostMegathreadComment).toHaveBeenCalledWith('round-123', {
|
|
403
|
+
text: longText,
|
|
404
|
+
conviction: 25,
|
|
405
|
+
tokenId: 'bitcoin',
|
|
406
|
+
roundDuration: 3600000,
|
|
407
|
+
});
|
|
408
|
+
// Verify truncated display
|
|
409
|
+
const output = consoleOutput.join('\n');
|
|
410
|
+
expect(output).toContain('A'.repeat(50) + '...');
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
describe('API error handling', () => {
|
|
414
|
+
it('shows error when API call fails', async () => {
|
|
415
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
416
|
+
mockPostMegathreadComment.mockRejectedValue(new Error('Network error'));
|
|
417
|
+
const command = createMegathreadCreateCommentCommand();
|
|
418
|
+
await expect(command.parseAsync([
|
|
419
|
+
'--agent',
|
|
420
|
+
'test-agent',
|
|
421
|
+
'--round',
|
|
422
|
+
'round-123',
|
|
423
|
+
'--conviction',
|
|
424
|
+
'50',
|
|
425
|
+
'--text',
|
|
426
|
+
'Test comment',
|
|
427
|
+
'--token',
|
|
428
|
+
'bitcoin',
|
|
429
|
+
'--duration',
|
|
430
|
+
'3600000',
|
|
431
|
+
], { from: 'user' })).rejects.toThrow('process.exit(1)');
|
|
432
|
+
expect(consoleErrorOutput.join('\n')).toContain('Failed to post comment');
|
|
433
|
+
expect(consoleErrorOutput.join('\n')).toContain('Network error');
|
|
434
|
+
});
|
|
435
|
+
it('handles non-Error exceptions', async () => {
|
|
436
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
437
|
+
mockPostMegathreadComment.mockRejectedValue('String error');
|
|
438
|
+
const command = createMegathreadCreateCommentCommand();
|
|
439
|
+
await expect(command.parseAsync([
|
|
440
|
+
'--agent',
|
|
441
|
+
'test-agent',
|
|
442
|
+
'--round',
|
|
443
|
+
'round-123',
|
|
444
|
+
'--conviction',
|
|
445
|
+
'50',
|
|
446
|
+
'--text',
|
|
447
|
+
'Test comment',
|
|
448
|
+
'--token',
|
|
449
|
+
'bitcoin',
|
|
450
|
+
'--duration',
|
|
451
|
+
'3600000',
|
|
452
|
+
], { from: 'user' })).rejects.toThrow('process.exit(1)');
|
|
453
|
+
expect(consoleErrorOutput.join('\n')).toContain('Failed to post comment');
|
|
454
|
+
expect(consoleErrorOutput.join('\n')).toContain('String error');
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
describe('works with different fixture agents', () => {
|
|
458
|
+
it('works with empty-agent', async () => {
|
|
459
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
460
|
+
mockPostMegathreadComment.mockResolvedValue(undefined);
|
|
461
|
+
const command = createMegathreadCreateCommentCommand();
|
|
462
|
+
await command.parseAsync([
|
|
463
|
+
'--agent',
|
|
464
|
+
'empty-agent',
|
|
465
|
+
'--round',
|
|
466
|
+
'round-123',
|
|
467
|
+
'--conviction',
|
|
468
|
+
'50',
|
|
469
|
+
'--text',
|
|
470
|
+
'Test comment',
|
|
471
|
+
'--token',
|
|
472
|
+
'bitcoin',
|
|
473
|
+
'--duration',
|
|
474
|
+
'3600000',
|
|
475
|
+
], { from: 'user' });
|
|
476
|
+
const output = consoleOutput.join('\n');
|
|
477
|
+
expect(output).toContain('Comment posted successfully');
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createMegathreadListCommand } from './list.js';
|
|
3
|
+
import { createMegathreadCreateCommentCommand } from './create-comment.js';
|
|
4
|
+
export function createMegathreadCommand() {
|
|
5
|
+
const megathreadCommand = new Command('megathread').description('Megathread operations');
|
|
6
|
+
megathreadCommand.addCommand(createMegathreadListCommand());
|
|
7
|
+
megathreadCommand.addCommand(createMegathreadCreateCommentCommand());
|
|
8
|
+
return megathreadCommand;
|
|
9
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { HiveClient, TIMEFRAME_DURATION_MS, Timeframe } from '@zhive/sdk';
|
|
3
|
+
import { styled, symbols, border } from '../../shared/theme.js';
|
|
4
|
+
import { HIVE_API_URL } from '../../../shared/config/constant.js';
|
|
5
|
+
import { findAgentByName, loadAgentCredentials, scanAgents } from '../../../shared/config/agent.js';
|
|
6
|
+
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
|
+
}
|
|
25
|
+
export function createMegathreadListCommand() {
|
|
26
|
+
const program = new Command('list')
|
|
27
|
+
.description('List unpredicted megathread rounds of an agent')
|
|
28
|
+
.requiredOption('--agent <name>', 'Agent name')
|
|
29
|
+
.option('--timeframe <timeframes>', 'Filter by timeframes (comma-separated: 1h,4h,24h)')
|
|
30
|
+
.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;
|
|
44
|
+
}
|
|
45
|
+
const agentConfig = await findAgentByName(agentName);
|
|
46
|
+
if (!agentConfig) {
|
|
47
|
+
const agents = await scanAgents();
|
|
48
|
+
if (agents.length === 0) {
|
|
49
|
+
console.error(styled.red(`${symbols.cross} No agents found. Create one with: npx @zhive/cli@latest create`));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const availableNames = agents.map((a) => a.name).join(', ');
|
|
53
|
+
console.error(styled.red(`${symbols.cross} Agent "${agentName}" not found. Available agents: ${availableNames}`));
|
|
54
|
+
}
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const credentials = await loadAgentCredentials(agentConfig.dir, agentConfig.name);
|
|
58
|
+
if (!credentials?.apiKey) {
|
|
59
|
+
console.error(styled.red(`${symbols.cross} No credentials found for agent "${agentName}". The agent may need to be registered first.`));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const client = new HiveClient(HIVE_API_URL, credentials.apiKey);
|
|
63
|
+
try {
|
|
64
|
+
const rounds = await client.getUnpredictedRounds(timeframes);
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log(styled.honeyBold(`${symbols.hive} Unpredicted Rounds for ${agentName}`));
|
|
67
|
+
console.log('');
|
|
68
|
+
if (rounds.length === 0) {
|
|
69
|
+
console.log(styled.gray(' No unpredicted rounds available.'));
|
|
70
|
+
console.log('');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const headers = ['Round ID', 'Token', 'Timeframe'];
|
|
74
|
+
const rows = rounds.map((r) => {
|
|
75
|
+
const tf = durationMsToTimeframe(r.durationMs);
|
|
76
|
+
const timeframeStr = tf ?? `${r.durationMs}ms`;
|
|
77
|
+
return [r.roundId, r.projectId, timeframeStr];
|
|
78
|
+
});
|
|
79
|
+
const colWidths = headers.map((h, i) => {
|
|
80
|
+
const dataMax = Math.max(...rows.map((row) => String(row[i]).length));
|
|
81
|
+
const width = Math.max(h.length, dataMax);
|
|
82
|
+
return width;
|
|
83
|
+
});
|
|
84
|
+
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
|
|
85
|
+
console.log(` ${styled.gray(headerLine)}`);
|
|
86
|
+
console.log(` ${styled.gray(border.horizontal.repeat(headerLine.length))}`);
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
const line = row.map((cell, i) => String(cell).padEnd(colWidths[i])).join(' ');
|
|
89
|
+
console.log(` ${line}`);
|
|
90
|
+
}
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(styled.gray(` Total: ${rounds.length} round(s)`));
|
|
93
|
+
console.log('');
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
+
console.error(styled.red(`${symbols.cross} Failed to fetch unpredicted rounds: ${message}`));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
return program;
|
|
102
|
+
}
|