@zhive/cli 0.5.5 → 0.6.1
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/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/shared/agent/handler.js +129 -0
- package/dist/shared/agent/runtime.js +15 -0
- package/dist/shared/config/agent.js +19 -0
- package/dist/shared/config/agent.test.js +115 -0
- package/package.json +4 -3
|
@@ -0,0 +1,206 @@
|
|
|
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: [{ label: 'OpenAI', package: '@ai-sdk/openai', envVar: 'OPENAI_API_KEY' }],
|
|
12
|
+
}));
|
|
13
|
+
vi.mock('@zhive/sdk', async () => {
|
|
14
|
+
const actual = await vi.importActual('@zhive/sdk');
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
HiveClient: vi.fn().mockImplementation(() => ({
|
|
18
|
+
getUnpredictedRounds: vi.fn(),
|
|
19
|
+
})),
|
|
20
|
+
loadCredentials: vi.fn(),
|
|
21
|
+
TIMEFRAME_DURATION_MS: {
|
|
22
|
+
H1: 3600000,
|
|
23
|
+
H4: 14400000,
|
|
24
|
+
H24: 86400000,
|
|
25
|
+
},
|
|
26
|
+
Timeframe: {
|
|
27
|
+
H1: 'H1',
|
|
28
|
+
H4: 'H4',
|
|
29
|
+
H24: 'H24',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
import { HiveClient, loadCredentials } from '@zhive/sdk';
|
|
34
|
+
import { createMegathreadListCommand } from './list.js';
|
|
35
|
+
const MockHiveClient = HiveClient;
|
|
36
|
+
const mockLoadCredentials = loadCredentials;
|
|
37
|
+
function createMockActiveRound(overrides = {}) {
|
|
38
|
+
return {
|
|
39
|
+
roundId: 'round-123',
|
|
40
|
+
projectId: 'bitcoin',
|
|
41
|
+
durationMs: 3600000,
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
describe('createMegathreadListCommand', () => {
|
|
46
|
+
let consoleLogSpy;
|
|
47
|
+
let consoleErrorSpy;
|
|
48
|
+
let processExitSpy;
|
|
49
|
+
let consoleOutput;
|
|
50
|
+
let consoleErrorOutput;
|
|
51
|
+
let mockGetUnpredictedRounds;
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.clearAllMocks();
|
|
54
|
+
consoleOutput = [];
|
|
55
|
+
consoleErrorOutput = [];
|
|
56
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation((...args) => {
|
|
57
|
+
consoleOutput.push(args.join(' '));
|
|
58
|
+
});
|
|
59
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
|
|
60
|
+
consoleErrorOutput.push(args.join(' '));
|
|
61
|
+
});
|
|
62
|
+
processExitSpy = vi
|
|
63
|
+
.spyOn(process, 'exit')
|
|
64
|
+
.mockImplementation((code) => {
|
|
65
|
+
throw new Error(`process.exit(${code})`);
|
|
66
|
+
});
|
|
67
|
+
mockGetUnpredictedRounds = vi.fn();
|
|
68
|
+
MockHiveClient.mockImplementation(() => ({
|
|
69
|
+
getUnpredictedRounds: mockGetUnpredictedRounds,
|
|
70
|
+
}));
|
|
71
|
+
});
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
consoleLogSpy.mockRestore();
|
|
74
|
+
consoleErrorSpy.mockRestore();
|
|
75
|
+
processExitSpy.mockRestore();
|
|
76
|
+
});
|
|
77
|
+
describe('timeframe validation', () => {
|
|
78
|
+
it('shows error for invalid timeframe value', async () => {
|
|
79
|
+
const command = createMegathreadListCommand();
|
|
80
|
+
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');
|
|
83
|
+
});
|
|
84
|
+
it('shows error for multiple invalid timeframes', async () => {
|
|
85
|
+
const command = createMegathreadListCommand();
|
|
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');
|
|
88
|
+
});
|
|
89
|
+
it('accepts valid timeframe values', async () => {
|
|
90
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
91
|
+
mockGetUnpredictedRounds.mockResolvedValue([]);
|
|
92
|
+
const command = createMegathreadListCommand();
|
|
93
|
+
await command.parseAsync(['--agent', 'test-agent', '--timeframe', '1h,4h'], { from: 'user' });
|
|
94
|
+
expect(mockGetUnpredictedRounds).toHaveBeenCalledWith(['1h', '4h']);
|
|
95
|
+
});
|
|
96
|
+
it('accepts single valid timeframe', async () => {
|
|
97
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
98
|
+
mockGetUnpredictedRounds.mockResolvedValue([]);
|
|
99
|
+
const command = createMegathreadListCommand();
|
|
100
|
+
await command.parseAsync(['--agent', 'test-agent', '--timeframe', '24h'], { from: 'user' });
|
|
101
|
+
expect(mockGetUnpredictedRounds).toHaveBeenCalledWith(['24h']);
|
|
102
|
+
});
|
|
103
|
+
it('passes undefined when no timeframe filter specified', async () => {
|
|
104
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
105
|
+
mockGetUnpredictedRounds.mockResolvedValue([]);
|
|
106
|
+
const command = createMegathreadListCommand();
|
|
107
|
+
await command.parseAsync(['--agent', 'test-agent'], { from: 'user' });
|
|
108
|
+
expect(mockGetUnpredictedRounds).toHaveBeenCalledWith(undefined);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('agent validation', () => {
|
|
112
|
+
it('shows error when agent not found and lists available agents', async () => {
|
|
113
|
+
const command = createMegathreadListCommand();
|
|
114
|
+
await expect(command.parseAsync(['--agent', 'non-existent'], { from: 'user' })).rejects.toThrow('process.exit(1)');
|
|
115
|
+
expect(consoleErrorOutput.join('\n')).toContain('Agent "non-existent" not found');
|
|
116
|
+
expect(consoleErrorOutput.join('\n')).toContain('Available agents:');
|
|
117
|
+
expect(consoleErrorOutput.join('\n')).toContain('test-agent');
|
|
118
|
+
expect(consoleErrorOutput.join('\n')).toContain('empty-agent');
|
|
119
|
+
expect(consoleErrorOutput.join('\n')).toContain('agent-no-skills');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('credentials validation', () => {
|
|
123
|
+
it('shows error when credentials are missing', async () => {
|
|
124
|
+
mockLoadCredentials.mockResolvedValue(null);
|
|
125
|
+
const command = createMegathreadListCommand();
|
|
126
|
+
await expect(command.parseAsync(['--agent', 'test-agent'], { from: 'user' })).rejects.toThrow('process.exit(1)');
|
|
127
|
+
expect(consoleErrorOutput.join('\n')).toContain('No credentials found for agent "test-agent"');
|
|
128
|
+
});
|
|
129
|
+
it('shows error when credentials have no API key', async () => {
|
|
130
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: null });
|
|
131
|
+
const command = createMegathreadListCommand();
|
|
132
|
+
await expect(command.parseAsync(['--agent', 'test-agent'], { from: 'user' })).rejects.toThrow('process.exit(1)');
|
|
133
|
+
expect(consoleErrorOutput.join('\n')).toContain('No credentials found');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('rounds display', () => {
|
|
137
|
+
it('shows message when no unpredicted rounds available', async () => {
|
|
138
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
139
|
+
mockGetUnpredictedRounds.mockResolvedValue([]);
|
|
140
|
+
const command = createMegathreadListCommand();
|
|
141
|
+
await command.parseAsync(['--agent', 'test-agent'], { from: 'user' });
|
|
142
|
+
const output = consoleOutput.join('\n');
|
|
143
|
+
expect(output).toContain('Unpredicted Rounds for test-agent');
|
|
144
|
+
expect(output).toContain('No unpredicted rounds available');
|
|
145
|
+
});
|
|
146
|
+
it('displays rounds in table format', async () => {
|
|
147
|
+
const mockRounds = [
|
|
148
|
+
createMockActiveRound({ roundId: 'round-1', projectId: 'bitcoin', durationMs: 3600000 }),
|
|
149
|
+
createMockActiveRound({ roundId: 'round-2', projectId: 'ethereum', durationMs: 14400000 }),
|
|
150
|
+
];
|
|
151
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
152
|
+
mockGetUnpredictedRounds.mockResolvedValue(mockRounds);
|
|
153
|
+
const command = createMegathreadListCommand();
|
|
154
|
+
await command.parseAsync(['--agent', 'test-agent'], { from: 'user' });
|
|
155
|
+
const output = consoleOutput.join('\n');
|
|
156
|
+
expect(output).toContain('Unpredicted Rounds for test-agent');
|
|
157
|
+
expect(output).toContain('Round ID');
|
|
158
|
+
expect(output).toContain('Token');
|
|
159
|
+
expect(output).toContain('Timeframe');
|
|
160
|
+
expect(output).toContain('round-1');
|
|
161
|
+
expect(output).toContain('bitcoin');
|
|
162
|
+
expect(output).toContain('round-2');
|
|
163
|
+
expect(output).toContain('ethereum');
|
|
164
|
+
expect(output).toContain('Total: 2 round(s)');
|
|
165
|
+
});
|
|
166
|
+
it('shows fallback duration when timeframe not recognized', async () => {
|
|
167
|
+
const mockRounds = [
|
|
168
|
+
createMockActiveRound({ roundId: 'round-1', projectId: 'bitcoin', durationMs: 7200000 }),
|
|
169
|
+
];
|
|
170
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
171
|
+
mockGetUnpredictedRounds.mockResolvedValue(mockRounds);
|
|
172
|
+
const command = createMegathreadListCommand();
|
|
173
|
+
await command.parseAsync(['--agent', 'test-agent'], { from: 'user' });
|
|
174
|
+
const output = consoleOutput.join('\n');
|
|
175
|
+
expect(output).toContain('7200000ms');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('API error handling', () => {
|
|
179
|
+
it('shows error when API call fails', async () => {
|
|
180
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
181
|
+
mockGetUnpredictedRounds.mockRejectedValue(new Error('Network error'));
|
|
182
|
+
const command = createMegathreadListCommand();
|
|
183
|
+
await expect(command.parseAsync(['--agent', 'test-agent'], { from: 'user' })).rejects.toThrow('process.exit(1)');
|
|
184
|
+
expect(consoleErrorOutput.join('\n')).toContain('Failed to fetch unpredicted rounds');
|
|
185
|
+
expect(consoleErrorOutput.join('\n')).toContain('Network error');
|
|
186
|
+
});
|
|
187
|
+
it('handles non-Error exceptions', async () => {
|
|
188
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
189
|
+
mockGetUnpredictedRounds.mockRejectedValue('String error');
|
|
190
|
+
const command = createMegathreadListCommand();
|
|
191
|
+
await expect(command.parseAsync(['--agent', 'test-agent'], { from: 'user' })).rejects.toThrow('process.exit(1)');
|
|
192
|
+
expect(consoleErrorOutput.join('\n')).toContain('Failed to fetch unpredicted rounds');
|
|
193
|
+
expect(consoleErrorOutput.join('\n')).toContain('String error');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('works with different fixture agents', () => {
|
|
197
|
+
it('works with empty-agent', async () => {
|
|
198
|
+
mockLoadCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
|
|
199
|
+
mockGetUnpredictedRounds.mockResolvedValue([]);
|
|
200
|
+
const command = createMegathreadListCommand();
|
|
201
|
+
await command.parseAsync(['--agent', 'empty-agent'], { from: 'user' });
|
|
202
|
+
const output = consoleOutput.join('\n');
|
|
203
|
+
expect(output).toContain('Unpredicted Rounds for empty-agent');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
1
2
|
import { render } from 'ink';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
import { showWelcome } from '../../shared/welcome.js';
|
|
4
5
|
import { MigrateApp } from '../ui/MigrateApp.js';
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
export const createMigrateTemplatesCommand = () => {
|
|
7
|
+
return new Command('migrate-templates')
|
|
8
|
+
.description('Migrate old-style agents')
|
|
9
|
+
.action(async () => {
|
|
10
|
+
await showWelcome();
|
|
11
|
+
const { waitUntilExit } = render(React.createElement(MigrateApp));
|
|
12
|
+
await waitUntilExit();
|
|
13
|
+
});
|
|
9
14
|
};
|
|
@@ -1,17 +1,22 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
1
2
|
import { access } from 'fs/promises';
|
|
2
3
|
import { join } from 'path';
|
|
3
4
|
import { runHeadless } from '../run-headless.js';
|
|
4
5
|
import { loadAgentEnv } from '../../../shared/config/env-loader.js';
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
export const createRunCommand = () => {
|
|
7
|
+
return new Command('run')
|
|
8
|
+
.description('Run agent headless (no TUI, used by start-all)')
|
|
9
|
+
.action(async () => {
|
|
10
|
+
// Headless agent run — no TUI, just console output.
|
|
11
|
+
// Used by start-all to spawn agents as child processes.
|
|
12
|
+
const isAgentDir = await access(join(process.cwd(), 'SOUL.md'))
|
|
13
|
+
.then(() => true)
|
|
14
|
+
.catch(() => false);
|
|
15
|
+
if (!isAgentDir) {
|
|
16
|
+
console.error('Error: "run" must be called from an agent directory (with SOUL.md)');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
await loadAgentEnv();
|
|
20
|
+
await runHeadless();
|
|
21
|
+
});
|
|
17
22
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { HiveAgent } from '@zhive/sdk';
|
|
2
|
-
import { initializeAgentRuntime, createMegathreadRoundHandler, } from '../../shared/agent/agent-runtime.js';
|
|
3
2
|
import { HIVE_API_URL, HIVE_FRONTEND_URL } from '../../shared/config/constant.js';
|
|
4
3
|
import { resolveModelInfo } from '../../shared/config/ai-providers.js';
|
|
5
4
|
import { formatTokenCount, formatTokenUsage } from '../../shared/agent/utils.js';
|
|
5
|
+
import { initializeAgentRuntime } from '../../shared/agent/runtime.js';
|
|
6
|
+
import { createMegathreadRoundHandler } from '../../shared/agent/handler.js';
|
|
6
7
|
function formatUsageLine(usage) {
|
|
7
8
|
const { input, output, tools } = formatTokenUsage(usage);
|
|
8
9
|
const toolSuffix = tools !== null ? ` \u00b7 ${tools}` : '';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
1
2
|
import { render } from 'ink';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
import { App } from '../ui/app.js';
|
|
@@ -6,43 +7,47 @@ import { showHoneycombBoot } from '../ui/HoneycombBoot.js';
|
|
|
6
7
|
import chalk from 'chalk';
|
|
7
8
|
import { symbols } from '../../shared/theme.js';
|
|
8
9
|
import { loadAgentEnv } from '../../../shared/config/env-loader.js';
|
|
9
|
-
export const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
await waitUntilExit();
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
// Interactive agent selection
|
|
26
|
-
let selectedAgent = null;
|
|
27
|
-
const { waitUntilExit: waitForSelect } = render(React.createElement(SelectAgentApp, {
|
|
28
|
-
onSelect: (agent) => {
|
|
29
|
-
selectedAgent = agent;
|
|
30
|
-
},
|
|
31
|
-
}));
|
|
32
|
-
await waitForSelect();
|
|
33
|
-
if (selectedAgent) {
|
|
34
|
-
const picked = selectedAgent;
|
|
35
|
-
await showHoneycombBoot(picked.name);
|
|
36
|
-
// Clear screen + scrollback so boot animation and agent picker
|
|
37
|
-
// don't appear when scrolling up in the agent TUI.
|
|
38
|
-
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
39
|
-
process.chdir(picked.dir);
|
|
10
|
+
export const createStartCommand = () => {
|
|
11
|
+
return new Command('start')
|
|
12
|
+
.description('Start an agent (auto-detects agent dir)')
|
|
13
|
+
.action(async () => {
|
|
14
|
+
// Detect if cwd is an agent directory (has SOUL.md).
|
|
15
|
+
// When called via agent's "npm start", cwd is the agent dir.
|
|
16
|
+
const { access } = await import('fs/promises');
|
|
17
|
+
const { join } = await import('path');
|
|
18
|
+
const isAgentDir = await access(join(process.cwd(), 'SOUL.md'))
|
|
19
|
+
.then(() => true)
|
|
20
|
+
.catch(() => false);
|
|
21
|
+
if (isAgentDir) {
|
|
22
|
+
// Direct agent run — cwd is already the agent directory.
|
|
40
23
|
await loadAgentEnv();
|
|
41
24
|
setupProcessLifecycle();
|
|
42
25
|
const { waitUntilExit } = render(React.createElement(App));
|
|
43
26
|
await waitUntilExit();
|
|
44
27
|
}
|
|
45
|
-
|
|
28
|
+
else {
|
|
29
|
+
// Interactive agent selection
|
|
30
|
+
let selectedAgent = null;
|
|
31
|
+
const { waitUntilExit: waitForSelect } = render(React.createElement(SelectAgentApp, {
|
|
32
|
+
onSelect: (agent) => {
|
|
33
|
+
selectedAgent = agent;
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
await waitForSelect();
|
|
37
|
+
if (selectedAgent) {
|
|
38
|
+
const picked = selectedAgent;
|
|
39
|
+
await showHoneycombBoot(picked.name);
|
|
40
|
+
// Clear screen + scrollback so boot animation and agent picker
|
|
41
|
+
// don't appear when scrolling up in the agent TUI.
|
|
42
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
43
|
+
process.chdir(picked.dir);
|
|
44
|
+
await loadAgentEnv();
|
|
45
|
+
setupProcessLifecycle();
|
|
46
|
+
const { waitUntilExit } = render(React.createElement(App));
|
|
47
|
+
await waitUntilExit();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
46
51
|
};
|
|
47
52
|
const exitImmediately = (exitCode = 0) => {
|
|
48
53
|
process.exit(exitCode);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { HiveAgent } from '@zhive/sdk';
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { createMegathreadRoundBatchHandler, createMegathreadRoundHandler, initializeAgentRuntime, } from '../../../shared/agent/agent-runtime.js';
|
|
4
3
|
import { extractErrorMessage } from '../../../shared/agent/utils.js';
|
|
5
4
|
import { resolveModelInfo } from '../../../shared/config/ai-providers.js';
|
|
6
5
|
import { fetchBulkStats } from '../../../shared/config/agent.js';
|
|
7
6
|
import { HIVE_API_URL } from '../../../shared/config/constant.js';
|
|
8
7
|
import { usePollActivity } from './usePollActivity.js';
|
|
8
|
+
import { initializeAgentRuntime } from '../../../shared/agent/runtime.js';
|
|
9
|
+
import { createMegathreadRoundBatchHandler, createMegathreadRoundHandler, } from '../../../shared/agent/handler.js';
|
|
9
10
|
const STATS_POLL_INTERVAL_MS = 5 * 60 * 1_000;
|
|
10
11
|
export function useAgent() {
|
|
11
12
|
const [connected, setConnected] = useState(false);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { processMegathreadRound } from '../../../../shared/agent/analysis.js';
|
|
2
|
-
import { initializeAgentRuntime } from '../../../../shared/agent/
|
|
2
|
+
import { initializeAgentRuntime } from '../../../../shared/agent/runtime.js';
|
|
3
3
|
/**
|
|
4
4
|
* Run a backtest against an agent configuration.
|
|
5
5
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import { render } from 'ink';
|
|
3
4
|
import { showWelcome } from '../../shared/welcome.js';
|
|
@@ -5,20 +6,24 @@ import { styled, symbols } from '../../shared/theme.js';
|
|
|
5
6
|
import { AgentProcessManager } from '../AgentProcessManager.js';
|
|
6
7
|
import { Dashboard } from '../ui/Dashboard.js';
|
|
7
8
|
import { fetchBulkStats, scanAgents, sortAgentsByHoney } from '../../../shared/config/agent.js';
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
9
|
+
export const createStartAllCommand = () => {
|
|
10
|
+
return new Command('start-all')
|
|
11
|
+
.description('Start all agents')
|
|
12
|
+
.action(async () => {
|
|
13
|
+
// Run welcome animation and scan agents in parallel
|
|
14
|
+
const results = await Promise.all([showWelcome(), scanAgents()]);
|
|
15
|
+
const discovered = results[1];
|
|
16
|
+
if (discovered.length === 0) {
|
|
17
|
+
console.log(`\n ${styled.honey(symbols.hive)} ${styled.red('No agents found in ~/.zhive/agents/')}\n`);
|
|
18
|
+
console.log(` ${styled.gray('Create agents with:')} ${styled.white('npx @zhive/cli@latest create')}\n`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const names = discovered.map((a) => a.name);
|
|
22
|
+
const statsMap = await fetchBulkStats(names);
|
|
23
|
+
const sortedDiscovered = sortAgentsByHoney(discovered, statsMap);
|
|
24
|
+
const manager = new AgentProcessManager();
|
|
25
|
+
manager.spawnAll(sortedDiscovered);
|
|
26
|
+
const { waitUntilExit } = render(React.createElement(Dashboard, { manager, statsMap }));
|
|
27
|
+
await waitUntilExit();
|
|
28
|
+
});
|
|
29
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,60 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
2
3
|
import { createRequire } from 'module';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { createAgentCommand } from './commands/agent/commands/index.js';
|
|
5
|
+
import { createCreateCommand } from './commands/create/commands/index.js';
|
|
6
|
+
import { createListCommand } from './commands/list/commands/index.js';
|
|
7
|
+
import { createMegathreadCommand } from './commands/megathread/commands/index.js';
|
|
8
|
+
import { createStartCommand } from './commands/start/commands/index.js';
|
|
9
|
+
import { createStartAllCommand } from './commands/start-all/commands/index.js';
|
|
10
|
+
import { createRunCommand } from './commands/run/commands/index.js';
|
|
11
|
+
import { createMigrateTemplatesCommand } from './commands/migrate-templates/commands/index.js';
|
|
9
12
|
const require = createRequire(import.meta.url);
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
npx @zhive/cli@latest start Pick an agent and run it
|
|
28
|
-
npx @zhive/cli@latest start-all Launch all agents as child processes`;
|
|
29
|
-
const command = process.argv[2];
|
|
30
|
-
if (command === '--version' || command === '-v') {
|
|
31
|
-
console.log(pkg.version);
|
|
32
|
-
process.exit(0);
|
|
33
|
-
}
|
|
34
|
-
if (!command || command === '--help' || command === '-h') {
|
|
35
|
-
console.log(HELP_TEXT);
|
|
36
|
-
process.exit(0);
|
|
37
|
-
}
|
|
38
|
-
if (command === 'list') {
|
|
39
|
-
await listCommand();
|
|
40
|
-
}
|
|
41
|
-
else if (command === 'start-all') {
|
|
42
|
-
await startAllCommand();
|
|
43
|
-
}
|
|
44
|
-
else if (command === 'create') {
|
|
45
|
-
await createCommand(process.argv);
|
|
46
|
-
}
|
|
47
|
-
else if (command === 'migrate-templates') {
|
|
48
|
-
await migrateTemplateCommand();
|
|
49
|
-
}
|
|
50
|
-
else if (command === 'run') {
|
|
51
|
-
await runCommand();
|
|
52
|
-
}
|
|
53
|
-
else if (command === 'start') {
|
|
54
|
-
await startCommand();
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
console.error(`Unknown command: ${command}\n`);
|
|
58
|
-
console.log(HELP_TEXT);
|
|
59
|
-
process.exit(1);
|
|
60
|
-
}
|
|
13
|
+
const packageJson = require('../package.json');
|
|
14
|
+
const program = new Command();
|
|
15
|
+
program.name('@zhive/cli').version(packageJson.version);
|
|
16
|
+
program.addCommand(createAgentCommand());
|
|
17
|
+
program.addCommand(createCreateCommand());
|
|
18
|
+
program.addCommand(createListCommand());
|
|
19
|
+
program.addCommand(createMegathreadCommand());
|
|
20
|
+
program.addCommand(createStartCommand());
|
|
21
|
+
program.addCommand(createStartAllCommand());
|
|
22
|
+
program.addCommand(createRunCommand());
|
|
23
|
+
program.addCommand(createMigrateTemplatesCommand());
|
|
24
|
+
// Show help with exit code 0 when no arguments provided
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
if (args.length === 0) {
|
|
27
|
+
program.help();
|
|
28
|
+
}
|
|
29
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { processMegathreadRound, screenMegathreadRound, } from './analysis.js';
|
|
2
|
+
import { getMarketClient } from './tools/market/index.js';
|
|
3
|
+
import { extractErrorMessage } from './utils.js';
|
|
4
|
+
export const fetchPrice = async (projectId, timestamp) => {
|
|
5
|
+
const client = getMarketClient();
|
|
6
|
+
const response = await client.getPrice(projectId, timestamp);
|
|
7
|
+
return response.price ?? undefined;
|
|
8
|
+
};
|
|
9
|
+
export async function fetchRoundPrices(projectId, roundTimestamp, currentTime) {
|
|
10
|
+
let priceAtStart;
|
|
11
|
+
let currentPrice;
|
|
12
|
+
try {
|
|
13
|
+
const client = getMarketClient();
|
|
14
|
+
[priceAtStart, currentPrice] = await Promise.all([
|
|
15
|
+
fetchPrice(projectId, roundTimestamp),
|
|
16
|
+
fetchPrice(projectId, currentTime ?? new Date().toISOString()),
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Price fetch failed — both stay undefined
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
priceAtStart,
|
|
24
|
+
currentPrice,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const calculateTimeframe = (round) => {
|
|
28
|
+
const hours = Math.round(round.durationMs / 3_600_000);
|
|
29
|
+
const timeframe = hours >= 1 ? `${hours}h` : `${Math.round(round.durationMs / 60_000)}m`;
|
|
30
|
+
return timeframe;
|
|
31
|
+
};
|
|
32
|
+
async function run({ round, runtime, reporter, recentComments, }) {
|
|
33
|
+
const timeframe = calculateTimeframe(round);
|
|
34
|
+
reporter.onRoundStart(round, timeframe);
|
|
35
|
+
// ── Fetch prices ──────────────────────────────
|
|
36
|
+
const roundStartTimestamp = round.roundId.split('@Z')[0];
|
|
37
|
+
const { priceAtStart, currentPrice } = await fetchRoundPrices(round.projectId, roundStartTimestamp);
|
|
38
|
+
if (priceAtStart !== undefined) {
|
|
39
|
+
reporter.onPriceInfo(priceAtStart, currentPrice);
|
|
40
|
+
}
|
|
41
|
+
// ── Quick screen (cheap engage check) ───────
|
|
42
|
+
const screenResult = await screenMegathreadRound(round.projectId, runtime.config.strategyContent);
|
|
43
|
+
if (!screenResult.engage) {
|
|
44
|
+
reporter.onScreenResult?.(round, screenResult);
|
|
45
|
+
return { skip: true, usage: screenResult.usage, screenResult };
|
|
46
|
+
}
|
|
47
|
+
reporter.onResearching(round.projectId);
|
|
48
|
+
// ── Run analysis ──────────────────────────────
|
|
49
|
+
const result = await processMegathreadRound({
|
|
50
|
+
projectId: round.projectId,
|
|
51
|
+
durationMs: round.durationMs,
|
|
52
|
+
recentComments,
|
|
53
|
+
agentRuntime: runtime,
|
|
54
|
+
priceAtStart,
|
|
55
|
+
currentPrice,
|
|
56
|
+
});
|
|
57
|
+
reporter.onToolsUsed(result.usage.toolNames, result.usage.toolCalls);
|
|
58
|
+
if (result.skip) {
|
|
59
|
+
reporter.onSkipped(round, result.usage);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
export function createMegathreadRoundBatchHandler(getAgent, runtime, reporter) {
|
|
64
|
+
const handler = async (rounds) => {
|
|
65
|
+
const agent = getAgent();
|
|
66
|
+
const promises = [];
|
|
67
|
+
// report item in order that it is polled to prevent out-of-order write to stdout
|
|
68
|
+
for (const round of rounds) {
|
|
69
|
+
promises.push(run({ round, runtime, reporter, recentComments: agent.recentComments }));
|
|
70
|
+
}
|
|
71
|
+
const results = await Promise.allSettled(promises);
|
|
72
|
+
for (let i = 0; i < results.length; i++) {
|
|
73
|
+
const round = rounds[i];
|
|
74
|
+
const result = results[i];
|
|
75
|
+
if (result.status === 'rejected') {
|
|
76
|
+
const raw = extractErrorMessage(result.reason);
|
|
77
|
+
const message = raw.length > 120 ? raw.slice(0, 120) + '\u2026' : raw;
|
|
78
|
+
reporter.onError(round, message);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const data = result.value;
|
|
82
|
+
if (data.skip) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// TODO: we can optimized this by create method to commit this in batch in hive sdk.
|
|
86
|
+
// postMegathreadComment cannot be run concurrently so we need to call it one by one.
|
|
87
|
+
await agent.postMegathreadComment(round.roundId, {
|
|
88
|
+
text: data.summary,
|
|
89
|
+
conviction: data.conviction,
|
|
90
|
+
tokenId: round.projectId,
|
|
91
|
+
roundDuration: round.durationMs,
|
|
92
|
+
});
|
|
93
|
+
const timeframe = calculateTimeframe(round);
|
|
94
|
+
reporter.onPosted(round, data.conviction, data.summary, timeframe, data.usage);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
return handler;
|
|
98
|
+
}
|
|
99
|
+
export function createMegathreadRoundHandler(getAgent, runtime, reporter) {
|
|
100
|
+
const handler = async (round) => {
|
|
101
|
+
const agent = getAgent();
|
|
102
|
+
try {
|
|
103
|
+
const result = await run({
|
|
104
|
+
round,
|
|
105
|
+
reporter,
|
|
106
|
+
recentComments: agent.recentComments,
|
|
107
|
+
runtime,
|
|
108
|
+
});
|
|
109
|
+
if (result.skip) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// ── Post comment ──────────────────────────────
|
|
113
|
+
await agent.postMegathreadComment(round.roundId, {
|
|
114
|
+
text: result.summary,
|
|
115
|
+
conviction: result.conviction,
|
|
116
|
+
tokenId: round.projectId,
|
|
117
|
+
roundDuration: round.durationMs,
|
|
118
|
+
});
|
|
119
|
+
const timeframe = calculateTimeframe(round);
|
|
120
|
+
reporter.onPosted(round, result.conviction, result.summary, timeframe, result.usage);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
const raw = extractErrorMessage(err);
|
|
124
|
+
const message = raw.length > 120 ? raw.slice(0, 120) + '\u2026' : raw;
|
|
125
|
+
reporter.onError(round, message);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
return handler;
|
|
129
|
+
}
|