@zhive/cli 0.5.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.
Files changed (189) hide show
  1. package/README.md +118 -0
  2. package/dist/agent/analysis.js +160 -0
  3. package/dist/agent/app.js +122 -0
  4. package/dist/agent/chat-prompt.js +65 -0
  5. package/dist/agent/commands/registry.js +12 -0
  6. package/dist/agent/components/AsciiTicker.js +81 -0
  7. package/dist/agent/components/CommandInput.js +65 -0
  8. package/dist/agent/components/HoneycombBoot.js +291 -0
  9. package/dist/agent/components/Spinner.js +37 -0
  10. package/dist/agent/config.js +75 -0
  11. package/dist/agent/edit-section.js +59 -0
  12. package/dist/agent/fetch-rules.js +21 -0
  13. package/dist/agent/helpers.js +22 -0
  14. package/dist/agent/hooks/useAgent.js +480 -0
  15. package/dist/agent/memory-prompt.js +47 -0
  16. package/dist/agent/model.js +92 -0
  17. package/dist/agent/objects.js +1 -0
  18. package/dist/agent/process-lifecycle.js +18 -0
  19. package/dist/agent/prompt.js +353 -0
  20. package/dist/agent/run-headless.js +189 -0
  21. package/dist/agent/skills/index.js +2 -0
  22. package/dist/agent/skills/skill-parser.js +149 -0
  23. package/dist/agent/skills/types.js +1 -0
  24. package/dist/agent/theme.js +41 -0
  25. package/dist/agent/tools/index.js +76 -0
  26. package/dist/agent/tools/market/client.js +41 -0
  27. package/dist/agent/tools/market/index.js +3 -0
  28. package/dist/agent/tools/market/tools.js +518 -0
  29. package/dist/agent/tools/mindshare/client.js +124 -0
  30. package/dist/agent/tools/mindshare/index.js +3 -0
  31. package/dist/agent/tools/mindshare/tools.js +563 -0
  32. package/dist/agent/tools/read-skill-tool.js +30 -0
  33. package/dist/agent/tools/ta/index.js +1 -0
  34. package/dist/agent/tools/ta/indicators.js +201 -0
  35. package/dist/agent/types.js +1 -0
  36. package/dist/agents.js +110 -0
  37. package/dist/ai-providers.js +66 -0
  38. package/dist/avatar.js +34 -0
  39. package/dist/backtest/default-backtest-data.js +200 -0
  40. package/dist/backtest/fetch.js +41 -0
  41. package/dist/backtest/import.js +106 -0
  42. package/dist/backtest/index.js +10 -0
  43. package/dist/backtest/results.js +113 -0
  44. package/dist/backtest/runner.js +134 -0
  45. package/dist/backtest/storage.js +11 -0
  46. package/dist/backtest/types.js +1 -0
  47. package/dist/commands/create/ai-generate.js +126 -0
  48. package/dist/commands/create/commands/index.js +10 -0
  49. package/dist/commands/create/generate.js +73 -0
  50. package/dist/commands/create/presets/data.js +225 -0
  51. package/dist/commands/create/presets/formatting.js +81 -0
  52. package/dist/commands/create/presets/index.js +3 -0
  53. package/dist/commands/create/presets/options.js +307 -0
  54. package/dist/commands/create/presets/types.js +1 -0
  55. package/dist/commands/create/presets.js +613 -0
  56. package/dist/commands/create/ui/CreateApp.js +172 -0
  57. package/dist/commands/create/ui/steps/ApiKeyStep.js +89 -0
  58. package/dist/commands/create/ui/steps/AvatarStep.js +16 -0
  59. package/dist/commands/create/ui/steps/DoneStep.js +14 -0
  60. package/dist/commands/create/ui/steps/IdentityStep.js +125 -0
  61. package/dist/commands/create/ui/steps/NameStep.js +148 -0
  62. package/dist/commands/create/ui/steps/ScaffoldStep.js +59 -0
  63. package/dist/commands/create/ui/steps/SoulStep.js +21 -0
  64. package/dist/commands/create/ui/steps/StrategyStep.js +20 -0
  65. package/dist/commands/create/ui/steps/StreamingGenerationStep.js +56 -0
  66. package/dist/commands/create/ui/validation.js +34 -0
  67. package/dist/commands/create/validate-api-key.js +27 -0
  68. package/dist/commands/install.js +50 -0
  69. package/dist/commands/list/commands/index.js +7 -0
  70. package/dist/commands/list/ui/ListApp.js +79 -0
  71. package/dist/commands/migrate-templates/commands/index.js +9 -0
  72. package/dist/commands/migrate-templates/migrate.js +87 -0
  73. package/dist/commands/migrate-templates/ui/MigrateApp.js +132 -0
  74. package/dist/commands/run/commands/index.js +17 -0
  75. package/dist/commands/run/run-headless.js +111 -0
  76. package/dist/commands/shared/theme.js +57 -0
  77. package/dist/commands/shared/welcome.js +304 -0
  78. package/dist/commands/start/commands/backtest.js +35 -0
  79. package/dist/commands/start/commands/index.js +62 -0
  80. package/dist/commands/start/commands/prediction.js +73 -0
  81. package/dist/commands/start/commands/skills.js +44 -0
  82. package/dist/commands/start/commands/skills.test.js +140 -0
  83. package/dist/commands/start/hooks/types.js +1 -0
  84. package/dist/commands/start/hooks/useAgent.js +177 -0
  85. package/dist/commands/start/hooks/useChat.js +266 -0
  86. package/dist/commands/start/hooks/usePollActivity.js +45 -0
  87. package/dist/commands/start/hooks/utils.js +152 -0
  88. package/dist/commands/start/services/backtest/default-backtest-data.js +200 -0
  89. package/dist/commands/start/services/backtest/fetch.js +42 -0
  90. package/dist/commands/start/services/backtest/import.js +109 -0
  91. package/dist/commands/start/services/backtest/index.js +10 -0
  92. package/dist/commands/start/services/backtest/results.js +113 -0
  93. package/dist/commands/start/services/backtest/runner.js +103 -0
  94. package/dist/commands/start/services/backtest/storage.js +11 -0
  95. package/dist/commands/start/services/backtest/types.js +1 -0
  96. package/dist/commands/start/services/command-registry.js +13 -0
  97. package/dist/commands/start/ui/AsciiTicker.js +81 -0
  98. package/dist/commands/start/ui/CommandInput.js +65 -0
  99. package/dist/commands/start/ui/HoneycombBoot.js +291 -0
  100. package/dist/commands/start/ui/PollText.js +23 -0
  101. package/dist/commands/start/ui/PredictionsPanel.js +88 -0
  102. package/dist/commands/start/ui/SelectAgentApp.js +93 -0
  103. package/dist/commands/start/ui/Spinner.js +29 -0
  104. package/dist/commands/start/ui/SpinnerContext.js +20 -0
  105. package/dist/commands/start/ui/app.js +36 -0
  106. package/dist/commands/start-all/AgentProcessManager.js +98 -0
  107. package/dist/commands/start-all/commands/index.js +24 -0
  108. package/dist/commands/start-all/ui/Dashboard.js +91 -0
  109. package/dist/components/AsciiTicker.js +81 -0
  110. package/dist/components/CharacterSummaryCard.js +33 -0
  111. package/dist/components/CodeBlock.js +11 -0
  112. package/dist/components/ColoredStats.js +18 -0
  113. package/dist/components/Header.js +10 -0
  114. package/dist/components/HoneycombLoader.js +190 -0
  115. package/dist/components/InputGuard.js +6 -0
  116. package/dist/components/MultiSelectPrompt.js +45 -0
  117. package/dist/components/SelectPrompt.js +20 -0
  118. package/dist/components/Spinner.js +16 -0
  119. package/dist/components/StepIndicator.js +31 -0
  120. package/dist/components/StreamingText.js +50 -0
  121. package/dist/components/TextPrompt.js +28 -0
  122. package/dist/components/stdout-spinner.js +48 -0
  123. package/dist/config.js +28 -0
  124. package/dist/create/CreateApp.js +153 -0
  125. package/dist/create/ai-generate.js +147 -0
  126. package/dist/create/generate.js +73 -0
  127. package/dist/create/steps/ApiKeyStep.js +97 -0
  128. package/dist/create/steps/AvatarStep.js +16 -0
  129. package/dist/create/steps/BioStep.js +14 -0
  130. package/dist/create/steps/DoneStep.js +14 -0
  131. package/dist/create/steps/IdentityStep.js +163 -0
  132. package/dist/create/steps/NameStep.js +71 -0
  133. package/dist/create/steps/ScaffoldStep.js +58 -0
  134. package/dist/create/steps/SoulStep.js +58 -0
  135. package/dist/create/steps/StrategyStep.js +58 -0
  136. package/dist/create/validate-api-key.js +47 -0
  137. package/dist/create/welcome.js +304 -0
  138. package/dist/index.js +60 -0
  139. package/dist/list/ListApp.js +79 -0
  140. package/dist/load-agent-env.js +30 -0
  141. package/dist/migrate-templates/MigrateApp.js +131 -0
  142. package/dist/migrate-templates/migrate.js +86 -0
  143. package/dist/presets.js +613 -0
  144. package/dist/shared/agent/agent-runtime.js +144 -0
  145. package/dist/shared/agent/analysis.js +171 -0
  146. package/dist/shared/agent/helpers.js +1 -0
  147. package/dist/shared/agent/prompts/chat-prompt.js +60 -0
  148. package/dist/shared/agent/prompts/megathread.js +202 -0
  149. package/dist/shared/agent/prompts/memory-prompt.js +47 -0
  150. package/dist/shared/agent/prompts/prompt.js +18 -0
  151. package/dist/shared/agent/skills/index.js +2 -0
  152. package/dist/shared/agent/skills/skill-parser.js +167 -0
  153. package/dist/shared/agent/skills/skill-parser.test.js +190 -0
  154. package/dist/shared/agent/skills/types.js +1 -0
  155. package/dist/shared/agent/tools/edit-section.js +60 -0
  156. package/dist/shared/agent/tools/execute-skill-tool.js +134 -0
  157. package/dist/shared/agent/tools/fetch-rules.js +22 -0
  158. package/dist/shared/agent/tools/formatting.js +48 -0
  159. package/dist/shared/agent/tools/index.js +87 -0
  160. package/dist/shared/agent/tools/market/client.js +41 -0
  161. package/dist/shared/agent/tools/market/index.js +3 -0
  162. package/dist/shared/agent/tools/market/tools.js +497 -0
  163. package/dist/shared/agent/tools/mindshare/client.js +124 -0
  164. package/dist/shared/agent/tools/mindshare/index.js +3 -0
  165. package/dist/shared/agent/tools/mindshare/tools.js +167 -0
  166. package/dist/shared/agent/tools/read-skill-tool.js +30 -0
  167. package/dist/shared/agent/tools/ta/index.js +1 -0
  168. package/dist/shared/agent/tools/ta/indicators.js +201 -0
  169. package/dist/shared/agent/types.js +1 -0
  170. package/dist/shared/agent/utils.js +43 -0
  171. package/dist/shared/config/agent.js +177 -0
  172. package/dist/shared/config/ai-providers.js +156 -0
  173. package/dist/shared/config/config.js +22 -0
  174. package/dist/shared/config/constant.js +8 -0
  175. package/dist/shared/config/env-loader.js +30 -0
  176. package/dist/shared/types.js +1 -0
  177. package/dist/start/AgentProcessManager.js +98 -0
  178. package/dist/start/Dashboard.js +92 -0
  179. package/dist/start/SelectAgentApp.js +81 -0
  180. package/dist/start/StartApp.js +189 -0
  181. package/dist/start/patch-headless.js +101 -0
  182. package/dist/start/patch-managed-mode.js +142 -0
  183. package/dist/start/start-command.js +24 -0
  184. package/dist/theme.js +54 -0
  185. package/package.json +68 -0
  186. package/templates/components/HoneycombBoot.tsx +343 -0
  187. package/templates/fetch-rules.ts +23 -0
  188. package/templates/skills/mindshare/SKILL.md +197 -0
  189. package/templates/skills/ta/SKILL.md +179 -0
@@ -0,0 +1,140 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { fileURLToPath } from 'node:url';
3
+ import * as path from 'node:path';
4
+ import { fetchSkills, formatSkills, skillsSlashCommand } from './skills.js';
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const FIXTURES_DIR = path.join(__dirname, '../../../tests/__fixtures__/mock-hive');
7
+ vi.mock('../../../shared/config/constant.js', () => ({
8
+ getHiveDir: vi.fn(() => path.join(__dirname, '../../../tests/__fixtures__/mock-hive')),
9
+ }));
10
+ function createMockSkill(overrides = {}) {
11
+ const skill = {
12
+ id: 'test-skill',
13
+ path: '/mock/.hive/agents/test-agent/skills/test-skill',
14
+ metadata: {
15
+ name: 'Test Skill',
16
+ description: 'A test skill description',
17
+ },
18
+ body: 'Skill body content',
19
+ ...overrides,
20
+ };
21
+ return skill;
22
+ }
23
+ describe('formatSkills', () => {
24
+ it('returns no skills message for empty array', () => {
25
+ const result = formatSkills([]);
26
+ expect(result).toContain('No skills loaded');
27
+ expect(result).toContain('skills/ directory');
28
+ });
29
+ it('formats a single skill correctly', () => {
30
+ const skills = [createMockSkill()];
31
+ const result = formatSkills(skills);
32
+ expect(result).toContain('Available Skills');
33
+ expect(result).toContain('Test Skill');
34
+ expect(result).toContain('A test skill description');
35
+ });
36
+ it('includes compatibility information when present', () => {
37
+ const skillWithCompat = createMockSkill({
38
+ metadata: {
39
+ name: 'Compat Skill',
40
+ description: 'Skill with compatibility',
41
+ compatibility: 'Requires Node 18+',
42
+ },
43
+ });
44
+ const skills = [skillWithCompat];
45
+ const result = formatSkills(skills);
46
+ expect(result).toContain('Compat Skill');
47
+ expect(result).toContain('Skill with compatibility');
48
+ expect(result).toContain('Requires Node 18+');
49
+ });
50
+ it('formats multiple skills correctly', () => {
51
+ const skills = [
52
+ createMockSkill({
53
+ id: 'skill-1',
54
+ metadata: { name: 'First Skill', description: 'First description' },
55
+ }),
56
+ createMockSkill({
57
+ id: 'skill-2',
58
+ metadata: { name: 'Second Skill', description: 'Second description' },
59
+ }),
60
+ ];
61
+ const result = formatSkills(skills);
62
+ expect(result).toContain('First Skill');
63
+ expect(result).toContain('First description');
64
+ expect(result).toContain('Second Skill');
65
+ expect(result).toContain('Second description');
66
+ });
67
+ });
68
+ describe('fetchSkills', () => {
69
+ beforeEach(() => {
70
+ vi.clearAllMocks();
71
+ });
72
+ it('returns empty array for non-existent agent', async () => {
73
+ const result = await fetchSkills('non-existent-agent');
74
+ expect(result.success).toBe(true);
75
+ if (result.success) {
76
+ expect(result.data).toEqual([]);
77
+ }
78
+ });
79
+ it('returns empty array for agent with empty skills directory', async () => {
80
+ const result = await fetchSkills('empty-agent');
81
+ expect(result.success).toBe(true);
82
+ if (result.success) {
83
+ expect(result.data).toEqual([]);
84
+ }
85
+ });
86
+ it('returns discovered skills successfully', async () => {
87
+ const result = await fetchSkills('test-agent');
88
+ expect(result.success).toBe(true);
89
+ if (result.success) {
90
+ expect(result.data).toHaveLength(2);
91
+ const skillIds = result.data.map((s) => s.id).sort();
92
+ expect(skillIds).toEqual(['skill-with-compat', 'valid-skill']);
93
+ }
94
+ });
95
+ it('loads skill metadata correctly', async () => {
96
+ const result = await fetchSkills('test-agent');
97
+ expect(result.success).toBe(true);
98
+ if (result.success) {
99
+ const validSkill = result.data.find((s) => s.id === 'valid-skill');
100
+ expect(validSkill).toBeDefined();
101
+ expect(validSkill?.metadata.name).toBe('Valid Skill');
102
+ expect(validSkill?.metadata.description).toBe('A valid test skill for testing');
103
+ }
104
+ });
105
+ });
106
+ describe('skillsSlashCommand', () => {
107
+ beforeEach(() => {
108
+ vi.clearAllMocks();
109
+ });
110
+ it('calls onSuccess with formatted output on success', async () => {
111
+ const onSuccess = vi.fn();
112
+ const onError = vi.fn();
113
+ await skillsSlashCommand('test-agent', { onSuccess, onError });
114
+ expect(onSuccess).toHaveBeenCalledTimes(1);
115
+ expect(onSuccess.mock.calls[0][0]).toContain('Available Skills');
116
+ expect(onSuccess.mock.calls[0][0]).toContain('Valid Skill');
117
+ expect(onSuccess.mock.calls[0][0]).toContain('Skill With Compatibility');
118
+ expect(onError).not.toHaveBeenCalled();
119
+ });
120
+ it('handles empty skills list', async () => {
121
+ const onSuccess = vi.fn();
122
+ const onError = vi.fn();
123
+ await skillsSlashCommand('empty-agent', { onSuccess, onError });
124
+ expect(onSuccess).toHaveBeenCalledTimes(1);
125
+ expect(onSuccess.mock.calls[0][0]).toContain('No skills loaded');
126
+ expect(onError).not.toHaveBeenCalled();
127
+ });
128
+ it('works without callbacks', async () => {
129
+ // Should not throw
130
+ await skillsSlashCommand('test-agent', {});
131
+ });
132
+ it('works with agent that has no skills directory', async () => {
133
+ const onSuccess = vi.fn();
134
+ const onError = vi.fn();
135
+ await skillsSlashCommand('agent-no-skills', { onSuccess, onError });
136
+ expect(onSuccess).toHaveBeenCalledTimes(1);
137
+ expect(onSuccess.mock.calls[0][0]).toContain('No skills loaded');
138
+ expect(onError).not.toHaveBeenCalled();
139
+ });
140
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,177 @@
1
+ import { HiveAgent } from '@zhive/sdk';
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { createMegathreadRoundBatchHandler, createMegathreadRoundHandler, initializeAgentRuntime, } from '../../../shared/agent/agent-runtime.js';
4
+ import { extractErrorMessage } from '../../../shared/agent/utils.js';
5
+ import { resolveModelInfo } from '../../../shared/config/ai-providers.js';
6
+ import { fetchBulkStats } from '../../../shared/config/agent.js';
7
+ import { HIVE_API_URL } from '../../../shared/config/constant.js';
8
+ import { usePollActivity } from './usePollActivity.js';
9
+ const STATS_POLL_INTERVAL_MS = 5 * 60 * 1_000;
10
+ export function useAgent() {
11
+ const [connected, setConnected] = useState(false);
12
+ const [agentName, setAgentName] = useState('agent');
13
+ const [agentBio, setAgentBio] = useState('');
14
+ const [modelInfo, setModelInfo] = useState(null);
15
+ const [sectorsDisplay, setSectorsDisplay] = useState(null);
16
+ const [timeframesDisplay, setTimeframesDisplay] = useState(null);
17
+ const [predictionCount, setPredictionCount] = useState(0);
18
+ const [termWidth, setTermWidth] = useState(process.stdout.columns || 60);
19
+ const [stats, setStats] = useState(null);
20
+ const [statsUpdatedAt, setStatsUpdatedAt] = useState(null);
21
+ const agentRef = useRef(null);
22
+ const recentPredictionsRef = useRef([]);
23
+ const predictionCountRef = useRef(0);
24
+ const { activePollActivities, settledPollActivities, addLog, addMegathreadActivity, finalizeMegathreadActivity, } = usePollActivity();
25
+ // ─── Terminal resize tracking ───────────────────────
26
+ useEffect(() => {
27
+ const onResize = () => {
28
+ setTermWidth(process.stdout.columns || 60);
29
+ };
30
+ process.stdout.on('resize', onResize);
31
+ return () => {
32
+ process.stdout.off('resize', onResize);
33
+ };
34
+ }, []);
35
+ // ─── Stats polling (every 5 min) ───────────────────
36
+ useEffect(() => {
37
+ if (!connected)
38
+ return;
39
+ const fetchStats = async () => {
40
+ const statsMap = await fetchBulkStats([agentName]);
41
+ const agentStats = statsMap.get(agentName) ?? null;
42
+ setStats(agentStats);
43
+ if (agentStats) {
44
+ setStatsUpdatedAt(new Date());
45
+ }
46
+ };
47
+ void fetchStats();
48
+ const timer = setInterval(() => void fetchStats(), STATS_POLL_INTERVAL_MS);
49
+ return () => clearInterval(timer);
50
+ }, [connected, agentName]);
51
+ // ─── Agent lifecycle ────────────────────────────────
52
+ useEffect(() => {
53
+ const start = async () => {
54
+ const runtime = await initializeAgentRuntime();
55
+ const { config, memory, tools, skills } = runtime;
56
+ setAgentName(config.name);
57
+ setAgentBio(config.bio ?? '');
58
+ const resolvedModelInfo = resolveModelInfo();
59
+ setModelInfo(resolvedModelInfo);
60
+ const tuiReporter = {
61
+ onRoundStart(round, timeframe) {
62
+ addMegathreadActivity({
63
+ id: round.roundId,
64
+ type: 'megathread',
65
+ projectId: round.projectId,
66
+ timeframe,
67
+ timestamp: new Date(),
68
+ status: 'analyzing',
69
+ });
70
+ },
71
+ onPriceInfo() {
72
+ // TUI does not display price info inline
73
+ },
74
+ onResearching() {
75
+ // TUI status is already 'analyzing' from onRoundStart
76
+ },
77
+ onScreenResult(round, result) {
78
+ if (!result.engage) {
79
+ finalizeMegathreadActivity(round.roundId, {
80
+ status: 'skipped',
81
+ skipReason: `screened: ${result.reason}`,
82
+ tokenUsage: result.usage,
83
+ });
84
+ }
85
+ },
86
+ onToolsUsed() {
87
+ // TUI does not display tool usage inline
88
+ },
89
+ onSkipped(round, usage) {
90
+ finalizeMegathreadActivity(round.roundId, {
91
+ status: 'skipped',
92
+ tokenUsage: usage,
93
+ });
94
+ },
95
+ onPosted(round, conviction, summary, _timeframe, usage) {
96
+ const sign = conviction >= 0 ? '+' : '';
97
+ finalizeMegathreadActivity(round.roundId, {
98
+ status: 'posted',
99
+ conviction,
100
+ summary,
101
+ tokenUsage: usage,
102
+ });
103
+ predictionCountRef.current += 1;
104
+ setPredictionCount(predictionCountRef.current);
105
+ const predSummary = `[${sign}${conviction.toFixed(2)}%] ${summary}`;
106
+ recentPredictionsRef.current = [predSummary, ...recentPredictionsRef.current].slice(0, 5);
107
+ },
108
+ onError(round, message) {
109
+ finalizeMegathreadActivity(round.roundId, { status: 'error', errorMessage: message });
110
+ },
111
+ };
112
+ // eslint-disable-next-line prefer-const
113
+ let agent;
114
+ const roundHandler = createMegathreadRoundHandler(() => agent, runtime, tuiReporter);
115
+ const batchHandler = createMegathreadRoundBatchHandler(() => agent, runtime, tuiReporter);
116
+ agent = new HiveAgent(HIVE_API_URL, {
117
+ name: config.name,
118
+ avatarUrl: config.avatarUrl,
119
+ bio: config.bio ?? undefined,
120
+ agentProfile: config.agentProfile,
121
+ onPollEmpty: () => {
122
+ addLog({
123
+ type: 'idle',
124
+ text: 'Polled but no new rounds',
125
+ timestamp: new Date(),
126
+ });
127
+ },
128
+ onNewMegathreadRound: roundHandler,
129
+ onNewMegathreadRounds: batchHandler,
130
+ });
131
+ agentRef.current = agent;
132
+ await agent.start();
133
+ setConnected(true);
134
+ const { agentProfile } = config;
135
+ const resolvedSectors = agentProfile.sectors.length > 0 ? agentProfile.sectors.join(', ') : 'all';
136
+ const resolvedTimeframes = agentProfile.timeframes.join(', ');
137
+ setSectorsDisplay(resolvedSectors);
138
+ setTimeframesDisplay(resolvedTimeframes);
139
+ const bio = config.bio ?? '';
140
+ if (bio) {
141
+ addLog({
142
+ type: 'online',
143
+ name: config.name,
144
+ bio,
145
+ timestamp: new Date(),
146
+ });
147
+ }
148
+ };
149
+ start().catch((err) => {
150
+ const raw = extractErrorMessage(err);
151
+ const isNameTaken = raw.includes('409');
152
+ const hint = isNameTaken ? ' Change the name in SOUL.md under "# Agent: <name>".' : '';
153
+ addLog({
154
+ type: 'error',
155
+ errorMessage: `Fatal: ${raw.slice(0, 120)}${hint}`,
156
+ timestamp: new Date(),
157
+ });
158
+ });
159
+ return () => {
160
+ agentRef.current?.stop().catch(() => { });
161
+ };
162
+ }, []);
163
+ return {
164
+ connected,
165
+ agentName,
166
+ agentBio,
167
+ modelInfo,
168
+ sectorsDisplay,
169
+ timeframesDisplay,
170
+ activePollActivities,
171
+ settledPollActivities,
172
+ predictionCount,
173
+ termWidth,
174
+ stats,
175
+ statsUpdatedAt,
176
+ };
177
+ }
@@ -0,0 +1,266 @@
1
+ import { ToolLoopAgent } from 'ai';
2
+ import { useCallback, useRef, useState } from 'react';
3
+ import { extractAndSaveMemory } from '../../../shared/agent/analysis.js';
4
+ import { buildChatPrompt } from '../../../shared/agent/prompts/chat-prompt.js';
5
+ import { editSectionTool } from '../../../shared/agent/tools/edit-section.js';
6
+ import { fetchRulesTool } from '../../../shared/agent/tools/fetch-rules.js';
7
+ import { extractErrorMessage } from '../../../shared/agent/utils.js';
8
+ import { loadAgentConfig } from '../../../shared/config/agent.js';
9
+ import { getModel } from '../../../shared/config/ai-providers.js';
10
+ import { backtestSlashCommand } from '../commands/backtest.js';
11
+ import { predictionSlashCommand } from '../commands/prediction.js';
12
+ import { skillsSlashCommand } from '../commands/skills.js';
13
+ import { SLASH_COMMANDS } from '../services/command-registry.js';
14
+ export function useChat(agentName) {
15
+ const [chatActivity, setChatActivity] = useState([]);
16
+ const [input, setInput] = useState('');
17
+ const [chatStreaming, setChatStreaming] = useState(false);
18
+ const [chatBuffer, setChatBuffer] = useState('');
19
+ const sessionMessagesRef = useRef([]);
20
+ const memoryRef = useRef('');
21
+ const chatCountSinceExtractRef = useRef(0);
22
+ const extractingRef = useRef(false);
23
+ const recentPredictionsRef = useRef([]);
24
+ const soulContentRef = useRef('');
25
+ const strategyContentRef = useRef('');
26
+ const skillToolsRef = useRef({});
27
+ const skillMetadataRef = useRef('');
28
+ // ─── Activity helpers ───────────────────────────────
29
+ const addChatActivity = useCallback((item) => {
30
+ setChatActivity((prev) => {
31
+ const updated = [...prev, { ...item, timestamp: new Date() }];
32
+ const maxItems = 50;
33
+ if (updated.length > maxItems) {
34
+ return updated.slice(updated.length - maxItems);
35
+ }
36
+ return updated;
37
+ });
38
+ }, []);
39
+ // ─── Chat submission ────────────────────────────────
40
+ const handleChatSubmit = useCallback(async (message) => {
41
+ if (!message.trim() || chatStreaming) {
42
+ return;
43
+ }
44
+ // Handle slash commands
45
+ if (message.startsWith('/')) {
46
+ const trimmedMessage = message.trim().toLowerCase();
47
+ const parts = trimmedMessage.split(/\s+/);
48
+ const baseCommand = parts[0];
49
+ const commandArg = parts[1];
50
+ const commandHandlers = {
51
+ '/skills': async () => {
52
+ await skillsSlashCommand(agentName, {
53
+ onSuccess: (output) => {
54
+ addChatActivity({ type: 'chat-agent', text: output });
55
+ },
56
+ onError: (error) => {
57
+ addChatActivity({
58
+ type: 'chat-error',
59
+ text: `Failed to load skills: ${error}`,
60
+ });
61
+ },
62
+ });
63
+ },
64
+ '/help': () => {
65
+ const helpText = SLASH_COMMANDS.map((cmd) => `${cmd.name} - ${cmd.description}`).join('\n');
66
+ addChatActivity({ type: 'chat-agent', text: helpText });
67
+ },
68
+ '/clear': () => {
69
+ setChatActivity([]);
70
+ sessionMessagesRef.current = [];
71
+ },
72
+ '/memory': () => {
73
+ const memoryOutput = memoryRef.current || 'No memory stored yet.';
74
+ addChatActivity({ type: 'chat-agent', text: memoryOutput });
75
+ },
76
+ '/backtest': async () => {
77
+ const config = {
78
+ agentPath: process.cwd(),
79
+ soulContent: soulContentRef.current,
80
+ strategyContent: strategyContentRef.current,
81
+ agentName: agentName,
82
+ };
83
+ await backtestSlashCommand(parts.slice(1), config, {
84
+ onFetchStart: (numThreads) => {
85
+ addChatActivity({
86
+ type: 'chat-agent',
87
+ text: `Fetching ${numThreads} resolved threads...`,
88
+ });
89
+ },
90
+ onFetchError: (error) => {
91
+ addChatActivity({
92
+ type: 'chat-agent',
93
+ text: `API fetch failed (${error}), falling back to default dataset...`,
94
+ });
95
+ addChatActivity({
96
+ type: 'chat-agent',
97
+ text: 'Starting backtest against default dataset...',
98
+ });
99
+ },
100
+ onThreadStart: (index, total, thread) => {
101
+ addChatActivity({
102
+ type: 'chat-agent',
103
+ text: `Processing ${index + 1}/${total}: ${thread.project_name}...`,
104
+ });
105
+ },
106
+ onBacktestSuccess: (report) => {
107
+ addChatActivity({ type: 'chat-agent', text: report });
108
+ sessionMessagesRef.current.push({ role: 'assistant', content: report });
109
+ },
110
+ onBacktestError: (err) => {
111
+ const errMessage = extractErrorMessage(err);
112
+ addChatActivity({ type: 'chat-error', text: `Backtest failed: ${errMessage}` });
113
+ },
114
+ });
115
+ },
116
+ '/prediction': async () => {
117
+ await predictionSlashCommand(agentName, {
118
+ onFetchStart: () => {
119
+ addChatActivity({
120
+ type: 'chat-agent',
121
+ text: 'Fetching your predictions...',
122
+ });
123
+ },
124
+ onSuccess: (output) => {
125
+ addChatActivity({ type: 'chat-agent', text: output });
126
+ },
127
+ onError: (error) => {
128
+ addChatActivity({
129
+ type: 'chat-error',
130
+ text: `Failed to fetch predictions: ${error}`,
131
+ });
132
+ },
133
+ });
134
+ },
135
+ };
136
+ const handler = commandHandlers[baseCommand];
137
+ if (handler) {
138
+ await handler();
139
+ return;
140
+ }
141
+ // Unknown command
142
+ addChatActivity({ type: 'chat-error', text: `Unknown command: ${message}` });
143
+ return;
144
+ }
145
+ addChatActivity({ type: 'chat-user', text: message });
146
+ sessionMessagesRef.current.push({ role: 'user', content: message });
147
+ chatCountSinceExtractRef.current += 1;
148
+ if (chatCountSinceExtractRef.current >= 3 && !extractingRef.current) {
149
+ extractingRef.current = true;
150
+ const messagesSnapshot = [...sessionMessagesRef.current];
151
+ extractAndSaveMemory(messagesSnapshot)
152
+ .then((newMemory) => {
153
+ if (newMemory !== null) {
154
+ memoryRef.current = newMemory;
155
+ }
156
+ chatCountSinceExtractRef.current = 0;
157
+ })
158
+ .catch(() => { })
159
+ .finally(() => {
160
+ extractingRef.current = false;
161
+ });
162
+ }
163
+ setChatStreaming(true);
164
+ setChatBuffer('');
165
+ try {
166
+ const { system, prompt } = buildChatPrompt(soulContentRef.current, strategyContentRef.current, {
167
+ recentPredictions: recentPredictionsRef.current,
168
+ sessionMessages: sessionMessagesRef.current.slice(-20),
169
+ memory: memoryRef.current,
170
+ userMessage: message,
171
+ });
172
+ const model = await getModel();
173
+ const cacheableSystem = {
174
+ role: 'system',
175
+ content: system,
176
+ providerOptions: {
177
+ anthropic: { cacheControl: { type: 'ephemeral' } },
178
+ },
179
+ };
180
+ const agent = new ToolLoopAgent({
181
+ model,
182
+ instructions: cacheableSystem,
183
+ tools: {
184
+ editSection: editSectionTool,
185
+ fetchRules: fetchRulesTool,
186
+ ...skillToolsRef.current,
187
+ },
188
+ maxOutputTokens: 4096,
189
+ });
190
+ const result = await agent.stream({
191
+ prompt,
192
+ onStepFinish: async ({ toolResults }) => {
193
+ for (const toolResult of toolResults) {
194
+ if (toolResult.toolName === 'editSection') {
195
+ const output = String(toolResult.output);
196
+ // Only reload if update was successful
197
+ if (output.startsWith('Updated')) {
198
+ const config = await loadAgentConfig();
199
+ soulContentRef.current = config.soulContent;
200
+ strategyContentRef.current = config.strategyContent;
201
+ }
202
+ }
203
+ }
204
+ },
205
+ });
206
+ let fullResponse = '';
207
+ let lastFlushTime = 0;
208
+ const THROTTLE_MS = 80;
209
+ const streamErrors = [];
210
+ for await (const part of result.fullStream) {
211
+ if (part.type === 'text-delta') {
212
+ fullResponse += part.text;
213
+ const now = Date.now();
214
+ if (now - lastFlushTime >= THROTTLE_MS) {
215
+ setChatBuffer(fullResponse);
216
+ lastFlushTime = now;
217
+ }
218
+ }
219
+ else if (part.type === 'error') {
220
+ const errMsg = typeof part.error === 'string' ? part.error : String(part.error);
221
+ streamErrors.push(errMsg);
222
+ }
223
+ }
224
+ // Always flush the final value
225
+ setChatBuffer(fullResponse);
226
+ // Surface tool results when the model didn't produce follow-up text
227
+ const steps = await result.steps;
228
+ for (const step of steps) {
229
+ for (const toolResult of step.toolResults) {
230
+ const output = String(toolResult.output);
231
+ if (!fullResponse.includes(output)) {
232
+ const suffix = `\n[${output}]`;
233
+ fullResponse += suffix;
234
+ setChatBuffer(fullResponse);
235
+ }
236
+ }
237
+ }
238
+ if (fullResponse.trim().length === 0) {
239
+ const errorText = streamErrors.length > 0
240
+ ? `Chat error: ${streamErrors.join('; ').slice(0, 120)}`
241
+ : 'No response generated. Try again or rephrase.';
242
+ addChatActivity({ type: 'chat-error', text: errorText });
243
+ setChatBuffer('');
244
+ return;
245
+ }
246
+ sessionMessagesRef.current.push({ role: 'assistant', content: fullResponse });
247
+ addChatActivity({ type: 'chat-agent', text: fullResponse });
248
+ setChatBuffer('');
249
+ }
250
+ catch (err) {
251
+ const raw = extractErrorMessage(err);
252
+ addChatActivity({ type: 'chat-error', text: `Chat error: ${raw.slice(0, 120)}` });
253
+ }
254
+ finally {
255
+ setChatStreaming(false);
256
+ }
257
+ }, [chatStreaming, addChatActivity, agentName]);
258
+ return {
259
+ chatActivity,
260
+ input,
261
+ chatStreaming,
262
+ chatBuffer,
263
+ setInput,
264
+ handleChatSubmit,
265
+ };
266
+ }
@@ -0,0 +1,45 @@
1
+ import { useCallback, useState } from 'react';
2
+ const MAX_ITEM = 50;
3
+ export const usePollActivity = () => {
4
+ const [pollActivityQueues, setPollActivityQueues] = useState({
5
+ active: [],
6
+ settled: [],
7
+ });
8
+ // idle, online, error
9
+ const addLog = useCallback((item) => {
10
+ setPollActivityQueues((prev) => {
11
+ const { active, settled } = prev;
12
+ let updated = [...settled, item];
13
+ if (updated.length > MAX_ITEM) {
14
+ updated = updated.slice(updated.length - MAX_ITEM);
15
+ }
16
+ return { active, settled: updated };
17
+ });
18
+ }, [setPollActivityQueues]);
19
+ const addMegathreadActivity = useCallback((item) => {
20
+ setPollActivityQueues((prev) => {
21
+ const { active, settled } = prev;
22
+ let updated = [...active, item];
23
+ return { active: updated, settled };
24
+ });
25
+ }, [setPollActivityQueues]);
26
+ const finalizeMegathreadActivity = useCallback((roundId, updates) => {
27
+ setPollActivityQueues(({ active, settled }) => {
28
+ const idx = active.findIndex((item) => item.type === 'megathread' && item.id === roundId);
29
+ if (idx === -1)
30
+ return { active, settled };
31
+ const tmp = active[idx];
32
+ const updated = { ...tmp, ...updates };
33
+ const updatedSettle = [...settled, updated];
34
+ const updatedActive = [...active.slice(0, idx), ...active.slice(idx + 1)];
35
+ return { active: updatedActive, settled: updatedSettle };
36
+ });
37
+ }, [setPollActivityQueues]);
38
+ return {
39
+ activePollActivities: pollActivityQueues.active,
40
+ settledPollActivities: pollActivityQueues.settled,
41
+ addLog,
42
+ addMegathreadActivity,
43
+ finalizeMegathreadActivity,
44
+ };
45
+ };