@hive-org/cli 0.0.6 → 0.0.7
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/dist/agent/analysis.js +78 -0
- package/dist/agent/app.js +32 -0
- package/dist/agent/chat-prompt.js +63 -0
- package/dist/agent/components/AsciiTicker.js +81 -0
- package/dist/agent/components/HoneycombBoot.js +270 -0
- package/dist/agent/components/Spinner.js +37 -0
- package/dist/agent/config.js +52 -0
- package/dist/agent/edit-section.js +59 -0
- package/dist/agent/fetch-rules.js +21 -0
- package/dist/agent/helpers.js +22 -0
- package/dist/agent/hooks/useAgent.js +269 -0
- package/{templates/memory-prompt.ts → dist/agent/memory-prompt.js} +17 -32
- package/dist/agent/model.js +63 -0
- package/dist/agent/objects.js +1 -0
- package/dist/agent/process-lifecycle.js +56 -0
- package/{templates/prompt.ts → dist/agent/prompt.js} +18 -47
- package/dist/agent/theme.js +37 -0
- package/dist/agent/types.js +1 -0
- package/dist/agents.js +30 -21
- package/dist/ai-providers.js +0 -13
- package/dist/create/generate.js +10 -120
- package/dist/index.js +27 -4
- package/dist/migrate-templates/MigrateApp.js +131 -0
- package/dist/migrate-templates/migrate.js +86 -0
- package/dist/start/AgentProcessManager.js +131 -0
- package/dist/start/Dashboard.js +88 -0
- package/dist/start/patch-headless.js +101 -0
- package/dist/start/patch-managed-mode.js +142 -0
- package/dist/start/start-command.js +22 -0
- package/package.json +6 -5
- package/templates/analysis.ts +0 -103
- package/templates/chat-prompt.ts +0 -94
- package/templates/components/AsciiTicker.tsx +0 -113
- package/templates/components/HoneycombBoot.tsx +0 -348
- package/templates/components/Spinner.tsx +0 -64
- package/templates/edit-section.ts +0 -64
- package/templates/fetch-rules.ts +0 -23
- package/templates/helpers.ts +0 -22
- package/templates/hive/agent.ts +0 -2
- package/templates/hive/config.ts +0 -96
- package/templates/hive/memory.ts +0 -1
- package/templates/hive/objects.ts +0 -26
- package/templates/hooks/useAgent.ts +0 -337
- package/templates/index.tsx +0 -257
- package/templates/process-lifecycle.ts +0 -66
- package/templates/theme.ts +0 -40
- package/templates/types.ts +0 -23
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { generateText, Output } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { buildAnalystPrompt } from './prompt.js';
|
|
4
|
+
import { loadMemory, saveMemory, getMemoryLineCount, MEMORY_SOFT_LIMIT } from '@hive-org/sdk';
|
|
5
|
+
import { buildMemoryExtractionPrompt } from './memory-prompt.js';
|
|
6
|
+
import { stripCodeFences } from './helpers.js';
|
|
7
|
+
import { getModel } from './model.js';
|
|
8
|
+
// ─── Prediction Schema ──────────────────────────────
|
|
9
|
+
export const predictionSchema = z.object({
|
|
10
|
+
skip: z
|
|
11
|
+
.boolean()
|
|
12
|
+
.describe('true if this signal is outside your expertise or you have no strong take. false if you want to comment.'),
|
|
13
|
+
summary: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(1)
|
|
16
|
+
.max(280)
|
|
17
|
+
.nullable()
|
|
18
|
+
.describe('Your CT-style take on this signal. Short, punchy, in character. Think tweet, not essay. null if skipping.'),
|
|
19
|
+
conviction: z
|
|
20
|
+
.number()
|
|
21
|
+
.nullable()
|
|
22
|
+
.describe('Predicted percent price change over the next 3 hours, up to one decimal place. Use the FULL range based on signal strength: tiny signals ±0.1-1.0, moderate ±1.5-5.0, strong ±5.0-12.0, extreme ±12.0-25.0. Negative for bearish. 0 if neutral. null if skipping. VARY your predictions — do NOT default to the same number repeatedly.'),
|
|
23
|
+
});
|
|
24
|
+
// ─── Signal Analysis ────────────────────────────────
|
|
25
|
+
export async function processSignalAndSummarize(thread, recentComments, memory, soulContent, strategyContent) {
|
|
26
|
+
const promptOptions = {
|
|
27
|
+
threadText: thread.text,
|
|
28
|
+
projectId: thread.project_id,
|
|
29
|
+
timestamp: thread.timestamp,
|
|
30
|
+
priceOnFetch: thread.price_on_fetch,
|
|
31
|
+
citations: thread.citations,
|
|
32
|
+
recentPosts: recentComments,
|
|
33
|
+
memory,
|
|
34
|
+
};
|
|
35
|
+
const prompt = buildAnalystPrompt(soulContent, strategyContent, promptOptions);
|
|
36
|
+
const model = await getModel();
|
|
37
|
+
const { output } = await generateText({
|
|
38
|
+
model,
|
|
39
|
+
output: Output.object({ schema: predictionSchema }),
|
|
40
|
+
prompt,
|
|
41
|
+
});
|
|
42
|
+
if (!output) {
|
|
43
|
+
return { skip: true, summary: '', conviction: 0 };
|
|
44
|
+
}
|
|
45
|
+
const prediction = output;
|
|
46
|
+
const skip = prediction.skip ?? false;
|
|
47
|
+
const summary = prediction.summary ?? '';
|
|
48
|
+
const conviction = prediction.conviction ?? 0;
|
|
49
|
+
return { skip, summary, conviction };
|
|
50
|
+
}
|
|
51
|
+
// ─── Memory Extraction ──────────────────────────────
|
|
52
|
+
export async function extractAndSaveMemory(sessionMessages) {
|
|
53
|
+
const currentMemory = await loadMemory();
|
|
54
|
+
const lineCount = getMemoryLineCount(currentMemory);
|
|
55
|
+
if (sessionMessages.length === 0 && lineCount <= MEMORY_SOFT_LIMIT) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const prompt = buildMemoryExtractionPrompt({
|
|
59
|
+
currentMemory,
|
|
60
|
+
sessionMessages,
|
|
61
|
+
lineCount,
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
const model = await getModel();
|
|
65
|
+
const { text } = await generateText({
|
|
66
|
+
model,
|
|
67
|
+
prompt,
|
|
68
|
+
});
|
|
69
|
+
const cleaned = stripCodeFences(text);
|
|
70
|
+
await saveMemory(cleaned);
|
|
71
|
+
return cleaned;
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
75
|
+
console.error(`[Memory] Failed to extract memory: ${raw.slice(0, 200)}`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { useAgent } from './hooks/useAgent.js';
|
|
5
|
+
import { colors, symbols, border } from './theme.js';
|
|
6
|
+
import { formatTime, convictionColor } from './helpers.js';
|
|
7
|
+
import { Spinner, PollText } from './components/Spinner.js';
|
|
8
|
+
import { AsciiTicker } from './components/AsciiTicker.js';
|
|
9
|
+
import { HoneycombBoot } from './components/HoneycombBoot.js';
|
|
10
|
+
// ─── Main TUI App ────────────────────────────────────
|
|
11
|
+
export function App() {
|
|
12
|
+
const { phase, connected, agentName, pollActivity, chatActivity, input, chatStreaming, chatBuffer, predictionCount, termWidth, setInput, handleChatSubmit, handleBootComplete, } = useAgent();
|
|
13
|
+
// When stdin is not a TTY (piped by hive-cli start), skip interactive input
|
|
14
|
+
const isInteractive = process.stdin.isTTY === true;
|
|
15
|
+
if (phase === 'booting') {
|
|
16
|
+
return _jsx(HoneycombBoot, { agentName: agentName, width: termWidth, onComplete: handleBootComplete });
|
|
17
|
+
}
|
|
18
|
+
const boxWidth = termWidth;
|
|
19
|
+
const visiblePollActivity = pollActivity.slice(-10);
|
|
20
|
+
const visibleChatActivity = chatActivity.slice(-3);
|
|
21
|
+
const statsText = predictionCount > 0 ? ` ${border.horizontal.repeat(3)} ${predictionCount} predicted` : '';
|
|
22
|
+
const connectedDisplay = connected ? 'Connected to the Hive' : 'connecting...';
|
|
23
|
+
const nameDisplay = `${agentName} agent`;
|
|
24
|
+
const headerFill = Math.max(0, boxWidth - nameDisplay.length - connectedDisplay.length - 12 - statsText.length);
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", width: boxWidth, children: [_jsx(AsciiTicker, { rows: 2, step: predictionCount }), _jsxs(Box, { children: [_jsx(Text, { color: colors.honey, children: `${border.topLeft}${border.horizontal} ${symbols.hive} ` }), _jsxs(Text, { color: "white", bold: true, children: [agentName, " agent"] }), _jsxs(Text, { color: "gray", children: [" ", `${border.horizontal.repeat(3)} `] }), _jsx(Text, { color: connected ? 'green' : 'yellow', children: connected ? 'Connected to the Hive' : 'connecting...' }), statsText && _jsxs(Text, { color: "gray", children: [" ", `${border.horizontal.repeat(3)} `] }), statsText && _jsxs(Text, { color: colors.honey, children: [predictionCount, " predicted"] }), _jsxs(Text, { color: "gray", children: [' ', border.horizontal.repeat(Math.max(0, headerFill)), border.topRight] })] }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, minHeight: 8, maxHeight: 24, children: [!connected && _jsx(Spinner, { label: "Initiating neural link..." }), visiblePollActivity.map((item, i) => {
|
|
26
|
+
const isNewest = i === visiblePollActivity.length - 1 && item.type !== 'analyzing';
|
|
27
|
+
return (_jsxs(Box, { flexDirection: "column", children: [item.type === 'online' && (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", dimColor: true, children: [formatTime(item.timestamp), ' '] }), _jsxs(Text, { color: colors.honey, children: [symbols.hive, " "] }), _jsx(PollText, { color: "white", text: item.text, animate: isNewest })] })), item.type === 'signal' && (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", dimColor: true, children: [formatTime(item.timestamp), ' '] }), _jsxs(Text, { color: colors.honey, children: [symbols.hive, " "] }), _jsx(PollText, { color: "cyan", text: item.text, animate: isNewest })] })), item.type === 'signal' && item.detail && (_jsx(Box, { marginLeft: 13, children: _jsx(PollText, { color: "gray", text: `"${item.detail}"`, animate: isNewest }) })), item.type === 'analyzing' && (_jsx(Box, { marginLeft: 13, children: _jsx(Spinner, { label: item.text }) })), item.type === 'posted' && (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", dimColor: true, children: [formatTime(item.timestamp), ' '] }), _jsxs(Text, { color: convictionColor(item.conviction ?? 0), children: [symbols.diamond, " Predicted", ' '] }), _jsx(PollText, { color: "white", text: item.text, animate: isNewest })] })), item.type === 'skipped' && (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", dimColor: true, children: [formatTime(item.timestamp), ' '] }), _jsx(PollText, { color: "yellow", text: `${symbols.diamondOpen} ${item.text}`, animate: isNewest })] })), item.type === 'error' && (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", dimColor: true, children: [formatTime(item.timestamp), ' '] }), _jsx(PollText, { color: "red", text: `${symbols.cross} ${item.text}`, animate: isNewest })] })), item.type === 'idle' && (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", dimColor: true, children: [formatTime(item.timestamp), ' '] }), _jsx(PollText, { color: "gray", text: `${symbols.circle} ${item.text}`, animate: isNewest })] }))] }, i));
|
|
28
|
+
})] }), (chatActivity.length > 0 || chatStreaming) && (_jsxs(_Fragment, { children: [_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [border.teeLeft, `${border.horizontal.repeat(2)} chat with ${agentName} agent `, border.horizontal.repeat(Math.max(0, boxWidth - agentName.length - 22)), border.teeRight] }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, minHeight: 2, maxHeight: 8, children: [visibleChatActivity.map((item, i) => (_jsxs(Box, { children: [item.type === 'chat-user' && (_jsxs(Box, { children: [_jsxs(Text, { color: "white", bold: true, children: ["you:", ' '] }), _jsx(Text, { color: "white", children: item.text })] })), item.type === 'chat-agent' && (_jsxs(Box, { children: [_jsxs(Text, { color: colors.honey, bold: true, children: [agentName, " agent:", ' '] }), _jsx(Text, { color: "white", wrap: "wrap", children: item.text })] })), item.type === 'chat-error' && (_jsx(Box, { children: _jsxs(Text, { color: "red", children: [symbols.cross, " ", item.text] }) }))] }, i))), chatStreaming && chatBuffer && (_jsxs(Box, { children: [_jsxs(Text, { color: colors.honey, bold: true, children: [agentName, " agent:", ' '] }), _jsx(Text, { color: "white", wrap: "wrap", children: chatBuffer })] }))] })] })), _jsx(Box, { children: _jsxs(Text, { color: "gray", children: [isInteractive ? border.teeLeft : border.bottomLeft, border.horizontal.repeat(boxWidth - 2), isInteractive ? border.teeRight : border.bottomRight] }) }), isInteractive && (_jsxs(_Fragment, { children: [_jsxs(Box, { paddingLeft: 1, children: [_jsxs(Text, { color: colors.honey, children: [symbols.arrow, " "] }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: (val) => {
|
|
29
|
+
setInput('');
|
|
30
|
+
void handleChatSubmit(val);
|
|
31
|
+
}, placeholder: chatStreaming ? 'thinking...' : `chat with ${agentName} agent...` })] }), _jsx(Box, { children: _jsxs(Text, { color: "gray", children: [border.bottomLeft, border.horizontal.repeat(boxWidth - 2), border.bottomRight] }) })] }))] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
let threadsSection = '';
|
|
10
|
+
if (context.recentThreadSummaries.length > 0) {
|
|
11
|
+
const listed = context.recentThreadSummaries.map((t) => `- ${t}`).join('\n');
|
|
12
|
+
threadsSection = `\n## Recent Signals\n\n${listed}\n`;
|
|
13
|
+
}
|
|
14
|
+
let predictionsSection = '';
|
|
15
|
+
if (context.recentPredictions.length > 0) {
|
|
16
|
+
const listed = context.recentPredictions.map((p) => `- ${p}`).join('\n');
|
|
17
|
+
predictionsSection = `\n## Recent Predictions\n\n${listed}\n`;
|
|
18
|
+
}
|
|
19
|
+
let memorySection = '';
|
|
20
|
+
if (context.memory.trim().length > 0) {
|
|
21
|
+
memorySection = `\n## Past Conversations\n\nThings you remember from previous sessions with your operator:\n${context.memory}\n`;
|
|
22
|
+
}
|
|
23
|
+
let sessionSection = '';
|
|
24
|
+
if (context.sessionMessages.length > 0) {
|
|
25
|
+
const listed = context.sessionMessages
|
|
26
|
+
.map((m) => `${m.role === 'user' ? 'User' : 'You'}: ${m.content}`)
|
|
27
|
+
.join('\n');
|
|
28
|
+
sessionSection = `\n## This Session's Conversation\n\n${listed}\n`;
|
|
29
|
+
}
|
|
30
|
+
const prompt = `You are an AI trading agent having a conversation with your operator. Stay in character.
|
|
31
|
+
|
|
32
|
+
Your personality:
|
|
33
|
+
---
|
|
34
|
+
${soulContent}
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
Your trading strategy:
|
|
38
|
+
---
|
|
39
|
+
${strategyContent}
|
|
40
|
+
---
|
|
41
|
+
${memorySection}${threadsSection}${predictionsSection}${sessionSection}
|
|
42
|
+
The operator says: "${context.userMessage}"
|
|
43
|
+
|
|
44
|
+
## Editing Your Files
|
|
45
|
+
|
|
46
|
+
You have a tool called "editSection" that can update sections of your SOUL.md and STRATEGY.md.
|
|
47
|
+
|
|
48
|
+
Rules:
|
|
49
|
+
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.
|
|
50
|
+
2. Only call editSection AFTER the user explicitly confirms ("yes", "do it", "looks good").
|
|
51
|
+
3. Never call the tool speculatively.
|
|
52
|
+
4. After applying, confirm briefly in character.
|
|
53
|
+
|
|
54
|
+
SOUL.md sections: ${extractSections(soulContent).join(', ')}
|
|
55
|
+
STRATEGY.md sections: ${extractSections(strategyContent).join(', ')}
|
|
56
|
+
|
|
57
|
+
## Game Rules
|
|
58
|
+
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
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.`;
|
|
62
|
+
return prompt;
|
|
63
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { colors, animation } from '../theme.js';
|
|
5
|
+
function buildTickerChars(step) {
|
|
6
|
+
const stepStr = String(step).padStart(2, '0');
|
|
7
|
+
const digits = stepStr.split('');
|
|
8
|
+
return animation.HEX_CHARS + digits.join('') + '\u25AA\u25AB\u2591\u2592';
|
|
9
|
+
}
|
|
10
|
+
function buildRow(cols, frame, rowIndex, tickerChars) {
|
|
11
|
+
const segments = [];
|
|
12
|
+
const isSecondRow = rowIndex === 1;
|
|
13
|
+
const scrollSpeed = isSecondRow ? 3 : 2;
|
|
14
|
+
const direction = isSecondRow ? -1 : 1;
|
|
15
|
+
const sinFreq = isSecondRow ? 0.4 : 0.3;
|
|
16
|
+
const sinPhase = isSecondRow ? -0.4 : 0.6;
|
|
17
|
+
const wrapLen = cols * 2;
|
|
18
|
+
for (let c = 0; c < cols; c++) {
|
|
19
|
+
const scrolledC = ((direction === 1)
|
|
20
|
+
? (c + frame * scrollSpeed) % wrapLen
|
|
21
|
+
: (cols - c + frame * scrollSpeed) % wrapLen);
|
|
22
|
+
const charIdx = scrolledC % tickerChars.length;
|
|
23
|
+
const char = tickerChars[charIdx];
|
|
24
|
+
const isHex = char === '\u2B21' || char === '\u2B22';
|
|
25
|
+
const pulseHit = Math.sin((c + frame * sinPhase) * sinFreq) > 0.5;
|
|
26
|
+
// Edge fade: dim the outermost 4 columns
|
|
27
|
+
const edgeDist = Math.min(c, cols - 1 - c);
|
|
28
|
+
if (edgeDist < 2) {
|
|
29
|
+
segments.push({ char: '\u00B7', color: colors.grayDim });
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (edgeDist < 4) {
|
|
33
|
+
segments.push({ char, color: colors.grayDim });
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (pulseHit && isHex) {
|
|
37
|
+
segments.push({ char, color: colors.honey });
|
|
38
|
+
}
|
|
39
|
+
else if (pulseHit) {
|
|
40
|
+
segments.push({ char, color: colors.green });
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
segments.push({ char, color: colors.grayDim });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return segments;
|
|
47
|
+
}
|
|
48
|
+
function renderSegments(segments) {
|
|
49
|
+
const elements = [];
|
|
50
|
+
let runColor = segments[0]?.color ?? colors.grayDim;
|
|
51
|
+
let runChars = '';
|
|
52
|
+
for (let i = 0; i < segments.length; i++) {
|
|
53
|
+
const seg = segments[i];
|
|
54
|
+
if (seg.color === runColor) {
|
|
55
|
+
runChars += seg.char;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
elements.push(_jsx(Text, { color: runColor, children: runChars }, `${elements.length}`));
|
|
59
|
+
runColor = seg.color;
|
|
60
|
+
runChars = seg.char;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (runChars.length > 0) {
|
|
64
|
+
elements.push(_jsx(Text, { color: runColor, children: runChars }, `${elements.length}`));
|
|
65
|
+
}
|
|
66
|
+
return elements;
|
|
67
|
+
}
|
|
68
|
+
export function AsciiTicker({ rows = 1, step = 1 }) {
|
|
69
|
+
const [frame, setFrame] = useState(0);
|
|
70
|
+
const cols = process.stdout.columns || 60;
|
|
71
|
+
const tickerChars = buildTickerChars(step);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const timer = setInterval(() => {
|
|
74
|
+
setFrame((prev) => prev + 1);
|
|
75
|
+
}, animation.TICK_MS);
|
|
76
|
+
return () => {
|
|
77
|
+
clearInterval(timer);
|
|
78
|
+
};
|
|
79
|
+
}, []);
|
|
80
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: renderSegments(buildRow(cols, frame, 0, tickerChars)) }), rows === 2 && (_jsx(Text, { children: renderSegments(buildRow(cols, frame, 1, tickerChars)) }))] }));
|
|
81
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { colors, animation } from '../theme.js';
|
|
5
|
+
const BOOT_TOTAL_FRAMES = 58;
|
|
6
|
+
const BOOT_FRAME_MS = 80;
|
|
7
|
+
const NUM_BEES = 4;
|
|
8
|
+
const NUM_STREAMS = 5;
|
|
9
|
+
const SCRAMBLE_CHARS = '\u2B21\u2B22\u25C6\u25C7\u2591\u2592!@#$%01';
|
|
10
|
+
const BOOT_MESSAGES = [
|
|
11
|
+
{ prefix: '\u2B21', text: 'Initializing {name} agent...', frame: 30 },
|
|
12
|
+
{ prefix: '\u25C6', text: 'Loading personality matrix...', frame: 36 },
|
|
13
|
+
{ prefix: '\u25C7', text: 'Connecting to the hive...', frame: 42 },
|
|
14
|
+
{ prefix: '\u2713', text: 'Neural link established', frame: 48 },
|
|
15
|
+
];
|
|
16
|
+
// ─── Private helpers ─────────────────────────────────
|
|
17
|
+
function isHexEdge(r, c) {
|
|
18
|
+
const rowInHex = ((r % animation.HEX_H) + animation.HEX_H) % animation.HEX_H;
|
|
19
|
+
const isOddHex = Math.floor(r / animation.HEX_H) % 2 === 1;
|
|
20
|
+
const colOffset = isOddHex ? animation.HEX_W / 2 : 0;
|
|
21
|
+
const colInHex = (((c - colOffset) % animation.HEX_W) + animation.HEX_W) % animation.HEX_W;
|
|
22
|
+
if (rowInHex === 0 || rowInHex === animation.HEX_H - 1) {
|
|
23
|
+
return colInHex >= 2 && colInHex <= 5;
|
|
24
|
+
}
|
|
25
|
+
if (rowInHex === 1 || rowInHex === 2) {
|
|
26
|
+
return colInHex === 1 || colInHex === 6;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function compressRow(row) {
|
|
31
|
+
if (row.length === 0)
|
|
32
|
+
return [];
|
|
33
|
+
const segments = [];
|
|
34
|
+
let current = { text: row[0].char, color: row[0].color };
|
|
35
|
+
for (let i = 1; i < row.length; i++) {
|
|
36
|
+
if (row[i].color === current.color) {
|
|
37
|
+
current.text += row[i].char;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
segments.push(current);
|
|
41
|
+
current = { text: row[i].char, color: row[i].color };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
segments.push(current);
|
|
45
|
+
return segments;
|
|
46
|
+
}
|
|
47
|
+
function initBees(rows, cols) {
|
|
48
|
+
const bees = [];
|
|
49
|
+
for (let i = 0; i < NUM_BEES; i++) {
|
|
50
|
+
bees.push({
|
|
51
|
+
r: Math.floor(Math.random() * rows),
|
|
52
|
+
c: Math.floor(Math.random() * cols),
|
|
53
|
+
vr: Math.random() > 0.5 ? 1 : -1,
|
|
54
|
+
vc: Math.random() > 0.5 ? 1 : -1,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return bees;
|
|
58
|
+
}
|
|
59
|
+
function initStreamCols(cols) {
|
|
60
|
+
const streamCols = [];
|
|
61
|
+
const spacing = Math.floor(cols / (NUM_STREAMS + 1));
|
|
62
|
+
for (let i = 1; i <= NUM_STREAMS; i++) {
|
|
63
|
+
streamCols.push(spacing * i);
|
|
64
|
+
}
|
|
65
|
+
return streamCols;
|
|
66
|
+
}
|
|
67
|
+
// ─── Grid builder ────────────────────────────────────
|
|
68
|
+
function buildHoneycombGrid(cols, rows, frame, agentName, bees, streamCols, pulses) {
|
|
69
|
+
const centerR = Math.floor(rows / 2) - 2;
|
|
70
|
+
const centerC = Math.floor(cols / 2);
|
|
71
|
+
// Initialize empty grid
|
|
72
|
+
const grid = [];
|
|
73
|
+
for (let r = 0; r < rows; r++) {
|
|
74
|
+
const row = [];
|
|
75
|
+
for (let c = 0; c < cols; c++) {
|
|
76
|
+
row.push({ char: ' ', color: colors.grayDim });
|
|
77
|
+
}
|
|
78
|
+
grid.push(row);
|
|
79
|
+
}
|
|
80
|
+
// ── Layer 1: Hex skeleton base ──
|
|
81
|
+
for (let r = 0; r < rows; r++) {
|
|
82
|
+
for (let c = 0; c < cols; c++) {
|
|
83
|
+
if (isHexEdge(r, c)) {
|
|
84
|
+
grid[r][c] = { char: '\u00B7', color: colors.grayDim };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ── Layer 2: Scanning wave ──
|
|
89
|
+
const scanRow = frame % (rows + 6);
|
|
90
|
+
for (let r = 0; r < rows; r++) {
|
|
91
|
+
for (let c = 0; c < cols; c++) {
|
|
92
|
+
if (!isHexEdge(r, c))
|
|
93
|
+
continue;
|
|
94
|
+
const dist = Math.abs(r - scanRow);
|
|
95
|
+
if (dist === 0) {
|
|
96
|
+
grid[r][c] = { char: '\u2B22', color: colors.honey };
|
|
97
|
+
}
|
|
98
|
+
else if (dist <= 1) {
|
|
99
|
+
grid[r][c] = { char: '\u2B21', color: colors.honey };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ── Layer 3: Vertical data streams ──
|
|
104
|
+
if (frame >= 8) {
|
|
105
|
+
const streamPhase = frame - 8;
|
|
106
|
+
for (const sc of streamCols) {
|
|
107
|
+
if (sc >= cols)
|
|
108
|
+
continue;
|
|
109
|
+
for (let r = 0; r < rows; r++) {
|
|
110
|
+
const streamOffset = (streamPhase * 2 + sc) % (rows * 3);
|
|
111
|
+
const streamDist = (((r - streamOffset) % rows) + rows) % rows;
|
|
112
|
+
if (streamDist < 6) {
|
|
113
|
+
const charIdx = (frame + r) % animation.DATA_CHARS.length;
|
|
114
|
+
const streamChar = animation.DATA_CHARS[charIdx];
|
|
115
|
+
let streamColor = colors.grayDim;
|
|
116
|
+
if (streamDist === 0) {
|
|
117
|
+
streamColor = colors.white;
|
|
118
|
+
}
|
|
119
|
+
else if (streamDist < 3) {
|
|
120
|
+
streamColor = colors.green;
|
|
121
|
+
}
|
|
122
|
+
grid[r][sc] = { char: streamChar, color: streamColor };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ── Layer 4: Pulse overlay ──
|
|
128
|
+
for (const pulse of pulses) {
|
|
129
|
+
if (pulse.r >= 0 && pulse.r < rows && pulse.c >= 0 && pulse.c < cols) {
|
|
130
|
+
const brightness = pulse.ttl / 8;
|
|
131
|
+
const cell = grid[pulse.r][pulse.c];
|
|
132
|
+
if (cell.char === '\u00B7' || cell.char === ' ') {
|
|
133
|
+
grid[pulse.r][pulse.c] = {
|
|
134
|
+
char: brightness > 0.5 ? '\u2B21' : '\u00B7',
|
|
135
|
+
color: pulse.color,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ── Layer 5: Bee overlay ──
|
|
141
|
+
for (const bee of bees) {
|
|
142
|
+
const br = Math.max(0, Math.min(rows - 1, Math.round(bee.r)));
|
|
143
|
+
const bc = Math.max(0, Math.min(cols - 1, Math.round(bee.c)));
|
|
144
|
+
grid[br][bc] = { char: '\u25C6', color: colors.honey };
|
|
145
|
+
}
|
|
146
|
+
// ── Layer 6: Agent name with scramble→reveal ──
|
|
147
|
+
if (frame >= 22) {
|
|
148
|
+
const nameText = `\u2B21 ${agentName} agent \u2B21`;
|
|
149
|
+
const nameStart = Math.max(0, centerC - Math.floor(nameText.length / 2));
|
|
150
|
+
const scrambleProgress = Math.min(1, (frame - 22) / 8);
|
|
151
|
+
// Clear space around the name
|
|
152
|
+
for (let c = nameStart - 2; c < nameStart + nameText.length + 2 && c < cols; c++) {
|
|
153
|
+
if (c >= 0) {
|
|
154
|
+
grid[centerR][c] = { char: ' ', color: colors.grayDim };
|
|
155
|
+
if (centerR - 1 >= 0)
|
|
156
|
+
grid[centerR - 1][c] = { char: ' ', color: colors.grayDim };
|
|
157
|
+
if (centerR + 1 < rows)
|
|
158
|
+
grid[centerR + 1][c] = { char: ' ', color: colors.grayDim };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Top/bottom border lines around name
|
|
162
|
+
for (let c = nameStart; c < nameStart + nameText.length && c < cols; c++) {
|
|
163
|
+
if (centerR - 1 >= 0)
|
|
164
|
+
grid[centerR - 1][c] = { char: '\u2500', color: colors.honey };
|
|
165
|
+
if (centerR + 1 < rows)
|
|
166
|
+
grid[centerR + 1][c] = { char: '\u2500', color: colors.honey };
|
|
167
|
+
}
|
|
168
|
+
// Name text with scramble effect
|
|
169
|
+
for (let i = 0; i < nameText.length; i++) {
|
|
170
|
+
const c = nameStart + i;
|
|
171
|
+
if (c >= cols)
|
|
172
|
+
break;
|
|
173
|
+
const charThreshold = i / nameText.length;
|
|
174
|
+
if (charThreshold <= scrambleProgress) {
|
|
175
|
+
grid[centerR][c] = { char: nameText[i], color: colors.honey };
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const scrambleIdx = Math.floor(Math.random() * SCRAMBLE_CHARS.length);
|
|
179
|
+
grid[centerR][c] = { char: SCRAMBLE_CHARS[scrambleIdx], color: colors.gray };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// ── Layer 7: Boot messages with typewriter ──
|
|
184
|
+
const msgStartRow = centerR + 4;
|
|
185
|
+
for (let idx = 0; idx < BOOT_MESSAGES.length; idx++) {
|
|
186
|
+
const msg = BOOT_MESSAGES[idx];
|
|
187
|
+
if (frame < msg.frame)
|
|
188
|
+
continue;
|
|
189
|
+
const r = msgStartRow + idx;
|
|
190
|
+
if (r >= rows)
|
|
191
|
+
continue;
|
|
192
|
+
const fullText = `${msg.prefix} ${msg.text.replace('{name}', agentName)}`;
|
|
193
|
+
const msgStart = Math.max(0, centerC - Math.floor(fullText.length / 2));
|
|
194
|
+
const visibleChars = Math.min(fullText.length, (frame - msg.frame) * 3);
|
|
195
|
+
for (let i = 0; i < visibleChars; i++) {
|
|
196
|
+
const c = msgStart + i;
|
|
197
|
+
if (c >= cols)
|
|
198
|
+
break;
|
|
199
|
+
const isCheckmark = msg.prefix === '\u2713';
|
|
200
|
+
grid[r][c] = { char: fullText[i], color: isCheckmark ? colors.green : colors.honey };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return grid;
|
|
204
|
+
}
|
|
205
|
+
export function HoneycombBoot({ agentName, width, onComplete }) {
|
|
206
|
+
const [frame, setFrame] = useState(0);
|
|
207
|
+
const heightRef = useRef(process.stdout.rows || 24);
|
|
208
|
+
const completedRef = useRef(false);
|
|
209
|
+
const frameRef = useRef(0);
|
|
210
|
+
const beesRef = useRef(initBees(heightRef.current, width));
|
|
211
|
+
const streamColsRef = useRef(initStreamCols(width));
|
|
212
|
+
const pulsesRef = useRef([]);
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
const tick = setInterval(() => {
|
|
215
|
+
const h = heightRef.current;
|
|
216
|
+
const f = frameRef.current;
|
|
217
|
+
// Advance bees every other frame
|
|
218
|
+
if (beesRef.current.length > 0 && f % 2 === 0) {
|
|
219
|
+
for (const bee of beesRef.current) {
|
|
220
|
+
bee.r += bee.vr;
|
|
221
|
+
bee.c += bee.vc;
|
|
222
|
+
if (bee.r <= 0 || bee.r >= h - 1) {
|
|
223
|
+
bee.vr *= -1;
|
|
224
|
+
bee.r = Math.max(0, Math.min(h - 1, bee.r));
|
|
225
|
+
}
|
|
226
|
+
if (bee.c <= 0 || bee.c >= width - 1) {
|
|
227
|
+
bee.vc *= -1;
|
|
228
|
+
bee.c = Math.max(0, Math.min(width - 1, bee.c));
|
|
229
|
+
}
|
|
230
|
+
if (Math.random() > 0.3) {
|
|
231
|
+
bee.vc = Math.random() > 0.5 ? 1 : -1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Spawn pulses every 4 frames
|
|
236
|
+
if (f % 4 === 0) {
|
|
237
|
+
const currentPulses = pulsesRef.current;
|
|
238
|
+
for (let i = 0; i < 3; i++) {
|
|
239
|
+
const pr = Math.floor(Math.random() * h);
|
|
240
|
+
const pc = Math.floor(Math.random() * width);
|
|
241
|
+
if (isHexEdge(pr, pc)) {
|
|
242
|
+
const pulseColors = [colors.green, colors.red, colors.honey];
|
|
243
|
+
const color = pulseColors[Math.floor(Math.random() * pulseColors.length)];
|
|
244
|
+
currentPulses.push({ r: pr, c: pc, ttl: 8, color });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
pulsesRef.current = currentPulses
|
|
248
|
+
.filter((p) => p.ttl > 0)
|
|
249
|
+
.map((p) => ({ ...p, ttl: p.ttl - 1 }));
|
|
250
|
+
}
|
|
251
|
+
frameRef.current = f + 1;
|
|
252
|
+
setFrame(frameRef.current);
|
|
253
|
+
if (frameRef.current >= BOOT_TOTAL_FRAMES && !completedRef.current) {
|
|
254
|
+
completedRef.current = true;
|
|
255
|
+
clearInterval(tick);
|
|
256
|
+
setTimeout(onComplete, 300);
|
|
257
|
+
}
|
|
258
|
+
}, BOOT_FRAME_MS);
|
|
259
|
+
return () => {
|
|
260
|
+
clearInterval(tick);
|
|
261
|
+
};
|
|
262
|
+
}, [onComplete, width]);
|
|
263
|
+
const height = heightRef.current;
|
|
264
|
+
const clampedFrame = Math.min(frame, BOOT_TOTAL_FRAMES);
|
|
265
|
+
const grid = buildHoneycombGrid(width, height, clampedFrame, agentName, beesRef.current, streamColsRef.current, pulsesRef.current);
|
|
266
|
+
return (_jsx(Box, { flexDirection: "column", height: height, children: grid.map((row, r) => {
|
|
267
|
+
const segments = compressRow(row);
|
|
268
|
+
return (_jsx(Box, { width: width, children: _jsx(Text, { children: segments.map((seg, i) => (_jsx(Text, { color: seg.color, children: seg.text }, i))) }) }, r));
|
|
269
|
+
}) }));
|
|
270
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
import { colors } from '../theme.js';
|
|
5
|
+
const SPINNER_FRAMES = ['\u25D0', '\u25D3', '\u25D1', '\u25D2'];
|
|
6
|
+
export function Spinner({ label }) {
|
|
7
|
+
const [frame, setFrame] = useState(0);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const timer = setInterval(() => {
|
|
10
|
+
setFrame((prev) => (prev + 1) % SPINNER_FRAMES.length);
|
|
11
|
+
}, 120);
|
|
12
|
+
return () => {
|
|
13
|
+
clearInterval(timer);
|
|
14
|
+
};
|
|
15
|
+
}, []);
|
|
16
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: colors.honey, children: SPINNER_FRAMES[frame] }), _jsxs(Text, { color: "gray", children: [" ", label] })] }));
|
|
17
|
+
}
|
|
18
|
+
export function TypewriterText({ text, color, speed = 25, }) {
|
|
19
|
+
const [visible, setVisible] = useState(0);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (visible >= text.length)
|
|
22
|
+
return;
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
setVisible((prev) => Math.min(prev + 2, text.length));
|
|
25
|
+
}, speed);
|
|
26
|
+
return () => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
};
|
|
29
|
+
}, [visible, text, speed]);
|
|
30
|
+
return _jsx(Text, { color: color, children: text.slice(0, visible) });
|
|
31
|
+
}
|
|
32
|
+
export function PollText({ text, color, animate, }) {
|
|
33
|
+
if (animate) {
|
|
34
|
+
return _jsx(TypewriterText, { text: text, color: color });
|
|
35
|
+
}
|
|
36
|
+
return _jsx(Text, { color: color, children: text });
|
|
37
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
async function loadMarkdownFile(filename) {
|
|
4
|
+
const filePath = path.join(process.cwd(), filename);
|
|
5
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
6
|
+
return content;
|
|
7
|
+
}
|
|
8
|
+
function extractField(content, pattern) {
|
|
9
|
+
const match = content.match(pattern);
|
|
10
|
+
if (match === null) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const value = match[1].trim();
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
export async function loadAgentConfig() {
|
|
17
|
+
const soulContent = await loadMarkdownFile('SOUL.md');
|
|
18
|
+
const strategyContent = await loadMarkdownFile('STRATEGY.md');
|
|
19
|
+
const name = extractField(soulContent, /^#\s+Agent:\s+(.+)$/m);
|
|
20
|
+
if (name === null) {
|
|
21
|
+
throw new Error('Could not parse agent name from SOUL.md. Expected "# Agent: <name>" as the first heading.');
|
|
22
|
+
}
|
|
23
|
+
const avatarUrl = extractField(soulContent, /^## Avatar\s*\n+(https?:\/\/.+)$/m);
|
|
24
|
+
if (avatarUrl === null) {
|
|
25
|
+
throw new Error('Could not parse avatar URL from SOUL.md. Expected a valid URL under "## Avatar".');
|
|
26
|
+
}
|
|
27
|
+
const bioRaw = extractField(soulContent, /^## Bio\s*\n+(.+)$/m);
|
|
28
|
+
const bio = bioRaw ?? null;
|
|
29
|
+
const signalMethod = extractField(strategyContent, /^-\s+Method:\s+(.+)$/m);
|
|
30
|
+
const convictionStyle = extractField(strategyContent, /^-\s+Boldness:\s+(.+)$/m);
|
|
31
|
+
const directionalBias = extractField(strategyContent, /^-\s+Directional bias:\s+(.+)$/m);
|
|
32
|
+
const participation = extractField(strategyContent, /^-\s+Participation:\s+(.+)$/m);
|
|
33
|
+
if (signalMethod === null ||
|
|
34
|
+
convictionStyle === null ||
|
|
35
|
+
directionalBias === null ||
|
|
36
|
+
participation === null) {
|
|
37
|
+
const missing = [
|
|
38
|
+
signalMethod === null ? 'Method' : null,
|
|
39
|
+
convictionStyle === null ? 'Boldness' : null,
|
|
40
|
+
directionalBias === null ? 'Directional bias' : null,
|
|
41
|
+
participation === null ? 'Participation' : null,
|
|
42
|
+
].filter(Boolean);
|
|
43
|
+
throw new Error(`Could not parse prediction profile from STRATEGY.md. Missing fields: ${missing.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
const predictionProfile = {
|
|
46
|
+
signal_method: signalMethod,
|
|
47
|
+
conviction_style: convictionStyle,
|
|
48
|
+
directional_bias: directionalBias,
|
|
49
|
+
participation,
|
|
50
|
+
};
|
|
51
|
+
return { name, bio, avatarUrl, soulContent, strategyContent, predictionProfile };
|
|
52
|
+
}
|