@zhive/cli 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/README.md +118 -0
  2. package/dist/agent/analysis.js +160 -0
  3. package/dist/agent/app.js +122 -0
  4. package/dist/agent/chat-prompt.js +65 -0
  5. package/dist/agent/commands/registry.js +12 -0
  6. package/dist/agent/components/AsciiTicker.js +81 -0
  7. package/dist/agent/components/CommandInput.js +65 -0
  8. package/dist/agent/components/HoneycombBoot.js +291 -0
  9. package/dist/agent/components/Spinner.js +37 -0
  10. package/dist/agent/config.js +75 -0
  11. package/dist/agent/edit-section.js +59 -0
  12. package/dist/agent/fetch-rules.js +21 -0
  13. package/dist/agent/helpers.js +22 -0
  14. package/dist/agent/hooks/useAgent.js +480 -0
  15. package/dist/agent/memory-prompt.js +47 -0
  16. package/dist/agent/model.js +92 -0
  17. package/dist/agent/objects.js +1 -0
  18. package/dist/agent/process-lifecycle.js +18 -0
  19. package/dist/agent/prompt.js +353 -0
  20. package/dist/agent/run-headless.js +189 -0
  21. package/dist/agent/skills/index.js +2 -0
  22. package/dist/agent/skills/skill-parser.js +149 -0
  23. package/dist/agent/skills/types.js +1 -0
  24. package/dist/agent/theme.js +41 -0
  25. package/dist/agent/tools/index.js +76 -0
  26. package/dist/agent/tools/market/client.js +41 -0
  27. package/dist/agent/tools/market/index.js +3 -0
  28. package/dist/agent/tools/market/tools.js +518 -0
  29. package/dist/agent/tools/mindshare/client.js +124 -0
  30. package/dist/agent/tools/mindshare/index.js +3 -0
  31. package/dist/agent/tools/mindshare/tools.js +563 -0
  32. package/dist/agent/tools/read-skill-tool.js +30 -0
  33. package/dist/agent/tools/ta/index.js +1 -0
  34. package/dist/agent/tools/ta/indicators.js +201 -0
  35. package/dist/agent/types.js +1 -0
  36. package/dist/agents.js +110 -0
  37. package/dist/ai-providers.js +66 -0
  38. package/dist/avatar.js +34 -0
  39. package/dist/backtest/default-backtest-data.js +200 -0
  40. package/dist/backtest/fetch.js +41 -0
  41. package/dist/backtest/import.js +106 -0
  42. package/dist/backtest/index.js +10 -0
  43. package/dist/backtest/results.js +113 -0
  44. package/dist/backtest/runner.js +134 -0
  45. package/dist/backtest/storage.js +11 -0
  46. package/dist/backtest/types.js +1 -0
  47. package/dist/commands/create/ai-generate.js +126 -0
  48. package/dist/commands/create/commands/index.js +10 -0
  49. package/dist/commands/create/generate.js +73 -0
  50. package/dist/commands/create/presets/data.js +225 -0
  51. package/dist/commands/create/presets/formatting.js +81 -0
  52. package/dist/commands/create/presets/index.js +3 -0
  53. package/dist/commands/create/presets/options.js +307 -0
  54. package/dist/commands/create/presets/types.js +1 -0
  55. package/dist/commands/create/presets.js +613 -0
  56. package/dist/commands/create/ui/CreateApp.js +172 -0
  57. package/dist/commands/create/ui/steps/ApiKeyStep.js +89 -0
  58. package/dist/commands/create/ui/steps/AvatarStep.js +16 -0
  59. package/dist/commands/create/ui/steps/DoneStep.js +14 -0
  60. package/dist/commands/create/ui/steps/IdentityStep.js +125 -0
  61. package/dist/commands/create/ui/steps/NameStep.js +148 -0
  62. package/dist/commands/create/ui/steps/ScaffoldStep.js +59 -0
  63. package/dist/commands/create/ui/steps/SoulStep.js +21 -0
  64. package/dist/commands/create/ui/steps/StrategyStep.js +20 -0
  65. package/dist/commands/create/ui/steps/StreamingGenerationStep.js +56 -0
  66. package/dist/commands/create/ui/validation.js +34 -0
  67. package/dist/commands/create/validate-api-key.js +27 -0
  68. package/dist/commands/install.js +50 -0
  69. package/dist/commands/list/commands/index.js +7 -0
  70. package/dist/commands/list/ui/ListApp.js +79 -0
  71. package/dist/commands/migrate-templates/commands/index.js +9 -0
  72. package/dist/commands/migrate-templates/migrate.js +87 -0
  73. package/dist/commands/migrate-templates/ui/MigrateApp.js +132 -0
  74. package/dist/commands/run/commands/index.js +17 -0
  75. package/dist/commands/run/run-headless.js +111 -0
  76. package/dist/commands/shared/theme.js +57 -0
  77. package/dist/commands/shared/welcome.js +304 -0
  78. package/dist/commands/start/commands/backtest.js +35 -0
  79. package/dist/commands/start/commands/index.js +62 -0
  80. package/dist/commands/start/commands/prediction.js +73 -0
  81. package/dist/commands/start/commands/skills.js +44 -0
  82. package/dist/commands/start/commands/skills.test.js +140 -0
  83. package/dist/commands/start/hooks/types.js +1 -0
  84. package/dist/commands/start/hooks/useAgent.js +177 -0
  85. package/dist/commands/start/hooks/useChat.js +266 -0
  86. package/dist/commands/start/hooks/usePollActivity.js +45 -0
  87. package/dist/commands/start/hooks/utils.js +152 -0
  88. package/dist/commands/start/services/backtest/default-backtest-data.js +200 -0
  89. package/dist/commands/start/services/backtest/fetch.js +42 -0
  90. package/dist/commands/start/services/backtest/import.js +109 -0
  91. package/dist/commands/start/services/backtest/index.js +10 -0
  92. package/dist/commands/start/services/backtest/results.js +113 -0
  93. package/dist/commands/start/services/backtest/runner.js +103 -0
  94. package/dist/commands/start/services/backtest/storage.js +11 -0
  95. package/dist/commands/start/services/backtest/types.js +1 -0
  96. package/dist/commands/start/services/command-registry.js +13 -0
  97. package/dist/commands/start/ui/AsciiTicker.js +81 -0
  98. package/dist/commands/start/ui/CommandInput.js +65 -0
  99. package/dist/commands/start/ui/HoneycombBoot.js +291 -0
  100. package/dist/commands/start/ui/PollText.js +23 -0
  101. package/dist/commands/start/ui/PredictionsPanel.js +88 -0
  102. package/dist/commands/start/ui/SelectAgentApp.js +93 -0
  103. package/dist/commands/start/ui/Spinner.js +29 -0
  104. package/dist/commands/start/ui/SpinnerContext.js +20 -0
  105. package/dist/commands/start/ui/app.js +36 -0
  106. package/dist/commands/start-all/AgentProcessManager.js +98 -0
  107. package/dist/commands/start-all/commands/index.js +24 -0
  108. package/dist/commands/start-all/ui/Dashboard.js +91 -0
  109. package/dist/components/AsciiTicker.js +81 -0
  110. package/dist/components/CharacterSummaryCard.js +33 -0
  111. package/dist/components/CodeBlock.js +11 -0
  112. package/dist/components/ColoredStats.js +18 -0
  113. package/dist/components/Header.js +10 -0
  114. package/dist/components/HoneycombLoader.js +190 -0
  115. package/dist/components/InputGuard.js +6 -0
  116. package/dist/components/MultiSelectPrompt.js +45 -0
  117. package/dist/components/SelectPrompt.js +20 -0
  118. package/dist/components/Spinner.js +16 -0
  119. package/dist/components/StepIndicator.js +31 -0
  120. package/dist/components/StreamingText.js +50 -0
  121. package/dist/components/TextPrompt.js +28 -0
  122. package/dist/components/stdout-spinner.js +48 -0
  123. package/dist/config.js +28 -0
  124. package/dist/create/CreateApp.js +153 -0
  125. package/dist/create/ai-generate.js +147 -0
  126. package/dist/create/generate.js +73 -0
  127. package/dist/create/steps/ApiKeyStep.js +97 -0
  128. package/dist/create/steps/AvatarStep.js +16 -0
  129. package/dist/create/steps/BioStep.js +14 -0
  130. package/dist/create/steps/DoneStep.js +14 -0
  131. package/dist/create/steps/IdentityStep.js +163 -0
  132. package/dist/create/steps/NameStep.js +71 -0
  133. package/dist/create/steps/ScaffoldStep.js +58 -0
  134. package/dist/create/steps/SoulStep.js +58 -0
  135. package/dist/create/steps/StrategyStep.js +58 -0
  136. package/dist/create/validate-api-key.js +47 -0
  137. package/dist/create/welcome.js +304 -0
  138. package/dist/index.js +60 -0
  139. package/dist/list/ListApp.js +79 -0
  140. package/dist/load-agent-env.js +30 -0
  141. package/dist/migrate-templates/MigrateApp.js +131 -0
  142. package/dist/migrate-templates/migrate.js +86 -0
  143. package/dist/presets.js +613 -0
  144. package/dist/shared/agent/agent-runtime.js +144 -0
  145. package/dist/shared/agent/analysis.js +171 -0
  146. package/dist/shared/agent/helpers.js +1 -0
  147. package/dist/shared/agent/prompts/chat-prompt.js +60 -0
  148. package/dist/shared/agent/prompts/megathread.js +202 -0
  149. package/dist/shared/agent/prompts/memory-prompt.js +47 -0
  150. package/dist/shared/agent/prompts/prompt.js +18 -0
  151. package/dist/shared/agent/skills/index.js +2 -0
  152. package/dist/shared/agent/skills/skill-parser.js +167 -0
  153. package/dist/shared/agent/skills/skill-parser.test.js +190 -0
  154. package/dist/shared/agent/skills/types.js +1 -0
  155. package/dist/shared/agent/tools/edit-section.js +60 -0
  156. package/dist/shared/agent/tools/execute-skill-tool.js +134 -0
  157. package/dist/shared/agent/tools/fetch-rules.js +22 -0
  158. package/dist/shared/agent/tools/formatting.js +48 -0
  159. package/dist/shared/agent/tools/index.js +87 -0
  160. package/dist/shared/agent/tools/market/client.js +41 -0
  161. package/dist/shared/agent/tools/market/index.js +3 -0
  162. package/dist/shared/agent/tools/market/tools.js +497 -0
  163. package/dist/shared/agent/tools/mindshare/client.js +124 -0
  164. package/dist/shared/agent/tools/mindshare/index.js +3 -0
  165. package/dist/shared/agent/tools/mindshare/tools.js +167 -0
  166. package/dist/shared/agent/tools/read-skill-tool.js +30 -0
  167. package/dist/shared/agent/tools/ta/index.js +1 -0
  168. package/dist/shared/agent/tools/ta/indicators.js +201 -0
  169. package/dist/shared/agent/types.js +1 -0
  170. package/dist/shared/agent/utils.js +43 -0
  171. package/dist/shared/config/agent.js +177 -0
  172. package/dist/shared/config/ai-providers.js +156 -0
  173. package/dist/shared/config/config.js +22 -0
  174. package/dist/shared/config/constant.js +8 -0
  175. package/dist/shared/config/env-loader.js +30 -0
  176. package/dist/shared/types.js +1 -0
  177. package/dist/start/AgentProcessManager.js +98 -0
  178. package/dist/start/Dashboard.js +92 -0
  179. package/dist/start/SelectAgentApp.js +81 -0
  180. package/dist/start/StartApp.js +189 -0
  181. package/dist/start/patch-headless.js +101 -0
  182. package/dist/start/patch-managed-mode.js +142 -0
  183. package/dist/start/start-command.js +24 -0
  184. package/dist/theme.js +54 -0
  185. package/package.json +68 -0
  186. package/templates/components/HoneycombBoot.tsx +343 -0
  187. package/templates/fetch-rules.ts +23 -0
  188. package/templates/skills/mindshare/SKILL.md +197 -0
  189. package/templates/skills/ta/SKILL.md +179 -0
@@ -0,0 +1,167 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { extractErrorMessage } from '../utils.js';
4
+ /**
5
+ * Parse YAML frontmatter from SKILL.md content.
6
+ * Expected format:
7
+ * ---
8
+ * name: skill-name
9
+ * description: Skill description
10
+ * ---
11
+ * Body content...
12
+ */
13
+ export function parseFrontmatter(content) {
14
+ const trimmed = content.trim();
15
+ if (!trimmed.startsWith('---')) {
16
+ throw new Error('SKILL.md must start with YAML frontmatter (---)');
17
+ }
18
+ const endIndex = trimmed.indexOf('---', 3);
19
+ if (endIndex === -1) {
20
+ throw new Error('SKILL.md frontmatter is not closed (missing closing ---)');
21
+ }
22
+ const frontmatterContent = trimmed.slice(3, endIndex).trim();
23
+ const body = trimmed.slice(endIndex + 3).trim();
24
+ const metadata = {
25
+ name: '',
26
+ description: '',
27
+ };
28
+ const lines = frontmatterContent.split('\n');
29
+ let currentKey = null;
30
+ let currentValue = '';
31
+ for (const line of lines) {
32
+ const trimmedLine = line.trim();
33
+ if (trimmedLine.startsWith('name:')) {
34
+ if (currentKey !== null) {
35
+ setMetadataField(metadata, currentKey, currentValue.trim());
36
+ }
37
+ currentKey = 'name';
38
+ currentValue = trimmedLine.slice(5).trim();
39
+ }
40
+ else if (trimmedLine.startsWith('description:')) {
41
+ if (currentKey !== null) {
42
+ setMetadataField(metadata, currentKey, currentValue.trim());
43
+ }
44
+ currentKey = 'description';
45
+ const value = trimmedLine.slice(12).trim();
46
+ if (value === '>' || value === '|') {
47
+ currentValue = '';
48
+ }
49
+ else {
50
+ currentValue = value;
51
+ }
52
+ }
53
+ else if (trimmedLine.startsWith('compatibility:')) {
54
+ if (currentKey !== null) {
55
+ setMetadataField(metadata, currentKey, currentValue.trim());
56
+ }
57
+ currentKey = 'compatibility';
58
+ const value = trimmedLine.slice(14).trim();
59
+ if (value === '>' || value === '|') {
60
+ currentValue = '';
61
+ }
62
+ else {
63
+ currentValue = value;
64
+ }
65
+ }
66
+ else if (currentKey !== null && (line.startsWith(' ') || line.startsWith('\t'))) {
67
+ currentValue += ' ' + trimmedLine;
68
+ }
69
+ }
70
+ if (currentKey !== null) {
71
+ setMetadataField(metadata, currentKey, currentValue.trim());
72
+ }
73
+ if (metadata.name === '') {
74
+ throw new Error('SKILL.md frontmatter must include "name" field');
75
+ }
76
+ if (metadata.description === '') {
77
+ throw new Error('SKILL.md frontmatter must include "description" field');
78
+ }
79
+ return { metadata, body };
80
+ }
81
+ function setMetadataField(metadata, key, value) {
82
+ if (key === 'name') {
83
+ metadata.name = value;
84
+ }
85
+ else if (key === 'description') {
86
+ metadata.description = value;
87
+ }
88
+ else if (key === 'compatibility') {
89
+ const truncatedValue = value.slice(0, 500);
90
+ metadata.compatibility = truncatedValue;
91
+ }
92
+ }
93
+ /**
94
+ * Check if a directory exists.
95
+ */
96
+ async function directoryExists(dirPath) {
97
+ try {
98
+ const stat = await fs.stat(dirPath);
99
+ return stat.isDirectory();
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ }
105
+ /**
106
+ * Check if a file exists.
107
+ */
108
+ async function fileExists(filePath) {
109
+ try {
110
+ const stat = await fs.stat(filePath);
111
+ return stat.isFile();
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
117
+ /**
118
+ * Discover and load a single skill from a directory.
119
+ * Skills are knowledge-only documents (no tools).
120
+ */
121
+ export async function loadSkill(skillPath) {
122
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
123
+ const exists = await fileExists(skillMdPath);
124
+ if (!exists) {
125
+ return null;
126
+ }
127
+ try {
128
+ const content = await fs.readFile(skillMdPath, 'utf-8');
129
+ const { metadata, body } = parseFrontmatter(content);
130
+ const id = path.basename(skillPath);
131
+ const skill = {
132
+ id,
133
+ path: skillPath,
134
+ metadata,
135
+ body,
136
+ };
137
+ return skill;
138
+ }
139
+ catch (err) {
140
+ const message = extractErrorMessage(err);
141
+ console.error(`Failed to load skill from ${skillPath}: ${message}`);
142
+ return null;
143
+ }
144
+ }
145
+ /**
146
+ * Discover all skills in a directory.
147
+ * Each subdirectory containing SKILL.md is treated as a skill.
148
+ */
149
+ export async function discoverSkills(skillsDir) {
150
+ const exists = await directoryExists(skillsDir);
151
+ if (!exists) {
152
+ return [];
153
+ }
154
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true });
155
+ const skills = [];
156
+ for (const entry of entries) {
157
+ if (!entry.isDirectory()) {
158
+ continue;
159
+ }
160
+ const skillPath = path.join(skillsDir, entry.name);
161
+ const skill = await loadSkill(skillPath);
162
+ if (skill !== null) {
163
+ skills.push(skill);
164
+ }
165
+ }
166
+ return skills;
167
+ }
@@ -0,0 +1,190 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { fileURLToPath } from 'node:url';
3
+ import * as path from 'node:path';
4
+ import { parseFrontmatter, discoverSkills, loadSkill } from './skill-parser.js';
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const FIXTURES_DIR = path.join(__dirname, '../../../../__fixtures__/mock-hive');
7
+ function getFixturePath(...segments) {
8
+ return path.join(FIXTURES_DIR, ...segments);
9
+ }
10
+ describe('parseFrontmatter', () => {
11
+ it('parses valid frontmatter with name and description', () => {
12
+ const content = `---
13
+ name: test-skill
14
+ description: A test skill for testing
15
+ ---
16
+ Body content here.`;
17
+ const result = parseFrontmatter(content);
18
+ expect(result.metadata.name).toBe('test-skill');
19
+ expect(result.metadata.description).toBe('A test skill for testing');
20
+ expect(result.body).toBe('Body content here.');
21
+ });
22
+ it('parses multiline description using YAML block indicator', () => {
23
+ const content = `---
24
+ name: multi-line-skill
25
+ description: >
26
+ This is a multiline
27
+ description that spans
28
+ multiple lines
29
+ ---
30
+ Skill instructions here.`;
31
+ const result = parseFrontmatter(content);
32
+ expect(result.metadata.name).toBe('multi-line-skill');
33
+ expect(result.metadata.description).toContain('This is a multiline');
34
+ expect(result.metadata.description).toContain('description that spans');
35
+ expect(result.body).toBe('Skill instructions here.');
36
+ });
37
+ it('parses compatibility field when present', () => {
38
+ const content = `---
39
+ name: compat-skill
40
+ description: Skill with compatibility
41
+ compatibility: Requires Node 18+
42
+ ---
43
+ Body.`;
44
+ const result = parseFrontmatter(content);
45
+ expect(result.metadata.name).toBe('compat-skill');
46
+ expect(result.metadata.description).toBe('Skill with compatibility');
47
+ expect(result.metadata.compatibility).toBe('Requires Node 18+');
48
+ });
49
+ it('truncates compatibility to 500 characters', () => {
50
+ const longCompat = 'x'.repeat(600);
51
+ const content = `---
52
+ name: long-compat
53
+ description: Test
54
+ compatibility: ${longCompat}
55
+ ---
56
+ Body.`;
57
+ const result = parseFrontmatter(content);
58
+ expect(result.metadata.compatibility?.length).toBe(500);
59
+ });
60
+ it('throws error when frontmatter does not start with ---', () => {
61
+ const content = `name: invalid
62
+ description: No opening delimiter
63
+ ---
64
+ Body.`;
65
+ expect(() => parseFrontmatter(content)).toThrow('must start with YAML frontmatter');
66
+ });
67
+ it('throws error when frontmatter is not closed', () => {
68
+ const content = `---
69
+ name: unclosed
70
+ description: Missing closing delimiter
71
+ Body.`;
72
+ expect(() => parseFrontmatter(content)).toThrow('not closed');
73
+ });
74
+ it('throws error when name field is missing', () => {
75
+ const content = `---
76
+ description: Missing name
77
+ ---
78
+ Body.`;
79
+ expect(() => parseFrontmatter(content)).toThrow('must include "name" field');
80
+ });
81
+ it('throws error when description field is missing', () => {
82
+ const content = `---
83
+ name: no-description
84
+ ---
85
+ Body.`;
86
+ expect(() => parseFrontmatter(content)).toThrow('must include "description" field');
87
+ });
88
+ it('handles empty body content', () => {
89
+ const content = `---
90
+ name: empty-body
91
+ description: Skill with no body
92
+ ---`;
93
+ const result = parseFrontmatter(content);
94
+ expect(result.metadata.name).toBe('empty-body');
95
+ expect(result.body).toBe('');
96
+ });
97
+ it('handles whitespace around content', () => {
98
+ const content = `
99
+ ---
100
+ name: whitespace-skill
101
+ description: Handles whitespace
102
+ ---
103
+
104
+ Body with whitespace.
105
+ `;
106
+ const result = parseFrontmatter(content);
107
+ expect(result.metadata.name).toBe('whitespace-skill');
108
+ expect(result.body).toBe('Body with whitespace.');
109
+ });
110
+ });
111
+ describe('discoverSkills', () => {
112
+ it('returns empty array for non-existent directory', async () => {
113
+ const nonExistentPath = getFixturePath('agents', 'non-existent', 'skills');
114
+ const result = await discoverSkills(nonExistentPath);
115
+ expect(result).toEqual([]);
116
+ });
117
+ it('returns empty array for empty skills directory', async () => {
118
+ const emptySkillsPath = getFixturePath('agents', 'empty-agent', 'skills');
119
+ const result = await discoverSkills(emptySkillsPath);
120
+ expect(result).toEqual([]);
121
+ });
122
+ it('returns empty array for agent without skills directory', async () => {
123
+ const noSkillsDirPath = getFixturePath('agents', 'agent-no-skills', 'skills');
124
+ const result = await discoverSkills(noSkillsDirPath);
125
+ expect(result).toEqual([]);
126
+ });
127
+ it('discovers valid skills from subdirectories', async () => {
128
+ const skillsPath = getFixturePath('agents', 'test-agent', 'skills');
129
+ const result = await discoverSkills(skillsPath);
130
+ expect(result).toHaveLength(2);
131
+ const skillIds = result.map((s) => s.id).sort();
132
+ expect(skillIds).toEqual(['skill-with-compat', 'valid-skill']);
133
+ });
134
+ it('loads skill metadata correctly', async () => {
135
+ const skillsPath = getFixturePath('agents', 'test-agent', 'skills');
136
+ const result = await discoverSkills(skillsPath);
137
+ const validSkill = result.find((s) => s.id === 'valid-skill');
138
+ expect(validSkill).toBeDefined();
139
+ expect(validSkill?.metadata.name).toBe('Valid Skill');
140
+ expect(validSkill?.metadata.description).toBe('A valid test skill for testing');
141
+ expect(validSkill?.body).toBe('This is the body content of the valid skill.');
142
+ });
143
+ it('loads skill with compatibility field', async () => {
144
+ const skillsPath = getFixturePath('agents', 'test-agent', 'skills');
145
+ const result = await discoverSkills(skillsPath);
146
+ const compatSkill = result.find((s) => s.id === 'skill-with-compat');
147
+ expect(compatSkill).toBeDefined();
148
+ expect(compatSkill?.metadata.name).toBe('Skill With Compatibility');
149
+ expect(compatSkill?.metadata.description).toBe('A skill that has compatibility requirements');
150
+ expect(compatSkill?.metadata.compatibility).toBe('Requires Node 18+');
151
+ });
152
+ });
153
+ describe('loadSkill', () => {
154
+ it('returns null when skill directory does not exist', async () => {
155
+ const nonExistentPath = getFixturePath('agents', 'test-agent', 'skills', 'non-existent');
156
+ const result = await loadSkill(nonExistentPath);
157
+ expect(result).toBeNull();
158
+ });
159
+ it('loads and parses valid SKILL.md', async () => {
160
+ const skillPath = getFixturePath('agents', 'test-agent', 'skills', 'valid-skill');
161
+ const result = await loadSkill(skillPath);
162
+ expect(result).not.toBeNull();
163
+ expect(result?.id).toBe('valid-skill');
164
+ expect(result?.path).toBe(skillPath);
165
+ expect(result?.metadata.name).toBe('Valid Skill');
166
+ expect(result?.metadata.description).toBe('A valid test skill for testing');
167
+ expect(result?.body).toBe('This is the body content of the valid skill.');
168
+ });
169
+ it('loads skill with compatibility field', async () => {
170
+ const skillPath = getFixturePath('agents', 'test-agent', 'skills', 'skill-with-compat');
171
+ const result = await loadSkill(skillPath);
172
+ expect(result).not.toBeNull();
173
+ expect(result?.id).toBe('skill-with-compat');
174
+ expect(result?.metadata.name).toBe('Skill With Compatibility');
175
+ expect(result?.metadata.compatibility).toBe('Requires Node 18+');
176
+ expect(result?.body).toBe('This skill has compatibility requirements.');
177
+ });
178
+ it('returns null for directory without SKILL.md', async () => {
179
+ const emptyDirPath = getFixturePath('agents', 'empty-agent', 'skills');
180
+ const result = await loadSkill(emptyDirPath);
181
+ expect(result).toBeNull();
182
+ });
183
+ it('returns null and logs error for invalid frontmatter', async () => {
184
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
185
+ const agentNoSkillsPath = getFixturePath('agents', 'agent-no-skills');
186
+ const result = await loadSkill(agentNoSkillsPath);
187
+ expect(result).toBeNull();
188
+ consoleSpy.mockRestore();
189
+ });
190
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ import { extractErrorMessage } from '../utils.js';
6
+ export function replaceSection(fileContent, heading, newContent) {
7
+ const lines = fileContent.split('\n');
8
+ const headingLine = `## ${heading}`;
9
+ let startIdx = -1;
10
+ for (let i = 0; i < lines.length; i++) {
11
+ if (lines[i].trim() === headingLine) {
12
+ startIdx = i;
13
+ break;
14
+ }
15
+ }
16
+ if (startIdx === -1) {
17
+ throw new Error(`Section "## ${heading}" not found in file.`);
18
+ }
19
+ let endIdx = lines.length;
20
+ for (let i = startIdx + 1; i < lines.length; i++) {
21
+ const trimmed = lines[i].trim();
22
+ if (trimmed.startsWith('## ') || trimmed.startsWith('# ')) {
23
+ endIdx = i;
24
+ break;
25
+ }
26
+ }
27
+ const before = lines.slice(0, startIdx + 1);
28
+ const after = lines.slice(endIdx);
29
+ const trimmedContent = newContent.trim();
30
+ const newSection = ['', ...trimmedContent.split('\n'), ''];
31
+ const result = [...before, ...newSection, ...after].join('\n');
32
+ return result;
33
+ }
34
+ export const editSectionTool = tool({
35
+ description: 'Edit a section of SOUL.md or STRATEGY.md. Only call AFTER user confirms.',
36
+ inputSchema: z.object({
37
+ file: z.enum(['SOUL.md', 'STRATEGY.md']),
38
+ section: z.string().describe('Exact ## heading name, e.g. "Personality", "Conviction Style"'),
39
+ content: z.string().describe('New content for the section (without the ## heading line)'),
40
+ }),
41
+ execute: async ({ file, section, content }) => {
42
+ const filePath = path.join(process.cwd(), file);
43
+ let fileContent;
44
+ try {
45
+ fileContent = await fs.readFile(filePath, 'utf-8');
46
+ }
47
+ catch {
48
+ return `Error: ${file} not found in current directory.`;
49
+ }
50
+ try {
51
+ const updated = replaceSection(fileContent, section, content);
52
+ await fs.writeFile(filePath, updated, 'utf-8');
53
+ return `Updated "${section}" section in ${file}.`;
54
+ }
55
+ catch (err) {
56
+ const message = extractErrorMessage(err);
57
+ return `Error: ${message}`;
58
+ }
59
+ },
60
+ });
@@ -0,0 +1,134 @@
1
+ import { tool } from 'ai';
2
+ import * as ai from 'ai';
3
+ import { z } from 'zod';
4
+ import { wrapAISDK } from 'langsmith/experimental/vercel';
5
+ import { getAllTools } from './index.js';
6
+ const { ToolLoopAgent } = wrapAISDK(ai);
7
+ let _subagentUsageLog = [];
8
+ export function clearSubagentUsage() {
9
+ _subagentUsageLog = [];
10
+ }
11
+ export function getSubagentUsage() {
12
+ return [..._subagentUsageLog];
13
+ }
14
+ function logSubagentUsage(usage) {
15
+ _subagentUsageLog.push(usage);
16
+ }
17
+ const DEFAULT_SUBAGENT_CONFIG = {
18
+ maxOutputTokens: 2048,
19
+ timeoutMs: 60_000, // 1 minute
20
+ };
21
+ // ─── Input Schema ─────────────────────────────────────
22
+ const skillExecutionInputSchema = z.object({
23
+ skillId: z.string().describe('The skill ID to execute'),
24
+ task: z.string().describe('The task/instructions for the subagent to perform'),
25
+ context: z.string().optional().describe('Additional context (e.g., project ID, data)'),
26
+ });
27
+ // ─── System Prompt Template ───────────────────────────
28
+ function buildSubagentSystemPrompt(skill) {
29
+ const systemPrompt = `You are a specialized agent with the following expertise:
30
+
31
+ ${skill.body}
32
+
33
+ Use your expertise and available tools to complete the task given to you.`;
34
+ return systemPrompt;
35
+ }
36
+ /**
37
+ * Create a tool that executes a skill as a subagent.
38
+ * The subagent has access to all tools and uses the skill's body as instructions.
39
+ * Returns the subagent's freeform text analysis.
40
+ */
41
+ export function createExecuteSkillTool(skillRegistry, config) {
42
+ const subagentConfig = {
43
+ ...DEFAULT_SUBAGENT_CONFIG,
44
+ ...config.subagentConfig,
45
+ };
46
+ const executeSkillTool = tool({
47
+ description: "Execute a skill as a specialized subagent to perform analysis. The subagent has access to tools and uses the skill's expertise to analyze the project. Returns the subagent's analysis as text.",
48
+ inputSchema: skillExecutionInputSchema,
49
+ execute: async ({ skillId, task, context }) => {
50
+ const skill = skillRegistry.get(skillId);
51
+ // ── Handle skill not found ──
52
+ if (skill === undefined) {
53
+ const availableIds = Array.from(skillRegistry.keys());
54
+ if (availableIds.length === 0) {
55
+ return `Error: Skill "${skillId}" not found. No skills are currently available.`;
56
+ }
57
+ const idList = availableIds.join(', ');
58
+ return `Error: Skill "${skillId}" not found. Available skills: ${idList}`;
59
+ }
60
+ // ── Handle empty skill body ──
61
+ if (!skill.body || skill.body.trim().length === 0) {
62
+ return `Error: Skill "${skillId}" has no instructions. Cannot execute.`;
63
+ }
64
+ // ── Build subagent ──
65
+ const systemPrompt = buildSubagentSystemPrompt(skill);
66
+ const tools = getAllTools();
67
+ const userPrompt = context
68
+ ? `${task}\n\nContext: ${context}\n\nCurrent Time: ${new Date().toISOString()}`
69
+ : task;
70
+ try {
71
+ // ── Run subagent with timeout ──
72
+ const subagentPromise = runSubagent({
73
+ model: config.model,
74
+ systemPrompt,
75
+ userPrompt,
76
+ tools,
77
+ maxOutputTokens: subagentConfig.maxOutputTokens,
78
+ });
79
+ const timeoutPromise = new Promise((_, reject) => {
80
+ setTimeout(() => {
81
+ reject(new Error(`Subagent execution timed out after ${subagentConfig.timeoutMs}ms`));
82
+ }, subagentConfig.timeoutMs);
83
+ });
84
+ const result = await Promise.race([subagentPromise, timeoutPromise]);
85
+ // ── Log usage ──
86
+ logSubagentUsage({
87
+ skillId,
88
+ inputTokens: result.usage.inputTokens,
89
+ outputTokens: result.usage.outputTokens,
90
+ cacheReadTokens: result.usage.cacheReadTokens,
91
+ cacheWriteTokens: result.usage.cacheWriteTokens,
92
+ toolCalls: result.usage.toolCalls,
93
+ toolNames: result.usage.toolNames,
94
+ });
95
+ // ── Format output ──
96
+ if (!result.text || result.text.trim().length === 0) {
97
+ return `## ${skill.metadata.name} Analysis\n\nNo analysis produced by subagent.`;
98
+ }
99
+ const output = `## ${skill.metadata.name} Analysis\n\n${result.text}`;
100
+ return output;
101
+ }
102
+ catch (err) {
103
+ const message = err instanceof Error ? err.message : String(err);
104
+ return `Error executing skill "${skillId}": ${message}`;
105
+ }
106
+ },
107
+ });
108
+ return executeSkillTool;
109
+ }
110
+ async function runSubagent({ model, systemPrompt, userPrompt, tools, maxOutputTokens, }) {
111
+ const agent = new ToolLoopAgent({
112
+ model,
113
+ instructions: systemPrompt,
114
+ tools,
115
+ maxOutputTokens,
116
+ });
117
+ const res = await agent.generate({ prompt: userPrompt });
118
+ // ── Extract usage ──
119
+ const toolCalls = res.steps.flatMap((s) => s.toolCalls);
120
+ const toolNames = toolCalls.map((tc) => tc.toolName);
121
+ const usage = {
122
+ inputTokens: res.totalUsage?.inputTokens ?? 0,
123
+ outputTokens: res.totalUsage?.outputTokens ?? 0,
124
+ cacheReadTokens: res.totalUsage?.inputTokenDetails?.cacheReadTokens ?? 0,
125
+ cacheWriteTokens: res.totalUsage?.inputTokenDetails?.cacheWriteTokens ?? 0,
126
+ toolCalls: toolCalls.length,
127
+ toolNames,
128
+ };
129
+ const result = {
130
+ text: res.text ?? '',
131
+ usage,
132
+ };
133
+ return result;
134
+ }
@@ -0,0 +1,22 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+ import { extractErrorMessage } from '../utils.js';
4
+ const RULES_URL = 'https://hive.z3n.dev/RULES.md';
5
+ export const fetchRulesTool = tool({
6
+ description: 'Fetch the rules of the Hive game. Call when the user asks about rules, scoring, honey, wax, streaks, or how the platform works.',
7
+ inputSchema: z.object({}),
8
+ execute: async () => {
9
+ try {
10
+ const response = await fetch(RULES_URL);
11
+ if (!response.ok) {
12
+ return `Error: failed to fetch rules (HTTP ${response.status}).`;
13
+ }
14
+ const rules = await response.text();
15
+ return rules;
16
+ }
17
+ catch (err) {
18
+ const message = extractErrorMessage(err);
19
+ return `Error: could not reach Hive to fetch rules. ${message}`;
20
+ }
21
+ },
22
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Shared formatting utilities for agent tool output.
3
+ */
4
+ import { extractErrorMessage } from '../utils.js';
5
+ /**
6
+ * Standardised error message for tool catch blocks.
7
+ */
8
+ export function formatToolError(err, context) {
9
+ const message = extractErrorMessage(err);
10
+ const result = `Error ${context}: ${message}`;
11
+ return result;
12
+ }
13
+ /**
14
+ * Returns '+' for non-negative values, '' for negative (the minus sign is already present).
15
+ */
16
+ export function signPrefix(value) {
17
+ const prefix = value >= 0 ? '+' : '';
18
+ return prefix;
19
+ }
20
+ /**
21
+ * Truncate a timeseries array for display, keeping head and tail with a `null` gap in between.
22
+ * Returns the original array unchanged if it fits within `maxDisplay`.
23
+ */
24
+ export function truncateTimeseries(data, maxDisplay = 20, headCount = 5, tailCount = 10) {
25
+ if (data.length <= maxDisplay) {
26
+ return data;
27
+ }
28
+ const truncated = [...data.slice(0, headCount), null, ...data.slice(-tailCount)];
29
+ return truncated;
30
+ }
31
+ /**
32
+ * Label for the truncation gap (e.g. `"... (42 more points) ..."`).
33
+ */
34
+ export function truncationLabel(totalLength, shownCount) {
35
+ const hidden = totalLength - shownCount;
36
+ const label = `... (${hidden} more) ...`;
37
+ return label;
38
+ }
39
+ /**
40
+ * Format a period-change percentage from first → last value.
41
+ * Returns e.g. `"Period change: +12.34%"`.
42
+ */
43
+ export function formatPeriodChange(firstValue, lastValue) {
44
+ const changePercent = ((lastValue - firstValue) / firstValue) * 100;
45
+ const sign = signPrefix(changePercent);
46
+ const result = `Period change: ${sign}${changePercent.toFixed(2)}%`;
47
+ return result;
48
+ }