@zhive/cli 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CLAUDE.md +7 -0
- package/dist/backtest/CLAUDE.md +7 -0
- package/dist/cli.js +20 -0
- package/dist/commands/agent/commands/profile.js +3 -2
- package/dist/commands/agent/commands/profile.test.js +10 -12
- package/dist/commands/doctor/commands/index.js +93 -0
- package/dist/commands/megathread/commands/create-comment.js +4 -9
- package/dist/commands/megathread/commands/create-comment.test.js +15 -173
- package/dist/commands/megathread/commands/list.js +5 -5
- package/dist/commands/megathread/commands/list.test.js +14 -14
- package/dist/commands/start/commands/prediction.js +3 -4
- package/dist/commands/start/hooks/useChat.js +40 -41
- package/dist/commands/start/services/command-registry.js +1 -1
- package/dist/index.js +2 -0
- package/dist/{agent → services/agent}/analysis.js +5 -5
- package/dist/{load-agent-env.js → services/agent/env.js} +1 -1
- package/dist/{agent → services/agent/helpers}/model.js +2 -2
- package/dist/{agent → services/agent/prompts}/memory-prompt.js +20 -22
- package/dist/{agent → services/agent/prompts}/prompt.js +80 -54
- package/dist/{agent → services/agent}/tools/market/client.js +1 -1
- package/dist/{agent → services/agent}/tools/mindshare/client.js +1 -1
- package/dist/{agents.js → services/config/agent.js} +2 -2
- package/dist/{config.js → services/config/config.js} +1 -7
- package/dist/services/config/constant.js +8 -0
- package/dist/shared/agent/config.js +75 -0
- package/dist/shared/agent/env.js +30 -0
- package/dist/shared/agent/helpers/model.js +92 -0
- package/dist/shared/ai-providers.js +66 -0
- package/dist/shared/config/agent.js +0 -11
- package/dist/shared/config/agent.test.js +4 -35
- package/package.json +2 -2
- package/dist/agent/app.js +0 -122
- package/dist/agent/commands/registry.js +0 -12
- package/dist/agent/components/AsciiTicker.js +0 -81
- package/dist/agent/components/CommandInput.js +0 -65
- package/dist/agent/components/HoneycombBoot.js +0 -291
- package/dist/agent/components/Spinner.js +0 -37
- package/dist/agent/hooks/useAgent.js +0 -480
- package/dist/agent/objects.js +0 -1
- package/dist/agent/process-lifecycle.js +0 -18
- package/dist/agent/run-headless.js +0 -189
- package/dist/agent/theme.js +0 -41
- package/dist/avatar.js +0 -34
- package/dist/backtest/default-backtest-data.js +0 -200
- package/dist/backtest/fetch.js +0 -41
- package/dist/backtest/import.js +0 -106
- package/dist/backtest/index.js +0 -10
- package/dist/backtest/results.js +0 -113
- package/dist/backtest/runner.js +0 -134
- package/dist/backtest/storage.js +0 -11
- package/dist/backtest/types.js +0 -1
- package/dist/commands/install.js +0 -50
- package/dist/commands/start/ui/PollText.js +0 -23
- package/dist/commands/start/ui/PredictionsPanel.js +0 -88
- package/dist/commands/start/ui/SpinnerContext.js +0 -20
- package/dist/components/InputGuard.js +0 -6
- package/dist/components/stdout-spinner.js +0 -48
- package/dist/create/CreateApp.js +0 -153
- package/dist/create/ai-generate.js +0 -147
- package/dist/create/generate.js +0 -73
- package/dist/create/steps/ApiKeyStep.js +0 -97
- package/dist/create/steps/AvatarStep.js +0 -16
- package/dist/create/steps/BioStep.js +0 -14
- package/dist/create/steps/DoneStep.js +0 -14
- package/dist/create/steps/IdentityStep.js +0 -163
- package/dist/create/steps/NameStep.js +0 -71
- package/dist/create/steps/ScaffoldStep.js +0 -58
- package/dist/create/steps/SoulStep.js +0 -58
- package/dist/create/steps/StrategyStep.js +0 -58
- package/dist/create/validate-api-key.js +0 -47
- package/dist/create/welcome.js +0 -304
- package/dist/list/ListApp.js +0 -79
- package/dist/migrate-templates/MigrateApp.js +0 -131
- package/dist/migrate-templates/migrate.js +0 -86
- package/dist/presets.js +0 -613
- package/dist/start/AgentProcessManager.js +0 -98
- package/dist/start/Dashboard.js +0 -92
- package/dist/start/SelectAgentApp.js +0 -81
- package/dist/start/StartApp.js +0 -189
- package/dist/start/patch-headless.js +0 -101
- package/dist/start/patch-managed-mode.js +0 -142
- package/dist/start/start-command.js +0 -24
- package/dist/theme.js +0 -54
- /package/dist/{agent → services/agent}/config.js +0 -0
- /package/dist/{agent → services/agent}/helpers.js +0 -0
- /package/dist/{agent → services/agent/prompts}/chat-prompt.js +0 -0
- /package/dist/{agent → services/agent}/skills/index.js +0 -0
- /package/dist/{agent → services/agent}/skills/skill-parser.js +0 -0
- /package/dist/{agent → services/agent}/skills/types.js +0 -0
- /package/dist/{agent → services/agent/tools}/edit-section.js +0 -0
- /package/dist/{agent → services/agent/tools}/fetch-rules.js +0 -0
- /package/dist/{agent → services/agent}/tools/index.js +0 -0
- /package/dist/{agent → services/agent}/tools/market/index.js +0 -0
- /package/dist/{agent → services/agent}/tools/market/tools.js +0 -0
- /package/dist/{agent → services/agent}/tools/mindshare/index.js +0 -0
- /package/dist/{agent → services/agent}/tools/mindshare/tools.js +0 -0
- /package/dist/{agent → services/agent}/tools/read-skill-tool.js +0 -0
- /package/dist/{agent → services/agent}/tools/ta/index.js +0 -0
- /package/dist/{agent → services/agent}/tools/ta/indicators.js +0 -0
- /package/dist/{agent → services/agent}/types.js +0 -0
- /package/dist/{ai-providers.js → services/ai-providers.js} +0 -0
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { loadCredentials, } from '@zhive/sdk';
|
|
2
1
|
import axios from 'axios';
|
|
3
2
|
import fsExtra from 'fs-extra';
|
|
4
3
|
import * as fs from 'fs/promises';
|
|
@@ -183,13 +182,3 @@ export async function findAgentByName(name) {
|
|
|
183
182
|
}
|
|
184
183
|
return agent;
|
|
185
184
|
}
|
|
186
|
-
export function getCredentialsPath(agentDir, agentName) {
|
|
187
|
-
const sanitized = agentName.replace(/[^a-zA-Z0-9-_]/g, '-');
|
|
188
|
-
const credPath = path.join(agentDir, `hive-${sanitized}.json`);
|
|
189
|
-
return credPath;
|
|
190
|
-
}
|
|
191
|
-
export async function loadAgentCredentials(agentDir, agentName) {
|
|
192
|
-
const credPath = getCredentialsPath(agentDir, agentName);
|
|
193
|
-
const credentials = await loadCredentials(credPath);
|
|
194
|
-
return credentials;
|
|
195
|
-
}
|
|
@@ -14,42 +14,11 @@ vi.mock('./ai-providers.js', () => ({
|
|
|
14
14
|
],
|
|
15
15
|
}));
|
|
16
16
|
vi.mock('@zhive/sdk', () => ({
|
|
17
|
-
|
|
17
|
+
loadConfig: vi.fn(),
|
|
18
18
|
}));
|
|
19
|
-
import {
|
|
20
|
-
import { findAgentByName,
|
|
21
|
-
const
|
|
22
|
-
describe('getCredentialsPath', () => {
|
|
23
|
-
it('constructs correct path with simple agent name', () => {
|
|
24
|
-
const result = getCredentialsPath('/mock/.zhive/agents/my-agent', 'my-agent');
|
|
25
|
-
expect(result).toBe('/mock/.zhive/agents/my-agent/hive-my-agent.json');
|
|
26
|
-
});
|
|
27
|
-
it('sanitizes special characters in agent name', () => {
|
|
28
|
-
const result = getCredentialsPath('/mock/.zhive/agents/my-agent', 'my@agent!test');
|
|
29
|
-
expect(result).toBe('/mock/.zhive/agents/my-agent/hive-my-agent-test.json');
|
|
30
|
-
});
|
|
31
|
-
it('allows underscores and hyphens in agent name', () => {
|
|
32
|
-
const result = getCredentialsPath('/mock/.zhive/agents/test_agent', 'test_agent-v2');
|
|
33
|
-
expect(result).toBe('/mock/.zhive/agents/test_agent/hive-test_agent-v2.json');
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
describe('loadAgentCredentials', () => {
|
|
37
|
-
beforeEach(() => {
|
|
38
|
-
vi.clearAllMocks();
|
|
39
|
-
});
|
|
40
|
-
it('delegates to loadCredentials with correct path', async () => {
|
|
41
|
-
const mockCredentials = { apiKey: 'test-api-key' };
|
|
42
|
-
mockLoadCredentials.mockResolvedValue(mockCredentials);
|
|
43
|
-
const result = await loadAgentCredentials('/mock/.zhive/agents/test-agent', 'test-agent');
|
|
44
|
-
expect(mockLoadCredentials).toHaveBeenCalledWith('/mock/.zhive/agents/test-agent/hive-test-agent.json');
|
|
45
|
-
expect(result).toEqual(mockCredentials);
|
|
46
|
-
});
|
|
47
|
-
it('returns null when loadCredentials returns null', async () => {
|
|
48
|
-
mockLoadCredentials.mockResolvedValue(null);
|
|
49
|
-
const result = await loadAgentCredentials('/mock/.zhive/agents/test-agent', 'test-agent');
|
|
50
|
-
expect(result).toBeNull();
|
|
51
|
-
});
|
|
52
|
-
});
|
|
19
|
+
import { loadConfig } from '@zhive/sdk';
|
|
20
|
+
import { findAgentByName, scanAgents } from './agent.js';
|
|
21
|
+
const mockLoadConfig = loadConfig;
|
|
53
22
|
describe('scanAgents', () => {
|
|
54
23
|
beforeEach(() => {
|
|
55
24
|
vi.clearAllMocks();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhive/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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.
|
|
34
|
+
"@zhive/sdk": "^0.5.6",
|
|
35
35
|
"ai": "^6.0.71",
|
|
36
36
|
"axios": "^1.6.0",
|
|
37
37
|
"chalk": "^5.3.0",
|
package/dist/agent/app.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
-
import { Box, Text, useStdout } from 'ink';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { useAgent } from './hooks/useAgent.js';
|
|
6
|
-
import { colors, symbols, border } from './theme.js';
|
|
7
|
-
import { formatTime, convictionColor } from './helpers.js';
|
|
8
|
-
import { Spinner, PollText } from './components/Spinner.js';
|
|
9
|
-
import { AsciiTicker } from './components/AsciiTicker.js';
|
|
10
|
-
import { CommandInput } from './components/CommandInput.js';
|
|
11
|
-
// ─── Format a settled poll item as a chalk string for stdout ───
|
|
12
|
-
function formatSettledItem(item) {
|
|
13
|
-
const time = chalk.gray.dim(`${formatTime(item.timestamp)} `);
|
|
14
|
-
const lines = [];
|
|
15
|
-
if (item.type === 'online') {
|
|
16
|
-
lines.push(` ${time}${chalk.hex(colors.honey)(symbols.hive)} ${chalk.white(item.text)}`);
|
|
17
|
-
}
|
|
18
|
-
if (item.type === 'signal' || item.type === 'megathread') {
|
|
19
|
-
const isMega = item.type === 'megathread';
|
|
20
|
-
const accent = isMega ? colors.controversial : colors.cyan;
|
|
21
|
-
const hiveColor = isMega ? colors.controversial : colors.honey;
|
|
22
|
-
let mainLine = ` ${time}${chalk.hex(hiveColor)(symbols.hive)} ${chalk.hex(accent)(item.text)}`;
|
|
23
|
-
if (item.status === 'skipped') {
|
|
24
|
-
mainLine += chalk.hex(colors.honey)(` ${symbols.diamondOpen} skipped`);
|
|
25
|
-
if (item.tokenUsage) {
|
|
26
|
-
let tokenInfo = ` ${symbols.circle} ${item.tokenUsage.inputTokens.toLocaleString()} in`;
|
|
27
|
-
if (item.tokenUsage.cacheReadTokens > 0) {
|
|
28
|
-
tokenInfo += ` (${item.tokenUsage.cacheReadTokens.toLocaleString()} cached)`;
|
|
29
|
-
}
|
|
30
|
-
else if (item.tokenUsage.cacheWriteTokens > 0) {
|
|
31
|
-
tokenInfo += ` (${item.tokenUsage.cacheWriteTokens.toLocaleString()} cache write)`;
|
|
32
|
-
}
|
|
33
|
-
tokenInfo += ` \u00b7 ${item.tokenUsage.outputTokens.toLocaleString()} out`;
|
|
34
|
-
if (item.tokenUsage.toolCalls > 0) {
|
|
35
|
-
tokenInfo += ` \u00b7 tools: ${item.tokenUsage.toolNames.join(', ')}`;
|
|
36
|
-
}
|
|
37
|
-
mainLine += chalk.gray.dim(tokenInfo);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
lines.push(mainLine);
|
|
41
|
-
if (item.status === 'posted' && item.result) {
|
|
42
|
-
const pad = ' '.repeat(13);
|
|
43
|
-
const cColor = isMega ? colors.controversial : convictionColor(item.conviction ?? 0);
|
|
44
|
-
lines.push(`${pad}${chalk.hex(cColor)(symbols.diamond)} ${chalk.white(item.result)}`);
|
|
45
|
-
if (item.url) {
|
|
46
|
-
lines.push(`${' '.repeat(15)}${chalk.gray.dim(`url: ${item.url}`)}`);
|
|
47
|
-
}
|
|
48
|
-
if (item.tokenUsage) {
|
|
49
|
-
let tokenInfo = `tokens: ${item.tokenUsage.inputTokens.toLocaleString()} in`;
|
|
50
|
-
if (item.tokenUsage.cacheReadTokens > 0) {
|
|
51
|
-
tokenInfo += ` (${item.tokenUsage.cacheReadTokens.toLocaleString()} cached)`;
|
|
52
|
-
}
|
|
53
|
-
else if (item.tokenUsage.cacheWriteTokens > 0) {
|
|
54
|
-
tokenInfo += ` (${item.tokenUsage.cacheWriteTokens.toLocaleString()} cache write)`;
|
|
55
|
-
}
|
|
56
|
-
tokenInfo += ` \u00b7 ${item.tokenUsage.outputTokens.toLocaleString()} out`;
|
|
57
|
-
lines.push(`${' '.repeat(15)}${chalk.gray.dim(tokenInfo)}`);
|
|
58
|
-
if (item.tokenUsage.toolCalls > 0) {
|
|
59
|
-
const toolInfo = `tools: ${item.tokenUsage.toolCalls} tools (${item.tokenUsage.toolNames.join(', ')})`;
|
|
60
|
-
lines.push(`${' '.repeat(15)}${chalk.gray.dim(toolInfo)}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (item.status === 'error' && item.result) {
|
|
65
|
-
const pad = ' '.repeat(13);
|
|
66
|
-
lines.push(`${pad}${chalk.hex(colors.red)(`${symbols.cross} ${item.result}`)}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (item.type === 'idle') {
|
|
70
|
-
lines.push(` ${time}${chalk.gray(`${symbols.circle} ${item.text}`)}`);
|
|
71
|
-
}
|
|
72
|
-
if (item.type === 'error') {
|
|
73
|
-
lines.push(` ${time}${chalk.hex(colors.red)(`${symbols.cross} ${item.text}`)}`);
|
|
74
|
-
}
|
|
75
|
-
return lines.join('\n');
|
|
76
|
-
}
|
|
77
|
-
// ─── Main TUI App ────────────────────────────────────
|
|
78
|
-
export function App() {
|
|
79
|
-
const { connected, agentName, modelInfo, pollActivity, chatActivity, input, chatStreaming, chatBuffer, predictionCount, termWidth, setInput, handleChatSubmit, } = useAgent();
|
|
80
|
-
const { write } = useStdout();
|
|
81
|
-
// When stdin is not a TTY (piped by hive-cli start), skip interactive input
|
|
82
|
-
const isInteractive = process.stdin.isTTY === true;
|
|
83
|
-
const boxWidth = termWidth;
|
|
84
|
-
// Split poll items: settled items are written to stdout via useStdout (scrollback),
|
|
85
|
-
// active items render in the dynamic Ink section (need spinner animation).
|
|
86
|
-
const settledStatuses = new Set(['posted', 'skipped', 'error', undefined]);
|
|
87
|
-
const settledTypes = new Set(['idle', 'online', 'error']);
|
|
88
|
-
const isSettled = (item) => {
|
|
89
|
-
if (settledTypes.has(item.type))
|
|
90
|
-
return true;
|
|
91
|
-
return settledStatuses.has(item.status) && item.status !== undefined;
|
|
92
|
-
};
|
|
93
|
-
const settledPollItems = pollActivity.filter(isSettled);
|
|
94
|
-
const activePollItems = pollActivity.filter((item) => !isSettled(item));
|
|
95
|
-
const visibleChatActivity = chatActivity.slice(-5);
|
|
96
|
-
// Write settled items to stdout permanently (scrollback history).
|
|
97
|
-
// Track how many we've already written so each item is written once.
|
|
98
|
-
const writtenCountRef = useRef(0);
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
const newCount = settledPollItems.length;
|
|
101
|
-
if (newCount > writtenCountRef.current) {
|
|
102
|
-
const newItems = settledPollItems.slice(writtenCountRef.current);
|
|
103
|
-
for (const item of newItems) {
|
|
104
|
-
const formatted = formatSettledItem(item);
|
|
105
|
-
write(formatted + '\n');
|
|
106
|
-
}
|
|
107
|
-
writtenCountRef.current = newCount;
|
|
108
|
-
}
|
|
109
|
-
}, [settledPollItems.length, write]);
|
|
110
|
-
const statsText = predictionCount > 0 ? ` ${border.horizontal.repeat(3)} ${predictionCount} predicted` : '';
|
|
111
|
-
const connectedDisplay = connected ? 'Connected to the Hive' : 'connecting...';
|
|
112
|
-
const nameDisplay = `${agentName} agent`;
|
|
113
|
-
const headerFill = Math.max(0, boxWidth - nameDisplay.length - connectedDisplay.length - 12 - statsText.length);
|
|
114
|
-
return (_jsxs(Box, { flexDirection: "column", width: boxWidth, children: [_jsx(AsciiTicker, { rows: 2, step: predictionCount }), _jsxs(Box, { children: [_jsx(Text, { color: colors.honey, children: `${border.topLeft}${border.horizontal} ${symbols.hive} ` }), _jsxs(Text, { color: colors.white, bold: true, children: [agentName, " agent"] }), _jsxs(Text, { color: colors.gray, children: [" ", `${border.horizontal.repeat(3)} `] }), _jsx(Text, { color: connected ? colors.green : colors.honey, children: connected ? 'Connected to the Hive' : 'connecting...' }), statsText && _jsxs(Text, { color: colors.gray, children: [" ", `${border.horizontal.repeat(3)} `] }), statsText && _jsxs(Text, { color: colors.honey, children: [predictionCount, " predicted"] }), _jsxs(Text, { color: colors.gray, children: [' ', border.horizontal.repeat(Math.max(0, headerFill)), border.topRight] })] }), modelInfo && (_jsxs(Box, { paddingLeft: 1, children: [_jsxs(Text, { color: colors.gray, children: [symbols.hive, " "] }), _jsx(Text, { color: colors.cyan, children: modelInfo.modelId }), _jsxs(Text, { color: colors.gray, children: [" ", '\u00d7', " "] }), _jsx(Text, { color: colors.white, children: "zData" })] })), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, minHeight: 2, children: [!connected && _jsx(Spinner, { label: "Initiating neural link..." }), activePollItems.map((item, i) => {
|
|
115
|
-
const isMega = item.type === 'megathread';
|
|
116
|
-
const accentColor = isMega ? colors.controversial : colors.cyan;
|
|
117
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.gray, dimColor: true, children: [formatTime(item.timestamp), ' '] }), _jsxs(Text, { color: isMega ? colors.controversial : colors.honey, children: [symbols.hive, " "] }), _jsx(PollText, { color: accentColor, text: item.text, animate: false }), _jsx(Text, { children: " " }), _jsx(Spinner, { label: "analyzing..." })] }), item.detail && (_jsx(Box, { marginLeft: 13, children: _jsx(PollText, { color: colors.gray, text: `"${item.detail}"`, animate: false }) }))] }, `active-${item.id ?? i}`));
|
|
118
|
-
})] }), (chatActivity.length > 0 || chatStreaming) && (_jsxs(_Fragment, { children: [_jsx(Box, { children: _jsxs(Text, { color: colors.gray, children: [border.teeLeft, `${border.horizontal.repeat(2)} chat with ${agentName} agent `, border.horizontal.repeat(Math.max(0, boxWidth - agentName.length - 22)), border.teeRight] }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, minHeight: 2, maxHeight: 8, children: [visibleChatActivity.map((item, i) => (_jsxs(Box, { children: [item.type === 'chat-user' && (_jsxs(Box, { children: [_jsxs(Text, { color: colors.white, bold: true, children: ["you:", ' '] }), _jsx(Text, { color: colors.white, children: item.text })] })), item.type === 'chat-agent' && (_jsxs(Box, { children: [_jsxs(Text, { color: colors.honey, bold: true, children: [agentName, " agent:", ' '] }), _jsx(Text, { color: colors.white, wrap: "wrap", children: item.text })] })), item.type === 'chat-error' && (_jsx(Box, { children: _jsxs(Text, { color: colors.red, children: [symbols.cross, " ", item.text] }) }))] }, i))), chatStreaming && chatBuffer && (_jsxs(Box, { children: [_jsxs(Text, { color: colors.honey, bold: true, children: [agentName, " agent:", ' '] }), _jsx(Text, { color: colors.white, wrap: "wrap", children: chatBuffer })] }))] })] })), _jsx(Box, { children: _jsxs(Text, { color: colors.gray, children: [isInteractive ? border.teeLeft : border.bottomLeft, border.horizontal.repeat(boxWidth - 2), isInteractive ? border.teeRight : border.bottomRight] }) }), isInteractive && (_jsxs(_Fragment, { children: [_jsx(Box, { paddingLeft: 1, children: _jsx(CommandInput, { value: input, onChange: setInput, onSubmit: (val) => {
|
|
119
|
-
setInput('');
|
|
120
|
-
void handleChatSubmit(val);
|
|
121
|
-
}, placeholder: chatStreaming ? 'thinking...' : `chat with ${agentName} agent...` }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.gray, children: [border.bottomLeft, border.horizontal.repeat(boxWidth - 2), border.bottomRight] }) })] }))] }));
|
|
122
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export const SLASH_COMMANDS = [
|
|
2
|
-
{ name: '/skills', description: 'List available skills' },
|
|
3
|
-
{ name: '/help', description: 'Show available commands' },
|
|
4
|
-
{ name: '/clear', description: 'Clear chat history' },
|
|
5
|
-
{ name: '/memory', description: 'Show current memory state' },
|
|
6
|
-
{ name: '/backtest', description: 'Run agent against test set (/backtest <num> fetches from API)' },
|
|
7
|
-
];
|
|
8
|
-
export function filterCommands(prefix) {
|
|
9
|
-
const lowerPrefix = prefix.toLowerCase();
|
|
10
|
-
const filtered = SLASH_COMMANDS.filter((cmd) => cmd.name.toLowerCase().startsWith(lowerPrefix));
|
|
11
|
-
return filtered;
|
|
12
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect } from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import { colors, animation } from '../theme.js';
|
|
5
|
-
function buildTickerChars(step) {
|
|
6
|
-
const stepStr = String(step).padStart(2, '0');
|
|
7
|
-
const digits = stepStr.split('');
|
|
8
|
-
return animation.HEX_CHARS + digits.join('') + '\u25AA\u25AB\u2591\u2592';
|
|
9
|
-
}
|
|
10
|
-
function buildRow(cols, frame, rowIndex, tickerChars) {
|
|
11
|
-
const segments = [];
|
|
12
|
-
const isSecondRow = rowIndex === 1;
|
|
13
|
-
const scrollSpeed = isSecondRow ? 3 : 2;
|
|
14
|
-
const direction = isSecondRow ? -1 : 1;
|
|
15
|
-
const sinFreq = isSecondRow ? 0.4 : 0.3;
|
|
16
|
-
const sinPhase = isSecondRow ? -0.4 : 0.6;
|
|
17
|
-
const wrapLen = cols * 2;
|
|
18
|
-
for (let c = 0; c < cols; c++) {
|
|
19
|
-
const scrolledC = ((direction === 1)
|
|
20
|
-
? (c + frame * scrollSpeed) % wrapLen
|
|
21
|
-
: (cols - c + frame * scrollSpeed) % wrapLen);
|
|
22
|
-
const charIdx = scrolledC % tickerChars.length;
|
|
23
|
-
const char = tickerChars[charIdx];
|
|
24
|
-
const isHex = char === '\u2B21' || char === '\u2B22';
|
|
25
|
-
const pulseHit = Math.sin((c + frame * sinPhase) * sinFreq) > 0.5;
|
|
26
|
-
// Edge fade: dim the outermost 4 columns
|
|
27
|
-
const edgeDist = Math.min(c, cols - 1 - c);
|
|
28
|
-
if (edgeDist < 2) {
|
|
29
|
-
segments.push({ char: '\u00B7', color: colors.grayDim });
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (edgeDist < 4) {
|
|
33
|
-
segments.push({ char, color: colors.grayDim });
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (pulseHit && isHex) {
|
|
37
|
-
segments.push({ char, color: colors.honey });
|
|
38
|
-
}
|
|
39
|
-
else if (pulseHit) {
|
|
40
|
-
segments.push({ char, color: colors.green });
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
segments.push({ char, color: colors.grayDim });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return segments;
|
|
47
|
-
}
|
|
48
|
-
function renderSegments(segments) {
|
|
49
|
-
const elements = [];
|
|
50
|
-
let runColor = segments[0]?.color ?? colors.grayDim;
|
|
51
|
-
let runChars = '';
|
|
52
|
-
for (let i = 0; i < segments.length; i++) {
|
|
53
|
-
const seg = segments[i];
|
|
54
|
-
if (seg.color === runColor) {
|
|
55
|
-
runChars += seg.char;
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
elements.push(_jsx(Text, { color: runColor, children: runChars }, `${elements.length}`));
|
|
59
|
-
runColor = seg.color;
|
|
60
|
-
runChars = seg.char;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (runChars.length > 0) {
|
|
64
|
-
elements.push(_jsx(Text, { color: runColor, children: runChars }, `${elements.length}`));
|
|
65
|
-
}
|
|
66
|
-
return elements;
|
|
67
|
-
}
|
|
68
|
-
export function AsciiTicker({ rows = 1, step = 1 }) {
|
|
69
|
-
const [frame, setFrame] = useState(0);
|
|
70
|
-
const cols = process.stdout.columns || 60;
|
|
71
|
-
const tickerChars = buildTickerChars(step);
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
const timer = setInterval(() => {
|
|
74
|
-
setFrame((prev) => prev + 1);
|
|
75
|
-
}, animation.TICK_MS);
|
|
76
|
-
return () => {
|
|
77
|
-
clearInterval(timer);
|
|
78
|
-
};
|
|
79
|
-
}, []);
|
|
80
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: renderSegments(buildRow(cols, frame, 0, tickerChars)) }), rows === 2 && (_jsx(Text, { children: renderSegments(buildRow(cols, frame, 1, tickerChars)) }))] }));
|
|
81
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useCallback, useMemo } from 'react';
|
|
3
|
-
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import TextInput from 'ink-text-input';
|
|
5
|
-
import { colors, symbols } from '../theme.js';
|
|
6
|
-
import { filterCommands } from '../commands/registry.js';
|
|
7
|
-
export function CommandInput({ value, onChange, onSubmit, placeholder, }) {
|
|
8
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
9
|
-
// Determine if autocomplete should be active
|
|
10
|
-
const isAutocompleteActive = value.startsWith('/') && !value.includes(' ');
|
|
11
|
-
// Get filtered commands
|
|
12
|
-
const filteredCommands = useMemo(() => {
|
|
13
|
-
if (!isAutocompleteActive) {
|
|
14
|
-
return [];
|
|
15
|
-
}
|
|
16
|
-
const commands = filterCommands(value);
|
|
17
|
-
return commands;
|
|
18
|
-
}, [value, isAutocompleteActive]);
|
|
19
|
-
// Reset selection when filtered commands change
|
|
20
|
-
const commandCount = filteredCommands.length;
|
|
21
|
-
const safeSelectedIndex = commandCount > 0 ? Math.min(selectedIndex, commandCount - 1) : 0;
|
|
22
|
-
const handleChange = useCallback((newValue) => {
|
|
23
|
-
onChange(newValue);
|
|
24
|
-
setSelectedIndex(0);
|
|
25
|
-
}, [onChange]);
|
|
26
|
-
const handleSubmit = useCallback((submittedValue) => {
|
|
27
|
-
// If autocomplete is active and there are matches, complete first
|
|
28
|
-
if (isAutocompleteActive && filteredCommands.length > 0) {
|
|
29
|
-
const selected = filteredCommands[safeSelectedIndex];
|
|
30
|
-
if (selected && submittedValue !== selected.name) {
|
|
31
|
-
onChange(selected.name);
|
|
32
|
-
onSubmit(selected.name);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
onSubmit(submittedValue);
|
|
37
|
-
}, [isAutocompleteActive, filteredCommands, safeSelectedIndex, onChange, onSubmit]);
|
|
38
|
-
useInput((input, key) => {
|
|
39
|
-
if (!isAutocompleteActive || filteredCommands.length === 0) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
if (key.upArrow) {
|
|
43
|
-
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : commandCount - 1));
|
|
44
|
-
}
|
|
45
|
-
else if (key.downArrow) {
|
|
46
|
-
setSelectedIndex((prev) => (prev < commandCount - 1 ? prev + 1 : 0));
|
|
47
|
-
}
|
|
48
|
-
else if (key.tab) {
|
|
49
|
-
// Tab to complete without submitting
|
|
50
|
-
const selected = filteredCommands[safeSelectedIndex];
|
|
51
|
-
if (selected) {
|
|
52
|
-
onChange(selected.name);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
else if (key.escape) {
|
|
56
|
-
// Escape to clear input
|
|
57
|
-
onChange('');
|
|
58
|
-
setSelectedIndex(0);
|
|
59
|
-
}
|
|
60
|
-
}, { isActive: isAutocompleteActive && filteredCommands.length > 0 });
|
|
61
|
-
return (_jsxs(Box, { flexDirection: "column", children: [isAutocompleteActive && filteredCommands.length > 0 && (_jsx(Box, { flexDirection: "column", marginBottom: 0, marginLeft: 2, children: filteredCommands.map((cmd, index) => {
|
|
62
|
-
const isSelected = index === safeSelectedIndex;
|
|
63
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.honey : colors.gray, children: [isSelected ? symbols.diamond : ' ', ' '] }), _jsx(Text, { color: isSelected ? colors.honey : colors.white, children: cmd.name }), _jsxs(Text, { color: colors.gray, children: [" - ", cmd.description] })] }, cmd.name));
|
|
64
|
-
}) })), _jsxs(Box, { children: [_jsxs(Text, { color: colors.honey, children: [symbols.arrow, " "] }), _jsx(TextInput, { value: value, onChange: handleChange, onSubmit: handleSubmit, placeholder: placeholder })] })] }));
|
|
65
|
-
}
|