@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.
Files changed (55) hide show
  1. package/README.md +192 -0
  2. package/bin/terminus-agent.js +2 -0
  3. package/dist/agent/executor.d.ts +24 -0
  4. package/dist/agent/executor.js +96 -0
  5. package/dist/agents/budget-planner.d.ts +3 -0
  6. package/dist/agents/budget-planner.js +43 -0
  7. package/dist/agents/career-coach.d.ts +3 -0
  8. package/dist/agents/career-coach.js +43 -0
  9. package/dist/agents/crypto-advisor.d.ts +3 -0
  10. package/dist/agents/crypto-advisor.js +49 -0
  11. package/dist/agents/event-planner.d.ts +3 -0
  12. package/dist/agents/event-planner.js +41 -0
  13. package/dist/agents/fitness-coach.d.ts +3 -0
  14. package/dist/agents/fitness-coach.js +43 -0
  15. package/dist/agents/food-expert.d.ts +3 -0
  16. package/dist/agents/food-expert.js +43 -0
  17. package/dist/agents/fundamental-analyst.d.ts +3 -0
  18. package/dist/agents/fundamental-analyst.js +54 -0
  19. package/dist/agents/health-advisor.d.ts +3 -0
  20. package/dist/agents/health-advisor.js +52 -0
  21. package/dist/agents/index.d.ts +25 -0
  22. package/dist/agents/index.js +99 -0
  23. package/dist/agents/language-tutor.d.ts +3 -0
  24. package/dist/agents/language-tutor.js +41 -0
  25. package/dist/agents/legal-advisor.d.ts +3 -0
  26. package/dist/agents/legal-advisor.js +42 -0
  27. package/dist/agents/real-estate.d.ts +3 -0
  28. package/dist/agents/real-estate.js +41 -0
  29. package/dist/agents/shopping-assistant.d.ts +3 -0
  30. package/dist/agents/shopping-assistant.js +44 -0
  31. package/dist/agents/tech-support.d.ts +3 -0
  32. package/dist/agents/tech-support.js +46 -0
  33. package/dist/agents/technical-analyst.d.ts +3 -0
  34. package/dist/agents/technical-analyst.js +45 -0
  35. package/dist/agents/travel-planner.d.ts +3 -0
  36. package/dist/agents/travel-planner.js +91 -0
  37. package/dist/agents/types.d.ts +20 -0
  38. package/dist/agents/types.js +4 -0
  39. package/dist/cli/doctor.d.ts +4 -0
  40. package/dist/cli/doctor.js +184 -0
  41. package/dist/cli/init.d.ts +16 -0
  42. package/dist/cli/init.js +429 -0
  43. package/dist/cli/run.d.ts +1 -0
  44. package/dist/cli/run.js +98 -0
  45. package/dist/cli/status.d.ts +1 -0
  46. package/dist/cli/status.js +104 -0
  47. package/dist/config/store.d.ts +19 -0
  48. package/dist/config/store.js +148 -0
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.js +61 -0
  51. package/dist/llm/provider.d.ts +56 -0
  52. package/dist/llm/provider.js +181 -0
  53. package/dist/network/client.d.ts +33 -0
  54. package/dist/network/client.js +389 -0
  55. package/package.json +63 -0
@@ -0,0 +1,20 @@
1
+ export type ToolParams = Record<string, unknown>;
2
+ export type ToolResult = {
3
+ success: boolean;
4
+ data: unknown;
5
+ };
6
+ export interface AgentTool {
7
+ name: string;
8
+ description: string;
9
+ parameters: string[];
10
+ }
11
+ export interface AgentDefinition {
12
+ id: string;
13
+ name: string;
14
+ description: string;
15
+ systemPrompt: string;
16
+ tools: AgentTool[];
17
+ keywords: string[];
18
+ }
19
+ export type ToolImplementation = (params: ToolParams) => Promise<unknown>;
20
+ export type ToolRegistry = Record<string, ToolImplementation>;
@@ -0,0 +1,4 @@
1
+ // =============================================================================
2
+ // TERMINUS AGENTS - Agent Types
3
+ // =============================================================================
4
+ export {};
@@ -0,0 +1,4 @@
1
+ export interface DoctorCommandOptions {
2
+ full?: boolean;
3
+ }
4
+ export declare function doctorCommand(options?: DoctorCommandOptions): Promise<void>;
@@ -0,0 +1,184 @@
1
+ // =============================================================================
2
+ // TERMINUS AGENT - Doctor Command
3
+ // =============================================================================
4
+ import chalk from 'chalk';
5
+ import WebSocket from 'ws';
6
+ import { ethers } from 'ethers';
7
+ import { checkOllamaAvailable } from '../llm/provider.js';
8
+ import { configExists, loadConfig } from '../config/store.js';
9
+ function printCheck(label, result) {
10
+ const status = result.ok ? chalk.green('PASS') : chalk.red('FAIL');
11
+ console.log(`${status} ${chalk.white(label)} ${chalk.gray('-')} ${result.message}`);
12
+ }
13
+ function isValidWallet(value) {
14
+ return /^0x[a-fA-F0-9]{40}$/.test(value);
15
+ }
16
+ function isWsUrl(value) {
17
+ return value.startsWith('ws://') || value.startsWith('wss://');
18
+ }
19
+ function getRuntimeGrokKey(storedApiKey) {
20
+ if (storedApiKey && storedApiKey !== '__ENV__')
21
+ return storedApiKey;
22
+ return process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim();
23
+ }
24
+ async function checkWebSocketReachability(url, timeoutMs) {
25
+ return new Promise((resolve) => {
26
+ let settled = false;
27
+ const ws = new WebSocket(url);
28
+ const timer = setTimeout(() => {
29
+ if (settled)
30
+ return;
31
+ settled = true;
32
+ ws.terminate();
33
+ resolve({ ok: false, message: `No response within ${timeoutMs}ms` });
34
+ }, timeoutMs);
35
+ ws.once('open', () => {
36
+ if (settled)
37
+ return;
38
+ settled = true;
39
+ clearTimeout(timer);
40
+ ws.close();
41
+ resolve({ ok: true, message: 'Socket is reachable' });
42
+ });
43
+ ws.once('error', (error) => {
44
+ if (settled)
45
+ return;
46
+ settled = true;
47
+ clearTimeout(timer);
48
+ resolve({ ok: false, message: error.message });
49
+ });
50
+ });
51
+ }
52
+ async function checkGrokApiKey(apiKey, fullCheck) {
53
+ if (!apiKey.startsWith('xai-')) {
54
+ return { ok: false, message: 'Key format invalid (expected xai-...)' };
55
+ }
56
+ if (!fullCheck) {
57
+ return { ok: true, message: 'Key format looks valid' };
58
+ }
59
+ try {
60
+ const response = await fetch('https://api.x.ai/v1/models', {
61
+ headers: {
62
+ Authorization: `Bearer ${apiKey}`,
63
+ },
64
+ });
65
+ if (response.ok) {
66
+ return { ok: true, message: 'xAI API reachable and key accepted' };
67
+ }
68
+ return { ok: false, message: `xAI API returned ${response.status}` };
69
+ }
70
+ catch (error) {
71
+ return { ok: false, message: error.message };
72
+ }
73
+ }
74
+ async function checkOpenAiCompatible(baseUrl, fullCheck) {
75
+ if (!baseUrl) {
76
+ return { ok: false, message: 'Base URL is missing' };
77
+ }
78
+ if (!fullCheck) {
79
+ return { ok: true, message: `Configured (${baseUrl})` };
80
+ }
81
+ try {
82
+ const response = await fetch(`${baseUrl.replace(/\/$/, '')}/v1/models`);
83
+ if (response.ok) {
84
+ return { ok: true, message: 'Server reachable' };
85
+ }
86
+ return { ok: false, message: `Server returned ${response.status}` };
87
+ }
88
+ catch (error) {
89
+ return { ok: false, message: error.message };
90
+ }
91
+ }
92
+ export async function doctorCommand(options = {}) {
93
+ const fullCheck = options.full === true;
94
+ let allOk = true;
95
+ console.log();
96
+ console.log(chalk.cyan.bold('Terminus Agent Doctor'));
97
+ console.log(chalk.gray('Checks local config, auth readiness, connectivity, and provider health.\n'));
98
+ if (!configExists()) {
99
+ printCheck('Config file', { ok: false, message: 'Missing config (~/.terminus/config.json)' });
100
+ console.log(chalk.cyan('\nRun: npx terminus-agent init\n'));
101
+ return;
102
+ }
103
+ const config = loadConfig();
104
+ if (!config) {
105
+ printCheck('Config file', { ok: false, message: 'Config exists but is invalid. Re-run init.' });
106
+ console.log(chalk.cyan('\nRun: npx terminus-agent init --force\n'));
107
+ return;
108
+ }
109
+ const walletCheck = isValidWallet(config.wallet)
110
+ ? { ok: true, message: `${config.wallet.slice(0, 10)}...${config.wallet.slice(-8)}` }
111
+ : { ok: false, message: 'Invalid wallet format' };
112
+ printCheck('Wallet format', walletCheck);
113
+ allOk = allOk && walletCheck.ok;
114
+ const privateKey = process.env.TERMINUS_WALLET_PRIVATE_KEY?.trim();
115
+ const privateKeyCheck = privateKey
116
+ ? { ok: true, message: 'TERMINUS_WALLET_PRIVATE_KEY is set' }
117
+ : { ok: false, message: 'Missing TERMINUS_WALLET_PRIVATE_KEY' };
118
+ printCheck('Runtime private key', privateKeyCheck);
119
+ allOk = allOk && privateKeyCheck.ok;
120
+ if (privateKey) {
121
+ try {
122
+ const signer = new ethers.Wallet(privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`);
123
+ const matches = signer.address.toLowerCase() === config.wallet.toLowerCase();
124
+ const matchResult = matches
125
+ ? { ok: true, message: 'Signer wallet matches configured wallet' }
126
+ : { ok: false, message: `Signer wallet ${signer.address} differs from config wallet` };
127
+ printCheck('Wallet/private key match', matchResult);
128
+ allOk = allOk && matchResult.ok;
129
+ }
130
+ catch (error) {
131
+ const invalidPk = { ok: false, message: `Invalid private key: ${error.message}` };
132
+ printCheck('Wallet/private key match', invalidPk);
133
+ allOk = false;
134
+ }
135
+ }
136
+ const cpUrlCheck = isWsUrl(config.controlPlaneUrl)
137
+ ? { ok: true, message: config.controlPlaneUrl }
138
+ : { ok: false, message: 'Control plane URL must start with ws:// or wss://' };
139
+ printCheck('Control plane URL format', cpUrlCheck);
140
+ allOk = allOk && cpUrlCheck.ok;
141
+ const mainnetWssCheck = config.networkProfile === 'mainnet' && !config.controlPlaneUrl.startsWith('wss://')
142
+ ? { ok: false, message: 'Mainnet profile requires wss:// URL' }
143
+ : { ok: true, message: 'Transport profile is valid' };
144
+ printCheck('Transport policy', mainnetWssCheck);
145
+ allOk = allOk && mainnetWssCheck.ok;
146
+ if (cpUrlCheck.ok) {
147
+ const reachability = await checkWebSocketReachability(config.controlPlaneUrl, 5000);
148
+ printCheck('Control plane reachability', reachability);
149
+ allOk = allOk && reachability.ok;
150
+ }
151
+ const provider = config.llmProvider || 'grok';
152
+ if (provider === 'grok') {
153
+ const key = getRuntimeGrokKey(config.apiKey);
154
+ const result = key
155
+ ? await checkGrokApiKey(key, fullCheck)
156
+ : { ok: false, message: 'No Grok key found (config or env)' };
157
+ printCheck('Grok provider', result);
158
+ allOk = allOk && result.ok;
159
+ }
160
+ else if (provider === 'ollama') {
161
+ const baseUrl = config.llmBaseUrl || 'http://localhost:11434';
162
+ const available = await checkOllamaAvailable(baseUrl);
163
+ const result = available
164
+ ? { ok: true, message: `Ollama reachable at ${baseUrl}` }
165
+ : { ok: false, message: `Ollama not reachable at ${baseUrl}` };
166
+ printCheck('Ollama provider', result);
167
+ allOk = allOk && result.ok;
168
+ }
169
+ else {
170
+ const result = await checkOpenAiCompatible(config.llmBaseUrl || '', fullCheck);
171
+ printCheck('OpenAI-compatible provider', result);
172
+ allOk = allOk && result.ok;
173
+ }
174
+ console.log();
175
+ if (allOk) {
176
+ console.log(chalk.green.bold('Doctor result: READY'));
177
+ console.log(chalk.cyan('Start node: npx terminus-agent run\n'));
178
+ }
179
+ else {
180
+ console.log(chalk.red.bold('Doctor result: NOT READY'));
181
+ console.log(chalk.cyan('Fix failed checks, then run: npx terminus-agent doctor --full\n'));
182
+ process.exitCode = 1;
183
+ }
184
+ }
@@ -0,0 +1,16 @@
1
+ import type { NetworkProfile } from '../config/store.js';
2
+ type LlmProvider = 'grok' | 'ollama' | 'openai-compatible';
3
+ export interface InitCommandOptions {
4
+ agentType?: string;
5
+ wallet?: string;
6
+ llmProvider?: LlmProvider;
7
+ apiKey?: string;
8
+ llmBaseUrl?: string;
9
+ llmModel?: string;
10
+ controlPlaneUrl?: string;
11
+ profile?: NetworkProfile;
12
+ yes?: boolean;
13
+ force?: boolean;
14
+ }
15
+ export declare function initCommand(rawOptions?: InitCommandOptions): Promise<void>;
16
+ export {};
@@ -0,0 +1,429 @@
1
+ // =============================================================================
2
+ // TERMINUS AGENT - Init Command (Onboarding Focused)
3
+ // =============================================================================
4
+ import inquirer from 'inquirer';
5
+ import chalk from 'chalk';
6
+ import { saveConfig, generateNodeId, getConfigPath, configExists, loadConfig } from '../config/store.js';
7
+ import { checkOllamaAvailable, listOllamaModels } from '../llm/provider.js';
8
+ import { AGENTS } from '../agents/index.js';
9
+ const BANNER = `
10
+ ╔═══════════════════════════════════════════════════════════════════╗
11
+ ║ ║
12
+ ║ ████████╗███████╗██████╗ ███╗ ███╗██╗███╗ ██╗██╗ ██╗███████╗ ║
13
+ ║ ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██║████╗ ██║██║ ██║██╔════╝ ║
14
+ ║ ██║ █████╗ ██████╔╝██╔████╔██║██║██╔██╗ ██║██║ ██║███████╗ ║
15
+ ║ ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║██║██║╚██╗██║██║ ██║╚════██║ ║
16
+ ║ ██║ ███████╗██║ ██║██║ ╚═╝ ██║██║██║ ╚████║╚██████╔╝███████║ ║
17
+ ║ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ║
18
+ ║ ║
19
+ ║ Terminus Agent Setup Wizard ║
20
+ ║ ║
21
+ ╚═══════════════════════════════════════════════════════════════════╝
22
+ `;
23
+ const AGENT_EMOJIS = {
24
+ 'travel-planner': '✈️',
25
+ 'budget-planner': '💰',
26
+ 'health-advisor': '🏥',
27
+ 'fundamental-analyst': '📊',
28
+ 'technical-analyst': '📈',
29
+ 'crypto-advisor': '🪙',
30
+ 'food-expert': '🍳',
31
+ 'fitness-coach': '💪',
32
+ 'legal-advisor': '⚖️',
33
+ 'real-estate': '🏠',
34
+ 'career-coach': '💼',
35
+ 'event-planner': '🎉',
36
+ 'tech-support': '🔧',
37
+ 'shopping-assistant': '🛒',
38
+ 'language-tutor': '🌍',
39
+ };
40
+ const NETWORK_LABELS = {
41
+ local: 'Local development',
42
+ testnet: 'Base Sepolia testnet',
43
+ mainnet: 'Base mainnet',
44
+ };
45
+ const NETWORK_URL_PRESETS = {
46
+ local: process.env.TERMINUS_CONTROL_PLANE_URL_LOCAL?.trim() || 'ws://localhost:8084',
47
+ testnet: process.env.TERMINUS_CONTROL_PLANE_URL_TESTNET?.trim() ||
48
+ process.env.TERMINUS_CONTROL_PLANE_URL?.trim() ||
49
+ 'ws://localhost:8084',
50
+ mainnet: process.env.TERMINUS_CONTROL_PLANE_URL_MAINNET?.trim() ||
51
+ process.env.TERMINUS_CONTROL_PLANE_URL?.trim() ||
52
+ 'wss://localhost:8084',
53
+ };
54
+ const LLM_CHOICES = [
55
+ {
56
+ name: `Grok API ${chalk.gray('xAI Cloud (best quality)')}`,
57
+ value: 'grok',
58
+ short: 'Grok API',
59
+ },
60
+ {
61
+ name: `Ollama ${chalk.gray('Local LLM (free)')}`,
62
+ value: 'ollama',
63
+ short: 'Ollama',
64
+ },
65
+ {
66
+ name: `OpenAI-Compatible ${chalk.gray('LM Studio / LocalAI / vLLM')}`,
67
+ value: 'openai-compatible',
68
+ short: 'OpenAI-Compatible',
69
+ },
70
+ ];
71
+ function printBanner() {
72
+ console.clear();
73
+ console.log(chalk.cyan(BANNER));
74
+ }
75
+ function printInfo(label, value) {
76
+ console.log(chalk.gray(` ${label}: `) + chalk.white(value));
77
+ }
78
+ function must(value, message) {
79
+ if (value === undefined || value === null) {
80
+ throw new Error(message);
81
+ }
82
+ return value;
83
+ }
84
+ function isValidWallet(input) {
85
+ return /^0x[a-fA-F0-9]{40}$/.test(input);
86
+ }
87
+ function isWsUrl(input) {
88
+ return input.startsWith('ws://') || input.startsWith('wss://');
89
+ }
90
+ function normalizeProvider(provider) {
91
+ if (!provider)
92
+ return undefined;
93
+ if (provider === 'grok' || provider === 'ollama' || provider === 'openai-compatible') {
94
+ return provider;
95
+ }
96
+ return undefined;
97
+ }
98
+ function normalizeProfile(profile) {
99
+ if (!profile)
100
+ return undefined;
101
+ if (profile === 'local' || profile === 'testnet' || profile === 'mainnet') {
102
+ return profile;
103
+ }
104
+ return undefined;
105
+ }
106
+ function getRuntimeGrokKey() {
107
+ return process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim();
108
+ }
109
+ async function promptForAgentType(initialValue, nonInteractive) {
110
+ if (initialValue) {
111
+ if (!AGENTS.some((agent) => agent.id === initialValue)) {
112
+ throw new Error(`Unknown agentType "${initialValue}".`);
113
+ }
114
+ return initialValue;
115
+ }
116
+ if (nonInteractive) {
117
+ return 'travel-planner';
118
+ }
119
+ const choices = AGENTS.map((agent) => ({
120
+ name: `${AGENT_EMOJIS[agent.id] || '🤖'} ${agent.name.padEnd(22)} ${chalk.gray(agent.description)}`,
121
+ value: agent.id,
122
+ short: agent.name,
123
+ }));
124
+ const { agentType } = await inquirer.prompt([
125
+ {
126
+ type: 'list',
127
+ name: 'agentType',
128
+ message: 'Select agent type:',
129
+ choices,
130
+ pageSize: 15,
131
+ loop: false,
132
+ },
133
+ ]);
134
+ return agentType;
135
+ }
136
+ async function promptForWallet(initialValue, nonInteractive) {
137
+ if (initialValue) {
138
+ if (!isValidWallet(initialValue)) {
139
+ throw new Error('Wallet must be a valid EVM address.');
140
+ }
141
+ return initialValue;
142
+ }
143
+ if (nonInteractive) {
144
+ throw new Error('wallet is required in non-interactive mode. Use --wallet 0x...');
145
+ }
146
+ const { wallet } = await inquirer.prompt([
147
+ {
148
+ type: 'input',
149
+ name: 'wallet',
150
+ message: 'Wallet address (0x...):',
151
+ validate: (input) => {
152
+ if (!input)
153
+ return 'Wallet address is required';
154
+ if (!isValidWallet(input))
155
+ return 'Invalid EVM wallet address';
156
+ return true;
157
+ },
158
+ },
159
+ ]);
160
+ return wallet;
161
+ }
162
+ async function promptForProvider(initialValue, nonInteractive) {
163
+ if (initialValue)
164
+ return initialValue;
165
+ if (nonInteractive)
166
+ return 'grok';
167
+ const { llmProvider } = await inquirer.prompt([
168
+ {
169
+ type: 'list',
170
+ name: 'llmProvider',
171
+ message: 'Select LLM provider:',
172
+ choices: LLM_CHOICES,
173
+ loop: false,
174
+ },
175
+ ]);
176
+ return llmProvider;
177
+ }
178
+ async function configureProvider(llmProvider, options, nonInteractive, ollamaModels) {
179
+ if (llmProvider === 'grok') {
180
+ const runtimeGrokKey = getRuntimeGrokKey();
181
+ if (options.apiKey?.trim()) {
182
+ return { apiKey: options.apiKey.trim() };
183
+ }
184
+ if (nonInteractive) {
185
+ if (!runtimeGrokKey) {
186
+ throw new Error('Grok selected but no key provided. Set --apiKey or TERMINUS_GROK_API_KEY/XAI_API_KEY.');
187
+ }
188
+ return { apiKey: '__ENV__' };
189
+ }
190
+ if (runtimeGrokKey) {
191
+ const { useRuntimeKey } = await inquirer.prompt([
192
+ {
193
+ type: 'confirm',
194
+ name: 'useRuntimeKey',
195
+ message: 'Use Grok API key from runtime environment (recommended)?',
196
+ default: true,
197
+ },
198
+ ]);
199
+ if (useRuntimeKey) {
200
+ return { apiKey: '__ENV__' };
201
+ }
202
+ }
203
+ const { key } = await inquirer.prompt([
204
+ {
205
+ type: 'password',
206
+ name: 'key',
207
+ message: 'Grok API key (xai-...):',
208
+ mask: '•',
209
+ validate: (input) => {
210
+ if (!input)
211
+ return 'API key is required';
212
+ if (!input.startsWith('xai-'))
213
+ return 'Grok API keys start with "xai-"';
214
+ if (input.length < 20)
215
+ return 'API key looks too short';
216
+ return true;
217
+ },
218
+ },
219
+ ]);
220
+ return { apiKey: key.trim() };
221
+ }
222
+ if (llmProvider === 'ollama') {
223
+ const defaultBaseUrl = options.llmBaseUrl || 'http://localhost:11434';
224
+ const defaultModel = options.llmModel || ollamaModels[0] || 'llama3';
225
+ if (nonInteractive) {
226
+ return {
227
+ apiKey: '',
228
+ llmBaseUrl: defaultBaseUrl,
229
+ llmModel: defaultModel,
230
+ };
231
+ }
232
+ const answers = await inquirer.prompt([
233
+ {
234
+ type: 'input',
235
+ name: 'baseUrl',
236
+ message: 'Ollama server URL:',
237
+ default: defaultBaseUrl,
238
+ },
239
+ {
240
+ type: ollamaModels.length > 0 ? 'list' : 'input',
241
+ name: 'model',
242
+ message: ollamaModels.length > 0 ? 'Select model:' : 'Enter model name:',
243
+ choices: ollamaModels.length > 0 ? ollamaModels : undefined,
244
+ default: defaultModel,
245
+ },
246
+ ]);
247
+ return {
248
+ apiKey: '',
249
+ llmBaseUrl: String(answers.baseUrl),
250
+ llmModel: String(answers.model),
251
+ };
252
+ }
253
+ const defaultBaseUrl = options.llmBaseUrl || 'http://localhost:1234';
254
+ const defaultModel = options.llmModel || 'local-model';
255
+ const defaultApiKey = options.apiKey?.trim() || '';
256
+ if (nonInteractive) {
257
+ return {
258
+ apiKey: defaultApiKey,
259
+ llmBaseUrl: defaultBaseUrl,
260
+ llmModel: defaultModel,
261
+ };
262
+ }
263
+ const answers = await inquirer.prompt([
264
+ {
265
+ type: 'input',
266
+ name: 'baseUrl',
267
+ message: 'OpenAI-compatible server URL:',
268
+ default: defaultBaseUrl,
269
+ },
270
+ {
271
+ type: 'input',
272
+ name: 'model',
273
+ message: 'Model name:',
274
+ default: defaultModel,
275
+ },
276
+ {
277
+ type: 'password',
278
+ name: 'apiKey',
279
+ message: 'API key (optional):',
280
+ mask: '•',
281
+ default: defaultApiKey,
282
+ },
283
+ ]);
284
+ return {
285
+ apiKey: String(answers.apiKey || '').trim(),
286
+ llmBaseUrl: String(answers.baseUrl),
287
+ llmModel: String(answers.model),
288
+ };
289
+ }
290
+ async function selectNetworkProfile(initialValue, nonInteractive) {
291
+ if (initialValue)
292
+ return initialValue;
293
+ if (nonInteractive)
294
+ return 'testnet';
295
+ const { profile } = await inquirer.prompt([
296
+ {
297
+ type: 'list',
298
+ name: 'profile',
299
+ message: 'Target network:',
300
+ choices: [
301
+ { name: 'Testnet (Base Sepolia)', value: 'testnet' },
302
+ { name: 'Mainnet (Base)', value: 'mainnet' },
303
+ { name: 'Local Development', value: 'local' },
304
+ ],
305
+ default: 'testnet',
306
+ },
307
+ ]);
308
+ return profile;
309
+ }
310
+ async function selectControlPlaneUrl(profile, initialValue, nonInteractive) {
311
+ const defaultUrl = initialValue?.trim() || NETWORK_URL_PRESETS[profile];
312
+ if (nonInteractive) {
313
+ if (!isWsUrl(defaultUrl)) {
314
+ throw new Error('controlPlaneUrl must start with ws:// or wss://');
315
+ }
316
+ if (profile === 'mainnet' && !defaultUrl.startsWith('wss://')) {
317
+ throw new Error('Mainnet profile requires wss:// control-plane URL.');
318
+ }
319
+ return defaultUrl;
320
+ }
321
+ const { controlPlaneUrl } = await inquirer.prompt([
322
+ {
323
+ type: 'input',
324
+ name: 'controlPlaneUrl',
325
+ message: 'Control Plane URL:',
326
+ default: defaultUrl,
327
+ validate: (input) => {
328
+ if (!isWsUrl(input))
329
+ return 'URL must start with ws:// or wss://';
330
+ if (profile === 'mainnet' && !input.startsWith('wss://')) {
331
+ return 'Mainnet requires wss://';
332
+ }
333
+ return true;
334
+ },
335
+ },
336
+ ]);
337
+ return String(controlPlaneUrl);
338
+ }
339
+ export async function initCommand(rawOptions = {}) {
340
+ const options = {
341
+ ...rawOptions,
342
+ llmProvider: normalizeProvider(rawOptions.llmProvider),
343
+ profile: normalizeProfile(rawOptions.profile),
344
+ };
345
+ const nonInteractive = options.yes === true;
346
+ printBanner();
347
+ if (configExists()) {
348
+ const existing = loadConfig();
349
+ console.log(chalk.yellow('Existing configuration detected.\n'));
350
+ if (existing) {
351
+ printInfo('Agent', existing.agentType);
352
+ printInfo('Wallet', existing.wallet);
353
+ printInfo('Provider', existing.llmProvider || 'grok');
354
+ printInfo('Network', NETWORK_LABELS[existing.networkProfile || 'local']);
355
+ }
356
+ console.log();
357
+ const shouldOverwrite = options.force === true || nonInteractive
358
+ ? true
359
+ : (await inquirer.prompt([
360
+ {
361
+ type: 'confirm',
362
+ name: 'overwrite',
363
+ message: 'Overwrite existing configuration?',
364
+ default: false,
365
+ },
366
+ ])).overwrite;
367
+ if (!shouldOverwrite) {
368
+ console.log(chalk.gray('\nSetup cancelled.\n'));
369
+ return;
370
+ }
371
+ }
372
+ console.log(chalk.gray('Checking local LLM availability...'));
373
+ const ollamaAvailable = await checkOllamaAvailable();
374
+ const ollamaModels = ollamaAvailable ? await listOllamaModels() : [];
375
+ if (ollamaAvailable) {
376
+ console.log(chalk.green(`✓ Ollama detected (${ollamaModels.length} models)`));
377
+ }
378
+ else {
379
+ console.log(chalk.gray('○ Ollama not detected'));
380
+ }
381
+ console.log();
382
+ const agentType = await promptForAgentType(options.agentType, nonInteractive);
383
+ const wallet = await promptForWallet(options.wallet, nonInteractive);
384
+ const llmProvider = await promptForProvider(options.llmProvider, nonInteractive);
385
+ const providerConfig = await configureProvider(llmProvider, options, nonInteractive, ollamaModels);
386
+ const profile = await selectNetworkProfile(options.profile, nonInteractive);
387
+ const controlPlaneUrl = await selectControlPlaneUrl(profile, options.controlPlaneUrl, nonInteractive);
388
+ const nodeId = generateNodeId(agentType, wallet);
389
+ const config = {
390
+ agentType,
391
+ wallet,
392
+ apiKey: providerConfig.apiKey,
393
+ controlPlaneUrl,
394
+ nodeId,
395
+ llmProvider,
396
+ llmBaseUrl: providerConfig.llmBaseUrl,
397
+ llmModel: providerConfig.llmModel,
398
+ networkProfile: profile,
399
+ };
400
+ saveConfig(config);
401
+ const selectedAgent = AGENTS.find((item) => item.id === agentType);
402
+ console.log(chalk.green.bold('\nSetup complete.\n'));
403
+ printInfo('Agent', `${AGENT_EMOJIS[agentType] || '🤖'} ${selectedAgent?.name || agentType}`);
404
+ printInfo('Node ID', nodeId);
405
+ printInfo('Wallet', `${wallet.slice(0, 10)}...${wallet.slice(-8)}`);
406
+ printInfo('Provider', llmProvider);
407
+ if (providerConfig.llmModel) {
408
+ printInfo('Model', providerConfig.llmModel);
409
+ }
410
+ printInfo('Profile', NETWORK_LABELS[profile]);
411
+ printInfo('Control Plane', controlPlaneUrl);
412
+ printInfo('Config File', getConfigPath());
413
+ console.log();
414
+ console.log(chalk.yellow('Important: set TERMINUS_WALLET_PRIVATE_KEY in your shell before running.'));
415
+ console.log(chalk.cyan(' export TERMINUS_WALLET_PRIVATE_KEY=0x...'));
416
+ if (llmProvider === 'grok' && providerConfig.apiKey === '__ENV__') {
417
+ console.log(chalk.yellow('Important: Grok key will be read from TERMINUS_GROK_API_KEY or XAI_API_KEY.'));
418
+ }
419
+ console.log(chalk.cyan('\nNext steps:'));
420
+ console.log(chalk.cyan(' npx terminus-agent doctor'));
421
+ console.log(chalk.cyan(' npx terminus-agent run\n'));
422
+ // Ensure options are not silently ignored in strict mode.
423
+ if (nonInteractive) {
424
+ must(config.agentType, 'agentType is required');
425
+ must(config.wallet, 'wallet is required');
426
+ must(config.llmProvider, 'llmProvider is required');
427
+ must(config.controlPlaneUrl, 'controlPlaneUrl is required');
428
+ }
429
+ }
@@ -0,0 +1 @@
1
+ export declare function runCommand(): Promise<void>;