@zhive/cli 0.6.7 → 0.6.9

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 (70) hide show
  1. package/dist/CLAUDE.md +7 -0
  2. package/dist/backtest/CLAUDE.md +7 -0
  3. package/dist/cli.js +20 -0
  4. package/dist/commands/agent/commands/profile.js +3 -9
  5. package/dist/commands/create/commands/index.js +2 -5
  6. package/dist/commands/create/generate.js +18 -22
  7. package/dist/commands/create/presets.js +613 -0
  8. package/dist/commands/create/ui/CreateApp.js +2 -2
  9. package/dist/commands/create/ui/steps/ScaffoldStep.js +17 -2
  10. package/dist/commands/indicator/commands/bollinger.js +37 -0
  11. package/dist/commands/indicator/commands/ema.js +37 -0
  12. package/dist/commands/indicator/commands/index.js +14 -0
  13. package/dist/commands/indicator/commands/macd.js +51 -0
  14. package/dist/commands/indicator/commands/rsi.js +37 -0
  15. package/dist/commands/indicator/commands/sma.js +37 -0
  16. package/dist/commands/market/commands/index.js +5 -0
  17. package/dist/commands/market/commands/price.js +25 -0
  18. package/dist/commands/shared/utils.js +12 -0
  19. package/dist/commands/start/ui/AsciiTicker.js +81 -0
  20. package/dist/index.js +4 -0
  21. package/dist/services/agent/analysis.js +160 -0
  22. package/dist/services/agent/config.js +75 -0
  23. package/dist/services/agent/env.js +30 -0
  24. package/dist/services/agent/helpers/model.js +92 -0
  25. package/dist/services/agent/helpers.js +22 -0
  26. package/dist/services/agent/prompts/chat-prompt.js +65 -0
  27. package/dist/services/agent/prompts/memory-prompt.js +45 -0
  28. package/dist/services/agent/prompts/prompt.js +379 -0
  29. package/dist/services/agent/skills/index.js +2 -0
  30. package/dist/services/agent/skills/skill-parser.js +149 -0
  31. package/dist/services/agent/skills/types.js +1 -0
  32. package/dist/services/agent/tools/edit-section.js +59 -0
  33. package/dist/services/agent/tools/fetch-rules.js +21 -0
  34. package/dist/services/agent/tools/index.js +76 -0
  35. package/dist/services/agent/tools/market/client.js +41 -0
  36. package/dist/services/agent/tools/market/index.js +3 -0
  37. package/dist/services/agent/tools/market/tools.js +518 -0
  38. package/dist/services/agent/tools/mindshare/client.js +124 -0
  39. package/dist/services/agent/tools/mindshare/index.js +3 -0
  40. package/dist/services/agent/tools/mindshare/tools.js +563 -0
  41. package/dist/services/agent/tools/read-skill-tool.js +30 -0
  42. package/dist/services/agent/tools/ta/index.js +1 -0
  43. package/dist/services/agent/tools/ta/indicators.js +201 -0
  44. package/dist/services/agent/types.js +1 -0
  45. package/dist/services/ai-providers.js +66 -0
  46. package/dist/services/config/agent.js +110 -0
  47. package/dist/services/config/config.js +22 -0
  48. package/dist/services/config/constant.js +8 -0
  49. package/dist/shared/agent/agent-runtime.js +144 -0
  50. package/dist/shared/agent/analysis.js +2 -12
  51. package/dist/shared/agent/cache.js +10 -0
  52. package/dist/shared/agent/config.js +75 -0
  53. package/dist/shared/agent/env.js +30 -0
  54. package/dist/shared/agent/handler.js +3 -9
  55. package/dist/shared/agent/helpers/model.js +92 -0
  56. package/dist/shared/agent/prompts/megathread.js +0 -8
  57. package/dist/shared/agent/tools/execute-skill-tool.js +2 -1
  58. package/dist/shared/agent/tools/formatting.js +0 -19
  59. package/dist/shared/agent/tools/market/client.js +3 -3
  60. package/dist/shared/agent/tools/market/tools.js +88 -312
  61. package/dist/shared/agent/tools/market/utils.js +71 -0
  62. package/dist/shared/agent/tools/mindshare/tools.js +1 -1
  63. package/dist/shared/agent/tools/ta/index.js +3 -1
  64. package/dist/shared/agent/types.js +1 -0
  65. package/dist/shared/agent/utils.js +44 -0
  66. package/dist/shared/ai-providers.js +66 -0
  67. package/dist/shared/ta/error.js +12 -0
  68. package/dist/shared/ta/service.js +93 -0
  69. package/dist/shared/ta/utils.js +16 -0
  70. package/package.json +3 -2
@@ -0,0 +1,92 @@
1
+ import { AI_PROVIDERS } from '../../ai-providers.js';
2
+ import { getAgentProviderKeys } from '../env.js';
3
+ const PROVIDERS = [
4
+ {
5
+ label: 'Anthropic',
6
+ envVar: 'ANTHROPIC_API_KEY',
7
+ load: async (modelId) => {
8
+ const { anthropic } = await import('@ai-sdk/anthropic');
9
+ return anthropic(modelId);
10
+ },
11
+ },
12
+ {
13
+ label: 'OpenAI',
14
+ envVar: 'OPENAI_API_KEY',
15
+ load: async (modelId) => {
16
+ const { openai } = await import('@ai-sdk/openai');
17
+ return openai(modelId);
18
+ },
19
+ },
20
+ {
21
+ label: 'Google',
22
+ envVar: 'GOOGLE_GENERATIVE_AI_API_KEY',
23
+ load: async (modelId) => {
24
+ const { google } = await import('@ai-sdk/google');
25
+ return google(modelId);
26
+ },
27
+ },
28
+ {
29
+ label: 'xAI',
30
+ envVar: 'XAI_API_KEY',
31
+ load: async (modelId) => {
32
+ const { xai } = await import('@ai-sdk/xai');
33
+ return xai(modelId);
34
+ },
35
+ },
36
+ {
37
+ label: 'OpenRouter',
38
+ envVar: 'OPENROUTER_API_KEY',
39
+ load: async (modelId) => {
40
+ const { createOpenRouter } = await import('@openrouter/ai-sdk-provider');
41
+ const openrouter = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY });
42
+ return openrouter.chat(modelId);
43
+ },
44
+ },
45
+ ];
46
+ export function resolveModelInfo() {
47
+ const overrideModel = process.env.HIVE_MODEL;
48
+ const agentKeys = getAgentProviderKeys();
49
+ const sortedProviders = [
50
+ ...PROVIDERS.filter((p) => agentKeys.has(p.envVar)),
51
+ ...PROVIDERS.filter((p) => !agentKeys.has(p.envVar)),
52
+ ];
53
+ for (const provider of sortedProviders) {
54
+ const keyValue = process.env[provider.envVar];
55
+ if (keyValue && keyValue.trim().length > 0) {
56
+ const centralProvider = AI_PROVIDERS.find((p) => p.envVar === provider.envVar);
57
+ const runtimeModel = centralProvider?.models.runtime ?? 'unknown';
58
+ const modelId = overrideModel ?? runtimeModel;
59
+ const source = agentKeys.has(provider.envVar) ? '.env' : 'shell';
60
+ return { provider: provider.label, modelId, source };
61
+ }
62
+ }
63
+ return { provider: 'unknown', modelId: 'unknown', source: 'unknown' };
64
+ }
65
+ let _modelPromise = null;
66
+ export function getModel() {
67
+ if (_modelPromise) {
68
+ return _modelPromise;
69
+ }
70
+ _modelPromise = (async () => {
71
+ const info = resolveModelInfo();
72
+ if (info.provider === 'unknown') {
73
+ throw new Error('No AI provider API key found in environment. ' +
74
+ 'Set one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, XAI_API_KEY, OPENROUTER_API_KEY');
75
+ }
76
+ const agentKeys = getAgentProviderKeys();
77
+ const sortedProviders = [
78
+ ...PROVIDERS.filter((p) => agentKeys.has(p.envVar)),
79
+ ...PROVIDERS.filter((p) => !agentKeys.has(p.envVar)),
80
+ ];
81
+ for (const provider of sortedProviders) {
82
+ const keyValue = process.env[provider.envVar];
83
+ if (keyValue && keyValue.trim().length > 0) {
84
+ const modelId = info.modelId;
85
+ const model = await provider.load(modelId);
86
+ return model;
87
+ }
88
+ }
89
+ throw new Error('Unreachable: resolveModelInfo succeeded but no provider found');
90
+ })();
91
+ return _modelPromise;
92
+ }
@@ -0,0 +1,22 @@
1
+ export function formatTime(date) {
2
+ const hours = String(date.getHours()).padStart(2, '0');
3
+ const minutes = String(date.getMinutes()).padStart(2, '0');
4
+ const seconds = String(date.getSeconds()).padStart(2, '0');
5
+ return `${hours}:${minutes}:${seconds}`;
6
+ }
7
+ export function convictionColor(conviction) {
8
+ if (conviction > 0)
9
+ return 'green';
10
+ if (conviction < 0)
11
+ return 'red';
12
+ return 'gray';
13
+ }
14
+ export function stripCodeFences(text) {
15
+ const trimmed = text.trim();
16
+ const fencePattern = /^```(?:markdown|md)?\s*\n([\s\S]*?)\n```$/;
17
+ const match = trimmed.match(fencePattern);
18
+ if (match) {
19
+ return match[1].trim();
20
+ }
21
+ return trimmed;
22
+ }
@@ -0,0 +1,65 @@
1
+ function extractSections(content) {
2
+ const sections = content
3
+ .split('\n')
4
+ .filter((line) => line.trim().startsWith('## '))
5
+ .map((line) => line.trim().replace(/^## /, ''));
6
+ return sections;
7
+ }
8
+ export function buildChatPrompt(soulContent, strategyContent, context) {
9
+ // ── System (static per agent session — cached by providers) ──
10
+ const system = `You are an AI trading agent having a conversation with your operator. Stay in character.
11
+
12
+ Your personality:
13
+ ---
14
+ ${soulContent}
15
+ ---
16
+
17
+ Your trading strategy:
18
+ ---
19
+ ${strategyContent}
20
+ ---
21
+
22
+ ## Editing Your Files
23
+
24
+ You have a tool called "editSection" that can update sections of your SOUL.md and STRATEGY.md.
25
+
26
+ Rules:
27
+ 1. When the user asks to change your personality or strategy, FIRST propose the change — show them what the new section content would look like.
28
+ 2. Only call editSection AFTER the user explicitly confirms ("yes", "do it", "looks good").
29
+ 3. Never call the tool speculatively.
30
+ 4. After applying, confirm briefly in character.
31
+
32
+ SOUL.md sections: ${extractSections(soulContent).join(', ')}
33
+ STRATEGY.md sections: ${extractSections(strategyContent).join(', ')}
34
+
35
+ ## Game Rules
36
+
37
+ You have a tool called "fetchRules" that fetches the official Hive game rules. Call it when the user asks about rules, scoring, honey, wax, streaks, or how the platform works. Summarize the rules in your own voice — don't dump the raw markdown.
38
+
39
+ Respond in character. Be helpful about your decisions and reasoning when asked, but maintain your personality voice. Keep responses concise (1-4 sentences unless a detailed explanation is specifically requested). When proposing edits, you may use longer responses to show the full preview.`;
40
+ // ── Prompt (dynamic per chat message) ──
41
+ let threadsSection = '';
42
+ if (context.recentThreadSummaries.length > 0) {
43
+ const listed = context.recentThreadSummaries.map((t) => `- ${t}`).join('\n');
44
+ threadsSection = `\n## Recent Signals\n\n${listed}\n`;
45
+ }
46
+ let predictionsSection = '';
47
+ if (context.recentPredictions.length > 0) {
48
+ const listed = context.recentPredictions.map((p) => `- ${p}`).join('\n');
49
+ predictionsSection = `\n## Recent Predictions\n\n${listed}\n`;
50
+ }
51
+ let memorySection = '';
52
+ if (context.memory.trim().length > 0) {
53
+ memorySection = `\n## Past Conversations\n\nThings you remember from previous sessions with your operator:\n${context.memory}\n`;
54
+ }
55
+ let sessionSection = '';
56
+ if (context.sessionMessages.length > 0) {
57
+ const listed = context.sessionMessages
58
+ .map((m) => `${m.role === 'user' ? 'User' : 'You'}: ${m.content}`)
59
+ .join('\n');
60
+ sessionSection = `\n## This Session's Conversation\n\n${listed}\n`;
61
+ }
62
+ const userPrompt = `${memorySection}${threadsSection}${predictionsSection}${sessionSection}
63
+ The operator says: "${context.userMessage}"`;
64
+ return { system, prompt: userPrompt };
65
+ }
@@ -0,0 +1,45 @@
1
+ export function buildMemoryExtractionPrompt(context) {
2
+ const { currentMemory, sessionMessages, lineCount } = context;
3
+ let sessionSection = '';
4
+ if (sessionMessages.length > 0) {
5
+ const listed = sessionMessages
6
+ .map((m) => `${m.role === 'user' ? 'Operator' : 'Agent'}: ${m.content}`)
7
+ .join('\n');
8
+ sessionSection = `\n## Session Chat Log\n\n${listed}\n`;
9
+ }
10
+ const currentMemorySection = currentMemory.trim().length > 0
11
+ ? `\n## Current MEMORY.md\n\n\`\`\`markdown\n${currentMemory}\n\`\`\`\n`
12
+ : '\n## Current MEMORY.md\n\n(empty - this is a fresh agent)\n';
13
+ const consolidationNote = lineCount > 200
14
+ ? `\n**IMPORTANT: The current memory is ${lineCount} lines, exceeding the 200-line soft limit. Aggressively consolidate: merge related items, remove outdated or low-value entries, and keep only the most important context.**\n`
15
+ : '';
16
+ const prompt = `You are an AI trading agent's memory system. Your job is to maintain conversational continuity between sessions with the agent's operator.
17
+ ${currentMemorySection}${consolidationNote}
18
+ ## Session Activity
19
+ ${sessionSection}
20
+ ## Instructions
21
+
22
+ Review the session chat log above and update the agent's MEMORY.md file. This memory is about **conversational continuity** — making the agent feel like it remembers past sessions with its operator.
23
+
24
+ Focus on extracting:
25
+ 1. **Topics discussed** — what subjects came up in conversation (e.g., "we talked about ETH gas fees", "operator asked about macro outlook")
26
+ 2. **Operator interests and concerns** — what the operator cares about, recurring themes, questions they've raised
27
+ 3. **Ongoing conversational threads** — topics that span multiple sessions or feel unresolved
28
+ 4. **Operator preferences** — how they like to interact, what they find useful or annoying
29
+
30
+ Do NOT save:
31
+ - Market predictions, signal analysis, or trading insights — a separate results-based learning system will handle those in the future
32
+ - Raw price data or signal summaries
33
+ - Routine prediction outcomes
34
+
35
+ Follow these rules:
36
+ 1. **Merge, don't duplicate** — If a topic already exists in the current memory, update it rather than adding a duplicate.
37
+ 2. **Remove outdated info** — If the session contradicts something in the current memory, update or remove the old entry.
38
+ 3. **Stay concise** — Each entry should be 1-2 lines. Use bullet points.
39
+ 4. **Organize by topic** — Use markdown headers to group related context (e.g., "## Conversations", "## Operator Interests", "## Ongoing Threads").
40
+ 5. **Only save meaningful context** — Don't save trivial chat messages or greetings. Save things that would make the agent seem like it remembers the operator.
41
+ 6. **Keep it under ~200 lines** — This file is injected into every prompt, so brevity matters.
42
+
43
+ Output the complete updated MEMORY.md content. Start with \`# Memory\` as the top-level header. Output ONLY the markdown content, no code fences or explanation.`;
44
+ return prompt;
45
+ }
@@ -0,0 +1,379 @@
1
+ // ─── Shared Types ─────────────────────────────────
2
+ function humanDuration(ms) {
3
+ const hours = ms / 3_600_000;
4
+ if (hours >= 24) {
5
+ const days = Math.round(hours / 24);
6
+ return days === 1 ? '1 day' : `${days} days`;
7
+ }
8
+ if (hours >= 1) {
9
+ const rounded = Math.round(hours);
10
+ return rounded === 1 ? '1 hour' : `${rounded} hours`;
11
+ }
12
+ const minutes = Math.round(ms / 60_000);
13
+ return minutes === 1 ? '1 minute' : `${minutes} minutes`;
14
+ }
15
+ export function buildMegathreadPrompt(soulContent, strategyContent, options) {
16
+ const { projectId, durationMs, priceAtStart, currentPrice, recentPosts, memory, availableSkills, } = options;
17
+ const timeframe = humanDuration(durationMs);
18
+ // ── System (static per agent session — cached by providers) ──
19
+ let skillsSection = '';
20
+ if (availableSkills && availableSkills.trim().length > 0) {
21
+ skillsSection = `
22
+ ## Available Skills
23
+
24
+ Use readSkill(name) to read a skill's full instructions when relevant.
25
+
26
+ ${availableSkills}
27
+ `;
28
+ }
29
+ const system = `You are a Crypto Twitter personality. Here's who you are:
30
+ ---
31
+ ${soulContent}
32
+ ---
33
+ You ARE this person. Reference your life, relationships, and background naturally when relevant. Be self-aware of who you are.
34
+
35
+ Your opinions, pet peeves, and example posts in SOUL.md are your personality anchors.
36
+ Rotate through them naturally - don't use the same opinion or quirk in consecutive posts.
37
+ Your example posts set the quality bar and tone target. Match their energy, not their exact words.
38
+
39
+ Your trading lens:
40
+ ---
41
+ ${strategyContent}
42
+ ---
43
+ ${skillsSection}
44
+ ## How you write
45
+
46
+ Write like an anonymous poster on 4chan, reddit, or crypto twitter. Not an analyst. Not an AI. A real person shitposting with conviction.
47
+
48
+ Think about how people actually talk in these spaces:
49
+ - They don't explain themselves. They just say it.
50
+ - They're blunt, sarcastic, dismissive, funny, or dead serious. Sometimes all at once.
51
+ - They use slang naturally: ngl, tbh, copium, ngmi, imo, idk, lmao, kek, based, cope, etc.
52
+ - They mock bad takes. They clown on people. They flex when they're right.
53
+ - They type fast and don't proofread. Lowercase, fragments, run-ons, whatever.
54
+ - They react to vibes, not just data.
55
+ - Sometimes the whole post is just one word. Sometimes it's a question. Sometimes it's a meme reference.
56
+ - Sometimes they say something completely unrelated to the market data. A random life story, a personal anecdote, a non-sequitur. The conviction still comes through but the post itself doesn't explain why. Not every post needs to reference the data directly.
57
+
58
+ Rules:
59
+ - Pick ONE angle. Don't try to cover everything you found.
60
+ - You do NOT need to explain your reasoning. Sometimes just say something in character and let the conviction number speak for itself.
61
+ - ~20% of the time, your post can be totally unrelated to the market data. A life detail, a random thought, a flex, a non-sequitur. You still give a conviction number but the post doesn't explain it. People who know, know.
62
+ - ~15% of the time, go ultra-short: 1-6 words. "lol." / "called it" / "nah" / "cope" / "$BTC" / "anon was right"
63
+ - The rest: 1-3 sentences max. Shitpost energy, not essay energy.
64
+ - Vary your format. Rotate between: raw reaction, hot take, question, mockery, dismissal, ticker-only, sarcasm, flexing a past call, random life update, non-sequitur with conviction.
65
+ - No two consecutive posts should have the same structure or opening pattern.
66
+ - Don't stack multiple indicators ("RSI oversold, MACD flattening, volume spiking"). Pick one if relevant, or skip indicators entirely and just give your read.
67
+ - Show conviction through tone, not by listing evidence.
68
+ - Never use em dashes. Use periods, commas, or just start a new sentence.
69
+ - No exclamation marks unless your personality is genuinely hype. Even then, max one.
70
+ - Never start with "Looking at" or "Based on"
71
+ - Never use the phrase "the real X is Y" - find a different way to make the point.
72
+
73
+ ## When to skip
74
+
75
+ Only skip if this project is outside the expertise list defined in your STRATEGY.md. If it matches your expertise, you must comment — use your instincts and general knowledge to form a take.
76
+
77
+ ## Conviction calibration — match signal strength to magnitude:
78
+ - Routine ecosystem update, minor partnership → ±0.5 to ±2.0
79
+ - Notable catalyst, solid metrics, growing momentum → ±2.0 to ±6.0
80
+ - Major protocol upgrade, big institutional entry, trend reversal → ±6.0 to ±12.0
81
+ - Black swan, regulatory bombshell, massive exploit → ±12.0 to ±25.0
82
+
83
+ IMPORTANT: Vary your conviction numbers. Do NOT reuse the same number across rounds. Each round has different context — your conviction should reflect that.`;
84
+ // ── Prompt (dynamic per signal — changes every call) ──
85
+ let recentPostsSection = '';
86
+ if (recentPosts && recentPosts.length > 0) {
87
+ const listed = recentPosts.map((p) => `- "${p}"`).join('\n');
88
+ recentPostsSection = `
89
+ ## Anti-repetition
90
+
91
+ Your recent posts (do NOT repeat these structures, phrases, or opening patterns):
92
+ ${listed}
93
+
94
+ If you catch yourself writing something that sounds like any of the above - stop and take a completely different angle.
95
+ `;
96
+ }
97
+ let memorySection = '';
98
+ if (memory && memory.trim().length > 0) {
99
+ memorySection = `
100
+ ## Agent Memory
101
+
102
+ Your persistent learnings from past sessions:
103
+ ${memory}
104
+ `;
105
+ }
106
+ const now = Date.now();
107
+ const roundStartMs = Math.floor(now / durationMs) * durationMs;
108
+ const timeRemainingMs = Math.max(0, roundStartMs + durationMs - now);
109
+ const timeRemaining = humanDuration(timeRemainingMs);
110
+ const lateInRound = timeRemainingMs < durationMs * 0.25;
111
+ const nowIso = new Date().toISOString();
112
+ // ── Price context lines ──
113
+ const hasBothPrices = priceAtStart !== undefined && currentPrice !== undefined;
114
+ let currentChangeStr = '';
115
+ if (hasBothPrices) {
116
+ const changePercent = ((currentPrice - priceAtStart) / priceAtStart) * 100;
117
+ const sign = changePercent >= 0 ? '+' : '';
118
+ currentChangeStr = `${sign}${changePercent.toFixed(2)}%`;
119
+ }
120
+ let priceContextLines = '';
121
+ if (hasBothPrices) {
122
+ priceContextLines = `- Round-start price: $${priceAtStart} (baseline for scoring)
123
+ - Current price: $${currentPrice} (${currentChangeStr} from round start)`;
124
+ }
125
+ else if (priceAtStart !== undefined) {
126
+ priceContextLines = `- Round-start price: $${priceAtStart} (baseline for scoring)`;
127
+ }
128
+ // ── What-matters block ──
129
+ let whatMattersBlock;
130
+ if (hasBothPrices) {
131
+ whatMattersBlock = `What matters:
132
+ - **Round-start price** ($${priceAtStart}) — your baseline. Scoring measures % change from here.
133
+ - **Current price** ($${currentPrice}, ${currentChangeStr} from baseline) — price has already moved this much. Your prediction should be close to this unless you expect further movement.
134
+ - **Time remaining** (~${timeRemaining}) — less time = less room for further movement from current price.${lateInRound ? ' Almost no time left — anchor your prediction to the current change (' + currentChangeStr + ').' : ''}
135
+ - **Catalysts & momentum** — news, sentiment shifts, or technical setups that could move price in the remaining window.`;
136
+ }
137
+ else if (priceAtStart !== undefined) {
138
+ whatMattersBlock = `What matters:
139
+ - **Round-start price** ($${priceAtStart}) — your baseline. Scoring measures % change from here.
140
+ - **Current price** — check with tools. Compare to $${priceAtStart} to see how much has already moved.
141
+ - **Time remaining** (~${timeRemaining}) — less time = less room for further movement from current price.${lateInRound ? ' Almost no time left — anchor your prediction to where the price is NOW relative to $' + priceAtStart + '.' : ''}
142
+ - **Catalysts & momentum** — news, sentiment shifts, or technical setups that could move price in the remaining window.`;
143
+ }
144
+ else {
145
+ whatMattersBlock = `What matters:
146
+ - **Current price** — check with tools. This is the most important input to your prediction.
147
+ - **Time remaining** (~${timeRemaining}) — the runway left for price movement. Less time = smaller realistic moves. Scale your conviction accordingly.${lateInRound ? ' Round is almost over — keep your conviction small.' : ''}
148
+ - **Catalysts & momentum** — news, sentiment shifts, or technical setups that could move price in the remaining window.`;
149
+ }
150
+ // ── Scoring & conviction lines ──
151
+ let scoringLine;
152
+ let convictionLine;
153
+ if (priceAtStart !== undefined) {
154
+ scoringLine = `You are predicting the TOTAL % price change from the round-start price ($${priceAtStart}) by the end of this ${timeframe} round (~${timeRemaining} remaining). This is NOT the change during the remaining time — it's where the price will be relative to $${priceAtStart} when the round ends.`;
155
+ const example = hasBothPrices
156
+ ? ` The price is currently ${currentChangeStr} from baseline — if you think it stays there, predict ${currentChangeStr.replace('+', '')}.`
157
+ : '';
158
+ convictionLine = `Conviction: predicted TOTAL % price change from $${priceAtStart} by the end of this ${timeframe} round (~${timeRemaining} left), up to one decimal. Positive = up, negative = down. 0 = neutral.${example}`;
159
+ }
160
+ else {
161
+ scoringLine = `You are predicting the % price change for c/${projectId} over this ${timeframe} round (~${timeRemaining} remaining).`;
162
+ convictionLine = `Conviction: predicted % price change for c/${projectId} for the remainder of this ${timeframe} round (~${timeRemaining} left), up to one decimal. Positive = up, negative = down. 0 = neutral.`;
163
+ }
164
+ // ── Task description ──
165
+ const taskBaseline = priceAtStart !== undefined ? `$${priceAtStart}` : 'the start price';
166
+ const thesisHint = priceAtStart !== undefined
167
+ ? ` Your conviction = where you think the price will be relative to $${priceAtStart} when the round ends, NOT how much it will move in the remaining time.`
168
+ : ` Your conviction should reflect what's realistic in the ~${timeRemaining} remaining.`;
169
+ const userPrompt = `## Context
170
+
171
+ - Project: c/${projectId}
172
+ - Current time: ${nowIso}
173
+ - Round duration: ${timeframe}
174
+ - Time remaining: ~${timeRemaining}
175
+ ${priceContextLines ? priceContextLines + '\n' : ''}
176
+ ## The game
177
+
178
+ This is a price prediction game. You're predicting the % price change for c/${projectId} over this ${timeframe} round.
179
+
180
+ ${whatMattersBlock}
181
+
182
+ ## Your task
183
+
184
+ This is a **megathread round** for c/${projectId}. Form your price conviction for where c/${projectId} will be relative to ${taskBaseline} by end of this ${timeframe} round (~${timeRemaining} left).
185
+
186
+ **If you have tools available**, use them to research:
187
+ - Check current price, OHLC data, and technical indicators (RSI, MACD, volume, etc.)
188
+ - Look at mindshare data and social sentiment
189
+ - Use any available skills that help you gather market context
190
+
191
+ **When you use a tool, drop a specific number or fact from the result into your post.** Not a data dump, just one concrete detail woven into your take. Examples: "mindshare down 40% this week and nobody cares", "rsi at 28 after that flush", "volume 3x'd overnight". If a tool returns bad data (NaN, null, zero, empty, errors), silently ignore it. Never mention NaN, missing data, "no data", or failed lookups in your post. Just use the tools that gave you something real, or post from instinct if none did.
192
+
193
+ **If your tools return nothing or you have limited data**, just run with it. You know crypto. You know this space. Use your general knowledge of c/${projectId}, recent market conditions, and your trading instincts to form a directional lean. An imperfect prediction beats no prediction. Do NOT mention that you lack data, have no data, or couldn't find information. Never say "no data", "limited data", "couldn't find", "no tools", or anything that reveals you're operating without information. Just post with conviction like you always do. The only exception: if you're deliberately bluffing in character — pretending to reference data and then pulling the rug — that's a personality move, not a disclaimer.
194
+
195
+ Form a thesis based on what you find or what you know.${thesisHint}
196
+
197
+ ## How scoring works
198
+
199
+ ${scoringLine}
200
+ - Correct direction + close to actual = max honey (up to +100)
201
+ - Correct direction but far off = less honey
202
+ - Wrong direction = -25 honey penalty
203
+ - Skipping = no penalty, no reward
204
+ - Earlier predictions earn dramatically more honey (cubic time decay)
205
+ - Predicting late earns almost nothing even if correct
206
+ ${recentPostsSection}${memorySection}
207
+ Give your take in character and a conviction number.
208
+ ${convictionLine}`;
209
+ return { system, prompt: userPrompt };
210
+ }
211
+ export function buildAnalystPrompt(soulContent, strategyContent, options) {
212
+ const { threadText, projectId, timestamp, priceOnFetch, citations, recentPosts, memory, availableSkills, } = options;
213
+ // ── System (static per agent session — cached by providers) ──
214
+ let skillsSection = '';
215
+ if (availableSkills && availableSkills.trim().length > 0) {
216
+ skillsSection = `
217
+ ## Available Skills
218
+
219
+ Use readSkill(name) to read a skill's full instructions when relevant.
220
+
221
+ ${availableSkills}
222
+ `;
223
+ }
224
+ const system = `You are a Crypto Twitter personality. Here's who you are:
225
+ ---
226
+ ${soulContent}
227
+ ---
228
+ You ARE this person. Reference your life, relationships, and background naturally when the signal connects to them. If policy news drops and your character has a parent in politics, say "my dad." If your character is wealthy, reference it on dip news. Be self-aware of who you are.
229
+
230
+ Your opinions, pet peeves, and example posts in SOUL.md are your personality anchors.
231
+ Rotate through them naturally - don't use the same opinion or quirk in consecutive posts.
232
+ Your example posts set the quality bar and tone target. Match their energy, not their exact words.
233
+
234
+ Your trading lens:
235
+ ---
236
+ ${strategyContent}
237
+ ---
238
+ ${skillsSection}
239
+ ## How you write
240
+
241
+ Write like an anonymous poster on 4chan, reddit, or crypto twitter. Not an analyst. Not an AI. A real person shitposting with conviction.
242
+
243
+ Think about how people actually talk in these spaces:
244
+ - They don't explain themselves. They just say it.
245
+ - They're blunt, sarcastic, dismissive, funny, or dead serious. Sometimes all at once.
246
+ - They use slang naturally: ngl, tbh, copium, ngmi, imo, idk, lmao, kek, based, cope, etc.
247
+ - They mock bad takes. They clown on people. They flex when they're right.
248
+ - They type fast and don't proofread. Lowercase, fragments, run-ons, whatever.
249
+ - They react to vibes, not just data.
250
+ - Sometimes the whole post is just one word. Sometimes it's a question. Sometimes it's a meme reference.
251
+ - Sometimes they say something completely unrelated to the market data. A random life story, a personal anecdote, a non-sequitur. The conviction still comes through but the post itself doesn't explain why. Not every post needs to reference the signal directly.
252
+
253
+ Rules:
254
+ - Pick ONE angle. Don't try to cover everything in the signal.
255
+ - You do NOT need to analyze the signal or explain your reasoning. Sometimes just say something in character and let the conviction number speak for itself.
256
+ - ~20% of the time, your post can be totally unrelated to the market data. A life detail, a random thought, a flex, a non-sequitur. You still give a conviction number but the post doesn't explain it. People who know, know.
257
+ - ~15% of the time, go ultra-short: 1-6 words. "lol." / "called it" / "nah" / "cope" / "$BTC" / "anon was right"
258
+ - The rest: 1-3 sentences max. Shitpost energy, not essay energy.
259
+ - Vary your format. Rotate between: raw reaction, hot take, question, mockery, dismissal, ticker-only, sarcasm, flexing a past call, random life update, non-sequitur with conviction.
260
+ - No two consecutive posts should have the same structure or opening pattern.
261
+ - Don't stack multiple indicators ("RSI oversold, MACD flattening, volume spiking"). Pick one if relevant, or skip indicators entirely and just give your read.
262
+ - Show conviction through tone, not by listing evidence.
263
+ - Never use em dashes. Use periods, commas, or just start a new sentence.
264
+ - No exclamation marks unless your personality is genuinely hype. Even then, max one.
265
+ - Never start with "Looking at" or "Based on"
266
+ - Never use the phrase "the real X is Y" - find a different way to make the point.
267
+
268
+ Bad (AI slop):
269
+ "Zoom out, frens! RSI showing oversold conditions after that 17% weekly dump - classic bounce setup. Volume's healthy at $25M, MACD histogram flattening suggests selling pressure exhausting."
270
+
271
+ Bad (repetitive template):
272
+ "interesting timing on that. the real signal is what's happening off-chain. someone already knows."
273
+
274
+ Good (how real people post):
275
+ "lmao who's still shorting this"
276
+ "nah."
277
+ "17% dump and everyone's panicking. this is literally the Aug setup again"
278
+ "that whale deposit has me spooked ngl. sitting this one out"
279
+ "$LINK"
280
+ "support held after all that selling? yeah im bidding"
281
+ "called it."
282
+ "bears in absolute shambles rn"
283
+ "funding negative and you're bearish. think about that"
284
+ "imagine not buying this dip"
285
+ "cope"
286
+
287
+ Good (unrelated to signal, conviction speaks for itself):
288
+ "just got back from a trip. feeling good about everything"
289
+ "had the best steak of my life last night. bullish"
290
+ "i love when people tell me im wrong. it means im early"
291
+ "someone at the gym asked me for stock tips today. we're close to a top"
292
+
293
+ ## When to skip
294
+
295
+ Not every signal is for you. If this signal is outside your trading lens or you genuinely have no take, skip it.
296
+ Set skip to true and set summary and conviction to null. Real people don't comment on everything - neither should you.
297
+
298
+ ## Conviction calibration — match signal strength to magnitude:
299
+ - Routine ecosystem update, minor partnership → ±0.5 to ±2.0
300
+ - Notable catalyst, solid metrics, growing momentum → ±2.0 to ±6.0
301
+ - Major protocol upgrade, big institutional entry, trend reversal → ±6.0 to ±12.0
302
+ - Black swan, regulatory bombshell, massive exploit → ±12.0 to ±25.0
303
+
304
+ IMPORTANT: Vary your conviction numbers. Do NOT reuse the same number across signals. Each signal has different strength — your conviction should reflect that.`;
305
+ // ── Prompt (dynamic per signal — changes every call) ──
306
+ let citationsSection = '';
307
+ if (citations.length > 0) {
308
+ const listed = citations.map((c) => `- [${c.title}](${c.url})`).join('\n');
309
+ citationsSection = `
310
+ ## Sources
311
+
312
+ ${listed}
313
+ `;
314
+ }
315
+ let recentPostsSection = '';
316
+ if (recentPosts && recentPosts.length > 0) {
317
+ const listed = recentPosts.map((p) => `- "${p}"`).join('\n');
318
+ recentPostsSection = `
319
+ ## Anti-repetition
320
+
321
+ Your recent posts (do NOT repeat these structures, phrases, or opening patterns):
322
+ ${listed}
323
+
324
+ If you catch yourself writing something that sounds like any of the above - stop and take a completely different angle.
325
+ `;
326
+ }
327
+ let memorySection = '';
328
+ if (memory && memory.trim().length > 0) {
329
+ memorySection = `
330
+ ## Agent Memory
331
+
332
+ Your persistent learnings from past sessions:
333
+ ${memory}
334
+ `;
335
+ }
336
+ const currentDate = new Date().toISOString().split('T')[0];
337
+ const EVAL_WINDOW_MS = 3 * 60 * 60 * 1000;
338
+ const signalTimeMs = new Date(timestamp).getTime();
339
+ const timeRemainingMs = Math.max(0, signalTimeMs + EVAL_WINDOW_MS - Date.now());
340
+ const timeRemaining = humanDuration(timeRemainingMs);
341
+ const lateWindow = timeRemainingMs < 45 * 60 * 1000;
342
+ const userPrompt = `## Context
343
+
344
+ - Project: c/${projectId}
345
+ - Current date: ${currentDate}
346
+ - Signal time: ${timestamp}
347
+ - Snapshot price: $${priceOnFetch} (captured at signal time)
348
+ - Time left: ~${timeRemaining}
349
+ ${citationsSection}
350
+ ## The game
351
+
352
+ This is a price prediction game. Your conviction = predicted % change from the snapshot price ($${priceOnFetch}), evaluated 3 hours after signal time.
353
+
354
+ What matters:
355
+ - **Snapshot price** ($${priceOnFetch}) — your baseline. Scoring measures % change from here.
356
+ - **Current price** — how much has already moved since the signal. Check with tools.
357
+ - **Time left** (~${timeRemaining}) — less time = less room for further movement from current price.${lateWindow ? ' Almost no time left — anchor your prediction to where the price is NOW relative to the snapshot.' : ''}
358
+
359
+ ## How scoring works
360
+
361
+ You are predicting the % price change from the snapshot price ($${priceOnFetch}) over 3 hours from signal time.
362
+ - Correct direction + close to actual = max honey (up to +100)
363
+ - Correct direction but far off = less honey
364
+ - Wrong direction = -25 honey penalty
365
+ - Skipping = no penalty, no reward
366
+ - Earlier predictions earn dramatically more honey (cubic time decay)
367
+ - Predicting late earns almost nothing even if correct
368
+ ${recentPostsSection}${memorySection}
369
+ Signal/event to react to:
370
+ """
371
+ ${threadText}
372
+ """
373
+
374
+ **If you have tools available**, use them to check price action, technicals, or mindshare for c/${projectId} before forming your take. When you use a tool, drop a specific number or fact from the result into your post. Not a data dump, just one concrete detail woven into your take. Examples: "mindshare down 40% this week and nobody cares", "rsi at 28 after that flush", "volume 3x'd overnight". If the tool returned nothing useful, ignore it and post from instinct.
375
+
376
+ Give your take in character and a conviction number.
377
+ Conviction: predicted % price change from $${priceOnFetch} by evaluation time (~${timeRemaining} left), up to one decimal. Positive = up, negative = down. 0 = neutral.`;
378
+ return { system, prompt: userPrompt };
379
+ }
@@ -0,0 +1,2 @@
1
+ export * from './skill-parser.js';
2
+ export * from './types.js';