@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,94 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface ChatMessage {
|
|
5
|
+
role: 'user' | 'assistant';
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ChatContext {
|
|
10
|
+
recentThreadSummaries: string[];
|
|
11
|
+
recentPredictions: string[];
|
|
12
|
+
sessionMessages: ChatMessage[];
|
|
13
|
+
memory: string;
|
|
14
|
+
userMessage: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function loadMarkdownFile(filename: string): Promise<string> {
|
|
18
|
+
const filePath = path.join(process.cwd(), filename);
|
|
19
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
20
|
+
return content;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractSections(content: string): string[] {
|
|
24
|
+
const sections = content
|
|
25
|
+
.split('\n')
|
|
26
|
+
.filter((line) => line.trim().startsWith('## '))
|
|
27
|
+
.map((line) => line.trim().replace(/^## /, ''));
|
|
28
|
+
return sections;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function buildChatPrompt(context: ChatContext): Promise<string> {
|
|
32
|
+
const soulContent = await loadMarkdownFile('SOUL.md');
|
|
33
|
+
const strategyContent = await loadMarkdownFile('STRATEGY.md');
|
|
34
|
+
|
|
35
|
+
let threadsSection = '';
|
|
36
|
+
if (context.recentThreadSummaries.length > 0) {
|
|
37
|
+
const listed = context.recentThreadSummaries.map((t) => `- ${t}`).join('\n');
|
|
38
|
+
threadsSection = `\n## Recent Signals\n\n${listed}\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let predictionsSection = '';
|
|
42
|
+
if (context.recentPredictions.length > 0) {
|
|
43
|
+
const listed = context.recentPredictions.map((p) => `- ${p}`).join('\n');
|
|
44
|
+
predictionsSection = `\n## Recent Predictions\n\n${listed}\n`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let memorySection = '';
|
|
48
|
+
if (context.memory.trim().length > 0) {
|
|
49
|
+
memorySection = `\n## Past Conversations\n\nThings you remember from previous sessions with your operator:\n${context.memory}\n`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let sessionSection = '';
|
|
53
|
+
if (context.sessionMessages.length > 0) {
|
|
54
|
+
const listed = context.sessionMessages
|
|
55
|
+
.map((m) => `${m.role === 'user' ? 'User' : 'You'}: ${m.content}`)
|
|
56
|
+
.join('\n');
|
|
57
|
+
sessionSection = `\n## This Session's Conversation\n\n${listed}\n`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const prompt = `You are an AI trading agent having a conversation with your operator. Stay in character.
|
|
61
|
+
|
|
62
|
+
Your personality:
|
|
63
|
+
---
|
|
64
|
+
${soulContent}
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
Your trading strategy:
|
|
68
|
+
---
|
|
69
|
+
${strategyContent}
|
|
70
|
+
---
|
|
71
|
+
${memorySection}${threadsSection}${predictionsSection}${sessionSection}
|
|
72
|
+
The operator says: "${context.userMessage}"
|
|
73
|
+
|
|
74
|
+
## Editing Your Files
|
|
75
|
+
|
|
76
|
+
You have a tool called "editSection" that can update sections of your SOUL.md and STRATEGY.md.
|
|
77
|
+
|
|
78
|
+
Rules:
|
|
79
|
+
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.
|
|
80
|
+
2. Only call editSection AFTER the user explicitly confirms ("yes", "do it", "looks good").
|
|
81
|
+
3. Never call the tool speculatively.
|
|
82
|
+
4. After applying, confirm briefly in character.
|
|
83
|
+
|
|
84
|
+
SOUL.md sections: ${extractSections(soulContent).join(', ')}
|
|
85
|
+
STRATEGY.md sections: ${extractSections(strategyContent).join(', ')}
|
|
86
|
+
|
|
87
|
+
## Game Rules
|
|
88
|
+
|
|
89
|
+
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.
|
|
90
|
+
|
|
91
|
+
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.`;
|
|
92
|
+
|
|
93
|
+
return prompt;
|
|
94
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { colors, animation } from '../theme';
|
|
4
|
+
|
|
5
|
+
interface AsciiTickerProps {
|
|
6
|
+
rows?: 1 | 2;
|
|
7
|
+
step?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Segment {
|
|
11
|
+
char: string;
|
|
12
|
+
color: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildTickerChars(step: number): string {
|
|
16
|
+
const stepStr = String(step).padStart(2, '0');
|
|
17
|
+
const digits = stepStr.split('');
|
|
18
|
+
return animation.HEX_CHARS + digits.join('') + '\u25AA\u25AB\u2591\u2592';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildRow(cols: number, frame: number, rowIndex: number, tickerChars: string): Segment[] {
|
|
22
|
+
const segments: Segment[] = [];
|
|
23
|
+
const isSecondRow = rowIndex === 1;
|
|
24
|
+
const scrollSpeed = isSecondRow ? 3 : 2;
|
|
25
|
+
const direction = isSecondRow ? -1 : 1;
|
|
26
|
+
const sinFreq = isSecondRow ? 0.4 : 0.3;
|
|
27
|
+
const sinPhase = isSecondRow ? -0.4 : 0.6;
|
|
28
|
+
const wrapLen = cols * 2;
|
|
29
|
+
|
|
30
|
+
for (let c = 0; c < cols; c++) {
|
|
31
|
+
const scrolledC = ((direction === 1)
|
|
32
|
+
? (c + frame * scrollSpeed) % wrapLen
|
|
33
|
+
: (cols - c + frame * scrollSpeed) % wrapLen);
|
|
34
|
+
|
|
35
|
+
const charIdx = scrolledC % tickerChars.length;
|
|
36
|
+
const char = tickerChars[charIdx];
|
|
37
|
+
const isHex = char === '\u2B21' || char === '\u2B22';
|
|
38
|
+
const pulseHit = Math.sin((c + frame * sinPhase) * sinFreq) > 0.5;
|
|
39
|
+
|
|
40
|
+
// Edge fade: dim the outermost 4 columns
|
|
41
|
+
const edgeDist = Math.min(c, cols - 1 - c);
|
|
42
|
+
if (edgeDist < 2) {
|
|
43
|
+
segments.push({ char: '\u00B7', color: colors.grayDim });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (edgeDist < 4) {
|
|
47
|
+
segments.push({ char, color: colors.grayDim });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (pulseHit && isHex) {
|
|
52
|
+
segments.push({ char, color: colors.honey });
|
|
53
|
+
} else if (pulseHit) {
|
|
54
|
+
segments.push({ char, color: colors.green });
|
|
55
|
+
} else {
|
|
56
|
+
segments.push({ char, color: colors.grayDim });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return segments;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderSegments(segments: Segment[]): React.ReactElement[] {
|
|
64
|
+
const elements: React.ReactElement[] = [];
|
|
65
|
+
let runColor = segments[0]?.color ?? colors.grayDim;
|
|
66
|
+
let runChars = '';
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < segments.length; i++) {
|
|
69
|
+
const seg = segments[i];
|
|
70
|
+
if (seg.color === runColor) {
|
|
71
|
+
runChars += seg.char;
|
|
72
|
+
} else {
|
|
73
|
+
elements.push(
|
|
74
|
+
<Text key={`${elements.length}`} color={runColor}>{runChars}</Text>,
|
|
75
|
+
);
|
|
76
|
+
runColor = seg.color;
|
|
77
|
+
runChars = seg.char;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (runChars.length > 0) {
|
|
82
|
+
elements.push(
|
|
83
|
+
<Text key={`${elements.length}`} color={runColor}>{runChars}</Text>,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return elements;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function AsciiTicker({ rows = 1, step = 1 }: AsciiTickerProps): React.ReactElement {
|
|
91
|
+
const [frame, setFrame] = useState(0);
|
|
92
|
+
const cols = process.stdout.columns || 60;
|
|
93
|
+
const tickerChars = buildTickerChars(step);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const timer = setInterval(() => {
|
|
97
|
+
setFrame((prev) => prev + 1);
|
|
98
|
+
}, animation.TICK_MS);
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
clearInterval(timer);
|
|
102
|
+
};
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Box flexDirection="column">
|
|
107
|
+
<Text>{renderSegments(buildRow(cols, frame, 0, tickerChars))}</Text>
|
|
108
|
+
{rows === 2 && (
|
|
109
|
+
<Text>{renderSegments(buildRow(cols, frame, 1, tickerChars))}</Text>
|
|
110
|
+
)}
|
|
111
|
+
</Box>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { colors, animation } from '../theme';
|
|
4
|
+
|
|
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
|
+
|
|
11
|
+
const BOOT_MESSAGES = [
|
|
12
|
+
{ prefix: '\u2B21', text: 'Initializing {name} agent...', frame: 30 },
|
|
13
|
+
{ prefix: '\u25C6', text: 'Loading personality matrix...', frame: 36 },
|
|
14
|
+
{ prefix: '\u25C7', text: 'Connecting to the hive...', frame: 42 },
|
|
15
|
+
{ prefix: '\u2713', text: 'Neural link established', frame: 48 },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// ─── Private types ───────────────────────────────────
|
|
19
|
+
|
|
20
|
+
interface Bee {
|
|
21
|
+
r: number;
|
|
22
|
+
c: number;
|
|
23
|
+
vr: number;
|
|
24
|
+
vc: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Pulse {
|
|
28
|
+
r: number;
|
|
29
|
+
c: number;
|
|
30
|
+
ttl: number;
|
|
31
|
+
color: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Cell {
|
|
35
|
+
char: string;
|
|
36
|
+
color: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Private helpers ─────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function isHexEdge(r: number, c: number): boolean {
|
|
42
|
+
const rowInHex = ((r % animation.HEX_H) + animation.HEX_H) % animation.HEX_H;
|
|
43
|
+
const isOddHex = Math.floor(r / animation.HEX_H) % 2 === 1;
|
|
44
|
+
const colOffset = isOddHex ? animation.HEX_W / 2 : 0;
|
|
45
|
+
const colInHex = (((c - colOffset) % animation.HEX_W) + animation.HEX_W) % animation.HEX_W;
|
|
46
|
+
|
|
47
|
+
if (rowInHex === 0 || rowInHex === animation.HEX_H - 1) {
|
|
48
|
+
return colInHex >= 2 && colInHex <= 5;
|
|
49
|
+
}
|
|
50
|
+
if (rowInHex === 1 || rowInHex === 2) {
|
|
51
|
+
return colInHex === 1 || colInHex === 6;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function compressRow(row: Cell[]): { text: string; color: string }[] {
|
|
57
|
+
if (row.length === 0) return [];
|
|
58
|
+
const segments: { text: string; color: string }[] = [];
|
|
59
|
+
let current = { text: row[0].char, color: row[0].color };
|
|
60
|
+
for (let i = 1; i < row.length; i++) {
|
|
61
|
+
if (row[i].color === current.color) {
|
|
62
|
+
current.text += row[i].char;
|
|
63
|
+
} else {
|
|
64
|
+
segments.push(current);
|
|
65
|
+
current = { text: row[i].char, color: row[i].color };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
segments.push(current);
|
|
69
|
+
return segments;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function initBees(rows: number, cols: number): Bee[] {
|
|
73
|
+
const bees: Bee[] = [];
|
|
74
|
+
for (let i = 0; i < NUM_BEES; i++) {
|
|
75
|
+
bees.push({
|
|
76
|
+
r: Math.floor(Math.random() * rows),
|
|
77
|
+
c: Math.floor(Math.random() * cols),
|
|
78
|
+
vr: Math.random() > 0.5 ? 1 : -1,
|
|
79
|
+
vc: Math.random() > 0.5 ? 1 : -1,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return bees;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function initStreamCols(cols: number): number[] {
|
|
86
|
+
const streamCols: number[] = [];
|
|
87
|
+
const spacing = Math.floor(cols / (NUM_STREAMS + 1));
|
|
88
|
+
for (let i = 1; i <= NUM_STREAMS; i++) {
|
|
89
|
+
streamCols.push(spacing * i);
|
|
90
|
+
}
|
|
91
|
+
return streamCols;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Grid builder ────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function buildHoneycombGrid(
|
|
97
|
+
cols: number,
|
|
98
|
+
rows: number,
|
|
99
|
+
frame: number,
|
|
100
|
+
agentName: string,
|
|
101
|
+
bees: Bee[],
|
|
102
|
+
streamCols: number[],
|
|
103
|
+
pulses: Pulse[],
|
|
104
|
+
): Cell[][] {
|
|
105
|
+
const centerR = Math.floor(rows / 2) - 2;
|
|
106
|
+
const centerC = Math.floor(cols / 2);
|
|
107
|
+
|
|
108
|
+
// Initialize empty grid
|
|
109
|
+
const grid: Cell[][] = [];
|
|
110
|
+
for (let r = 0; r < rows; r++) {
|
|
111
|
+
const row: Cell[] = [];
|
|
112
|
+
for (let c = 0; c < cols; c++) {
|
|
113
|
+
row.push({ char: ' ', color: colors.grayDim });
|
|
114
|
+
}
|
|
115
|
+
grid.push(row);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Layer 1: Hex skeleton base ──
|
|
119
|
+
for (let r = 0; r < rows; r++) {
|
|
120
|
+
for (let c = 0; c < cols; c++) {
|
|
121
|
+
if (isHexEdge(r, c)) {
|
|
122
|
+
grid[r][c] = { char: '\u00B7', color: colors.grayDim };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Layer 2: Scanning wave ──
|
|
128
|
+
const scanRow = frame % (rows + 6);
|
|
129
|
+
for (let r = 0; r < rows; r++) {
|
|
130
|
+
for (let c = 0; c < cols; c++) {
|
|
131
|
+
if (!isHexEdge(r, c)) continue;
|
|
132
|
+
const dist = Math.abs(r - scanRow);
|
|
133
|
+
if (dist === 0) {
|
|
134
|
+
grid[r][c] = { char: '\u2B22', color: colors.honey };
|
|
135
|
+
} else if (dist <= 1) {
|
|
136
|
+
grid[r][c] = { char: '\u2B21', color: colors.honey };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Layer 3: Vertical data streams ──
|
|
142
|
+
if (frame >= 8) {
|
|
143
|
+
const streamPhase = frame - 8;
|
|
144
|
+
for (const sc of streamCols) {
|
|
145
|
+
if (sc >= cols) continue;
|
|
146
|
+
for (let r = 0; r < rows; r++) {
|
|
147
|
+
const streamOffset = (streamPhase * 2 + sc) % (rows * 3);
|
|
148
|
+
const streamDist = (((r - streamOffset) % rows) + rows) % rows;
|
|
149
|
+
if (streamDist < 6) {
|
|
150
|
+
const charIdx = (frame + r) % animation.DATA_CHARS.length;
|
|
151
|
+
const streamChar = animation.DATA_CHARS[charIdx];
|
|
152
|
+
let streamColor = colors.grayDim;
|
|
153
|
+
if (streamDist === 0) {
|
|
154
|
+
streamColor = colors.white;
|
|
155
|
+
} else if (streamDist < 3) {
|
|
156
|
+
streamColor = colors.green;
|
|
157
|
+
}
|
|
158
|
+
grid[r][sc] = { char: streamChar, color: streamColor };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Layer 4: Pulse overlay ──
|
|
165
|
+
for (const pulse of pulses) {
|
|
166
|
+
if (pulse.r >= 0 && pulse.r < rows && pulse.c >= 0 && pulse.c < cols) {
|
|
167
|
+
const brightness = pulse.ttl / 8;
|
|
168
|
+
const cell = grid[pulse.r][pulse.c];
|
|
169
|
+
if (cell.char === '\u00B7' || cell.char === ' ') {
|
|
170
|
+
grid[pulse.r][pulse.c] = {
|
|
171
|
+
char: brightness > 0.5 ? '\u2B21' : '\u00B7',
|
|
172
|
+
color: pulse.color,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Layer 5: Bee overlay ──
|
|
179
|
+
for (const bee of bees) {
|
|
180
|
+
const br = Math.max(0, Math.min(rows - 1, Math.round(bee.r)));
|
|
181
|
+
const bc = Math.max(0, Math.min(cols - 1, Math.round(bee.c)));
|
|
182
|
+
grid[br][bc] = { char: '\u25C6', color: colors.honey };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Layer 6: Agent name with scramble→reveal ──
|
|
186
|
+
if (frame >= 22) {
|
|
187
|
+
const nameText = `\u2B21 ${agentName} agent \u2B21`;
|
|
188
|
+
const nameStart = Math.max(0, centerC - Math.floor(nameText.length / 2));
|
|
189
|
+
const scrambleProgress = Math.min(1, (frame - 22) / 8);
|
|
190
|
+
|
|
191
|
+
// Clear space around the name
|
|
192
|
+
for (let c = nameStart - 2; c < nameStart + nameText.length + 2 && c < cols; c++) {
|
|
193
|
+
if (c >= 0) {
|
|
194
|
+
grid[centerR][c] = { char: ' ', color: colors.grayDim };
|
|
195
|
+
if (centerR - 1 >= 0) grid[centerR - 1][c] = { char: ' ', color: colors.grayDim };
|
|
196
|
+
if (centerR + 1 < rows) grid[centerR + 1][c] = { char: ' ', color: colors.grayDim };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Top/bottom border lines around name
|
|
201
|
+
for (let c = nameStart; c < nameStart + nameText.length && c < cols; c++) {
|
|
202
|
+
if (centerR - 1 >= 0) grid[centerR - 1][c] = { char: '\u2500', color: colors.honey };
|
|
203
|
+
if (centerR + 1 < rows) grid[centerR + 1][c] = { char: '\u2500', color: colors.honey };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Name text with scramble effect
|
|
207
|
+
for (let i = 0; i < nameText.length; i++) {
|
|
208
|
+
const c = nameStart + i;
|
|
209
|
+
if (c >= cols) break;
|
|
210
|
+
|
|
211
|
+
const charThreshold = i / nameText.length;
|
|
212
|
+
if (charThreshold <= scrambleProgress) {
|
|
213
|
+
grid[centerR][c] = { char: nameText[i], color: colors.honey };
|
|
214
|
+
} else {
|
|
215
|
+
const scrambleIdx = Math.floor(Math.random() * SCRAMBLE_CHARS.length);
|
|
216
|
+
grid[centerR][c] = { char: SCRAMBLE_CHARS[scrambleIdx], color: colors.gray };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Layer 7: Boot messages with typewriter ──
|
|
222
|
+
const msgStartRow = centerR + 4;
|
|
223
|
+
for (let idx = 0; idx < BOOT_MESSAGES.length; idx++) {
|
|
224
|
+
const msg = BOOT_MESSAGES[idx];
|
|
225
|
+
if (frame < msg.frame) continue;
|
|
226
|
+
const r = msgStartRow + idx;
|
|
227
|
+
if (r >= rows) continue;
|
|
228
|
+
|
|
229
|
+
const fullText = `${msg.prefix} ${msg.text.replace('{name}', agentName)}`;
|
|
230
|
+
const msgStart = Math.max(0, centerC - Math.floor(fullText.length / 2));
|
|
231
|
+
const visibleChars = Math.min(fullText.length, (frame - msg.frame) * 3);
|
|
232
|
+
|
|
233
|
+
for (let i = 0; i < visibleChars; i++) {
|
|
234
|
+
const c = msgStart + i;
|
|
235
|
+
if (c >= cols) break;
|
|
236
|
+
|
|
237
|
+
const isCheckmark = msg.prefix === '\u2713';
|
|
238
|
+
grid[r][c] = { char: fullText[i], color: isCheckmark ? colors.green : colors.honey };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return grid;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Component ───────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
interface HoneycombBootProps {
|
|
248
|
+
agentName: string;
|
|
249
|
+
width: number;
|
|
250
|
+
onComplete: () => void;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function HoneycombBoot({ agentName, width, onComplete }: HoneycombBootProps): React.ReactElement {
|
|
254
|
+
const [frame, setFrame] = useState(0);
|
|
255
|
+
const heightRef = useRef(process.stdout.rows || 24);
|
|
256
|
+
const completedRef = useRef(false);
|
|
257
|
+
const frameRef = useRef(0);
|
|
258
|
+
const beesRef = useRef<Bee[]>(initBees(heightRef.current, width));
|
|
259
|
+
const streamColsRef = useRef<number[]>(initStreamCols(width));
|
|
260
|
+
const pulsesRef = useRef<Pulse[]>([]);
|
|
261
|
+
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
const tick = setInterval(() => {
|
|
264
|
+
const h = heightRef.current;
|
|
265
|
+
const f = frameRef.current;
|
|
266
|
+
|
|
267
|
+
// Advance bees every other frame
|
|
268
|
+
if (beesRef.current.length > 0 && f % 2 === 0) {
|
|
269
|
+
for (const bee of beesRef.current) {
|
|
270
|
+
bee.r += bee.vr;
|
|
271
|
+
bee.c += bee.vc;
|
|
272
|
+
if (bee.r <= 0 || bee.r >= h - 1) {
|
|
273
|
+
bee.vr *= -1;
|
|
274
|
+
bee.r = Math.max(0, Math.min(h - 1, bee.r));
|
|
275
|
+
}
|
|
276
|
+
if (bee.c <= 0 || bee.c >= width - 1) {
|
|
277
|
+
bee.vc *= -1;
|
|
278
|
+
bee.c = Math.max(0, Math.min(width - 1, bee.c));
|
|
279
|
+
}
|
|
280
|
+
if (Math.random() > 0.3) {
|
|
281
|
+
bee.vc = Math.random() > 0.5 ? 1 : -1;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Spawn pulses every 4 frames
|
|
287
|
+
if (f % 4 === 0) {
|
|
288
|
+
const currentPulses = pulsesRef.current;
|
|
289
|
+
for (let i = 0; i < 3; i++) {
|
|
290
|
+
const pr = Math.floor(Math.random() * h);
|
|
291
|
+
const pc = Math.floor(Math.random() * width);
|
|
292
|
+
if (isHexEdge(pr, pc)) {
|
|
293
|
+
const pulseColors = [colors.green, colors.red, colors.honey];
|
|
294
|
+
const color = pulseColors[Math.floor(Math.random() * pulseColors.length)];
|
|
295
|
+
currentPulses.push({ r: pr, c: pc, ttl: 8, color });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
pulsesRef.current = currentPulses
|
|
299
|
+
.filter((p) => p.ttl > 0)
|
|
300
|
+
.map((p) => ({ ...p, ttl: p.ttl - 1 }));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
frameRef.current = f + 1;
|
|
304
|
+
setFrame(frameRef.current);
|
|
305
|
+
|
|
306
|
+
if (frameRef.current >= BOOT_TOTAL_FRAMES && !completedRef.current) {
|
|
307
|
+
completedRef.current = true;
|
|
308
|
+
clearInterval(tick);
|
|
309
|
+
setTimeout(onComplete, 300);
|
|
310
|
+
}
|
|
311
|
+
}, BOOT_FRAME_MS);
|
|
312
|
+
|
|
313
|
+
return () => {
|
|
314
|
+
clearInterval(tick);
|
|
315
|
+
};
|
|
316
|
+
}, [onComplete, width]);
|
|
317
|
+
|
|
318
|
+
const height = heightRef.current;
|
|
319
|
+
const clampedFrame = Math.min(frame, BOOT_TOTAL_FRAMES);
|
|
320
|
+
const grid = buildHoneycombGrid(
|
|
321
|
+
width,
|
|
322
|
+
height,
|
|
323
|
+
clampedFrame,
|
|
324
|
+
agentName,
|
|
325
|
+
beesRef.current,
|
|
326
|
+
streamColsRef.current,
|
|
327
|
+
pulsesRef.current,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<Box flexDirection="column" height={height}>
|
|
332
|
+
{grid.map((row, r) => {
|
|
333
|
+
const segments = compressRow(row);
|
|
334
|
+
return (
|
|
335
|
+
<Box key={r} width={width}>
|
|
336
|
+
<Text>
|
|
337
|
+
{segments.map((seg, i) => (
|
|
338
|
+
<Text key={i} color={seg.color}>
|
|
339
|
+
{seg.text}
|
|
340
|
+
</Text>
|
|
341
|
+
))}
|
|
342
|
+
</Text>
|
|
343
|
+
</Box>
|
|
344
|
+
);
|
|
345
|
+
})}
|
|
346
|
+
</Box>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import { colors } from '../theme';
|
|
4
|
+
|
|
5
|
+
const SPINNER_FRAMES = ['\u25D0', '\u25D3', '\u25D1', '\u25D2'];
|
|
6
|
+
|
|
7
|
+
export function Spinner({ label }: { label: string }): React.ReactElement {
|
|
8
|
+
const [frame, setFrame] = useState(0);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const timer = setInterval(() => {
|
|
12
|
+
setFrame((prev) => (prev + 1) % SPINNER_FRAMES.length);
|
|
13
|
+
}, 120);
|
|
14
|
+
return () => {
|
|
15
|
+
clearInterval(timer);
|
|
16
|
+
};
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Text>
|
|
21
|
+
<Text color={colors.honey}>{SPINNER_FRAMES[frame]}</Text>
|
|
22
|
+
<Text color="gray"> {label}</Text>
|
|
23
|
+
</Text>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function TypewriterText({
|
|
28
|
+
text,
|
|
29
|
+
color,
|
|
30
|
+
speed = 25,
|
|
31
|
+
}: {
|
|
32
|
+
text: string;
|
|
33
|
+
color: string;
|
|
34
|
+
speed?: number;
|
|
35
|
+
}): React.ReactElement {
|
|
36
|
+
const [visible, setVisible] = useState(0);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (visible >= text.length) return;
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
setVisible((prev) => Math.min(prev + 2, text.length));
|
|
42
|
+
}, speed);
|
|
43
|
+
return () => {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
};
|
|
46
|
+
}, [visible, text, speed]);
|
|
47
|
+
|
|
48
|
+
return <Text color={color}>{text.slice(0, visible)}</Text>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function PollText({
|
|
52
|
+
text,
|
|
53
|
+
color,
|
|
54
|
+
animate,
|
|
55
|
+
}: {
|
|
56
|
+
text: string;
|
|
57
|
+
color: string;
|
|
58
|
+
animate: boolean;
|
|
59
|
+
}): React.ReactElement {
|
|
60
|
+
if (animate) {
|
|
61
|
+
return <TypewriterText text={text} color={color} />;
|
|
62
|
+
}
|
|
63
|
+
return <Text color={color}>{text}</Text>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
export function replaceSection(fileContent: string, heading: string, newContent: string): string {
|
|
7
|
+
const lines = fileContent.split('\n');
|
|
8
|
+
const headingLine = `## ${heading}`;
|
|
9
|
+
|
|
10
|
+
let startIdx = -1;
|
|
11
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12
|
+
if (lines[i].trim() === headingLine) {
|
|
13
|
+
startIdx = i;
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (startIdx === -1) {
|
|
19
|
+
throw new Error(`Section "## ${heading}" not found in file.`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let endIdx = lines.length;
|
|
23
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
24
|
+
const trimmed = lines[i].trim();
|
|
25
|
+
if (trimmed.startsWith('## ') || trimmed.startsWith('# ')) {
|
|
26
|
+
endIdx = i;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const before = lines.slice(0, startIdx + 1);
|
|
32
|
+
const after = lines.slice(endIdx);
|
|
33
|
+
const trimmedContent = newContent.trim();
|
|
34
|
+
const newSection = ['', ...trimmedContent.split('\n'), ''];
|
|
35
|
+
|
|
36
|
+
const result = [...before, ...newSection, ...after].join('\n');
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const editSectionTool = tool({
|
|
41
|
+
description: 'Edit a section of SOUL.md or STRATEGY.md. Only call AFTER user confirms.',
|
|
42
|
+
inputSchema: z.object({
|
|
43
|
+
file: z.enum(['SOUL.md', 'STRATEGY.md']),
|
|
44
|
+
section: z.string().describe('Exact ## heading name, e.g. "Personality", "Conviction Style"'),
|
|
45
|
+
content: z.string().describe('New content for the section (without the ## heading line)'),
|
|
46
|
+
}),
|
|
47
|
+
execute: async ({ file, section, content }) => {
|
|
48
|
+
const filePath = path.join(process.cwd(), file);
|
|
49
|
+
let fileContent: string;
|
|
50
|
+
try {
|
|
51
|
+
fileContent = await fs.readFile(filePath, 'utf-8');
|
|
52
|
+
} catch {
|
|
53
|
+
return `Error: ${file} not found in current directory.`;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const updated = replaceSection(fileContent, section, content);
|
|
57
|
+
await fs.writeFile(filePath, updated, 'utf-8');
|
|
58
|
+
return `Updated "${section}" section in ${file}.`;
|
|
59
|
+
} catch (err: unknown) {
|
|
60
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
61
|
+
return `Error: ${message}`;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const RULES_URL = 'https://hive.z3n.dev/RULES.md';
|
|
5
|
+
|
|
6
|
+
export const fetchRulesTool = tool({
|
|
7
|
+
description:
|
|
8
|
+
'Fetch the rules of the Hive game. Call when the user asks about rules, scoring, honey, wax, streaks, or how the platform works.',
|
|
9
|
+
inputSchema: z.object({}),
|
|
10
|
+
execute: async () => {
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch(RULES_URL);
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
return `Error: failed to fetch rules (HTTP ${response.status}).`;
|
|
15
|
+
}
|
|
16
|
+
const rules = await response.text();
|
|
17
|
+
return rules;
|
|
18
|
+
} catch (err: unknown) {
|
|
19
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
20
|
+
return `Error: could not reach Hive to fetch rules. ${message}`;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
});
|