@terminusagents/agents 0.1.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.
- package/README.md +192 -0
- package/bin/terminus-agent.js +2 -0
- package/dist/agent/executor.d.ts +24 -0
- package/dist/agent/executor.js +96 -0
- package/dist/agents/budget-planner.d.ts +3 -0
- package/dist/agents/budget-planner.js +43 -0
- package/dist/agents/career-coach.d.ts +3 -0
- package/dist/agents/career-coach.js +43 -0
- package/dist/agents/crypto-advisor.d.ts +3 -0
- package/dist/agents/crypto-advisor.js +49 -0
- package/dist/agents/event-planner.d.ts +3 -0
- package/dist/agents/event-planner.js +41 -0
- package/dist/agents/fitness-coach.d.ts +3 -0
- package/dist/agents/fitness-coach.js +43 -0
- package/dist/agents/food-expert.d.ts +3 -0
- package/dist/agents/food-expert.js +43 -0
- package/dist/agents/fundamental-analyst.d.ts +3 -0
- package/dist/agents/fundamental-analyst.js +54 -0
- package/dist/agents/health-advisor.d.ts +3 -0
- package/dist/agents/health-advisor.js +52 -0
- package/dist/agents/index.d.ts +25 -0
- package/dist/agents/index.js +99 -0
- package/dist/agents/language-tutor.d.ts +3 -0
- package/dist/agents/language-tutor.js +41 -0
- package/dist/agents/legal-advisor.d.ts +3 -0
- package/dist/agents/legal-advisor.js +42 -0
- package/dist/agents/real-estate.d.ts +3 -0
- package/dist/agents/real-estate.js +41 -0
- package/dist/agents/shopping-assistant.d.ts +3 -0
- package/dist/agents/shopping-assistant.js +44 -0
- package/dist/agents/tech-support.d.ts +3 -0
- package/dist/agents/tech-support.js +46 -0
- package/dist/agents/technical-analyst.d.ts +3 -0
- package/dist/agents/technical-analyst.js +45 -0
- package/dist/agents/travel-planner.d.ts +3 -0
- package/dist/agents/travel-planner.js +91 -0
- package/dist/agents/types.d.ts +20 -0
- package/dist/agents/types.js +4 -0
- package/dist/cli/doctor.d.ts +4 -0
- package/dist/cli/doctor.js +184 -0
- package/dist/cli/init.d.ts +16 -0
- package/dist/cli/init.js +429 -0
- package/dist/cli/run.d.ts +1 -0
- package/dist/cli/run.js +98 -0
- package/dist/cli/status.d.ts +1 -0
- package/dist/cli/status.js +104 -0
- package/dist/config/store.d.ts +19 -0
- package/dist/config/store.js +148 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +61 -0
- package/dist/llm/provider.d.ts +56 -0
- package/dist/llm/provider.js +181 -0
- package/dist/network/client.d.ts +33 -0
- package/dist/network/client.js +389 -0
- package/package.json +63 -0
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// TERMINUS AGENT - Run Command (Enhanced UX)
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { loadConfig, configExists } from '../config/store.js';
|
|
6
|
+
import { AgentClient } from '../network/client.js';
|
|
7
|
+
const AGENT_EMOJIS = {
|
|
8
|
+
'travel-planner': 'āļø',
|
|
9
|
+
'budget-planner': 'š°',
|
|
10
|
+
'health-advisor': 'š„',
|
|
11
|
+
'fundamental-analyst': 'š',
|
|
12
|
+
'technical-analyst': 'š',
|
|
13
|
+
'crypto-advisor': 'šŖ',
|
|
14
|
+
'food-expert': 'š³',
|
|
15
|
+
'fitness-coach': 'šŖ',
|
|
16
|
+
'legal-advisor': 'āļø',
|
|
17
|
+
'real-estate': 'š ',
|
|
18
|
+
'career-coach': 'š¼',
|
|
19
|
+
'event-planner': 'š',
|
|
20
|
+
'tech-support': 'š§',
|
|
21
|
+
'shopping-assistant': 'š',
|
|
22
|
+
'language-tutor': 'š',
|
|
23
|
+
};
|
|
24
|
+
function printStartupBanner(config) {
|
|
25
|
+
if (!config)
|
|
26
|
+
return;
|
|
27
|
+
const emoji = AGENT_EMOJIS[config.agentType] || 'š¤';
|
|
28
|
+
const provider = config.llmProvider || 'grok';
|
|
29
|
+
const providerIcon = provider === 'grok' ? 'š' : provider === 'ollama' ? 'š¦' : 'š§';
|
|
30
|
+
console.log();
|
|
31
|
+
console.log(chalk.cyan.bold('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
32
|
+
console.log(chalk.cyan.bold('ā š TERMINUS AGENT STARTING ā'));
|
|
33
|
+
console.log(chalk.cyan.bold('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(` ${chalk.gray('Agent:')} ${emoji} ${chalk.white.bold(config.agentType)}`);
|
|
36
|
+
console.log(` ${chalk.gray('Node ID:')} ${chalk.cyan(config.nodeId)}`);
|
|
37
|
+
console.log(` ${chalk.gray('Wallet:')} ${config.wallet.slice(0, 10)}...${config.wallet.slice(-8)}`);
|
|
38
|
+
console.log(` ${chalk.gray('LLM Provider:')} ${providerIcon} ${provider}${config.llmModel ? ` (${config.llmModel})` : ''}`);
|
|
39
|
+
console.log(` ${chalk.gray('Profile:')} ${config.networkProfile || 'local'}`);
|
|
40
|
+
console.log(` ${chalk.gray('Control Plane:')} ${config.controlPlaneUrl}`);
|
|
41
|
+
console.log(chalk.gray('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n'));
|
|
42
|
+
console.log(chalk.yellow(' ā³ Connecting to Control Plane...\n'));
|
|
43
|
+
}
|
|
44
|
+
export async function runCommand() {
|
|
45
|
+
if (!configExists()) {
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(chalk.red.bold('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
48
|
+
console.log(chalk.red.bold('ā ā NOT CONFIGURED ā'));
|
|
49
|
+
console.log(chalk.red.bold('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
50
|
+
console.log();
|
|
51
|
+
console.log(chalk.gray(' No configuration found. Run setup first:\n'));
|
|
52
|
+
console.log(chalk.cyan(' npx terminus-agent init\n'));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
const config = loadConfig();
|
|
56
|
+
if (!config) {
|
|
57
|
+
console.log(chalk.red('\n ā Failed to load configuration.\n'));
|
|
58
|
+
console.log(chalk.gray(' Try reconfiguring:'));
|
|
59
|
+
console.log(chalk.cyan(' npx terminus-agent init\n'));
|
|
60
|
+
console.log(chalk.gray(' Or run diagnostics:'));
|
|
61
|
+
console.log(chalk.cyan(' npx terminus-agent doctor\n'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (config.networkProfile === 'mainnet' && !config.controlPlaneUrl.startsWith('wss://')) {
|
|
65
|
+
console.log(chalk.red('\n ā Mainnet profile requires wss:// control-plane URL.\n'));
|
|
66
|
+
console.log(chalk.gray(' Fix with:'));
|
|
67
|
+
console.log(chalk.cyan(' npx terminus-agent init --profile mainnet\n'));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
printStartupBanner(config);
|
|
71
|
+
if (!process.env.TERMINUS_WALLET_PRIVATE_KEY) {
|
|
72
|
+
console.log(chalk.yellow(' ā TERMINUS_WALLET_PRIVATE_KEY is not set. Challenge-signature auth will fail in strict remote mode.\n'));
|
|
73
|
+
}
|
|
74
|
+
const client = new AgentClient(config);
|
|
75
|
+
// Handle graceful shutdown
|
|
76
|
+
process.on('SIGINT', () => {
|
|
77
|
+
console.log(chalk.yellow('\n\n š Shutting down gracefully...\n'));
|
|
78
|
+
client.disconnect();
|
|
79
|
+
console.log(chalk.gray(' Agent disconnected. Goodbye! š\n'));
|
|
80
|
+
process.exit(0);
|
|
81
|
+
});
|
|
82
|
+
process.on('SIGTERM', () => {
|
|
83
|
+
console.log(chalk.yellow('\n\n š Received SIGTERM...\n'));
|
|
84
|
+
client.disconnect();
|
|
85
|
+
process.exit(0);
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
await client.connect();
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.log(chalk.red(`\n ā Connection failed: ${error.message}\n`));
|
|
92
|
+
console.log(chalk.gray(' Check if Control Plane is running:'));
|
|
93
|
+
console.log(chalk.gray(` ${config.controlPlaneUrl}\n`));
|
|
94
|
+
console.log(chalk.gray(' Run diagnostics:'));
|
|
95
|
+
console.log(chalk.cyan(' npx terminus-agent doctor\n'));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function statusCommand(): Promise<void>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// TERMINUS AGENT - Status Command (Enhanced UX)
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { loadConfig, configExists, getConfigPath } from '../config/store.js';
|
|
6
|
+
import { checkOllamaAvailable } from '../llm/provider.js';
|
|
7
|
+
const AGENT_EMOJIS = {
|
|
8
|
+
'travel-planner': 'āļø',
|
|
9
|
+
'budget-planner': 'š°',
|
|
10
|
+
'health-advisor': 'š„',
|
|
11
|
+
'fundamental-analyst': 'š',
|
|
12
|
+
'technical-analyst': 'š',
|
|
13
|
+
'crypto-advisor': 'šŖ',
|
|
14
|
+
'food-expert': 'š³',
|
|
15
|
+
'fitness-coach': 'šŖ',
|
|
16
|
+
'legal-advisor': 'āļø',
|
|
17
|
+
'real-estate': 'š ',
|
|
18
|
+
'career-coach': 'š¼',
|
|
19
|
+
'event-planner': 'š',
|
|
20
|
+
'tech-support': 'š§',
|
|
21
|
+
'shopping-assistant': 'š',
|
|
22
|
+
'language-tutor': 'š',
|
|
23
|
+
};
|
|
24
|
+
export async function statusCommand() {
|
|
25
|
+
console.log();
|
|
26
|
+
console.log(chalk.cyan.bold('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
27
|
+
console.log(chalk.cyan.bold('ā š TERMINUS AGENT STATUS ā'));
|
|
28
|
+
console.log(chalk.cyan.bold('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
29
|
+
console.log();
|
|
30
|
+
if (!configExists()) {
|
|
31
|
+
console.log(chalk.red(' ā No configuration found.\n'));
|
|
32
|
+
console.log(chalk.gray(' Run this command to set up:'));
|
|
33
|
+
console.log(chalk.cyan(' npx terminus-agent init\n'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const config = loadConfig();
|
|
37
|
+
if (!config) {
|
|
38
|
+
console.log(chalk.red(' ā Failed to load configuration.\n'));
|
|
39
|
+
console.log(chalk.gray(' Try reconfiguring:'));
|
|
40
|
+
console.log(chalk.cyan(' npx terminus-agent init\n'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Agent Info
|
|
44
|
+
console.log(chalk.yellow('āā Agent Configuration āāāāāāāāāāāāāāāāāā\n'));
|
|
45
|
+
const emoji = AGENT_EMOJIS[config.agentType] || 'š¤';
|
|
46
|
+
console.log(` ${chalk.gray('Agent Type:')} ${emoji} ${chalk.white.bold(config.agentType)}`);
|
|
47
|
+
console.log(` ${chalk.gray('Node ID:')} ${chalk.cyan(config.nodeId)}`);
|
|
48
|
+
console.log(` ${chalk.gray('Config File:')} ${chalk.gray(getConfigPath())}`);
|
|
49
|
+
// Wallet Info
|
|
50
|
+
console.log(chalk.yellow('\nāā Wallet āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n'));
|
|
51
|
+
const walletShort = `${config.wallet.slice(0, 10)}...${config.wallet.slice(-8)}`;
|
|
52
|
+
const runtimePrivateKey = process.env.TERMINUS_WALLET_PRIVATE_KEY?.trim();
|
|
53
|
+
console.log(` ${chalk.gray('Address:')} ${chalk.green(walletShort)}`);
|
|
54
|
+
console.log(` ${chalk.gray('Private Key:')} ${runtimePrivateKey ? chalk.green('ā Runtime env configured') : chalk.gray('ā Missing (set TERMINUS_WALLET_PRIVATE_KEY)')}`);
|
|
55
|
+
// LLM Provider Info
|
|
56
|
+
console.log(chalk.yellow('\nāā LLM Provider āāāāāāāāāāāāāāāāāāāāāāāāāā\n'));
|
|
57
|
+
const provider = config.llmProvider || 'grok';
|
|
58
|
+
const providerIcon = provider === 'grok' ? 'š' : provider === 'ollama' ? 'š¦' : 'š§';
|
|
59
|
+
const providerName = provider === 'grok' ? 'Grok API (xAI Cloud)'
|
|
60
|
+
: provider === 'ollama' ? 'Ollama (Local)'
|
|
61
|
+
: 'OpenAI-Compatible';
|
|
62
|
+
console.log(` ${chalk.gray('Provider:')} ${providerIcon} ${chalk.white(providerName)}`);
|
|
63
|
+
if (config.llmModel) {
|
|
64
|
+
console.log(` ${chalk.gray('Model:')} ${chalk.white(config.llmModel)}`);
|
|
65
|
+
}
|
|
66
|
+
if (config.llmBaseUrl) {
|
|
67
|
+
console.log(` ${chalk.gray('Base URL:')} ${chalk.gray(config.llmBaseUrl)}`);
|
|
68
|
+
}
|
|
69
|
+
// Provider Status Check
|
|
70
|
+
if (provider === 'ollama') {
|
|
71
|
+
const ollamaOk = await checkOllamaAvailable(config.llmBaseUrl);
|
|
72
|
+
console.log(` ${chalk.gray('Status:')} ${ollamaOk ? chalk.green('ā Connected') : chalk.red('ā Not reachable')}`);
|
|
73
|
+
}
|
|
74
|
+
else if (provider === 'grok') {
|
|
75
|
+
const hasRuntimeKey = Boolean(process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim());
|
|
76
|
+
const hasStoredKey = Boolean(config.apiKey?.trim() && config.apiKey !== '__ENV__');
|
|
77
|
+
const source = hasRuntimeKey ? 'Runtime env' : hasStoredKey ? 'Config file' : 'Missing';
|
|
78
|
+
console.log(` ${chalk.gray('API Key:')} ${hasRuntimeKey || hasStoredKey ? chalk.green(`ā ${source}`) : chalk.red('ā Missing')}`);
|
|
79
|
+
}
|
|
80
|
+
// Network Info
|
|
81
|
+
console.log(chalk.yellow('\nāā Network āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n'));
|
|
82
|
+
console.log(` ${chalk.gray('Control Plane:')} ${chalk.cyan(config.controlPlaneUrl)}`);
|
|
83
|
+
// Test connection hint
|
|
84
|
+
const profile = config.networkProfile || (config.controlPlaneUrl.includes('localhost') ? 'local' : 'testnet');
|
|
85
|
+
if (profile === 'local') {
|
|
86
|
+
console.log(` ${chalk.gray('Profile:')} ${chalk.yellow('Local development')}`);
|
|
87
|
+
}
|
|
88
|
+
else if (profile === 'testnet') {
|
|
89
|
+
console.log(` ${chalk.gray('Profile:')} ${chalk.cyan('Base Sepolia testnet')}`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log(` ${chalk.gray('Profile:')} ${chalk.green('Base mainnet')}`);
|
|
93
|
+
}
|
|
94
|
+
if (profile === 'mainnet' && !config.controlPlaneUrl.startsWith('wss://')) {
|
|
95
|
+
console.log(` ${chalk.gray('Transport:')} ${chalk.red('Invalid for mainnet (requires wss://)')}`);
|
|
96
|
+
}
|
|
97
|
+
// Footer
|
|
98
|
+
console.log(chalk.gray('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n'));
|
|
99
|
+
console.log(chalk.white.bold('š Quick Commands:\n'));
|
|
100
|
+
console.log(` ${chalk.cyan('npx terminus-agent run')} ${chalk.gray('Start the agent')}`);
|
|
101
|
+
console.log(` ${chalk.cyan('npx terminus-agent init')} ${chalk.gray('Reconfigure')}`);
|
|
102
|
+
console.log(` ${chalk.cyan('npx terminus-agent doctor')} ${chalk.gray('Run readiness checks')}`);
|
|
103
|
+
console.log();
|
|
104
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type NetworkProfile = 'local' | 'testnet' | 'mainnet';
|
|
2
|
+
export interface AgentConfig {
|
|
3
|
+
agentType: string;
|
|
4
|
+
wallet: string;
|
|
5
|
+
privateKey?: string;
|
|
6
|
+
apiKey: string;
|
|
7
|
+
controlPlaneUrl: string;
|
|
8
|
+
nodeId: string;
|
|
9
|
+
llmProvider?: 'grok' | 'ollama' | 'openai-compatible';
|
|
10
|
+
llmBaseUrl?: string;
|
|
11
|
+
llmModel?: string;
|
|
12
|
+
networkProfile?: NetworkProfile;
|
|
13
|
+
}
|
|
14
|
+
export declare function getConfigPath(): string;
|
|
15
|
+
export declare function configExists(): boolean;
|
|
16
|
+
export declare function loadConfig(): AgentConfig | null;
|
|
17
|
+
export declare function validateConfig(config: AgentConfig): string[];
|
|
18
|
+
export declare function saveConfig(config: AgentConfig): void;
|
|
19
|
+
export declare function generateNodeId(agentType: string, wallet: string): string;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// TERMINUS AGENT - Config Store
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
const CONFIG_DIR = join(homedir(), '.terminus');
|
|
9
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
10
|
+
const FILE_MODE = 0o600;
|
|
11
|
+
const DIR_MODE = 0o700;
|
|
12
|
+
export function getConfigPath() {
|
|
13
|
+
return CONFIG_FILE;
|
|
14
|
+
}
|
|
15
|
+
export function configExists() {
|
|
16
|
+
return existsSync(CONFIG_FILE);
|
|
17
|
+
}
|
|
18
|
+
export function loadConfig() {
|
|
19
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
securePermissions(CONFIG_DIR, DIR_MODE);
|
|
23
|
+
securePermissions(CONFIG_FILE, FILE_MODE);
|
|
24
|
+
try {
|
|
25
|
+
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
|
26
|
+
const parsed = JSON.parse(content);
|
|
27
|
+
const normalized = normalizeConfig(parsed);
|
|
28
|
+
if (!normalized) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const errors = validateConfig(normalized);
|
|
32
|
+
return errors.length === 0 ? normalized : null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function validateConfig(config) {
|
|
39
|
+
const errors = [];
|
|
40
|
+
if (!config.agentType || typeof config.agentType !== 'string') {
|
|
41
|
+
errors.push('agentType is required');
|
|
42
|
+
}
|
|
43
|
+
if (!isValidWallet(config.wallet)) {
|
|
44
|
+
errors.push('wallet must be a valid EVM address');
|
|
45
|
+
}
|
|
46
|
+
if (!config.controlPlaneUrl || !isWsUrl(config.controlPlaneUrl)) {
|
|
47
|
+
errors.push('controlPlaneUrl must start with ws:// or wss://');
|
|
48
|
+
}
|
|
49
|
+
if (!config.nodeId || typeof config.nodeId !== 'string') {
|
|
50
|
+
errors.push('nodeId is required');
|
|
51
|
+
}
|
|
52
|
+
const provider = config.llmProvider || 'grok';
|
|
53
|
+
if (provider === 'grok') {
|
|
54
|
+
const hasStoredKey = typeof config.apiKey === 'string' && config.apiKey.length > 0 && config.apiKey !== '__ENV__';
|
|
55
|
+
const hasRuntimeKey = Boolean(process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim());
|
|
56
|
+
if (!hasStoredKey && !hasRuntimeKey) {
|
|
57
|
+
errors.push('grok provider requires apiKey in config or TERMINUS_GROK_API_KEY/XAI_API_KEY env');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (provider === 'openai-compatible' && !config.llmBaseUrl) {
|
|
61
|
+
errors.push('openai-compatible provider requires llmBaseUrl');
|
|
62
|
+
}
|
|
63
|
+
if (provider === 'ollama' && !config.llmBaseUrl) {
|
|
64
|
+
errors.push('ollama provider should include llmBaseUrl');
|
|
65
|
+
}
|
|
66
|
+
return errors;
|
|
67
|
+
}
|
|
68
|
+
export function saveConfig(config) {
|
|
69
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
70
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: DIR_MODE });
|
|
71
|
+
}
|
|
72
|
+
securePermissions(CONFIG_DIR, DIR_MODE);
|
|
73
|
+
const provider = config.llmProvider || 'grok';
|
|
74
|
+
const normalizedApiKey = provider === 'grok'
|
|
75
|
+
? (config.apiKey || '__ENV__')
|
|
76
|
+
: (config.apiKey || '');
|
|
77
|
+
const normalizedProfile = normalizeNetworkProfile(config.networkProfile);
|
|
78
|
+
const sanitized = {
|
|
79
|
+
agentType: config.agentType.trim(),
|
|
80
|
+
wallet: config.wallet.trim(),
|
|
81
|
+
apiKey: normalizedApiKey.trim(),
|
|
82
|
+
controlPlaneUrl: config.controlPlaneUrl.trim(),
|
|
83
|
+
nodeId: config.nodeId.trim(),
|
|
84
|
+
llmProvider: provider,
|
|
85
|
+
llmBaseUrl: config.llmBaseUrl?.trim() || undefined,
|
|
86
|
+
llmModel: config.llmModel?.trim() || undefined,
|
|
87
|
+
networkProfile: normalizedProfile,
|
|
88
|
+
privateKey: undefined,
|
|
89
|
+
};
|
|
90
|
+
const validationErrors = validateConfig(sanitized);
|
|
91
|
+
if (validationErrors.length > 0) {
|
|
92
|
+
throw new Error(`Invalid configuration: ${validationErrors.join('; ')}`);
|
|
93
|
+
}
|
|
94
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(sanitized, null, 2), { mode: FILE_MODE });
|
|
95
|
+
securePermissions(CONFIG_FILE, FILE_MODE);
|
|
96
|
+
}
|
|
97
|
+
export function generateNodeId(agentType, wallet) {
|
|
98
|
+
// Stable readable prefix + entropy suffix for collision resistance across operators.
|
|
99
|
+
const walletPrefix = wallet.slice(2, 8).toLowerCase();
|
|
100
|
+
const suffix = randomUUID().split('-')[0];
|
|
101
|
+
return `${agentType}-${walletPrefix}-${suffix}`;
|
|
102
|
+
}
|
|
103
|
+
function normalizeConfig(raw) {
|
|
104
|
+
if (typeof raw !== 'object' || raw === null)
|
|
105
|
+
return null;
|
|
106
|
+
const value = raw;
|
|
107
|
+
const llmProvider = value.llmProvider;
|
|
108
|
+
const provider = llmProvider === 'ollama' || llmProvider === 'openai-compatible' || llmProvider === 'grok'
|
|
109
|
+
? llmProvider
|
|
110
|
+
: 'grok';
|
|
111
|
+
const networkProfile = normalizeNetworkProfile(value.networkProfile);
|
|
112
|
+
const llmBaseUrl = typeof value.llmBaseUrl === 'string'
|
|
113
|
+
? value.llmBaseUrl.trim()
|
|
114
|
+
: provider === 'ollama'
|
|
115
|
+
? 'http://localhost:11434'
|
|
116
|
+
: undefined;
|
|
117
|
+
return {
|
|
118
|
+
agentType: typeof value.agentType === 'string' ? value.agentType.trim() : '',
|
|
119
|
+
wallet: typeof value.wallet === 'string' ? value.wallet.trim() : '',
|
|
120
|
+
apiKey: typeof value.apiKey === 'string' ? value.apiKey.trim() : '',
|
|
121
|
+
controlPlaneUrl: typeof value.controlPlaneUrl === 'string' ? value.controlPlaneUrl.trim() : '',
|
|
122
|
+
nodeId: typeof value.nodeId === 'string' ? value.nodeId.trim() : '',
|
|
123
|
+
llmProvider: provider,
|
|
124
|
+
llmBaseUrl,
|
|
125
|
+
llmModel: typeof value.llmModel === 'string' ? value.llmModel.trim() : undefined,
|
|
126
|
+
networkProfile,
|
|
127
|
+
privateKey: undefined,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function isWsUrl(url) {
|
|
131
|
+
return url.startsWith('ws://') || url.startsWith('wss://');
|
|
132
|
+
}
|
|
133
|
+
function isValidWallet(wallet) {
|
|
134
|
+
return /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
|
135
|
+
}
|
|
136
|
+
function securePermissions(path, mode) {
|
|
137
|
+
try {
|
|
138
|
+
chmodSync(path, mode);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Some environments (e.g., Windows) may not apply POSIX modes.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function normalizeNetworkProfile(value) {
|
|
145
|
+
if (value === 'testnet' || value === 'mainnet')
|
|
146
|
+
return value;
|
|
147
|
+
return 'local';
|
|
148
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// TERMINUS AGENT - CLI Entry Point
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import { program } from 'commander';
|
|
5
|
+
import { initCommand } from './cli/init.js';
|
|
6
|
+
import { runCommand } from './cli/run.js';
|
|
7
|
+
import { statusCommand } from './cli/status.js';
|
|
8
|
+
import { doctorCommand } from './cli/doctor.js';
|
|
9
|
+
async function withCliErrorHandling(task) {
|
|
10
|
+
try {
|
|
11
|
+
await task();
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
15
|
+
console.error(`\nā ${message}\n`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
program
|
|
20
|
+
.name('terminus-agent')
|
|
21
|
+
.description('Standalone agent runner for Terminus network')
|
|
22
|
+
.version('0.1.0');
|
|
23
|
+
program
|
|
24
|
+
.command('init')
|
|
25
|
+
.description('Initialize agent configuration')
|
|
26
|
+
.option('--agent-type <id>', 'Agent type id (example: travel-planner)')
|
|
27
|
+
.option('--wallet <address>', 'Wallet address for payouts')
|
|
28
|
+
.option('--llm-provider <provider>', 'grok | ollama | openai-compatible')
|
|
29
|
+
.option('--api-key <key>', 'API key for provider')
|
|
30
|
+
.option('--llm-base-url <url>', 'Provider base URL')
|
|
31
|
+
.option('--llm-model <name>', 'LLM model name')
|
|
32
|
+
.option('--control-plane-url <url>', 'Control plane websocket URL')
|
|
33
|
+
.option('--profile <profile>', 'local | testnet | mainnet')
|
|
34
|
+
.option('--yes', 'Non-interactive mode (use flags/env/defaults)')
|
|
35
|
+
.option('--force', 'Overwrite existing config without confirmation')
|
|
36
|
+
.action((options) => withCliErrorHandling(() => initCommand({
|
|
37
|
+
agentType: options.agentType,
|
|
38
|
+
wallet: options.wallet,
|
|
39
|
+
llmProvider: options.llmProvider,
|
|
40
|
+
apiKey: options.apiKey,
|
|
41
|
+
llmBaseUrl: options.llmBaseUrl,
|
|
42
|
+
llmModel: options.llmModel,
|
|
43
|
+
controlPlaneUrl: options.controlPlaneUrl,
|
|
44
|
+
profile: options.profile,
|
|
45
|
+
yes: Boolean(options.yes),
|
|
46
|
+
force: Boolean(options.force),
|
|
47
|
+
})));
|
|
48
|
+
program
|
|
49
|
+
.command('run')
|
|
50
|
+
.description('Start the agent and connect to Control Plane')
|
|
51
|
+
.action(() => withCliErrorHandling(runCommand));
|
|
52
|
+
program
|
|
53
|
+
.command('status')
|
|
54
|
+
.description('Check agent status')
|
|
55
|
+
.action(() => withCliErrorHandling(statusCommand));
|
|
56
|
+
program
|
|
57
|
+
.command('doctor')
|
|
58
|
+
.description('Run onboarding and runtime diagnostics')
|
|
59
|
+
.option('--full', 'Run connectivity/API checks in addition to static checks')
|
|
60
|
+
.action((options) => withCliErrorHandling(() => doctorCommand({ full: Boolean(options.full) })));
|
|
61
|
+
program.parse();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface LLMMessage {
|
|
2
|
+
role: 'system' | 'user' | 'assistant';
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface LLMResponse {
|
|
6
|
+
content: string;
|
|
7
|
+
model: string;
|
|
8
|
+
tokensUsed?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface LLMProviderConfig {
|
|
11
|
+
provider: 'grok' | 'ollama' | 'openai-compatible';
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface LLMProvider {
|
|
17
|
+
name: string;
|
|
18
|
+
chat(messages: LLMMessage[], options?: {
|
|
19
|
+
maxTokens?: number;
|
|
20
|
+
temperature?: number;
|
|
21
|
+
}): Promise<LLMResponse>;
|
|
22
|
+
}
|
|
23
|
+
export declare class GrokProvider implements LLMProvider {
|
|
24
|
+
name: string;
|
|
25
|
+
private apiKey;
|
|
26
|
+
private model;
|
|
27
|
+
constructor(apiKey: string, model?: string);
|
|
28
|
+
chat(messages: LLMMessage[], options?: {
|
|
29
|
+
maxTokens?: number;
|
|
30
|
+
temperature?: number;
|
|
31
|
+
}): Promise<LLMResponse>;
|
|
32
|
+
}
|
|
33
|
+
export declare class OllamaProvider implements LLMProvider {
|
|
34
|
+
name: string;
|
|
35
|
+
private baseUrl;
|
|
36
|
+
private model;
|
|
37
|
+
constructor(baseUrl?: string, model?: string);
|
|
38
|
+
chat(messages: LLMMessage[], options?: {
|
|
39
|
+
maxTokens?: number;
|
|
40
|
+
temperature?: number;
|
|
41
|
+
}): Promise<LLMResponse>;
|
|
42
|
+
}
|
|
43
|
+
export declare class OpenAICompatibleProvider implements LLMProvider {
|
|
44
|
+
name: string;
|
|
45
|
+
private baseUrl;
|
|
46
|
+
private apiKey;
|
|
47
|
+
private model;
|
|
48
|
+
constructor(baseUrl: string, model: string, apiKey?: string);
|
|
49
|
+
chat(messages: LLMMessage[], options?: {
|
|
50
|
+
maxTokens?: number;
|
|
51
|
+
temperature?: number;
|
|
52
|
+
}): Promise<LLMResponse>;
|
|
53
|
+
}
|
|
54
|
+
export declare function createLLMProvider(config: LLMProviderConfig): LLMProvider;
|
|
55
|
+
export declare function checkOllamaAvailable(baseUrl?: string): Promise<boolean>;
|
|
56
|
+
export declare function listOllamaModels(baseUrl?: string): Promise<string[]>;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// TERMINUS AGENT - LLM Provider Interface
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Abstraction layer for different LLM backends.
|
|
5
|
+
// Supports: xAI Grok (API), Ollama (local), OpenAI-compatible (local/cloud)
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// xAI Grok Provider
|
|
9
|
+
// =============================================================================
|
|
10
|
+
export class GrokProvider {
|
|
11
|
+
name = 'grok';
|
|
12
|
+
apiKey;
|
|
13
|
+
model;
|
|
14
|
+
constructor(apiKey, model = 'grok-4-1-fast-non-reasoning') {
|
|
15
|
+
this.apiKey = apiKey;
|
|
16
|
+
this.model = model;
|
|
17
|
+
}
|
|
18
|
+
async chat(messages, options) {
|
|
19
|
+
const response = await fetch('https://api.x.ai/v1/chat/completions', {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
model: this.model,
|
|
27
|
+
messages,
|
|
28
|
+
max_tokens: options?.maxTokens ?? 1024,
|
|
29
|
+
temperature: options?.temperature ?? 0.7,
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const error = await response.text();
|
|
34
|
+
throw new Error(`Grok API error: ${response.status} - ${error}`);
|
|
35
|
+
}
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
return {
|
|
38
|
+
content: data.choices[0]?.message?.content || '',
|
|
39
|
+
model: this.model,
|
|
40
|
+
tokensUsed: data.usage?.total_tokens,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Ollama Provider (Local LLM)
|
|
46
|
+
// =============================================================================
|
|
47
|
+
export class OllamaProvider {
|
|
48
|
+
name = 'ollama';
|
|
49
|
+
baseUrl;
|
|
50
|
+
model;
|
|
51
|
+
constructor(baseUrl = 'http://localhost:11434', model = 'llama3') {
|
|
52
|
+
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
53
|
+
this.model = model;
|
|
54
|
+
}
|
|
55
|
+
async chat(messages, options) {
|
|
56
|
+
// Ollama uses /api/chat endpoint
|
|
57
|
+
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
model: this.model,
|
|
64
|
+
messages,
|
|
65
|
+
stream: false,
|
|
66
|
+
options: {
|
|
67
|
+
num_predict: options?.maxTokens ?? 1024,
|
|
68
|
+
temperature: options?.temperature ?? 0.7,
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const error = await response.text();
|
|
74
|
+
throw new Error(`Ollama error: ${response.status} - ${error}`);
|
|
75
|
+
}
|
|
76
|
+
const data = await response.json();
|
|
77
|
+
return {
|
|
78
|
+
content: data.message?.content || '',
|
|
79
|
+
model: this.model,
|
|
80
|
+
tokensUsed: (data.eval_count || 0) + (data.prompt_eval_count || 0),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// =============================================================================
|
|
85
|
+
// OpenAI-Compatible Provider (LM Studio, LocalAI, vLLM, etc.)
|
|
86
|
+
// =============================================================================
|
|
87
|
+
export class OpenAICompatibleProvider {
|
|
88
|
+
name = 'openai-compatible';
|
|
89
|
+
baseUrl;
|
|
90
|
+
apiKey;
|
|
91
|
+
model;
|
|
92
|
+
constructor(baseUrl, model, apiKey) {
|
|
93
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
94
|
+
this.model = model;
|
|
95
|
+
const normalized = apiKey?.trim();
|
|
96
|
+
this.apiKey = normalized && normalized !== 'not-required' ? normalized : undefined;
|
|
97
|
+
}
|
|
98
|
+
async chat(messages, options) {
|
|
99
|
+
const headers = {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
};
|
|
102
|
+
if (this.apiKey) {
|
|
103
|
+
headers.Authorization = `Bearer ${this.apiKey}`;
|
|
104
|
+
}
|
|
105
|
+
const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers,
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
model: this.model,
|
|
110
|
+
messages,
|
|
111
|
+
max_tokens: options?.maxTokens ?? 1024,
|
|
112
|
+
temperature: options?.temperature ?? 0.7,
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
const error = await response.text();
|
|
117
|
+
throw new Error(`OpenAI-compatible API error: ${response.status} - ${error}`);
|
|
118
|
+
}
|
|
119
|
+
const data = await response.json();
|
|
120
|
+
return {
|
|
121
|
+
content: data.choices[0]?.message?.content || '',
|
|
122
|
+
model: this.model,
|
|
123
|
+
tokensUsed: data.usage?.total_tokens,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// =============================================================================
|
|
128
|
+
// Factory Function
|
|
129
|
+
// =============================================================================
|
|
130
|
+
export function createLLMProvider(config) {
|
|
131
|
+
switch (config.provider) {
|
|
132
|
+
case 'grok':
|
|
133
|
+
{
|
|
134
|
+
const apiKey = resolveGrokApiKey(config.apiKey);
|
|
135
|
+
if (!apiKey) {
|
|
136
|
+
throw new Error('Grok provider requires a key in config or TERMINUS_GROK_API_KEY/XAI_API_KEY env');
|
|
137
|
+
}
|
|
138
|
+
return new GrokProvider(apiKey, config.model);
|
|
139
|
+
}
|
|
140
|
+
case 'ollama':
|
|
141
|
+
return new OllamaProvider(config.baseUrl || 'http://localhost:11434', config.model || 'llama3');
|
|
142
|
+
case 'openai-compatible':
|
|
143
|
+
if (!config.baseUrl) {
|
|
144
|
+
throw new Error('OpenAI-compatible provider requires baseUrl');
|
|
145
|
+
}
|
|
146
|
+
return new OpenAICompatibleProvider(config.baseUrl, config.model || 'gpt-3.5-turbo', config.apiKey);
|
|
147
|
+
default:
|
|
148
|
+
throw new Error(`Unknown LLM provider: ${config.provider}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// =============================================================================
|
|
152
|
+
// Helper to detect Ollama availability
|
|
153
|
+
// =============================================================================
|
|
154
|
+
export async function checkOllamaAvailable(baseUrl = 'http://localhost:11434') {
|
|
155
|
+
try {
|
|
156
|
+
const response = await fetch(`${baseUrl}/api/tags`, { method: 'GET' });
|
|
157
|
+
return response.ok;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export async function listOllamaModels(baseUrl = 'http://localhost:11434') {
|
|
164
|
+
try {
|
|
165
|
+
const response = await fetch(`${baseUrl}/api/tags`, { method: 'GET' });
|
|
166
|
+
if (!response.ok)
|
|
167
|
+
return [];
|
|
168
|
+
const data = await response.json();
|
|
169
|
+
return data.models?.map(m => m.name) || [];
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function resolveGrokApiKey(configApiKey) {
|
|
176
|
+
const key = configApiKey?.trim();
|
|
177
|
+
if (key && key !== '__ENV__')
|
|
178
|
+
return key;
|
|
179
|
+
const runtimeKey = process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim();
|
|
180
|
+
return runtimeKey || undefined;
|
|
181
|
+
}
|