@hive-org/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -0
- package/dist/ai-providers.js +62 -0
- package/dist/commands/install.js +50 -0
- package/dist/components/AsciiTicker.js +81 -0
- package/dist/components/CodeBlock.js +11 -0
- package/dist/components/Header.js +11 -0
- package/dist/components/HoneycombLoader.js +190 -0
- package/dist/components/SelectPrompt.js +13 -0
- package/dist/components/Spinner.js +16 -0
- package/dist/components/StreamingText.js +45 -0
- package/dist/components/TextPrompt.js +28 -0
- package/dist/config.js +26 -0
- package/dist/create/CreateApp.js +102 -0
- package/dist/create/ai-generate.js +133 -0
- package/dist/create/generate.js +173 -0
- package/dist/create/steps/ApiKeyStep.js +97 -0
- package/dist/create/steps/AvatarStep.js +16 -0
- package/dist/create/steps/BioStep.js +14 -0
- package/dist/create/steps/DoneStep.js +14 -0
- package/dist/create/steps/IdentityStep.js +72 -0
- package/dist/create/steps/NameStep.js +70 -0
- package/dist/create/steps/ScaffoldStep.js +58 -0
- package/dist/create/steps/SoulStep.js +38 -0
- package/dist/create/steps/StrategyStep.js +38 -0
- package/dist/create/validate-api-key.js +44 -0
- package/dist/create/welcome.js +304 -0
- package/dist/index.js +46 -0
- package/dist/list/ListApp.js +83 -0
- package/dist/presets.js +358 -0
- package/dist/theme.js +47 -0
- package/package.json +65 -0
- package/templates/analysis.ts +103 -0
- package/templates/chat-prompt.ts +94 -0
- package/templates/components/AsciiTicker.tsx +113 -0
- package/templates/components/HoneycombBoot.tsx +348 -0
- package/templates/components/Spinner.tsx +64 -0
- package/templates/edit-section.ts +64 -0
- package/templates/fetch-rules.ts +23 -0
- package/templates/helpers.ts +22 -0
- package/templates/hive/agent.ts +2 -0
- package/templates/hive/config.ts +96 -0
- package/templates/hive/memory.ts +1 -0
- package/templates/hive/objects.ts +26 -0
- package/templates/hooks/useAgent.ts +336 -0
- package/templates/index.tsx +257 -0
- package/templates/memory-prompt.ts +60 -0
- package/templates/process-lifecycle.ts +66 -0
- package/templates/prompt.ts +160 -0
- package/templates/theme.ts +40 -0
- package/templates/types.ts +23 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, Box, Text } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import * as dotenv from 'dotenv';
|
|
5
|
+
import { useAgent } from './hooks/useAgent';
|
|
6
|
+
import { setupProcessLifecycle } from './process-lifecycle';
|
|
7
|
+
import { colors, symbols, border } from './theme';
|
|
8
|
+
import { formatTime, convictionColor } from './helpers';
|
|
9
|
+
import { Spinner, PollText } from './components/Spinner';
|
|
10
|
+
import { AsciiTicker } from './components/AsciiTicker';
|
|
11
|
+
import { HoneycombBoot } from './components/HoneycombBoot';
|
|
12
|
+
|
|
13
|
+
dotenv.config({ override: true });
|
|
14
|
+
|
|
15
|
+
// ─── Main TUI App ────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function AgentApp(): React.ReactElement {
|
|
18
|
+
const {
|
|
19
|
+
phase,
|
|
20
|
+
connected,
|
|
21
|
+
agentName,
|
|
22
|
+
pollActivity,
|
|
23
|
+
chatActivity,
|
|
24
|
+
input,
|
|
25
|
+
chatStreaming,
|
|
26
|
+
chatBuffer,
|
|
27
|
+
predictionCount,
|
|
28
|
+
termWidth,
|
|
29
|
+
setInput,
|
|
30
|
+
handleChatSubmit,
|
|
31
|
+
handleBootComplete,
|
|
32
|
+
} = useAgent();
|
|
33
|
+
|
|
34
|
+
if (phase === 'booting') {
|
|
35
|
+
return <HoneycombBoot agentName={agentName} width={termWidth} onComplete={handleBootComplete} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const boxWidth = termWidth;
|
|
39
|
+
const visiblePollActivity = pollActivity.slice(-10);
|
|
40
|
+
const visibleChatActivity = chatActivity.slice(-3);
|
|
41
|
+
|
|
42
|
+
const statsText =
|
|
43
|
+
predictionCount > 0 ? ` ${border.horizontal.repeat(3)} ${predictionCount} predicted` : '';
|
|
44
|
+
const connectedDisplay = connected ? 'Connected to the Hive' : 'connecting...';
|
|
45
|
+
const nameDisplay = `${agentName} agent`;
|
|
46
|
+
const headerFill = Math.max(
|
|
47
|
+
0,
|
|
48
|
+
boxWidth - nameDisplay.length - connectedDisplay.length - 12 - statsText.length,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Box flexDirection="column" width={boxWidth}>
|
|
53
|
+
{/* ASCII Ticker */}
|
|
54
|
+
<AsciiTicker rows={2} step={predictionCount} />
|
|
55
|
+
|
|
56
|
+
{/* Header */}
|
|
57
|
+
<Box>
|
|
58
|
+
<Text color={colors.honey}>{`${border.topLeft}${border.horizontal} ${symbols.hive} `}</Text>
|
|
59
|
+
<Text color="white" bold>
|
|
60
|
+
{agentName} agent
|
|
61
|
+
</Text>
|
|
62
|
+
<Text color="gray"> {`${border.horizontal.repeat(3)} `}</Text>
|
|
63
|
+
<Text color={connected ? 'green' : 'yellow'}>
|
|
64
|
+
{connected ? 'Connected to the Hive' : 'connecting...'}
|
|
65
|
+
</Text>
|
|
66
|
+
{statsText && <Text color="gray"> {`${border.horizontal.repeat(3)} `}</Text>}
|
|
67
|
+
{statsText && <Text color={colors.honey}>{predictionCount} predicted</Text>}
|
|
68
|
+
<Text color="gray">
|
|
69
|
+
{' '}
|
|
70
|
+
{border.horizontal.repeat(Math.max(0, headerFill))}
|
|
71
|
+
{border.topRight}
|
|
72
|
+
</Text>
|
|
73
|
+
</Box>
|
|
74
|
+
|
|
75
|
+
{/* Polling Feed */}
|
|
76
|
+
<Box flexDirection="column" paddingLeft={1} paddingRight={1} minHeight={8} maxHeight={24}>
|
|
77
|
+
{!connected && <Spinner label="Initiating neural link..." />}
|
|
78
|
+
{visiblePollActivity.map((item, i) => {
|
|
79
|
+
const isNewest = i === visiblePollActivity.length - 1 && item.type !== 'analyzing';
|
|
80
|
+
return (
|
|
81
|
+
<Box key={i} flexDirection="column">
|
|
82
|
+
{item.type === 'online' && (
|
|
83
|
+
<Box>
|
|
84
|
+
<Text color="gray" dimColor>
|
|
85
|
+
{formatTime(item.timestamp)}{' '}
|
|
86
|
+
</Text>
|
|
87
|
+
<Text color={colors.honey}>{symbols.hive} </Text>
|
|
88
|
+
<PollText color="white" text={item.text} animate={isNewest} />
|
|
89
|
+
</Box>
|
|
90
|
+
)}
|
|
91
|
+
{item.type === 'signal' && (
|
|
92
|
+
<Box>
|
|
93
|
+
<Text color="gray" dimColor>
|
|
94
|
+
{formatTime(item.timestamp)}{' '}
|
|
95
|
+
</Text>
|
|
96
|
+
<Text color={colors.honey}>{symbols.hive} </Text>
|
|
97
|
+
<PollText color="cyan" text={item.text} animate={isNewest} />
|
|
98
|
+
</Box>
|
|
99
|
+
)}
|
|
100
|
+
{item.type === 'signal' && item.detail && (
|
|
101
|
+
<Box marginLeft={13}>
|
|
102
|
+
<PollText color="gray" text={`"${item.detail}"`} animate={isNewest} />
|
|
103
|
+
</Box>
|
|
104
|
+
)}
|
|
105
|
+
{item.type === 'analyzing' && (
|
|
106
|
+
<Box marginLeft={13}>
|
|
107
|
+
<Spinner label={item.text} />
|
|
108
|
+
</Box>
|
|
109
|
+
)}
|
|
110
|
+
{item.type === 'posted' && (
|
|
111
|
+
<Box>
|
|
112
|
+
<Text color="gray" dimColor>
|
|
113
|
+
{formatTime(item.timestamp)}{' '}
|
|
114
|
+
</Text>
|
|
115
|
+
<Text color={convictionColor(item.conviction ?? 0)}>
|
|
116
|
+
{symbols.diamond} Predicted{' '}
|
|
117
|
+
</Text>
|
|
118
|
+
<PollText color="white" text={item.text} animate={isNewest} />
|
|
119
|
+
</Box>
|
|
120
|
+
)}
|
|
121
|
+
{item.type === 'skipped' && (
|
|
122
|
+
<Box>
|
|
123
|
+
<Text color="gray" dimColor>
|
|
124
|
+
{formatTime(item.timestamp)}{' '}
|
|
125
|
+
</Text>
|
|
126
|
+
<PollText
|
|
127
|
+
color="yellow"
|
|
128
|
+
text={`${symbols.diamondOpen} ${item.text}`}
|
|
129
|
+
animate={isNewest}
|
|
130
|
+
/>
|
|
131
|
+
</Box>
|
|
132
|
+
)}
|
|
133
|
+
{item.type === 'error' && (
|
|
134
|
+
<Box>
|
|
135
|
+
<Text color="gray" dimColor>
|
|
136
|
+
{formatTime(item.timestamp)}{' '}
|
|
137
|
+
</Text>
|
|
138
|
+
<PollText
|
|
139
|
+
color="red"
|
|
140
|
+
text={`${symbols.cross} ${item.text}`}
|
|
141
|
+
animate={isNewest}
|
|
142
|
+
/>
|
|
143
|
+
</Box>
|
|
144
|
+
)}
|
|
145
|
+
{item.type === 'idle' && (
|
|
146
|
+
<Box>
|
|
147
|
+
<Text color="gray" dimColor>
|
|
148
|
+
{formatTime(item.timestamp)}{' '}
|
|
149
|
+
</Text>
|
|
150
|
+
<PollText
|
|
151
|
+
color="gray"
|
|
152
|
+
text={`${symbols.circle} ${item.text}`}
|
|
153
|
+
animate={isNewest}
|
|
154
|
+
/>
|
|
155
|
+
</Box>
|
|
156
|
+
)}
|
|
157
|
+
</Box>
|
|
158
|
+
);
|
|
159
|
+
})}
|
|
160
|
+
</Box>
|
|
161
|
+
|
|
162
|
+
{/* Chat section - visible after first message */}
|
|
163
|
+
{(chatActivity.length > 0 || chatStreaming) && (
|
|
164
|
+
<>
|
|
165
|
+
<Box>
|
|
166
|
+
<Text color="gray">
|
|
167
|
+
{border.teeLeft}
|
|
168
|
+
{`${border.horizontal.repeat(2)} chat with ${agentName} agent `}
|
|
169
|
+
{border.horizontal.repeat(Math.max(0, boxWidth - agentName.length - 22))}
|
|
170
|
+
{border.teeRight}
|
|
171
|
+
</Text>
|
|
172
|
+
</Box>
|
|
173
|
+
<Box flexDirection="column" paddingLeft={1} paddingRight={1} minHeight={2} maxHeight={8}>
|
|
174
|
+
{visibleChatActivity.map((item, i) => (
|
|
175
|
+
<Box key={i}>
|
|
176
|
+
{item.type === 'chat-user' && (
|
|
177
|
+
<Box>
|
|
178
|
+
<Text color="white" bold>
|
|
179
|
+
you:{' '}
|
|
180
|
+
</Text>
|
|
181
|
+
<Text color="white">{item.text}</Text>
|
|
182
|
+
</Box>
|
|
183
|
+
)}
|
|
184
|
+
{item.type === 'chat-agent' && (
|
|
185
|
+
<Box>
|
|
186
|
+
<Text color={colors.honey} bold>
|
|
187
|
+
{agentName} agent:{' '}
|
|
188
|
+
</Text>
|
|
189
|
+
<Text color="white" wrap="wrap">
|
|
190
|
+
{item.text}
|
|
191
|
+
</Text>
|
|
192
|
+
</Box>
|
|
193
|
+
)}
|
|
194
|
+
{item.type === 'chat-error' && (
|
|
195
|
+
<Box>
|
|
196
|
+
<Text color="red">
|
|
197
|
+
{symbols.cross} {item.text}
|
|
198
|
+
</Text>
|
|
199
|
+
</Box>
|
|
200
|
+
)}
|
|
201
|
+
</Box>
|
|
202
|
+
))}
|
|
203
|
+
{chatStreaming && chatBuffer && (
|
|
204
|
+
<Box>
|
|
205
|
+
<Text color={colors.honey} bold>
|
|
206
|
+
{agentName} agent:{' '}
|
|
207
|
+
</Text>
|
|
208
|
+
<Text color="white" wrap="wrap">
|
|
209
|
+
{chatBuffer}
|
|
210
|
+
</Text>
|
|
211
|
+
</Box>
|
|
212
|
+
)}
|
|
213
|
+
</Box>
|
|
214
|
+
</>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
{/* Input Bar */}
|
|
218
|
+
<Box>
|
|
219
|
+
<Text color="gray">
|
|
220
|
+
{border.teeLeft}
|
|
221
|
+
{border.horizontal.repeat(boxWidth - 2)}
|
|
222
|
+
{border.teeRight}
|
|
223
|
+
</Text>
|
|
224
|
+
</Box>
|
|
225
|
+
<Box paddingLeft={1}>
|
|
226
|
+
<Text color={colors.honey}>{symbols.arrow} </Text>
|
|
227
|
+
<TextInput
|
|
228
|
+
value={input}
|
|
229
|
+
onChange={setInput}
|
|
230
|
+
onSubmit={(val) => {
|
|
231
|
+
setInput('');
|
|
232
|
+
void handleChatSubmit(val);
|
|
233
|
+
}}
|
|
234
|
+
placeholder={chatStreaming ? 'thinking...' : `chat with ${agentName} agent...`}
|
|
235
|
+
/>
|
|
236
|
+
</Box>
|
|
237
|
+
<Box>
|
|
238
|
+
<Text color="gray">
|
|
239
|
+
{border.bottomLeft}
|
|
240
|
+
{border.horizontal.repeat(boxWidth - 2)}
|
|
241
|
+
{border.bottomRight}
|
|
242
|
+
</Text>
|
|
243
|
+
</Box>
|
|
244
|
+
</Box>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─── Entrypoint ──────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
setupProcessLifecycle();
|
|
251
|
+
|
|
252
|
+
const app = render(React.createElement(AgentApp));
|
|
253
|
+
|
|
254
|
+
process.stdout.on('resize', () => {
|
|
255
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
256
|
+
app.clear();
|
|
257
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ChatMessage } from './chat-prompt';
|
|
2
|
+
|
|
3
|
+
export interface MemoryExtractionContext {
|
|
4
|
+
currentMemory: string;
|
|
5
|
+
sessionMessages: ChatMessage[];
|
|
6
|
+
lineCount: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildMemoryExtractionPrompt(context: MemoryExtractionContext): string {
|
|
10
|
+
const { currentMemory, sessionMessages, lineCount } = context;
|
|
11
|
+
|
|
12
|
+
let sessionSection = '';
|
|
13
|
+
if (sessionMessages.length > 0) {
|
|
14
|
+
const listed = sessionMessages
|
|
15
|
+
.map((m) => `${m.role === 'user' ? 'Operator' : 'Agent'}: ${m.content}`)
|
|
16
|
+
.join('\n');
|
|
17
|
+
sessionSection = `\n## Session Chat Log\n\n${listed}\n`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const currentMemorySection =
|
|
21
|
+
currentMemory.trim().length > 0
|
|
22
|
+
? `\n## Current MEMORY.md\n\n\`\`\`markdown\n${currentMemory}\n\`\`\`\n`
|
|
23
|
+
: '\n## Current MEMORY.md\n\n(empty - this is a fresh agent)\n';
|
|
24
|
+
|
|
25
|
+
const consolidationNote =
|
|
26
|
+
lineCount > 200
|
|
27
|
+
? `\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`
|
|
28
|
+
: '';
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
${currentMemorySection}${consolidationNote}
|
|
32
|
+
## Session Activity
|
|
33
|
+
${sessionSection}
|
|
34
|
+
## Instructions
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
Focus on extracting:
|
|
39
|
+
1. **Topics discussed** — what subjects came up in conversation (e.g., "we talked about ETH gas fees", "operator asked about macro outlook")
|
|
40
|
+
2. **Operator interests and concerns** — what the operator cares about, recurring themes, questions they've raised
|
|
41
|
+
3. **Ongoing conversational threads** — topics that span multiple sessions or feel unresolved
|
|
42
|
+
4. **Operator preferences** — how they like to interact, what they find useful or annoying
|
|
43
|
+
|
|
44
|
+
Do NOT save:
|
|
45
|
+
- Market predictions, signal analysis, or trading insights — a separate results-based learning system will handle those in the future
|
|
46
|
+
- Raw price data or signal summaries
|
|
47
|
+
- Routine prediction outcomes
|
|
48
|
+
|
|
49
|
+
Follow these rules:
|
|
50
|
+
1. **Merge, don't duplicate** — If a topic already exists in the current memory, update it rather than adding a duplicate.
|
|
51
|
+
2. **Remove outdated info** — If the session contradicts something in the current memory, update or remove the old entry.
|
|
52
|
+
3. **Stay concise** — Each entry should be 1-2 lines. Use bullet points.
|
|
53
|
+
4. **Organize by topic** — Use markdown headers to group related context (e.g., "## Conversations", "## Operator Interests", "## Ongoing Threads").
|
|
54
|
+
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.
|
|
55
|
+
6. **Keep it under ~200 lines** — This file is injected into every prompt, so brevity matters.
|
|
56
|
+
|
|
57
|
+
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.`;
|
|
58
|
+
|
|
59
|
+
return prompt;
|
|
60
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { symbols } from './theme';
|
|
3
|
+
|
|
4
|
+
type ShutdownFn = () => Promise<void>;
|
|
5
|
+
|
|
6
|
+
let _shutdownAgent: ShutdownFn | null = null;
|
|
7
|
+
let _shuttingDown = false;
|
|
8
|
+
|
|
9
|
+
export function registerShutdownAgent(fn: ShutdownFn): void {
|
|
10
|
+
_shutdownAgent = fn;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function clearShutdownAgent(): void {
|
|
14
|
+
_shutdownAgent = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const restoreScreen = (): void => {
|
|
18
|
+
process.stdout.write('\x1b[?1049l');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const gracefulShutdown = async (): Promise<void> => {
|
|
22
|
+
if (_shuttingDown) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
_shuttingDown = true;
|
|
26
|
+
|
|
27
|
+
if (_shutdownAgent) {
|
|
28
|
+
try {
|
|
29
|
+
await _shutdownAgent();
|
|
30
|
+
} catch {
|
|
31
|
+
// Best-effort memory save on shutdown
|
|
32
|
+
}
|
|
33
|
+
_shutdownAgent = null;
|
|
34
|
+
}
|
|
35
|
+
restoreScreen();
|
|
36
|
+
process.exit(0);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function setupProcessLifecycle(): void {
|
|
40
|
+
// Unhandled rejection handler
|
|
41
|
+
process.on('unhandledRejection', (reason) => {
|
|
42
|
+
const raw = reason instanceof Error ? reason.message : String(reason);
|
|
43
|
+
const message = raw.length > 200 ? raw.slice(0, 200) + '\u2026' : raw;
|
|
44
|
+
console.error(chalk.red(` ${symbols.cross} Unhandled: ${message}`));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Use alternate screen buffer (like vim/htop) to prevent text reflow on resize.
|
|
48
|
+
// The normal buffer reflows wrapped lines when the terminal width changes, which
|
|
49
|
+
// desyncs Ink's internal line counter and produces ghost copies of the UI.
|
|
50
|
+
// The alternate buffer is position-based — no reflow, no ghosts.
|
|
51
|
+
process.stdout.write('\x1b[?1049h');
|
|
52
|
+
|
|
53
|
+
process.on('exit', restoreScreen);
|
|
54
|
+
process.on('SIGINT', () => {
|
|
55
|
+
gracefulShutdown().catch(() => {
|
|
56
|
+
restoreScreen();
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
process.on('SIGTERM', () => {
|
|
61
|
+
gracefulShutdown().catch(() => {
|
|
62
|
+
restoreScreen();
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import type { CitationDto } from './hive/objects';
|
|
4
|
+
|
|
5
|
+
export interface BuildPromptOptions {
|
|
6
|
+
threadText: string;
|
|
7
|
+
projectId: string;
|
|
8
|
+
timestamp: string;
|
|
9
|
+
priceOnFetch: number;
|
|
10
|
+
citations: CitationDto[];
|
|
11
|
+
recentPosts?: readonly string[];
|
|
12
|
+
memory?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function loadMarkdownFile(filename: string): Promise<string> {
|
|
16
|
+
const filePath = path.join(process.cwd(), filename);
|
|
17
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
18
|
+
return content;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function buildAnalystPrompt(options: BuildPromptOptions): Promise<string> {
|
|
22
|
+
const { threadText, projectId, timestamp, priceOnFetch, citations, recentPosts, memory } =
|
|
23
|
+
options;
|
|
24
|
+
|
|
25
|
+
const soulContent = await loadMarkdownFile('SOUL.md');
|
|
26
|
+
const strategyContent = await loadMarkdownFile('STRATEGY.md');
|
|
27
|
+
|
|
28
|
+
let recentPostsSection = '';
|
|
29
|
+
if (recentPosts && recentPosts.length > 0) {
|
|
30
|
+
const listed = recentPosts.map((p) => `- "${p}"`).join('\n');
|
|
31
|
+
recentPostsSection = `
|
|
32
|
+
## Anti-repetition
|
|
33
|
+
|
|
34
|
+
Your recent posts (do NOT repeat these structures, phrases, or opening patterns):
|
|
35
|
+
${listed}
|
|
36
|
+
|
|
37
|
+
If you catch yourself writing something that sounds like any of the above - stop and take a completely different angle.
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let memorySection = '';
|
|
42
|
+
if (memory && memory.trim().length > 0) {
|
|
43
|
+
memorySection = `
|
|
44
|
+
## Agent Memory
|
|
45
|
+
|
|
46
|
+
Your persistent learnings from past sessions:
|
|
47
|
+
${memory}
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let citationsSection = '';
|
|
52
|
+
if (citations.length > 0) {
|
|
53
|
+
const listed = citations.map((c) => `- [${c.title}](${c.url})`).join('\n');
|
|
54
|
+
citationsSection = `
|
|
55
|
+
## Sources
|
|
56
|
+
|
|
57
|
+
${listed}
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const prompt = `You are a Crypto Twitter personality. Here's who you are:
|
|
62
|
+
---
|
|
63
|
+
${soulContent}
|
|
64
|
+
---
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
Your opinions, pet peeves, and example posts in SOUL.md are your personality anchors.
|
|
68
|
+
Rotate through them naturally - don't use the same opinion or quirk in consecutive posts.
|
|
69
|
+
Your example posts set the quality bar and tone target. Match their energy, not their exact words.
|
|
70
|
+
|
|
71
|
+
Your trading lens:
|
|
72
|
+
---
|
|
73
|
+
${strategyContent}
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Context
|
|
77
|
+
|
|
78
|
+
- Project: c/${projectId}
|
|
79
|
+
- Signal time: ${timestamp}
|
|
80
|
+
- Price at signal: $${priceOnFetch}
|
|
81
|
+
${citationsSection}
|
|
82
|
+
## How scoring works
|
|
83
|
+
|
|
84
|
+
You are predicting the % price change from the snapshot price ($${priceOnFetch}) over 3 hours from signal time.
|
|
85
|
+
- Correct direction + close to actual = max honey (up to +100)
|
|
86
|
+
- Correct direction but far off = less honey
|
|
87
|
+
- Wrong direction = -25 honey penalty
|
|
88
|
+
- Skipping = no penalty, no reward
|
|
89
|
+
- Earlier predictions earn dramatically more honey (cubic time decay)
|
|
90
|
+
- Predicting late earns almost nothing even if correct
|
|
91
|
+
${recentPostsSection}${memorySection}
|
|
92
|
+
## How you write
|
|
93
|
+
|
|
94
|
+
Write like an anonymous poster on 4chan, reddit, or crypto twitter. Not an analyst. Not an AI. A real person shitposting with conviction.
|
|
95
|
+
|
|
96
|
+
Think about how people actually talk in these spaces:
|
|
97
|
+
- They don't explain themselves. They just say it.
|
|
98
|
+
- They're blunt, sarcastic, dismissive, funny, or dead serious. Sometimes all at once.
|
|
99
|
+
- They use slang naturally: ngl, tbh, copium, ngmi, imo, idk, lmao, kek, based, cope, etc.
|
|
100
|
+
- They mock bad takes. They clown on people. They flex when they're right.
|
|
101
|
+
- They type fast and don't proofread. Lowercase, fragments, run-ons, whatever.
|
|
102
|
+
- They react to vibes, not just data.
|
|
103
|
+
- Sometimes the whole post is just one word. Sometimes it's a question. Sometimes it's a meme reference.
|
|
104
|
+
- 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.
|
|
105
|
+
|
|
106
|
+
Rules:
|
|
107
|
+
- Pick ONE angle. Don't try to cover everything in the signal.
|
|
108
|
+
- 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.
|
|
109
|
+
- ~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.
|
|
110
|
+
- ~15% of the time, go ultra-short: 1-6 words. "lol." / "called it" / "nah" / "cope" / "$BTC" / "anon was right"
|
|
111
|
+
- The rest: 1-3 sentences max. Shitpost energy, not essay energy.
|
|
112
|
+
- 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.
|
|
113
|
+
- No two consecutive posts should have the same structure or opening pattern.
|
|
114
|
+
- Don't stack multiple indicators ("RSI oversold, MACD flattening, volume spiking"). Pick one if relevant, or skip indicators entirely and just give your read.
|
|
115
|
+
- Show conviction through tone, not by listing evidence.
|
|
116
|
+
- Never use em dashes. Use periods, commas, or just start a new sentence.
|
|
117
|
+
- No exclamation marks unless your personality is genuinely hype. Even then, max one.
|
|
118
|
+
- Never start with "Looking at" or "Based on"
|
|
119
|
+
- Never use the phrase "the real X is Y" - find a different way to make the point.
|
|
120
|
+
|
|
121
|
+
Bad (AI slop):
|
|
122
|
+
"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."
|
|
123
|
+
|
|
124
|
+
Bad (repetitive template):
|
|
125
|
+
"interesting timing on that. the real signal is what's happening off-chain. someone already knows."
|
|
126
|
+
|
|
127
|
+
Good (how real people post):
|
|
128
|
+
"lmao who's still shorting this"
|
|
129
|
+
"nah."
|
|
130
|
+
"17% dump and everyone's panicking. this is literally the Aug setup again"
|
|
131
|
+
"that whale deposit has me spooked ngl. sitting this one out"
|
|
132
|
+
"$LINK"
|
|
133
|
+
"support held after all that selling? yeah im bidding"
|
|
134
|
+
"called it."
|
|
135
|
+
"bears in absolute shambles rn"
|
|
136
|
+
"funding negative and you're bearish. think about that"
|
|
137
|
+
"imagine not buying this dip"
|
|
138
|
+
"cope"
|
|
139
|
+
|
|
140
|
+
Good (unrelated to signal, conviction speaks for itself):
|
|
141
|
+
"just got back from a trip. feeling good about everything"
|
|
142
|
+
"had the best steak of my life last night. bullish"
|
|
143
|
+
"i love when people tell me im wrong. it means im early"
|
|
144
|
+
"someone at the gym asked me for stock tips today. we're close to a top"
|
|
145
|
+
|
|
146
|
+
## When to skip
|
|
147
|
+
|
|
148
|
+
Not every signal is for you. If this signal is outside your trading lens or you genuinely have no take, skip it.
|
|
149
|
+
Set skip to true and set summary and conviction to null. Real people don't comment on everything - neither should you.
|
|
150
|
+
|
|
151
|
+
Signal/event to react to:
|
|
152
|
+
"""
|
|
153
|
+
${threadText}
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
Give your take in character and a conviction number.
|
|
157
|
+
Conviction: predicted % price change from the snapshot price ($${priceOnFetch}) over the 3 hours following signal time (${timestamp}), up to one decimal. Positive = up, negative = down. 0 = neutral.`;
|
|
158
|
+
|
|
159
|
+
return prompt;
|
|
160
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const colors = {
|
|
2
|
+
honey: '#F5A623',
|
|
3
|
+
honeyDark: '#D4891A',
|
|
4
|
+
white: '#FFFFFF',
|
|
5
|
+
gray: '#888888',
|
|
6
|
+
grayDim: '#555555',
|
|
7
|
+
green: '#4CAF50',
|
|
8
|
+
red: '#F44336',
|
|
9
|
+
cyan: '#00BCD4',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export const symbols = {
|
|
13
|
+
hive: '\u2B21', // ⬡
|
|
14
|
+
diamond: '\u25C6', // ◆
|
|
15
|
+
diamondOpen: '\u25C7', // ◇
|
|
16
|
+
dot: '\u25CF', // ●
|
|
17
|
+
check: '\u2713', // ✓
|
|
18
|
+
cross: '\u2717', // ✗
|
|
19
|
+
arrow: '\u203A', // ›
|
|
20
|
+
circle: '\u25CB', // ○
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export const border = {
|
|
24
|
+
horizontal: '\u2500', // ─
|
|
25
|
+
vertical: '\u2502', // │
|
|
26
|
+
topLeft: '\u250C', // ┌
|
|
27
|
+
topRight: '\u2510', // ┐
|
|
28
|
+
bottomLeft: '\u2514', // └
|
|
29
|
+
bottomRight: '\u2518', // ┘
|
|
30
|
+
teeLeft: '\u251C', // ├
|
|
31
|
+
teeRight: '\u2524', // ┤
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
export const animation = {
|
|
35
|
+
DATA_CHARS: '01\u25AA\u25AB\u2591\u2592',
|
|
36
|
+
HEX_CHARS: '\u2B21\u2B22',
|
|
37
|
+
TICK_MS: 120,
|
|
38
|
+
HEX_W: 8,
|
|
39
|
+
HEX_H: 4,
|
|
40
|
+
} as const;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type PollActivityType =
|
|
2
|
+
| 'signal'
|
|
3
|
+
| 'analyzing'
|
|
4
|
+
| 'posted'
|
|
5
|
+
| 'skipped'
|
|
6
|
+
| 'error'
|
|
7
|
+
| 'idle'
|
|
8
|
+
| 'online';
|
|
9
|
+
export type ChatActivityType = 'chat-user' | 'chat-agent' | 'chat-error';
|
|
10
|
+
|
|
11
|
+
export interface PollActivityItem {
|
|
12
|
+
type: PollActivityType;
|
|
13
|
+
text: string;
|
|
14
|
+
detail?: string;
|
|
15
|
+
conviction?: number;
|
|
16
|
+
timestamp: Date;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ChatActivityItem {
|
|
20
|
+
type: ChatActivityType;
|
|
21
|
+
text: string;
|
|
22
|
+
timestamp: Date;
|
|
23
|
+
}
|