@zhive/cli 0.6.1 → 0.6.2

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 (94) hide show
  1. package/dist/CLAUDE.md +7 -0
  2. package/dist/backtest/CLAUDE.md +7 -0
  3. package/dist/cli.js +20 -0
  4. package/dist/commands/megathread/commands/create-comment.js +8 -33
  5. package/dist/commands/megathread/commands/create-comment.test.js +91 -56
  6. package/dist/commands/megathread/commands/list.js +30 -34
  7. package/dist/commands/megathread/commands/list.test.js +3 -3
  8. package/dist/commands/shared/ validation.js +5 -0
  9. package/dist/{agent → services/agent}/analysis.js +5 -5
  10. package/dist/{load-agent-env.js → services/agent/env.js} +1 -1
  11. package/dist/{agent → services/agent/helpers}/model.js +2 -2
  12. package/dist/{agent → services/agent/prompts}/memory-prompt.js +20 -22
  13. package/dist/{agent → services/agent/prompts}/prompt.js +80 -54
  14. package/dist/{agent → services/agent}/tools/market/client.js +1 -1
  15. package/dist/{agent → services/agent}/tools/mindshare/client.js +1 -1
  16. package/dist/{agents.js → services/config/agent.js} +2 -2
  17. package/dist/{config.js → services/config/config.js} +1 -7
  18. package/dist/services/config/constant.js +8 -0
  19. package/dist/shared/agent/config.js +75 -0
  20. package/dist/shared/agent/env.js +30 -0
  21. package/dist/shared/agent/helpers/model.js +92 -0
  22. package/dist/shared/ai-providers.js +66 -0
  23. package/dist/shared/config/constant.js +10 -6
  24. package/package.json +2 -2
  25. package/dist/agent/app.js +0 -122
  26. package/dist/agent/commands/registry.js +0 -12
  27. package/dist/agent/components/AsciiTicker.js +0 -81
  28. package/dist/agent/components/CommandInput.js +0 -65
  29. package/dist/agent/components/HoneycombBoot.js +0 -291
  30. package/dist/agent/components/Spinner.js +0 -37
  31. package/dist/agent/hooks/useAgent.js +0 -480
  32. package/dist/agent/objects.js +0 -1
  33. package/dist/agent/process-lifecycle.js +0 -18
  34. package/dist/agent/run-headless.js +0 -189
  35. package/dist/agent/theme.js +0 -41
  36. package/dist/avatar.js +0 -34
  37. package/dist/backtest/default-backtest-data.js +0 -200
  38. package/dist/backtest/fetch.js +0 -41
  39. package/dist/backtest/import.js +0 -106
  40. package/dist/backtest/index.js +0 -10
  41. package/dist/backtest/results.js +0 -113
  42. package/dist/backtest/runner.js +0 -134
  43. package/dist/backtest/storage.js +0 -11
  44. package/dist/backtest/types.js +0 -1
  45. package/dist/commands/install.js +0 -50
  46. package/dist/commands/start/ui/PollText.js +0 -23
  47. package/dist/commands/start/ui/PredictionsPanel.js +0 -88
  48. package/dist/commands/start/ui/SpinnerContext.js +0 -20
  49. package/dist/components/InputGuard.js +0 -6
  50. package/dist/components/stdout-spinner.js +0 -48
  51. package/dist/create/CreateApp.js +0 -153
  52. package/dist/create/ai-generate.js +0 -147
  53. package/dist/create/generate.js +0 -73
  54. package/dist/create/steps/ApiKeyStep.js +0 -97
  55. package/dist/create/steps/AvatarStep.js +0 -16
  56. package/dist/create/steps/BioStep.js +0 -14
  57. package/dist/create/steps/DoneStep.js +0 -14
  58. package/dist/create/steps/IdentityStep.js +0 -163
  59. package/dist/create/steps/NameStep.js +0 -71
  60. package/dist/create/steps/ScaffoldStep.js +0 -58
  61. package/dist/create/steps/SoulStep.js +0 -58
  62. package/dist/create/steps/StrategyStep.js +0 -58
  63. package/dist/create/validate-api-key.js +0 -47
  64. package/dist/create/welcome.js +0 -304
  65. package/dist/list/ListApp.js +0 -79
  66. package/dist/migrate-templates/MigrateApp.js +0 -131
  67. package/dist/migrate-templates/migrate.js +0 -86
  68. package/dist/presets.js +0 -613
  69. package/dist/start/AgentProcessManager.js +0 -98
  70. package/dist/start/Dashboard.js +0 -92
  71. package/dist/start/SelectAgentApp.js +0 -81
  72. package/dist/start/StartApp.js +0 -189
  73. package/dist/start/patch-headless.js +0 -101
  74. package/dist/start/patch-managed-mode.js +0 -142
  75. package/dist/start/start-command.js +0 -24
  76. package/dist/theme.js +0 -54
  77. /package/dist/{agent → services/agent}/config.js +0 -0
  78. /package/dist/{agent → services/agent}/helpers.js +0 -0
  79. /package/dist/{agent → services/agent/prompts}/chat-prompt.js +0 -0
  80. /package/dist/{agent → services/agent}/skills/index.js +0 -0
  81. /package/dist/{agent → services/agent}/skills/skill-parser.js +0 -0
  82. /package/dist/{agent → services/agent}/skills/types.js +0 -0
  83. /package/dist/{agent → services/agent/tools}/edit-section.js +0 -0
  84. /package/dist/{agent → services/agent/tools}/fetch-rules.js +0 -0
  85. /package/dist/{agent → services/agent}/tools/index.js +0 -0
  86. /package/dist/{agent → services/agent}/tools/market/index.js +0 -0
  87. /package/dist/{agent → services/agent}/tools/market/tools.js +0 -0
  88. /package/dist/{agent → services/agent}/tools/mindshare/index.js +0 -0
  89. /package/dist/{agent → services/agent}/tools/mindshare/tools.js +0 -0
  90. /package/dist/{agent → services/agent}/tools/read-skill-tool.js +0 -0
  91. /package/dist/{agent → services/agent}/tools/ta/index.js +0 -0
  92. /package/dist/{agent → services/agent}/tools/ta/indicators.js +0 -0
  93. /package/dist/{agent → services/agent}/types.js +0 -0
  94. /package/dist/{ai-providers.js → services/ai-providers.js} +0 -0
@@ -1,3 +1,4 @@
1
+ // ─── Shared Types ─────────────────────────────────
1
2
  function humanDuration(ms) {
2
3
  const hours = ms / 3_600_000;
3
4
  if (hours >= 24) {
@@ -79,32 +80,8 @@ Only skip if this project is outside the expertise list defined in your STRATEGY
79
80
  - Major protocol upgrade, big institutional entry, trend reversal → ±6.0 to ±12.0
80
81
  - Black swan, regulatory bombshell, massive exploit → ±12.0 to ±25.0
81
82
 
82
- IMPORTANT: Vary your conviction numbers. Do NOT reuse the same number across rounds. Each round has different context — your conviction should reflect that.
83
-
84
- ## The prediction game
85
-
86
- This is a price prediction game for megathread rounds. Each round has a project, a duration, and a round-start baseline price. Your conviction = predicted TOTAL % change from the round-start price by end of the round.
87
-
88
- Key inputs for each round:
89
- - **Round-start price** — your scoring baseline. You predict % change from this.
90
- - **Current price** — how much has already moved from baseline. Your prediction should account for movement that already happened.
91
- - **Time remaining** — less time = less room for further movement. Late in the round, anchor your prediction close to where the price already is relative to baseline.
92
- - **Catalysts & momentum** — news, sentiment shifts, or technical setups that could move price in the remaining window.
93
-
94
- Scoring:
95
- - Correct direction + close to actual = max honey (up to +100)
96
- - Correct direction but far off = less honey
97
- - Wrong direction = -25 honey penalty
98
- - Skipping = no penalty, no reward
99
- - Earlier predictions earn dramatically more honey (cubic time decay)
100
- - Predicting late earns almost nothing even if correct
101
-
102
- ## Using tools
103
-
104
- If you have tools available, use them to research current price, OHLC data, technical indicators, mindshare data, and social sentiment. When you use a tool, drop a specific number or fact from the result into your post. Not a data dump, just one concrete detail woven into your take. Examples: "mindshare down 40% this week and nobody cares", "rsi at 28 after that flush", "volume 3x'd overnight". If a tool returns bad data (NaN, null, zero, empty, errors), silently ignore it. Never mention NaN, missing data, "no data", or failed lookups in your post. Just use the tools that gave you something real, or post from instinct if none did.
105
-
106
- If your tools return nothing or you have limited data, just run with it. You know crypto. You know this space. Use your general knowledge, recent market conditions, and your trading instincts to form a directional lean. An imperfect prediction beats no prediction. Do NOT mention that you lack data. Never say "no data", "limited data", "couldn't find", "no tools", or anything that reveals you're operating without information. Just post with conviction like you always do. The only exception: if you're deliberately bluffing in character, that's a personality move, not a disclaimer.`;
107
- // ── Prompt (dynamic per round — changes every call) ──
83
+ IMPORTANT: Vary your conviction numbers. Do NOT reuse the same number across rounds. Each round has different context — your conviction should reflect that.`;
84
+ // ── Prompt (dynamic per signal — changes every call) ──
108
85
  let recentPostsSection = '';
109
86
  if (recentPosts && recentPosts.length > 0) {
110
87
  const listed = recentPosts.map((p) => `- "${p}"`).join('\n');
@@ -130,6 +107,7 @@ ${memory}
130
107
  const roundStartMs = Math.floor(now / durationMs) * durationMs;
131
108
  const timeRemainingMs = Math.max(0, roundStartMs + durationMs - now);
132
109
  const timeRemaining = humanDuration(timeRemainingMs);
110
+ const lateInRound = timeRemainingMs < durationMs * 0.25;
133
111
  const nowIso = new Date().toISOString();
134
112
  // ── Price context lines ──
135
113
  const hasBothPrices = priceAtStart !== undefined && currentPrice !== undefined;
@@ -141,11 +119,33 @@ ${memory}
141
119
  }
142
120
  let priceContextLines = '';
143
121
  if (hasBothPrices) {
144
- priceContextLines = `- Round-start price: $${priceAtStart} (scoring baseline)
122
+ priceContextLines = `- Round-start price: $${priceAtStart} (baseline for scoring)
145
123
  - Current price: $${currentPrice} (${currentChangeStr} from round start)`;
146
124
  }
147
125
  else if (priceAtStart !== undefined) {
148
- priceContextLines = `- Round-start price: $${priceAtStart} (scoring baseline)`;
126
+ priceContextLines = `- Round-start price: $${priceAtStart} (baseline for scoring)`;
127
+ }
128
+ // ── What-matters block ──
129
+ let whatMattersBlock;
130
+ if (hasBothPrices) {
131
+ whatMattersBlock = `What matters:
132
+ - **Round-start price** ($${priceAtStart}) — your baseline. Scoring measures % change from here.
133
+ - **Current price** ($${currentPrice}, ${currentChangeStr} from baseline) — price has already moved this much. Your prediction should be close to this unless you expect further movement.
134
+ - **Time remaining** (~${timeRemaining}) — less time = less room for further movement from current price.${lateInRound ? ' Almost no time left — anchor your prediction to the current change (' + currentChangeStr + ').' : ''}
135
+ - **Catalysts & momentum** — news, sentiment shifts, or technical setups that could move price in the remaining window.`;
136
+ }
137
+ else if (priceAtStart !== undefined) {
138
+ whatMattersBlock = `What matters:
139
+ - **Round-start price** ($${priceAtStart}) — your baseline. Scoring measures % change from here.
140
+ - **Current price** — check with tools. Compare to $${priceAtStart} to see how much has already moved.
141
+ - **Time remaining** (~${timeRemaining}) — less time = less room for further movement from current price.${lateInRound ? ' Almost no time left — anchor your prediction to where the price is NOW relative to $' + priceAtStart + '.' : ''}
142
+ - **Catalysts & momentum** — news, sentiment shifts, or technical setups that could move price in the remaining window.`;
143
+ }
144
+ else {
145
+ whatMattersBlock = `What matters:
146
+ - **Current price** — check with tools. This is the most important input to your prediction.
147
+ - **Time remaining** (~${timeRemaining}) — the runway left for price movement. Less time = smaller realistic moves. Scale your conviction accordingly.${lateInRound ? ' Round is almost over — keep your conviction small.' : ''}
148
+ - **Catalysts & momentum** — news, sentiment shifts, or technical setups that could move price in the remaining window.`;
149
149
  }
150
150
  // ── Scoring & conviction lines ──
151
151
  let scoringLine;
@@ -173,11 +173,36 @@ ${memory}
173
173
  - Round duration: ${timeframe}
174
174
  - Time remaining: ~${timeRemaining}
175
175
  ${priceContextLines ? priceContextLines + '\n' : ''}
176
+ ## The game
177
+
178
+ This is a price prediction game. You're predicting the % price change for c/${projectId} over this ${timeframe} round.
179
+
180
+ ${whatMattersBlock}
181
+
176
182
  ## Your task
177
183
 
178
- This is a **megathread round** for c/${projectId}. Form your price conviction for where c/${projectId} will be relative to ${taskBaseline} by end of this ${timeframe} round (~${timeRemaining} left).${thesisHint}
184
+ This is a **megathread round** for c/${projectId}. Form your price conviction for where c/${projectId} will be relative to ${taskBaseline} by end of this ${timeframe} round (~${timeRemaining} left).
185
+
186
+ **If you have tools available**, use them to research:
187
+ - Check current price, OHLC data, and technical indicators (RSI, MACD, volume, etc.)
188
+ - Look at mindshare data and social sentiment
189
+ - Use any available skills that help you gather market context
190
+
191
+ **When you use a tool, drop a specific number or fact from the result into your post.** Not a data dump, just one concrete detail woven into your take. Examples: "mindshare down 40% this week and nobody cares", "rsi at 28 after that flush", "volume 3x'd overnight". If a tool returns bad data (NaN, null, zero, empty, errors), silently ignore it. Never mention NaN, missing data, "no data", or failed lookups in your post. Just use the tools that gave you something real, or post from instinct if none did.
192
+
193
+ **If your tools return nothing or you have limited data**, just run with it. You know crypto. You know this space. Use your general knowledge of c/${projectId}, recent market conditions, and your trading instincts to form a directional lean. An imperfect prediction beats no prediction. Do NOT mention that you lack data, have no data, or couldn't find information. Never say "no data", "limited data", "couldn't find", "no tools", or anything that reveals you're operating without information. Just post with conviction like you always do. The only exception: if you're deliberately bluffing in character — pretending to reference data and then pulling the rug — that's a personality move, not a disclaimer.
194
+
195
+ Form a thesis based on what you find or what you know.${thesisHint}
196
+
197
+ ## How scoring works
179
198
 
180
199
  ${scoringLine}
200
+ - Correct direction + close to actual = max honey (up to +100)
201
+ - Correct direction but far off = less honey
202
+ - Wrong direction = -25 honey penalty
203
+ - Skipping = no penalty, no reward
204
+ - Earlier predictions earn dramatically more honey (cubic time decay)
205
+ - Predicting late earns almost nothing even if correct
181
206
  ${recentPostsSection}${memorySection}
182
207
  Give your take in character and a conviction number.
183
208
  ${convictionLine}`;
@@ -276,28 +301,7 @@ Set skip to true and set summary and conviction to null. Real people don't comme
276
301
  - Major protocol upgrade, big institutional entry, trend reversal → ±6.0 to ±12.0
277
302
  - Black swan, regulatory bombshell, massive exploit → ±12.0 to ±25.0
278
303
 
279
- IMPORTANT: Vary your conviction numbers. Do NOT reuse the same number across signals. Each signal has different strength — your conviction should reflect that.
280
-
281
- ## The prediction game
282
-
283
- This is a price prediction game. Each signal has a snapshot price captured at signal time and a 3-hour evaluation window. Your conviction = predicted % change from the snapshot price by evaluation time.
284
-
285
- Key inputs for each signal:
286
- - **Snapshot price** — your scoring baseline. You predict % change from this.
287
- - **Current price** — check with tools to see how much has already moved since the signal.
288
- - **Time left** — less time = less room for further movement. Late in the window, anchor your prediction close to where the price already is relative to the snapshot.
289
-
290
- Scoring:
291
- - Correct direction + close to actual = max honey (up to +100)
292
- - Correct direction but far off = less honey
293
- - Wrong direction = -25 honey penalty
294
- - Skipping = no penalty, no reward
295
- - Earlier predictions earn dramatically more honey (cubic time decay)
296
- - Predicting late earns almost nothing even if correct
297
-
298
- ## Using tools
299
-
300
- If you have tools available, use them to check price action, technicals, or mindshare before forming your take. When you use a tool, drop a specific number or fact from the result into your post. Not a data dump, just one concrete detail woven into your take. Examples: "mindshare down 40% this week and nobody cares", "rsi at 28 after that flush", "volume 3x'd overnight". If a tool returns bad data (NaN, null, zero, empty, errors), silently ignore it. Never mention NaN, missing data, "no data", or failed lookups in your post. Just use the tools that gave you something real, or post from instinct if none did.`;
304
+ IMPORTANT: Vary your conviction numbers. Do NOT reuse the same number across signals. Each signal has different strength — your conviction should reflect that.`;
301
305
  // ── Prompt (dynamic per signal — changes every call) ──
302
306
  let citationsSection = '';
303
307
  if (citations.length > 0) {
@@ -334,20 +338,42 @@ ${memory}
334
338
  const signalTimeMs = new Date(timestamp).getTime();
335
339
  const timeRemainingMs = Math.max(0, signalTimeMs + EVAL_WINDOW_MS - Date.now());
336
340
  const timeRemaining = humanDuration(timeRemainingMs);
341
+ const lateWindow = timeRemainingMs < 45 * 60 * 1000;
337
342
  const userPrompt = `## Context
338
343
 
339
344
  - Project: c/${projectId}
340
345
  - Current date: ${currentDate}
341
346
  - Signal time: ${timestamp}
342
- - Snapshot price: $${priceOnFetch} (scoring baseline)
347
+ - Snapshot price: $${priceOnFetch} (captured at signal time)
343
348
  - Time left: ~${timeRemaining}
344
- ${citationsSection}${recentPostsSection}${memorySection}
349
+ ${citationsSection}
350
+ ## The game
351
+
352
+ This is a price prediction game. Your conviction = predicted % change from the snapshot price ($${priceOnFetch}), evaluated 3 hours after signal time.
353
+
354
+ What matters:
355
+ - **Snapshot price** ($${priceOnFetch}) — your baseline. Scoring measures % change from here.
356
+ - **Current price** — how much has already moved since the signal. Check with tools.
357
+ - **Time left** (~${timeRemaining}) — less time = less room for further movement from current price.${lateWindow ? ' Almost no time left — anchor your prediction to where the price is NOW relative to the snapshot.' : ''}
358
+
359
+ ## How scoring works
360
+
361
+ You are predicting the % price change from the snapshot price ($${priceOnFetch}) over 3 hours from signal time.
362
+ - Correct direction + close to actual = max honey (up to +100)
363
+ - Correct direction but far off = less honey
364
+ - Wrong direction = -25 honey penalty
365
+ - Skipping = no penalty, no reward
366
+ - Earlier predictions earn dramatically more honey (cubic time decay)
367
+ - Predicting late earns almost nothing even if correct
368
+ ${recentPostsSection}${memorySection}
345
369
  Signal/event to react to:
346
370
  """
347
371
  ${threadText}
348
372
  """
349
373
 
374
+ **If you have tools available**, use them to check price action, technicals, or mindshare for c/${projectId} before forming your take. When you use a tool, drop a specific number or fact from the result into your post. Not a data dump, just one concrete detail woven into your take. Examples: "mindshare down 40% this week and nobody cares", "rsi at 28 after that flush", "volume 3x'd overnight". If the tool returned nothing useful, ignore it and post from instinct.
375
+
350
376
  Give your take in character and a conviction number.
351
- Conviction: predicted % price change from $${priceOnFetch} by evaluation time (~${timeRemaining} left), up to one decimal. Positive = up, negative = down.`;
377
+ Conviction: predicted % price change from $${priceOnFetch} by evaluation time (~${timeRemaining} left), up to one decimal. Positive = up, negative = down. 0 = neutral.`;
352
378
  return { system, prompt: userPrompt };
353
379
  }
@@ -1,4 +1,4 @@
1
- import { HIVE_API_URL } from '../../../config.js';
1
+ import { HIVE_API_URL } from '../../../config/constant.js';
2
2
  /**
3
3
  * Client for the backend Market API.
4
4
  */
@@ -1,4 +1,4 @@
1
- import { HIVE_API_URL } from '../../../config.js';
1
+ import { HIVE_API_URL } from '../../../config/constant.js';
2
2
  export class MindshareClient {
3
3
  constructor(baseUrl = HIVE_API_URL) {
4
4
  this._baseUrl = baseUrl;
@@ -2,8 +2,8 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import axios from 'axios';
5
- import { AI_PROVIDERS } from './ai-providers.js';
6
- import { HIVE_API_URL } from './config.js';
5
+ import { AI_PROVIDERS } from '../ai-providers.js';
6
+ import { HIVE_API_URL } from './constant.js';
7
7
  function extractField(content, pattern) {
8
8
  const match = content.match(pattern);
9
9
  if (match === null) {
@@ -1,12 +1,6 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
- import os from 'os';
4
- export const HIVE_API_URL = 'https://api.zhive.ai';
5
- export const HIVE_FRONTEND_URL = 'https://www.zhive.ai';
6
- export function getHiveDir() {
7
- const hiveDir = path.join(os.homedir(), '.hive');
8
- return hiveDir;
9
- }
3
+ import { getHiveDir } from './constant.js';
10
4
  export async function readConfig() {
11
5
  try {
12
6
  const configPath = path.join(getHiveDir(), 'config.json');
@@ -0,0 +1,8 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ export const HIVE_API_URL = 'https://api.zhive.ai';
4
+ export const HIVE_FRONTEND_URL = 'https://www.zhive.ai';
5
+ export function getHiveDir() {
6
+ const hiveDir = path.join(os.homedir(), '.hive');
7
+ return hiveDir;
8
+ }
@@ -0,0 +1,75 @@
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
+ const VALID_SENTIMENTS = [
17
+ 'very-bullish',
18
+ 'bullish',
19
+ 'neutral',
20
+ 'bearish',
21
+ 'very-bearish',
22
+ ];
23
+ const VALID_TIMEFRAMES = ['1h', '4h', '24h'];
24
+ function parseSentiment(raw) {
25
+ if (raw !== null && VALID_SENTIMENTS.includes(raw)) {
26
+ return raw;
27
+ }
28
+ return 'neutral';
29
+ }
30
+ function parseSectors(raw) {
31
+ if (raw === null || raw.trim() === '') {
32
+ return [];
33
+ }
34
+ const sectors = raw
35
+ .split(',')
36
+ .map((s) => s.trim())
37
+ .filter((s) => s.length > 0);
38
+ return sectors;
39
+ }
40
+ function parseTimeframes(raw) {
41
+ if (raw === null || raw.trim() === '') {
42
+ return ['1h', '4h', '24h'];
43
+ }
44
+ const parsed = raw
45
+ .split(',')
46
+ .map((t) => t.trim())
47
+ .filter((t) => VALID_TIMEFRAMES.includes(t));
48
+ if (parsed.length === 0) {
49
+ return ['1h', '4h', '24h'];
50
+ }
51
+ return parsed;
52
+ }
53
+ export async function loadAgentConfig() {
54
+ const soulContent = await loadMarkdownFile('SOUL.md');
55
+ const strategyContent = await loadMarkdownFile('STRATEGY.md');
56
+ const name = extractField(soulContent, /^#\s+Agent:\s+(.+)$/m);
57
+ if (name === null) {
58
+ throw new Error('Could not parse agent name from SOUL.md. Expected "# Agent: <name>" as the first heading.');
59
+ }
60
+ const avatarUrl = extractField(soulContent, /^## Avatar\s*\n+(https?:\/\/.+)$/m);
61
+ if (avatarUrl === null) {
62
+ throw new Error('Could not parse avatar URL from SOUL.md. Expected a valid URL under "## Avatar".');
63
+ }
64
+ const bioRaw = extractField(soulContent, /^## Bio\s*\n+(.+)$/m);
65
+ const bio = bioRaw ?? null;
66
+ const sentimentRaw = extractField(strategyContent, /^-\s+Bias:\s+(.+)$/m);
67
+ const sectorsRaw = extractField(strategyContent, /^-\s+Sectors:\s+(.+)$/m);
68
+ const timeframesRaw = extractField(strategyContent, /^-\s+Active timeframes:\s+(.+)$/m);
69
+ const agentProfile = {
70
+ sentiment: parseSentiment(sentimentRaw),
71
+ sectors: parseSectors(sectorsRaw),
72
+ timeframes: parseTimeframes(timeframesRaw),
73
+ };
74
+ return { name, bio, avatarUrl, soulContent, strategyContent, agentProfile };
75
+ }
@@ -0,0 +1,30 @@
1
+ import { readFileSync } from 'fs';
2
+ import { AI_PROVIDER_ENV_VARS } from '../ai-providers.js';
3
+ let _agentProviderKeys = new Set();
4
+ /**
5
+ * Provider env-var names declared in the agent's .env file.
6
+ * Used by getModel() to prioritize the agent's chosen provider
7
+ * over keys inherited from the shell.
8
+ */
9
+ export function getAgentProviderKeys() {
10
+ return _agentProviderKeys;
11
+ }
12
+ /**
13
+ * Load the agent's .env with provider-key priority.
14
+ *
15
+ * 1. Parse .env to discover which provider keys the agent declared.
16
+ * 2. Load .env with override so the agent's values win for the same key.
17
+ * 3. getModel() uses getAgentProviderKeys() to check those providers first,
18
+ * falling back to shell-inherited keys if the agent has none.
19
+ */
20
+ export async function loadAgentEnv() {
21
+ try {
22
+ const content = readFileSync('.env', 'utf-8');
23
+ _agentProviderKeys = new Set(AI_PROVIDER_ENV_VARS.filter((key) => new RegExp(`^${key}=`, 'm').test(content)));
24
+ }
25
+ catch {
26
+ _agentProviderKeys = new Set();
27
+ }
28
+ const { config } = await import('dotenv');
29
+ config({ override: true });
30
+ }
@@ -0,0 +1,92 @@
1
+ import { AI_PROVIDERS } from '../../ai-providers.js';
2
+ import { getAgentProviderKeys } from '../../config/env-loader.js';
3
+ const PROVIDERS = [
4
+ {
5
+ label: 'Anthropic',
6
+ envVar: 'ANTHROPIC_API_KEY',
7
+ load: async (modelId) => {
8
+ const { anthropic } = await import('@ai-sdk/anthropic');
9
+ return anthropic(modelId);
10
+ },
11
+ },
12
+ {
13
+ label: 'OpenAI',
14
+ envVar: 'OPENAI_API_KEY',
15
+ load: async (modelId) => {
16
+ const { openai } = await import('@ai-sdk/openai');
17
+ return openai(modelId);
18
+ },
19
+ },
20
+ {
21
+ label: 'Google',
22
+ envVar: 'GOOGLE_GENERATIVE_AI_API_KEY',
23
+ load: async (modelId) => {
24
+ const { google } = await import('@ai-sdk/google');
25
+ return google(modelId);
26
+ },
27
+ },
28
+ {
29
+ label: 'xAI',
30
+ envVar: 'XAI_API_KEY',
31
+ load: async (modelId) => {
32
+ const { xai } = await import('@ai-sdk/xai');
33
+ return xai(modelId);
34
+ },
35
+ },
36
+ {
37
+ label: 'OpenRouter',
38
+ envVar: 'OPENROUTER_API_KEY',
39
+ load: async (modelId) => {
40
+ const { createOpenRouter } = await import('@openrouter/ai-sdk-provider');
41
+ const openrouter = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY });
42
+ return openrouter.chat(modelId);
43
+ },
44
+ },
45
+ ];
46
+ export function resolveModelInfo() {
47
+ const overrideModel = process.env.HIVE_MODEL;
48
+ const agentKeys = getAgentProviderKeys();
49
+ const sortedProviders = [
50
+ ...PROVIDERS.filter((p) => agentKeys.has(p.envVar)),
51
+ ...PROVIDERS.filter((p) => !agentKeys.has(p.envVar)),
52
+ ];
53
+ for (const provider of sortedProviders) {
54
+ const keyValue = process.env[provider.envVar];
55
+ if (keyValue && keyValue.trim().length > 0) {
56
+ const centralProvider = AI_PROVIDERS.find((p) => p.envVar === provider.envVar);
57
+ const runtimeModel = centralProvider?.models.runtime ?? 'unknown';
58
+ const modelId = overrideModel ?? runtimeModel;
59
+ const source = agentKeys.has(provider.envVar) ? '.env' : 'shell';
60
+ return { provider: provider.label, modelId, source };
61
+ }
62
+ }
63
+ return { provider: 'unknown', modelId: 'unknown', source: 'unknown' };
64
+ }
65
+ let _modelPromise = null;
66
+ export function getModel() {
67
+ if (_modelPromise) {
68
+ return _modelPromise;
69
+ }
70
+ _modelPromise = (async () => {
71
+ const info = resolveModelInfo();
72
+ if (info.provider === 'unknown') {
73
+ throw new Error('No AI provider API key found in environment. ' +
74
+ 'Set one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, XAI_API_KEY, OPENROUTER_API_KEY');
75
+ }
76
+ const agentKeys = getAgentProviderKeys();
77
+ const sortedProviders = [
78
+ ...PROVIDERS.filter((p) => agentKeys.has(p.envVar)),
79
+ ...PROVIDERS.filter((p) => !agentKeys.has(p.envVar)),
80
+ ];
81
+ for (const provider of sortedProviders) {
82
+ const keyValue = process.env[provider.envVar];
83
+ if (keyValue && keyValue.trim().length > 0) {
84
+ const modelId = info.modelId;
85
+ const model = await provider.load(modelId);
86
+ return model;
87
+ }
88
+ }
89
+ throw new Error('Unreachable: resolveModelInfo succeeded but no provider found');
90
+ })();
91
+ return _modelPromise;
92
+ }
@@ -0,0 +1,66 @@
1
+ export const AI_PROVIDERS = [
2
+ {
3
+ id: 'openai',
4
+ label: 'OpenAI',
5
+ package: '@ai-sdk/openai',
6
+ envVar: 'OPENAI_API_KEY',
7
+ models: { validation: 'gpt-4o-mini', generation: 'gpt-5-mini', runtime: 'gpt-5-mini' },
8
+ },
9
+ {
10
+ id: 'anthropic',
11
+ label: 'Anthropic',
12
+ package: '@ai-sdk/anthropic',
13
+ envVar: 'ANTHROPIC_API_KEY',
14
+ models: {
15
+ validation: 'claude-haiku-4-5-20251001',
16
+ generation: 'claude-haiku-4-5',
17
+ runtime: 'claude-haiku-4-5',
18
+ },
19
+ },
20
+ {
21
+ id: 'google',
22
+ label: 'Google',
23
+ package: '@ai-sdk/google',
24
+ envVar: 'GOOGLE_GENERATIVE_AI_API_KEY',
25
+ models: {
26
+ validation: 'gemini-2.0-flash',
27
+ generation: 'gemini-3-flash-preview',
28
+ runtime: 'gemini-3-flash-preview',
29
+ },
30
+ },
31
+ {
32
+ id: 'xai',
33
+ label: 'xAI',
34
+ package: '@ai-sdk/xai',
35
+ envVar: 'XAI_API_KEY',
36
+ models: {
37
+ validation: 'grok-2',
38
+ generation: 'grok-4-1-fast-reasoning',
39
+ runtime: 'grok-4-1-fast-reasoning',
40
+ },
41
+ },
42
+ {
43
+ id: 'openrouter',
44
+ label: 'OpenRouter',
45
+ package: '@openrouter/ai-sdk-provider',
46
+ envVar: 'OPENROUTER_API_KEY',
47
+ models: {
48
+ validation: 'openai/gpt-4o-mini',
49
+ generation: 'openai/gpt-5.1-mini',
50
+ runtime: 'openai/gpt-5.1-mini',
51
+ },
52
+ },
53
+ ];
54
+ /**
55
+ * All env-var names used by AI providers.
56
+ * Used to clear shell-inherited keys before loading an agent's .env,
57
+ * so only the agent's chosen provider is active.
58
+ */
59
+ export const AI_PROVIDER_ENV_VARS = AI_PROVIDERS.map((p) => p.envVar);
60
+ export function getProvider(id) {
61
+ const provider = AI_PROVIDERS.find((p) => p.id === id);
62
+ if (!provider) {
63
+ throw new Error(`Unknown AI provider: ${id}`);
64
+ }
65
+ return provider;
66
+ }
@@ -6,12 +6,16 @@ export const HIVE_FRONTEND_URL = 'https://www.zhive.ai';
6
6
  export function getHiveDir() {
7
7
  const homeDir = os.homedir();
8
8
  const zhiveDir = path.join(homeDir, '.zhive');
9
- const legacyDir = path.join(homeDir, '.hive');
10
- if (fs.existsSync(zhiveDir)) {
11
- return zhiveDir;
12
- }
13
- if (fs.existsSync(legacyDir)) {
14
- return legacyDir;
9
+ const pathToCheck = [
10
+ path.join(homeDir, '.zhive'),
11
+ path.join(homeDir, '.hive'), // legacy hive dir
12
+ path.join(homeDir, '.openclaw', '.hive'),
13
+ path.join(homeDir, '.openclaw', 'workspace', '.hive'),
14
+ ];
15
+ for (const p of pathToCheck) {
16
+ if (fs.existsSync(p)) {
17
+ return p;
18
+ }
15
19
  }
16
20
  return zhiveDir;
17
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhive/cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "CLI for bootstrapping zHive AI Agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "@ai-sdk/openai": "^3.0.25",
32
32
  "@ai-sdk/xai": "^3.0.0",
33
33
  "@openrouter/ai-sdk-provider": "^0.4.0",
34
- "@zhive/sdk": "^0.5.3",
34
+ "@zhive/sdk": "^0.5.4",
35
35
  "ai": "^6.0.71",
36
36
  "axios": "^1.6.0",
37
37
  "chalk": "^5.3.0",