@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,87 @@
1
+ import * as path from 'node:path';
2
+ import { discoverSkills } from '../skills/skill-parser.js';
3
+ import { marketTools } from './market/index.js';
4
+ import { mindshareTools } from './mindshare/index.js';
5
+ import { createExecuteSkillTool, clearSubagentUsage, getSubagentUsage, } from './execute-skill-tool.js';
6
+ export { clearSubagentUsage, getSubagentUsage };
7
+ /**
8
+ * Get all tools that are always available to agents.
9
+ * Tools are bundled with the CLI and don't require skill installation.
10
+ * Skills provide knowledge/guidance on when and how to use these tools.
11
+ */
12
+ export function getAllTools() {
13
+ const tools = {};
14
+ for (const [name, tool] of Object.entries(marketTools)) {
15
+ const namespacedName = `market_${name}`;
16
+ tools[namespacedName] = tool;
17
+ }
18
+ for (const [name, tool] of Object.entries(mindshareTools)) {
19
+ const namespacedName = `mindshare_${name}`;
20
+ tools[namespacedName] = tool;
21
+ }
22
+ return tools;
23
+ }
24
+ /**
25
+ * Initialize skills from agent's skills/ directory.
26
+ * Skills are knowledge-only documents that help agents understand
27
+ * when and how to use the always-available tools.
28
+ * Returns a registry map that should be passed to other skill functions.
29
+ */
30
+ export async function initializeSkills(agentPath) {
31
+ const skillRegistry = new Map();
32
+ const skillsPath = path.join(agentPath ?? process.cwd(), 'skills');
33
+ const agentSkills = await discoverSkills(skillsPath);
34
+ for (const skill of agentSkills) {
35
+ skillRegistry.set(skill.id, skill);
36
+ }
37
+ return skillRegistry;
38
+ }
39
+ /**
40
+ * Get a skill metadata list for prompt injection.
41
+ * Shows skill ID, description, and compatibility to help the agent decide what to use.
42
+ */
43
+ export function getSkillMetadataList(skillRegistry) {
44
+ if (skillRegistry.size === 0) {
45
+ return '';
46
+ }
47
+ const entries = Array.from(skillRegistry.values()).map((s) => {
48
+ const lines = [`### ${s.id}`, s.metadata.description];
49
+ if (s.metadata.compatibility) {
50
+ lines.push(`**Compatibility:** ${s.metadata.compatibility}`);
51
+ }
52
+ const entry = lines.join('\n');
53
+ return entry;
54
+ });
55
+ const output = `Available skills:\n\n${entries.join('\n\n')}`;
56
+ return output;
57
+ }
58
+ /**
59
+ * Get the executeSkill tool for running skills as subagents.
60
+ * The subagent uses the skill's body as instructions and has access to all tools.
61
+ */
62
+ export function getExecuteSkillTool(skillRegistry, model, subagentConfig) {
63
+ const config = {
64
+ model,
65
+ subagentConfig,
66
+ };
67
+ const executeSkillTool = createExecuteSkillTool(skillRegistry, config);
68
+ return executeSkillTool;
69
+ }
70
+ /**
71
+ * Get a skill definition by ID.
72
+ */
73
+ export function getSkill(skillRegistry, id) {
74
+ return skillRegistry.get(id);
75
+ }
76
+ /**
77
+ * Get all registered skills.
78
+ */
79
+ export function getAllSkills(skillRegistry) {
80
+ return Array.from(skillRegistry.values());
81
+ }
82
+ /**
83
+ * Check if any skills are registered.
84
+ */
85
+ export function hasSkills(skillRegistry) {
86
+ return skillRegistry.size > 0;
87
+ }
@@ -0,0 +1,41 @@
1
+ import { HIVE_API_URL } from '../../../config/constant.js';
2
+ /**
3
+ * Client for the backend Market API.
4
+ */
5
+ export class MarketClient {
6
+ constructor(baseUrl = HIVE_API_URL) {
7
+ this._baseUrl = baseUrl;
8
+ }
9
+ async getPrice(projectId, timestamp) {
10
+ const url = `${this._baseUrl}/market/price/${encodeURIComponent(projectId)}?timestamp=${encodeURIComponent(timestamp)}`;
11
+ const response = await fetch(url);
12
+ if (!response.ok) {
13
+ const text = await response.text();
14
+ throw new Error(`Market price request failed: ${response.status} - ${text}`);
15
+ }
16
+ const data = (await response.json());
17
+ return data;
18
+ }
19
+ async getOHLC(id, from, to, interval = 'daily') {
20
+ const params = new URLSearchParams({
21
+ from,
22
+ to,
23
+ interval,
24
+ });
25
+ const url = `${this._baseUrl}/market/ohlc/${encodeURIComponent(id)}?${params.toString()}`;
26
+ const response = await fetch(url);
27
+ if (!response.ok) {
28
+ const text = await response.text();
29
+ throw new Error(`Market OHLC request failed: ${response.status} - ${text}`);
30
+ }
31
+ const data = (await response.json());
32
+ return data;
33
+ }
34
+ }
35
+ let clientInstance = null;
36
+ export function getMarketClient() {
37
+ if (clientInstance === null) {
38
+ clientInstance = new MarketClient();
39
+ }
40
+ return clientInstance;
41
+ }
@@ -0,0 +1,3 @@
1
+ export { marketTools } from './tools.js';
2
+ export { getPriceTool, getOHLCTool, getSMATool, getEMATool, getRSITool, getMACDTool, getBollingerTool, } from './tools.js';
3
+ export { MarketClient, getMarketClient } from './client.js';
@@ -0,0 +1,497 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+ import { getMarketClient } from './client.js';
4
+ import { computeSMA, computeEMA, computeRSI, computeMACD, computeBollingerBands, } from '../ta/indicators.js';
5
+ import { formatToolError, signPrefix, truncateTimeseries, truncationLabel } from '../formatting.js';
6
+ /**
7
+ * Adjusts the 'from' date backwards to ensure sufficient data points are fetched
8
+ * for indicator calculation. Adds a 30% buffer for weekends/gaps in data.
9
+ */
10
+ function adjustFromDate(from, minPoints, interval) {
11
+ const fromDate = new Date(from);
12
+ const buffer = Math.ceil(minPoints * 0.3);
13
+ const totalPoints = minPoints + buffer;
14
+ if (interval === 'hourly') {
15
+ fromDate.setTime(fromDate.getTime() - totalPoints * 60 * 60 * 1000);
16
+ }
17
+ else {
18
+ fromDate.setTime(fromDate.getTime() - totalPoints * 24 * 60 * 60 * 1000);
19
+ }
20
+ return fromDate.toISOString();
21
+ }
22
+ const timeRangeSchema = z.object({
23
+ from: z
24
+ .string()
25
+ .describe('Start date in ISO 8601 format. Use recent dates relative to the current date from context (e.g., "2024-01-01T00:00:00Z")'),
26
+ to: z
27
+ .string()
28
+ .describe('End date in ISO 8601 format. Use the current date from context as the end date. (e.g., "2024-01-01T00:00:00Z")'),
29
+ });
30
+ export const getPriceTool = tool({
31
+ description: 'Get cryptocurrency price. Returns the current price by default, or the price at a specific timestamp if provided.',
32
+ inputSchema: z.object({
33
+ projectId: z
34
+ .string()
35
+ .describe('Project ID to get price for (e.g., "bitcoin", "ethereum"). Use lowercase.'),
36
+ timestamp: z
37
+ .string()
38
+ .optional()
39
+ .describe('Optional timestamp in ISO 8601 format. If omitted, returns the current price.'),
40
+ }),
41
+ execute: async ({ projectId, timestamp }) => {
42
+ try {
43
+ const effectiveTimestamp = timestamp ?? new Date().toISOString();
44
+ const client = getMarketClient();
45
+ const priceData = await client.getPrice(projectId, effectiveTimestamp);
46
+ if (priceData.price === null) {
47
+ return `No price data available for ${projectId} at ${effectiveTimestamp}.`;
48
+ }
49
+ const price = priceData.price;
50
+ const priceFormatted = price.toLocaleString('en-US', {
51
+ style: 'currency',
52
+ currency: 'USD',
53
+ minimumFractionDigits: 2,
54
+ maximumFractionDigits: price < 1 ? 6 : 2,
55
+ });
56
+ const timeLabel = timestamp ? `at ${timestamp}` : '(current)';
57
+ const output = `${projectId}: ${priceFormatted} ${timeLabel}`;
58
+ return output;
59
+ }
60
+ catch (err) {
61
+ return formatToolError(err, 'fetching price');
62
+ }
63
+ },
64
+ });
65
+ export const getOHLCTool = tool({
66
+ description: 'Get historical OHLC (Open, High, Low, Close) candlestick data for a cryptocurrency. Use this to analyze price history and patterns.',
67
+ inputSchema: z.object({
68
+ ...timeRangeSchema.shape,
69
+ id: z.string().describe('Coin ID (e.g., "bitcoin", "ethereum"). Use lowercase.'),
70
+ interval: z
71
+ .enum(['daily', 'hourly'])
72
+ .optional()
73
+ .describe('Data interval: "daily" or "hourly". Defaults to "daily".'),
74
+ }),
75
+ execute: async ({ id, from, to, interval }) => {
76
+ try {
77
+ const client = getMarketClient();
78
+ const effectiveInterval = interval ?? 'daily';
79
+ const ohlcData = await client.getOHLC(id, from, to, effectiveInterval);
80
+ if (ohlcData.length === 0) {
81
+ return `No OHLC data available for ${id} from ${from} to ${to}.`;
82
+ }
83
+ const lines = [
84
+ `OHLC data for ${id} (${effectiveInterval}, ${ohlcData.length} data points):`,
85
+ '',
86
+ 'Date | Open | High | Low | Close',
87
+ '--- | --- | --- | --- | ---',
88
+ ];
89
+ const displayData = truncateTimeseries(ohlcData, 30, 10, 10);
90
+ for (const point of displayData) {
91
+ if (point === null) {
92
+ lines.push(truncationLabel(ohlcData.length, 20));
93
+ continue;
94
+ }
95
+ const date = new Date(point[0]).toISOString().split('T')[0];
96
+ const open = point[1].toFixed(2);
97
+ const high = point[2].toFixed(2);
98
+ const low = point[3].toFixed(2);
99
+ const close = point[4].toFixed(2);
100
+ lines.push(`${date} | $${open} | $${high} | $${low} | $${close}`);
101
+ }
102
+ const firstClose = ohlcData[0][4];
103
+ const lastClose = ohlcData[ohlcData.length - 1][4];
104
+ const changePercent = ((lastClose - firstClose) / firstClose) * 100;
105
+ lines.push('');
106
+ lines.push(`Period change: ${signPrefix(changePercent)}${changePercent.toFixed(2)}% ($${firstClose.toFixed(2)} → $${lastClose.toFixed(2)})`);
107
+ const output = lines.join('\n');
108
+ return output;
109
+ }
110
+ catch (err) {
111
+ return formatToolError(err, 'fetching OHLC data');
112
+ }
113
+ },
114
+ });
115
+ export const getSMATool = tool({
116
+ description: 'Calculate Simple Moving Average (SMA) for a cryptocurrency. SMA smooths price data by averaging prices over a specified period. Commonly used periods: 20 (short-term), 50 (medium-term), 200 (long-term).',
117
+ inputSchema: z.object({
118
+ ...timeRangeSchema.shape,
119
+ id: z.string().describe('Coin ID (e.g., "bitcoin", "ethereum"). Use lowercase.'),
120
+ period: z
121
+ .number()
122
+ .int()
123
+ .min(2)
124
+ .max(200)
125
+ .describe('Number of periods for the SMA (e.g., 20, 50, 200).'),
126
+ interval: z
127
+ .enum(['daily', 'hourly'])
128
+ .optional()
129
+ .describe('Data interval: "daily" or "hourly". Defaults to "daily".'),
130
+ }),
131
+ execute: async ({ id, period, from, to, interval }) => {
132
+ try {
133
+ const client = getMarketClient();
134
+ const effectiveInterval = interval ?? 'daily';
135
+ const adjustedFrom = adjustFromDate(from, period, effectiveInterval);
136
+ const ohlcData = await client.getOHLC(id, adjustedFrom, to, effectiveInterval);
137
+ if (ohlcData.length < period) {
138
+ return `Insufficient data: got ${ohlcData.length} data points but need at least ${period} for SMA-${period}.`;
139
+ }
140
+ const smaData = computeSMA(ohlcData, period);
141
+ if (smaData.length === 0) {
142
+ return `Could not compute SMA-${period} for ${id}.`;
143
+ }
144
+ // Filter results to original date range
145
+ const originalFromTime = new Date(from).getTime();
146
+ const filteredData = smaData.filter((point) => point.timestamp >= originalFromTime);
147
+ if (filteredData.length === 0) {
148
+ return `No SMA-${period} data available within the requested date range for ${id}.`;
149
+ }
150
+ const lines = [
151
+ `SMA-${period} for ${id} (${effectiveInterval}, ${filteredData.length} values):`,
152
+ '',
153
+ ];
154
+ for (const point of filteredData) {
155
+ const date = new Date(point.timestamp).toISOString().split('T')[0];
156
+ lines.push(`${date}: $${point.value.toFixed(2)}`);
157
+ }
158
+ const latestSMA = filteredData[filteredData.length - 1].value;
159
+ const latestPrice = ohlcData[ohlcData.length - 1][4];
160
+ const aboveBelow = latestPrice > latestSMA ? 'above' : 'below';
161
+ const diff = ((latestPrice - latestSMA) / latestSMA) * 100;
162
+ lines.push('');
163
+ lines.push(`Current price ($${latestPrice.toFixed(2)}) is ${aboveBelow} SMA-${period} ($${latestSMA.toFixed(2)}) by ${signPrefix(diff)}${diff.toFixed(2)}%`);
164
+ const output = lines.join('\n');
165
+ return output;
166
+ }
167
+ catch (err) {
168
+ return formatToolError(err, 'calculating SMA');
169
+ }
170
+ },
171
+ });
172
+ export const getEMATool = tool({
173
+ description: 'Calculate Exponential Moving Average (EMA) for a cryptocurrency. EMA gives more weight to recent prices, making it more responsive than SMA. Common periods: 12, 26 (for MACD), 9 (signal line), 20, 50, 200.',
174
+ inputSchema: z.object({
175
+ ...timeRangeSchema.shape,
176
+ id: z.string().describe('Coin ID (e.g., "bitcoin", "ethereum"). Use lowercase.'),
177
+ period: z
178
+ .number()
179
+ .int()
180
+ .min(2)
181
+ .max(200)
182
+ .describe('Number of periods for the EMA (e.g., 12, 26, 50, 200).'),
183
+ interval: z
184
+ .enum(['daily', 'hourly'])
185
+ .optional()
186
+ .describe('Data interval: "daily" or "hourly". Defaults to "daily".'),
187
+ }),
188
+ execute: async ({ id, period, from, to, interval }) => {
189
+ try {
190
+ const client = getMarketClient();
191
+ const effectiveInterval = interval ?? 'daily';
192
+ const adjustedFrom = adjustFromDate(from, period, effectiveInterval);
193
+ const ohlcData = await client.getOHLC(id, adjustedFrom, to, effectiveInterval);
194
+ if (ohlcData.length < period) {
195
+ return `Insufficient data: got ${ohlcData.length} data points but need at least ${period} for EMA-${period}.`;
196
+ }
197
+ const emaData = computeEMA(ohlcData, period);
198
+ if (emaData.length === 0) {
199
+ return `Could not compute EMA-${period} for ${id}.`;
200
+ }
201
+ // Filter results to original date range
202
+ const originalFromTime = new Date(from).getTime();
203
+ const filteredData = emaData.filter((point) => point.timestamp >= originalFromTime);
204
+ if (filteredData.length === 0) {
205
+ return `No EMA-${period} data available within the requested date range for ${id}.`;
206
+ }
207
+ const lines = [
208
+ `EMA-${period} for ${id} (${effectiveInterval}, ${filteredData.length} values):`,
209
+ '',
210
+ ];
211
+ for (const point of filteredData) {
212
+ const date = new Date(point.timestamp).toISOString().split('T')[0];
213
+ lines.push(`${date}: $${point.value.toFixed(2)}`);
214
+ }
215
+ const latestEMA = filteredData[filteredData.length - 1].value;
216
+ const latestPrice = ohlcData[ohlcData.length - 1][4];
217
+ const aboveBelow = latestPrice > latestEMA ? 'above' : 'below';
218
+ const diff = ((latestPrice - latestEMA) / latestEMA) * 100;
219
+ lines.push('');
220
+ lines.push(`Current price ($${latestPrice.toFixed(2)}) is ${aboveBelow} EMA-${period} ($${latestEMA.toFixed(2)}) by ${signPrefix(diff)}${diff.toFixed(2)}%`);
221
+ const output = lines.join('\n');
222
+ return output;
223
+ }
224
+ catch (err) {
225
+ return formatToolError(err, 'calculating EMA');
226
+ }
227
+ },
228
+ });
229
+ export const getRSITool = tool({
230
+ description: 'Calculate Relative Strength Index (RSI) for a cryptocurrency. RSI measures momentum on a 0-100 scale. Readings above 70 suggest overbought conditions; below 30 suggests oversold. Standard period is 14.',
231
+ inputSchema: z.object({
232
+ ...timeRangeSchema.shape,
233
+ id: z.string().describe('Coin ID (e.g., "bitcoin", "ethereum"). Use lowercase.'),
234
+ period: z.number().int().min(2).max(50).optional().describe('RSI period. Defaults to 14.'),
235
+ interval: z
236
+ .enum(['daily', 'hourly'])
237
+ .optional()
238
+ .describe('Data interval: "daily" or "hourly". Defaults to "daily".'),
239
+ }),
240
+ execute: async ({ id, period, from, to, interval }) => {
241
+ try {
242
+ const effectivePeriod = period ?? 14;
243
+ const minRequired = effectivePeriod + 1;
244
+ const client = getMarketClient();
245
+ const effectiveInterval = interval ?? 'daily';
246
+ const adjustedFrom = adjustFromDate(from, minRequired, effectiveInterval);
247
+ const ohlcData = await client.getOHLC(id, adjustedFrom, to, effectiveInterval);
248
+ if (ohlcData.length < minRequired) {
249
+ return `Insufficient data: got ${ohlcData.length} data points but need at least ${minRequired} for RSI-${effectivePeriod}.`;
250
+ }
251
+ const rsiData = computeRSI(ohlcData, effectivePeriod);
252
+ if (rsiData.length === 0) {
253
+ return `Could not compute RSI-${effectivePeriod} for ${id}.`;
254
+ }
255
+ // Filter results to original date range
256
+ const originalFromTime = new Date(from).getTime();
257
+ const filteredData = rsiData.filter((point) => point.timestamp >= originalFromTime);
258
+ if (filteredData.length === 0) {
259
+ return `No RSI-${effectivePeriod} data available within the requested date range for ${id}.`;
260
+ }
261
+ const lines = [
262
+ `RSI-${effectivePeriod} for ${id} (${effectiveInterval}, ${filteredData.length} values):`,
263
+ '',
264
+ ];
265
+ for (const point of filteredData) {
266
+ const date = new Date(point.timestamp).toISOString().split('T')[0];
267
+ const rsiValue = point.value.toFixed(2);
268
+ let status = '';
269
+ if (point.value >= 70) {
270
+ status = ' [OVERBOUGHT]';
271
+ }
272
+ else if (point.value <= 30) {
273
+ status = ' [OVERSOLD]';
274
+ }
275
+ lines.push(`${date}: ${rsiValue}${status}`);
276
+ }
277
+ const latestRSI = filteredData[filteredData.length - 1].value;
278
+ let interpretation = '';
279
+ if (latestRSI >= 70) {
280
+ interpretation =
281
+ 'The RSI is in overbought territory (>=70), which may indicate the asset is overvalued and could see a pullback.';
282
+ }
283
+ else if (latestRSI <= 30) {
284
+ interpretation =
285
+ 'The RSI is in oversold territory (<=30), which may indicate the asset is undervalued and could see a bounce.';
286
+ }
287
+ else if (latestRSI >= 50) {
288
+ interpretation = 'The RSI shows bullish momentum (above 50).';
289
+ }
290
+ else {
291
+ interpretation = 'The RSI shows bearish momentum (below 50).';
292
+ }
293
+ lines.push('');
294
+ lines.push(`Current RSI: ${latestRSI.toFixed(2)}`);
295
+ lines.push(interpretation);
296
+ const output = lines.join('\n');
297
+ return output;
298
+ }
299
+ catch (err) {
300
+ return formatToolError(err, 'calculating RSI');
301
+ }
302
+ },
303
+ });
304
+ export const getMACDTool = tool({
305
+ description: 'Calculate MACD (Moving Average Convergence Divergence) for a cryptocurrency. MACD is a trend-following momentum indicator. Standard settings: fast=12, slow=26, signal=9. Bullish when MACD crosses above signal line; bearish when below.',
306
+ inputSchema: z.object({
307
+ ...timeRangeSchema.shape,
308
+ id: z.string().describe('Coin ID (e.g., "bitcoin", "ethereum"). Use lowercase.'),
309
+ fastPeriod: z
310
+ .number()
311
+ .int()
312
+ .min(2)
313
+ .max(50)
314
+ .optional()
315
+ .describe('Fast EMA period. Defaults to 12.'),
316
+ slowPeriod: z
317
+ .number()
318
+ .int()
319
+ .min(2)
320
+ .max(100)
321
+ .optional()
322
+ .describe('Slow EMA period. Defaults to 26.'),
323
+ signalPeriod: z
324
+ .number()
325
+ .int()
326
+ .min(2)
327
+ .max(50)
328
+ .optional()
329
+ .describe('Signal line EMA period. Defaults to 9.'),
330
+ interval: z
331
+ .enum(['daily', 'hourly'])
332
+ .optional()
333
+ .describe('Data interval: "daily" or "hourly". Defaults to "daily".'),
334
+ }),
335
+ execute: async ({ id, fastPeriod, slowPeriod, signalPeriod, from, to, interval }) => {
336
+ try {
337
+ const fast = fastPeriod ?? 12;
338
+ const slow = slowPeriod ?? 26;
339
+ const signal = signalPeriod ?? 9;
340
+ const minRequired = slow + signal;
341
+ const client = getMarketClient();
342
+ const effectiveInterval = interval ?? 'daily';
343
+ const adjustedFrom = adjustFromDate(from, minRequired, effectiveInterval);
344
+ const ohlcData = await client.getOHLC(id, adjustedFrom, to, effectiveInterval);
345
+ if (ohlcData.length < minRequired) {
346
+ return `Insufficient data: got ${ohlcData.length} data points but need at least ${minRequired} for MACD(${fast},${slow},${signal}).`;
347
+ }
348
+ const macdData = computeMACD(ohlcData, fast, slow, signal);
349
+ if (macdData.length === 0) {
350
+ return `Could not compute MACD(${fast},${slow},${signal}) for ${id}.`;
351
+ }
352
+ // Filter results to original date range
353
+ const originalFromTime = new Date(from).getTime();
354
+ const filteredData = macdData.filter((point) => point.timestamp >= originalFromTime);
355
+ if (filteredData.length === 0) {
356
+ return `No MACD(${fast},${slow},${signal}) data available within the requested date range for ${id}.`;
357
+ }
358
+ const lines = [
359
+ `MACD(${fast},${slow},${signal}) for ${id} (${effectiveInterval}, ${filteredData.length} values):`,
360
+ '',
361
+ 'Date | MACD | Signal | Histogram',
362
+ '--- | --- | --- | ---',
363
+ ];
364
+ for (const point of filteredData) {
365
+ const date = new Date(point.timestamp).toISOString().split('T')[0];
366
+ const macdVal = point.macd.toFixed(4);
367
+ const signalVal = point.signal.toFixed(4);
368
+ const histVal = point.histogram.toFixed(4);
369
+ lines.push(`${date} | ${macdVal} | ${signalVal} | ${signPrefix(point.histogram)}${histVal}`);
370
+ }
371
+ const latest = filteredData[filteredData.length - 1];
372
+ const previous = filteredData.length > 1 ? filteredData[filteredData.length - 2] : null;
373
+ let crossover = '';
374
+ if (previous !== null) {
375
+ if (previous.macd <= previous.signal && latest.macd > latest.signal) {
376
+ crossover = 'BULLISH CROSSOVER detected (MACD crossed above Signal line)!';
377
+ }
378
+ else if (previous.macd >= previous.signal && latest.macd < latest.signal) {
379
+ crossover = 'BEARISH CROSSOVER detected (MACD crossed below Signal line)!';
380
+ }
381
+ }
382
+ const trend = latest.macd > latest.signal ? 'bullish (MACD above signal)' : 'bearish (MACD below signal)';
383
+ const momentum = latest.histogram > 0 ? 'positive' : 'negative';
384
+ lines.push('');
385
+ lines.push(`Current trend: ${trend}`);
386
+ lines.push(`Histogram momentum: ${momentum}`);
387
+ if (crossover) {
388
+ lines.push(crossover);
389
+ }
390
+ const output = lines.join('\n');
391
+ return output;
392
+ }
393
+ catch (err) {
394
+ return formatToolError(err, 'calculating MACD');
395
+ }
396
+ },
397
+ });
398
+ export const getBollingerTool = tool({
399
+ description: 'Calculate Bollinger Bands for a cryptocurrency. Bollinger Bands consist of a middle SMA band with upper and lower bands based on standard deviation. Price near upper band may indicate overbought; near lower band may indicate oversold. Standard settings: period=20, stdDev=2.',
400
+ inputSchema: z.object({
401
+ ...timeRangeSchema.shape,
402
+ id: z.string().describe('Coin ID (e.g., "bitcoin", "ethereum"). Use lowercase.'),
403
+ period: z
404
+ .number()
405
+ .int()
406
+ .min(2)
407
+ .max(100)
408
+ .optional()
409
+ .describe('SMA period for the middle band. Defaults to 20.'),
410
+ stdDev: z
411
+ .number()
412
+ .min(0.5)
413
+ .max(5)
414
+ .optional()
415
+ .describe('Standard deviation multiplier for bands. Defaults to 2.'),
416
+ interval: z
417
+ .enum(['daily', 'hourly'])
418
+ .optional()
419
+ .describe('Data interval: "daily" or "hourly". Defaults to "daily".'),
420
+ }),
421
+ execute: async ({ id, period, stdDev, from, to, interval }) => {
422
+ try {
423
+ const effectivePeriod = period ?? 20;
424
+ const effectiveStdDev = stdDev ?? 2;
425
+ const client = getMarketClient();
426
+ const effectiveInterval = interval ?? 'daily';
427
+ const adjustedFrom = adjustFromDate(from, effectivePeriod, effectiveInterval);
428
+ const ohlcData = await client.getOHLC(id, adjustedFrom, to, effectiveInterval);
429
+ if (ohlcData.length < effectivePeriod) {
430
+ return `Insufficient data: got ${ohlcData.length} data points but need at least ${effectivePeriod} for Bollinger Bands(${effectivePeriod},${effectiveStdDev}).`;
431
+ }
432
+ const bbData = computeBollingerBands(ohlcData, effectivePeriod, effectiveStdDev);
433
+ if (bbData.length === 0) {
434
+ return `Could not compute Bollinger Bands(${effectivePeriod},${effectiveStdDev}) for ${id}.`;
435
+ }
436
+ // Filter results to original date range
437
+ const originalFromTime = new Date(from).getTime();
438
+ const filteredData = bbData.filter((point) => point.timestamp >= originalFromTime);
439
+ if (filteredData.length === 0) {
440
+ return `No Bollinger Bands(${effectivePeriod},${effectiveStdDev}) data available within the requested date range for ${id}.`;
441
+ }
442
+ const lines = [
443
+ `Bollinger Bands(${effectivePeriod},${effectiveStdDev}) for ${id} (${effectiveInterval}, ${filteredData.length} values):`,
444
+ '',
445
+ 'Date | Upper | Middle | Lower',
446
+ '--- | --- | --- | ---',
447
+ ];
448
+ for (const point of filteredData) {
449
+ const date = new Date(point.timestamp).toISOString().split('T')[0];
450
+ lines.push(`${date} | $${point.upper.toFixed(2)} | $${point.middle.toFixed(2)} | $${point.lower.toFixed(2)}`);
451
+ }
452
+ const latest = filteredData[filteredData.length - 1];
453
+ const latestPrice = ohlcData[ohlcData.length - 1][4];
454
+ const bandwidth = ((latest.upper - latest.lower) / latest.middle) * 100;
455
+ let position = '';
456
+ const percentB = (latestPrice - latest.lower) / (latest.upper - latest.lower);
457
+ if (latestPrice > latest.upper) {
458
+ position = 'ABOVE upper band (potential overbought)';
459
+ }
460
+ else if (latestPrice < latest.lower) {
461
+ position = 'BELOW lower band (potential oversold)';
462
+ }
463
+ else if (percentB > 0.8) {
464
+ position = 'near upper band (approaching overbought)';
465
+ }
466
+ else if (percentB < 0.2) {
467
+ position = 'near lower band (approaching oversold)';
468
+ }
469
+ else {
470
+ position = 'within bands (neutral zone)';
471
+ }
472
+ lines.push('');
473
+ lines.push(`Current price: $${latestPrice.toFixed(2)}`);
474
+ lines.push(`Bands: Upper $${latest.upper.toFixed(2)} | Middle $${latest.middle.toFixed(2)} | Lower $${latest.lower.toFixed(2)}`);
475
+ lines.push(`Bandwidth: ${bandwidth.toFixed(2)}%`);
476
+ lines.push(`%B (position within bands): ${(percentB * 100).toFixed(1)}%`);
477
+ lines.push(`Price position: ${position}`);
478
+ const output = lines.join('\n');
479
+ return output;
480
+ }
481
+ catch (err) {
482
+ return formatToolError(err, 'calculating Bollinger Bands');
483
+ }
484
+ },
485
+ });
486
+ /**
487
+ * All market tools for export.
488
+ */
489
+ export const marketTools = {
490
+ getPrice: getPriceTool,
491
+ getOHLC: getOHLCTool,
492
+ getSMA: getSMATool,
493
+ getEMA: getEMATool,
494
+ getRSI: getRSITool,
495
+ getMACD: getMACDTool,
496
+ getBollinger: getBollingerTool,
497
+ };