@terminusagents/agents 0.1.1 → 0.1.3

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 CHANGED
@@ -4,7 +4,7 @@ Run a Terminus agent on your machine, receive jobs from the control plane, and e
4
4
 
5
5
  This package supports:
6
6
  - challenge-signature websocket auth
7
- - Grok / Ollama / OpenAI-compatible providers
7
+ - Grok / OpenAI / Claude / Gemini / Ollama / OpenAI-compatible providers
8
8
  - testnet/mainnet/local network profiles
9
9
  - one-command diagnostics with `doctor`
10
10
 
@@ -53,12 +53,18 @@ npx terminus-agent init
53
53
  `init` uses production-safe defaults:
54
54
  - testnet: `wss://cp-sepolia.termn.xyz/ws`
55
55
  - mainnet: `wss://cp-mainnet.termn.xyz/ws`
56
+ - if `TERMINUS_WALLET_PRIVATE_KEY` is set, wallet is derived automatically
57
+ - if your wallet already has a Terminus identity NFT, agent type is auto-detected from chain
56
58
 
57
59
  3. Export required runtime secrets:
58
60
 
59
61
  ```bash
60
62
  export TERMINUS_WALLET_PRIVATE_KEY=0x...
61
- export TERMINUS_GROK_API_KEY=xai-... # only if using Grok
63
+ # Use one provider key based on your llmProvider choice:
64
+ export TERMINUS_GROK_API_KEY=xai-... # grok
65
+ export TERMINUS_OPENAI_API_KEY=sk-... # openai
66
+ export TERMINUS_ANTHROPIC_API_KEY=sk-ant-... # anthropic (claude)
67
+ export TERMINUS_GOOGLE_API_KEY=AIza... # google (gemini)
62
68
  ```
63
69
 
64
70
  4. Run diagnostics:
@@ -79,15 +85,14 @@ Use this for scripted onboarding:
79
85
 
80
86
  ```bash
81
87
  export TERMINUS_WALLET_PRIVATE_KEY=0x...
82
- export TERMINUS_GROK_API_KEY=xai-...
88
+ export TERMINUS_OPENAI_API_KEY=sk-...
83
89
 
84
90
  npx terminus-agent init \
85
91
  --yes \
86
92
  --force \
87
93
  --profile testnet \
88
- --agent-type travel-planner \
89
- --wallet 0x1234567890abcdef1234567890abcdef12345678 \
90
- --llm-provider grok
94
+ --llm-provider openai \
95
+ --llm-model gpt-4o-mini
91
96
  ```
92
97
 
93
98
  Then:
@@ -153,6 +158,10 @@ Example:
153
158
  ```
154
159
 
155
160
  `"apiKey": "__ENV__"` means runtime key from `TERMINUS_GROK_API_KEY` or `XAI_API_KEY`.
161
+ For other cloud providers, runtime keys can also come from:
162
+ - OpenAI: `TERMINUS_OPENAI_API_KEY` or `OPENAI_API_KEY`
163
+ - Claude: `TERMINUS_ANTHROPIC_API_KEY` or `ANTHROPIC_API_KEY`
164
+ - Gemini: `TERMINUS_GOOGLE_API_KEY` or `GOOGLE_API_KEY` or `GEMINI_API_KEY`
156
165
 
157
166
  ## Troubleshooting
158
167
 
@@ -166,10 +175,12 @@ Example:
166
175
  - verify private key signer address matches configured wallet
167
176
  - rerun `npx terminus-agent init --force` if wallet changed
168
177
 
169
- ### `Grok key missing`
170
- - set one of:
171
- - `TERMINUS_GROK_API_KEY`
172
- - `XAI_API_KEY`
178
+ ### `Provider key missing`
179
+ - set the env key for your provider:
180
+ - Grok: `TERMINUS_GROK_API_KEY` or `XAI_API_KEY`
181
+ - OpenAI: `TERMINUS_OPENAI_API_KEY` or `OPENAI_API_KEY`
182
+ - Claude: `TERMINUS_ANTHROPIC_API_KEY` or `ANTHROPIC_API_KEY`
183
+ - Gemini: `TERMINUS_GOOGLE_API_KEY` or `GOOGLE_API_KEY` or `GEMINI_API_KEY`
173
184
  - or store key directly during `init`
174
185
 
175
186
  ### `Ollama not reachable`
@@ -16,10 +16,25 @@ function isValidWallet(value) {
16
16
  function isWsUrl(value) {
17
17
  return value.startsWith('ws://') || value.startsWith('wss://');
18
18
  }
19
- function getRuntimeGrokKey(storedApiKey) {
19
+ function getRuntimeProviderKey(provider, storedApiKey) {
20
20
  if (storedApiKey && storedApiKey !== '__ENV__')
21
21
  return storedApiKey;
22
- return process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim();
22
+ if (provider === 'grok') {
23
+ return process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim() || undefined;
24
+ }
25
+ if (provider === 'openai') {
26
+ return process.env.TERMINUS_OPENAI_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim() || undefined;
27
+ }
28
+ if (provider === 'anthropic') {
29
+ return process.env.TERMINUS_ANTHROPIC_API_KEY?.trim() || process.env.ANTHROPIC_API_KEY?.trim() || undefined;
30
+ }
31
+ if (provider === 'google') {
32
+ return (process.env.TERMINUS_GOOGLE_API_KEY?.trim()
33
+ || process.env.GOOGLE_API_KEY?.trim()
34
+ || process.env.GEMINI_API_KEY?.trim()
35
+ || undefined);
36
+ }
37
+ return undefined;
23
38
  }
24
39
  async function checkWebSocketReachability(url, timeoutMs) {
25
40
  return new Promise((resolve) => {
@@ -71,6 +86,69 @@ async function checkGrokApiKey(apiKey, fullCheck) {
71
86
  return { ok: false, message: error.message };
72
87
  }
73
88
  }
89
+ async function checkOpenAiApiKey(apiKey, fullCheck) {
90
+ if (apiKey.length < 12) {
91
+ return { ok: false, message: 'Key format looks invalid (too short)' };
92
+ }
93
+ if (!fullCheck) {
94
+ return { ok: true, message: 'Key format looks valid' };
95
+ }
96
+ try {
97
+ const response = await fetch('https://api.openai.com/v1/models', {
98
+ headers: {
99
+ Authorization: `Bearer ${apiKey}`,
100
+ },
101
+ });
102
+ if (response.ok) {
103
+ return { ok: true, message: 'OpenAI API reachable and key accepted' };
104
+ }
105
+ return { ok: false, message: `OpenAI API returned ${response.status}` };
106
+ }
107
+ catch (error) {
108
+ return { ok: false, message: error.message };
109
+ }
110
+ }
111
+ async function checkAnthropicApiKey(apiKey, fullCheck) {
112
+ if (apiKey.length < 12) {
113
+ return { ok: false, message: 'Key format looks invalid (too short)' };
114
+ }
115
+ if (!fullCheck) {
116
+ return { ok: true, message: 'Key format looks valid' };
117
+ }
118
+ try {
119
+ const response = await fetch('https://api.anthropic.com/v1/models', {
120
+ headers: {
121
+ 'x-api-key': apiKey,
122
+ 'anthropic-version': '2023-06-01',
123
+ },
124
+ });
125
+ if (response.ok) {
126
+ return { ok: true, message: 'Anthropic API reachable and key accepted' };
127
+ }
128
+ return { ok: false, message: `Anthropic API returned ${response.status}` };
129
+ }
130
+ catch (error) {
131
+ return { ok: false, message: error.message };
132
+ }
133
+ }
134
+ async function checkGoogleApiKey(apiKey, fullCheck) {
135
+ if (apiKey.length < 12) {
136
+ return { ok: false, message: 'Key format looks invalid (too short)' };
137
+ }
138
+ if (!fullCheck) {
139
+ return { ok: true, message: 'Key format looks valid' };
140
+ }
141
+ try {
142
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
143
+ if (response.ok) {
144
+ return { ok: true, message: 'Google Gemini API reachable and key accepted' };
145
+ }
146
+ return { ok: false, message: `Google Gemini API returned ${response.status}` };
147
+ }
148
+ catch (error) {
149
+ return { ok: false, message: error.message };
150
+ }
151
+ }
74
152
  async function checkOpenAiCompatible(baseUrl, fullCheck) {
75
153
  if (!baseUrl) {
76
154
  return { ok: false, message: 'Base URL is missing' };
@@ -150,13 +228,37 @@ export async function doctorCommand(options = {}) {
150
228
  }
151
229
  const provider = config.llmProvider || 'grok';
152
230
  if (provider === 'grok') {
153
- const key = getRuntimeGrokKey(config.apiKey);
231
+ const key = getRuntimeProviderKey(provider, config.apiKey);
154
232
  const result = key
155
233
  ? await checkGrokApiKey(key, fullCheck)
156
234
  : { ok: false, message: 'No Grok key found (config or env)' };
157
235
  printCheck('Grok provider', result);
158
236
  allOk = allOk && result.ok;
159
237
  }
238
+ else if (provider === 'openai') {
239
+ const key = getRuntimeProviderKey(provider, config.apiKey);
240
+ const result = key
241
+ ? await checkOpenAiApiKey(key, fullCheck)
242
+ : { ok: false, message: 'No OpenAI key found (config or env)' };
243
+ printCheck('OpenAI provider', result);
244
+ allOk = allOk && result.ok;
245
+ }
246
+ else if (provider === 'anthropic') {
247
+ const key = getRuntimeProviderKey(provider, config.apiKey);
248
+ const result = key
249
+ ? await checkAnthropicApiKey(key, fullCheck)
250
+ : { ok: false, message: 'No Anthropic key found (config or env)' };
251
+ printCheck('Anthropic provider', result);
252
+ allOk = allOk && result.ok;
253
+ }
254
+ else if (provider === 'google') {
255
+ const key = getRuntimeProviderKey(provider, config.apiKey);
256
+ const result = key
257
+ ? await checkGoogleApiKey(key, fullCheck)
258
+ : { ok: false, message: 'No Google key found (config or env)' };
259
+ printCheck('Google provider', result);
260
+ allOk = allOk && result.ok;
261
+ }
160
262
  else if (provider === 'ollama') {
161
263
  const baseUrl = config.llmBaseUrl || 'http://localhost:11434';
162
264
  const available = await checkOllamaAvailable(baseUrl);
@@ -1,5 +1,5 @@
1
1
  import type { NetworkProfile } from '../config/store.js';
2
- type LlmProvider = 'grok' | 'ollama' | 'openai-compatible';
2
+ type LlmProvider = 'grok' | 'openai' | 'anthropic' | 'google' | 'ollama' | 'openai-compatible';
3
3
  export interface InitCommandOptions {
4
4
  agentType?: string;
5
5
  wallet?: string;
package/dist/cli/init.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // =============================================================================
4
4
  import inquirer from 'inquirer';
5
5
  import chalk from 'chalk';
6
+ import { ethers } from 'ethers';
6
7
  import { saveConfig, generateNodeId, getConfigPath, configExists, loadConfig } from '../config/store.js';
7
8
  import { checkOllamaAvailable, listOllamaModels } from '../llm/provider.js';
8
9
  import { AGENTS } from '../agents/index.js';
@@ -44,6 +45,15 @@ const NETWORK_LABELS = {
44
45
  };
45
46
  const DEFAULT_TESTNET_CONTROL_PLANE_URL = 'wss://cp-sepolia.termn.xyz/ws';
46
47
  const DEFAULT_MAINNET_CONTROL_PLANE_URL = 'wss://cp-mainnet.termn.xyz/ws';
48
+ const DEFAULT_TESTNET_REGISTRY_CONTRACT = '0x1439F2c1Fb1B3F99efe2aB02A90B377BaE5D2B02';
49
+ const DEFAULT_TESTNET_RPC_URL = 'https://sepolia.base.org';
50
+ const DEFAULT_MAINNET_RPC_URL = 'https://mainnet.base.org';
51
+ const AGENT_IDENTITY_REGISTRY_ABI = [
52
+ 'function balanceOf(address owner) view returns (uint256)',
53
+ 'function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)',
54
+ 'function getAgentType(uint256 tokenId) view returns (string)',
55
+ 'function getCollectionTokenId(uint256 tokenId) view returns (uint256)',
56
+ ];
47
57
  const NETWORK_URL_PRESETS = {
48
58
  local: process.env.TERMINUS_CONTROL_PLANE_URL_LOCAL?.trim() || 'ws://localhost:8084/ws',
49
59
  testnet: process.env.TERMINUS_CONTROL_PLANE_URL_TESTNET?.trim() ||
@@ -59,6 +69,21 @@ const LLM_CHOICES = [
59
69
  value: 'grok',
60
70
  short: 'Grok API',
61
71
  },
72
+ {
73
+ name: `OpenAI ${chalk.gray('ChatGPT API')}`,
74
+ value: 'openai',
75
+ short: 'OpenAI',
76
+ },
77
+ {
78
+ name: `Claude ${chalk.gray('Anthropic API')}`,
79
+ value: 'anthropic',
80
+ short: 'Claude',
81
+ },
82
+ {
83
+ name: `Gemini ${chalk.gray('Google AI Studio API')}`,
84
+ value: 'google',
85
+ short: 'Gemini',
86
+ },
62
87
  {
63
88
  name: `Ollama ${chalk.gray('Local LLM (free)')}`,
64
89
  value: 'ollama',
@@ -108,7 +133,12 @@ function normalizeControlPlaneUrl(input) {
108
133
  function normalizeProvider(provider) {
109
134
  if (!provider)
110
135
  return undefined;
111
- if (provider === 'grok' || provider === 'ollama' || provider === 'openai-compatible') {
136
+ if (provider === 'grok'
137
+ || provider === 'openai'
138
+ || provider === 'anthropic'
139
+ || provider === 'google'
140
+ || provider === 'ollama'
141
+ || provider === 'openai-compatible') {
112
142
  return provider;
113
143
  }
114
144
  return undefined;
@@ -121,8 +151,186 @@ function normalizeProfile(profile) {
121
151
  }
122
152
  return undefined;
123
153
  }
124
- function getRuntimeGrokKey() {
125
- return process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim();
154
+ function getRuntimeProviderKey(provider) {
155
+ if (provider === 'grok') {
156
+ return process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim() || undefined;
157
+ }
158
+ if (provider === 'openai') {
159
+ return process.env.TERMINUS_OPENAI_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim() || undefined;
160
+ }
161
+ if (provider === 'anthropic') {
162
+ return process.env.TERMINUS_ANTHROPIC_API_KEY?.trim() || process.env.ANTHROPIC_API_KEY?.trim() || undefined;
163
+ }
164
+ return (process.env.TERMINUS_GOOGLE_API_KEY?.trim()
165
+ || process.env.GOOGLE_API_KEY?.trim()
166
+ || process.env.GEMINI_API_KEY?.trim()
167
+ || undefined);
168
+ }
169
+ function providerEnvHint(provider) {
170
+ if (provider === 'grok')
171
+ return 'TERMINUS_GROK_API_KEY or XAI_API_KEY';
172
+ if (provider === 'openai')
173
+ return 'TERMINUS_OPENAI_API_KEY or OPENAI_API_KEY';
174
+ if (provider === 'anthropic')
175
+ return 'TERMINUS_ANTHROPIC_API_KEY or ANTHROPIC_API_KEY';
176
+ return 'TERMINUS_GOOGLE_API_KEY or GOOGLE_API_KEY or GEMINI_API_KEY';
177
+ }
178
+ function providerLabel(provider) {
179
+ if (provider === 'grok')
180
+ return 'Grok';
181
+ if (provider === 'openai')
182
+ return 'OpenAI';
183
+ if (provider === 'anthropic')
184
+ return 'Claude';
185
+ return 'Gemini';
186
+ }
187
+ function providerDefaultModel(provider) {
188
+ if (provider === 'grok')
189
+ return 'grok-4-1-fast-non-reasoning';
190
+ if (provider === 'openai')
191
+ return 'gpt-4o-mini';
192
+ if (provider === 'anthropic')
193
+ return 'claude-3-5-haiku-latest';
194
+ return 'gemini-2.0-flash';
195
+ }
196
+ function getWalletFromRuntimePrivateKey() {
197
+ const privateKey = process.env.TERMINUS_WALLET_PRIVATE_KEY?.trim();
198
+ if (!privateKey)
199
+ return undefined;
200
+ try {
201
+ const normalizedPk = privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`;
202
+ return new ethers.Wallet(normalizedPk).address;
203
+ }
204
+ catch {
205
+ return undefined;
206
+ }
207
+ }
208
+ function resolveRegistryAddress(profile) {
209
+ if (profile === 'testnet') {
210
+ const candidate = (process.env.TERMINUS_AGENT_IDENTITY_REGISTRY_CONTRACT_TESTNET?.trim()
211
+ || process.env.AGENT_IDENTITY_REGISTRY_CONTRACT_TESTNET?.trim()
212
+ || process.env.NEXT_PUBLIC_SEPOLIA_REGISTRY_CONTRACT?.trim()
213
+ || process.env.AGENT_IDENTITY_REGISTRY_CONTRACT?.trim()
214
+ || DEFAULT_TESTNET_REGISTRY_CONTRACT);
215
+ return isValidWallet(candidate) ? candidate : undefined;
216
+ }
217
+ if (profile === 'mainnet') {
218
+ const candidate = (process.env.TERMINUS_AGENT_IDENTITY_REGISTRY_CONTRACT_MAINNET?.trim()
219
+ || process.env.AGENT_IDENTITY_REGISTRY_CONTRACT_MAINNET?.trim()
220
+ || process.env.NEXT_PUBLIC_MAINNET_REGISTRY_CONTRACT?.trim()
221
+ || process.env.AGENT_IDENTITY_REGISTRY_CONTRACT?.trim());
222
+ return candidate && isValidWallet(candidate) ? candidate : undefined;
223
+ }
224
+ return undefined;
225
+ }
226
+ function resolveRpcUrl(profile) {
227
+ if (profile === 'testnet') {
228
+ return (process.env.TERMINUS_BASE_SEPOLIA_RPC?.trim()
229
+ || process.env.BASE_SEPOLIA_RPC?.trim()
230
+ || DEFAULT_TESTNET_RPC_URL);
231
+ }
232
+ if (profile === 'mainnet') {
233
+ return (process.env.TERMINUS_BASE_MAINNET_RPC?.trim()
234
+ || process.env.BASE_MAINNET_RPC?.trim()
235
+ || DEFAULT_MAINNET_RPC_URL);
236
+ }
237
+ return undefined;
238
+ }
239
+ function resolveRequiredCollectionTokenId(profile) {
240
+ if (profile === 'testnet') {
241
+ return (process.env.TERMINUS_COLLECTION_TOKEN_ID_TESTNET?.trim()
242
+ || process.env.NEXT_PUBLIC_SEPOLIA_COLLECTION_TOKEN_ID?.trim()
243
+ || process.env.TERMINUS_COLLECTION_TOKEN_ID?.trim()
244
+ || undefined);
245
+ }
246
+ if (profile === 'mainnet') {
247
+ return (process.env.TERMINUS_COLLECTION_TOKEN_ID_MAINNET?.trim()
248
+ || process.env.NEXT_PUBLIC_MAINNET_COLLECTION_TOKEN_ID?.trim()
249
+ || process.env.TERMINUS_COLLECTION_TOKEN_ID?.trim()
250
+ || undefined);
251
+ }
252
+ return undefined;
253
+ }
254
+ async function detectAgentIdentitiesFromChain(profile, wallet) {
255
+ const registryAddress = resolveRegistryAddress(profile);
256
+ const rpcUrl = resolveRpcUrl(profile);
257
+ if (!registryAddress || !rpcUrl || profile === 'local') {
258
+ return { identities: [], registryAddress, rpcUrl };
259
+ }
260
+ const requiredCollectionTokenId = resolveRequiredCollectionTokenId(profile);
261
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
262
+ const contract = new ethers.Contract(registryAddress, AGENT_IDENTITY_REGISTRY_ABI, provider);
263
+ const balance = await contract.balanceOf(wallet);
264
+ if (balance === 0n) {
265
+ return { identities: [], registryAddress, rpcUrl };
266
+ }
267
+ const identities = [];
268
+ for (let i = 0n; i < balance; i++) {
269
+ const tokenId = await contract.tokenOfOwnerByIndex(wallet, i);
270
+ const agentTypeRaw = await contract.getAgentType(tokenId);
271
+ const agentType = agentTypeRaw.trim();
272
+ if (!AGENTS.some((agent) => agent.id === agentType)) {
273
+ continue;
274
+ }
275
+ let collectionTokenId;
276
+ try {
277
+ const rawCollectionTokenId = await contract.getCollectionTokenId(tokenId);
278
+ collectionTokenId = rawCollectionTokenId.toString();
279
+ }
280
+ catch {
281
+ collectionTokenId = undefined;
282
+ }
283
+ if (requiredCollectionTokenId && collectionTokenId && collectionTokenId !== requiredCollectionTokenId) {
284
+ continue;
285
+ }
286
+ identities.push({
287
+ tokenId: tokenId.toString(),
288
+ agentType,
289
+ collectionTokenId,
290
+ });
291
+ }
292
+ identities.sort((a, b) => {
293
+ const tokenA = BigInt(a.tokenId);
294
+ const tokenB = BigInt(b.tokenId);
295
+ if (tokenA < tokenB)
296
+ return -1;
297
+ if (tokenA > tokenB)
298
+ return 1;
299
+ return 0;
300
+ });
301
+ return { identities, registryAddress, rpcUrl };
302
+ }
303
+ async function autoSelectAgentType(profile, wallet, nonInteractive) {
304
+ if (profile === 'local') {
305
+ return {};
306
+ }
307
+ try {
308
+ const { identities } = await detectAgentIdentitiesFromChain(profile, wallet);
309
+ if (identities.length === 0) {
310
+ return {};
311
+ }
312
+ if (identities.length === 1 || nonInteractive) {
313
+ return { agentType: identities[0].agentType, identity: identities[0] };
314
+ }
315
+ const { selectedTokenId } = await inquirer.prompt([
316
+ {
317
+ type: 'list',
318
+ name: 'selectedTokenId',
319
+ message: 'Multiple agent identities detected. Select one:',
320
+ choices: identities.map((item) => ({
321
+ name: `${item.agentType} ${chalk.gray(`(tokenId=${item.tokenId}${item.collectionTokenId ? `, collection=${item.collectionTokenId}` : ''})`)}`,
322
+ value: item.tokenId,
323
+ })),
324
+ loop: false,
325
+ },
326
+ ]);
327
+ const selected = identities.find((item) => item.tokenId === selectedTokenId) || identities[0];
328
+ return { agentType: selected.agentType, identity: selected };
329
+ }
330
+ catch (error) {
331
+ console.log(chalk.yellow(`⚠ Could not auto-detect agent identity: ${error.message}`));
332
+ return {};
333
+ }
126
334
  }
127
335
  async function promptForAgentType(initialValue, nonInteractive) {
128
336
  if (initialValue) {
@@ -194,48 +402,71 @@ async function promptForProvider(initialValue, nonInteractive) {
194
402
  return llmProvider;
195
403
  }
196
404
  async function configureProvider(llmProvider, options, nonInteractive, ollamaModels) {
197
- if (llmProvider === 'grok') {
198
- const runtimeGrokKey = getRuntimeGrokKey();
405
+ if (llmProvider === 'grok' || llmProvider === 'openai' || llmProvider === 'anthropic' || llmProvider === 'google') {
406
+ const provider = llmProvider;
407
+ const runtimeProviderKey = getRuntimeProviderKey(provider);
408
+ const defaultModel = options.llmModel || providerDefaultModel(provider);
199
409
  if (options.apiKey?.trim()) {
200
- return { apiKey: options.apiKey.trim() };
410
+ return {
411
+ apiKey: options.apiKey.trim(),
412
+ llmModel: defaultModel,
413
+ };
201
414
  }
202
415
  if (nonInteractive) {
203
- if (!runtimeGrokKey) {
204
- throw new Error('Grok selected but no key provided. Set --apiKey or TERMINUS_GROK_API_KEY/XAI_API_KEY.');
416
+ if (!runtimeProviderKey) {
417
+ throw new Error(`${providerLabel(provider)} selected but no key provided. Set --apiKey or ${providerEnvHint(provider)}.`);
205
418
  }
206
- return { apiKey: '__ENV__' };
419
+ return {
420
+ apiKey: '__ENV__',
421
+ llmModel: defaultModel,
422
+ };
207
423
  }
208
- if (runtimeGrokKey) {
209
- const { useRuntimeKey } = await inquirer.prompt([
424
+ let useRuntimeKey = false;
425
+ if (runtimeProviderKey) {
426
+ const answer = await inquirer.prompt([
210
427
  {
211
428
  type: 'confirm',
212
429
  name: 'useRuntimeKey',
213
- message: 'Use Grok API key from runtime environment (recommended)?',
430
+ message: `Use ${providerLabel(provider)} API key from runtime environment (recommended)?`,
214
431
  default: true,
215
432
  },
216
433
  ]);
217
- if (useRuntimeKey) {
218
- return { apiKey: '__ENV__' };
219
- }
434
+ useRuntimeKey = Boolean(answer.useRuntimeKey);
220
435
  }
221
- const { key } = await inquirer.prompt([
222
- {
223
- type: 'password',
224
- name: 'key',
225
- message: 'Grok API key (xai-...):',
226
- mask: '',
227
- validate: (input) => {
228
- if (!input)
229
- return 'API key is required';
230
- if (!input.startsWith('xai-'))
231
- return 'Grok API keys start with "xai-"';
232
- if (input.length < 20)
233
- return 'API key looks too short';
234
- return true;
436
+ let apiKey = '__ENV__';
437
+ if (!useRuntimeKey) {
438
+ const { key } = await inquirer.prompt([
439
+ {
440
+ type: 'password',
441
+ name: 'key',
442
+ message: `${providerLabel(provider)} API key:`,
443
+ mask: '•',
444
+ validate: (input) => {
445
+ if (!input)
446
+ return 'API key is required';
447
+ if (provider === 'grok' && !input.startsWith('xai-')) {
448
+ return 'Grok API keys start with \"xai-\"';
449
+ }
450
+ if (input.length < 12)
451
+ return 'API key looks too short';
452
+ return true;
453
+ },
235
454
  },
455
+ ]);
456
+ apiKey = String(key).trim();
457
+ }
458
+ const { model } = await inquirer.prompt([
459
+ {
460
+ type: 'input',
461
+ name: 'model',
462
+ message: `${providerLabel(provider)} model name:`,
463
+ default: defaultModel,
236
464
  },
237
465
  ]);
238
- return { apiKey: key.trim() };
466
+ return {
467
+ apiKey,
468
+ llmModel: String(model).trim(),
469
+ };
239
470
  }
240
471
  if (llmProvider === 'ollama') {
241
472
  const defaultBaseUrl = options.llmBaseUrl || 'http://localhost:11434';
@@ -398,11 +629,25 @@ export async function initCommand(rawOptions = {}) {
398
629
  console.log(chalk.gray('○ Ollama not detected'));
399
630
  }
400
631
  console.log();
401
- const agentType = await promptForAgentType(options.agentType, nonInteractive);
402
- const wallet = await promptForWallet(options.wallet, nonInteractive);
632
+ const profile = await selectNetworkProfile(options.profile, nonInteractive);
633
+ const derivedWallet = getWalletFromRuntimePrivateKey();
634
+ const wallet = await promptForWallet(options.wallet || derivedWallet, nonInteractive);
635
+ if (!options.wallet && derivedWallet) {
636
+ console.log(chalk.green(`✓ Wallet auto-detected from TERMINUS_WALLET_PRIVATE_KEY: ${wallet}`));
637
+ }
638
+ let detectedIdentity;
639
+ let selectedAgentType = options.agentType;
640
+ if (!selectedAgentType) {
641
+ const detection = await autoSelectAgentType(profile, wallet, nonInteractive);
642
+ if (detection.agentType) {
643
+ selectedAgentType = detection.agentType;
644
+ detectedIdentity = detection.identity;
645
+ console.log(chalk.green(`✓ Agent type auto-detected: ${selectedAgentType}${detectedIdentity ? ` (tokenId=${detectedIdentity.tokenId})` : ''}`));
646
+ }
647
+ }
648
+ const agentType = await promptForAgentType(selectedAgentType, nonInteractive);
403
649
  const llmProvider = await promptForProvider(options.llmProvider, nonInteractive);
404
650
  const providerConfig = await configureProvider(llmProvider, options, nonInteractive, ollamaModels);
405
- const profile = await selectNetworkProfile(options.profile, nonInteractive);
406
651
  const controlPlaneUrl = await selectControlPlaneUrl(profile, options.controlPlaneUrl, nonInteractive);
407
652
  const nodeId = generateNodeId(agentType, wallet);
408
653
  const config = {
@@ -420,6 +665,12 @@ export async function initCommand(rawOptions = {}) {
420
665
  const selectedAgent = AGENTS.find((item) => item.id === agentType);
421
666
  console.log(chalk.green.bold('\nSetup complete.\n'));
422
667
  printInfo('Agent', `${AGENT_EMOJIS[agentType] || '🤖'} ${selectedAgent?.name || agentType}`);
668
+ if (detectedIdentity) {
669
+ printInfo('Identity Token', detectedIdentity.tokenId);
670
+ if (detectedIdentity.collectionTokenId) {
671
+ printInfo('Collection Token', detectedIdentity.collectionTokenId);
672
+ }
673
+ }
423
674
  printInfo('Node ID', nodeId);
424
675
  printInfo('Wallet', `${wallet.slice(0, 10)}...${wallet.slice(-8)}`);
425
676
  printInfo('Provider', llmProvider);
@@ -432,8 +683,9 @@ export async function initCommand(rawOptions = {}) {
432
683
  console.log();
433
684
  console.log(chalk.yellow('Important: set TERMINUS_WALLET_PRIVATE_KEY in your shell before running.'));
434
685
  console.log(chalk.cyan(' export TERMINUS_WALLET_PRIVATE_KEY=0x...'));
435
- if (llmProvider === 'grok' && providerConfig.apiKey === '__ENV__') {
436
- console.log(chalk.yellow('Important: Grok key will be read from TERMINUS_GROK_API_KEY or XAI_API_KEY.'));
686
+ if ((llmProvider === 'grok' || llmProvider === 'openai' || llmProvider === 'anthropic' || llmProvider === 'google')
687
+ && providerConfig.apiKey === '__ENV__') {
688
+ console.log(chalk.yellow(`Important: ${providerLabel(llmProvider)} key will be read from ${providerEnvHint(llmProvider)}.`));
437
689
  }
438
690
  console.log(chalk.cyan('\nNext steps:'));
439
691
  console.log(chalk.cyan(' npx terminus-agent doctor'));
package/dist/cli/run.js CHANGED
@@ -26,7 +26,7 @@ function printStartupBanner(config) {
26
26
  return;
27
27
  const emoji = AGENT_EMOJIS[config.agentType] || '🤖';
28
28
  const provider = config.llmProvider || 'grok';
29
- const providerIcon = provider === 'grok' ? '🌐' : provider === 'ollama' ? '🦙' : '🔧';
29
+ const providerIcon = getProviderIcon(provider);
30
30
  console.log();
31
31
  console.log(chalk.cyan.bold('╔════════════════════════════════════════════════════════════╗'));
32
32
  console.log(chalk.cyan.bold('║ 🚀 TERMINUS AGENT STARTING ║'));
@@ -41,6 +41,19 @@ function printStartupBanner(config) {
41
41
  console.log(chalk.gray('\n────────────────────────────────────────────────────────────\n'));
42
42
  console.log(chalk.yellow(' ⏳ Connecting to Control Plane...\n'));
43
43
  }
44
+ function getProviderIcon(provider) {
45
+ if (provider === 'grok')
46
+ return '🌐';
47
+ if (provider === 'openai')
48
+ return '🧠';
49
+ if (provider === 'anthropic')
50
+ return '🧩';
51
+ if (provider === 'google')
52
+ return '🔷';
53
+ if (provider === 'ollama')
54
+ return '🦙';
55
+ return '🔧';
56
+ }
44
57
  export async function runCommand() {
45
58
  if (!configExists()) {
46
59
  console.log();
@@ -67,10 +80,14 @@ export async function runCommand() {
67
80
  console.log(chalk.cyan(' npx terminus-agent init --profile mainnet\n'));
68
81
  process.exit(1);
69
82
  }
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'));
83
+ const runtimePrivateKey = process.env.TERMINUS_WALLET_PRIVATE_KEY?.trim();
84
+ if ((config.networkProfile === 'testnet' || config.networkProfile === 'mainnet') && !runtimePrivateKey) {
85
+ console.log(chalk.red('\n TERMINUS_WALLET_PRIVATE_KEY is required for testnet/mainnet challenge-signature auth.\n'));
86
+ console.log(chalk.gray(' Set it and retry:'));
87
+ console.log(chalk.cyan(' export TERMINUS_WALLET_PRIVATE_KEY=0x...\n'));
88
+ process.exit(1);
73
89
  }
90
+ printStartupBanner(config);
74
91
  const client = new AgentClient(config);
75
92
  // Handle graceful shutdown
76
93
  process.on('SIGINT', () => {
@@ -55,10 +55,8 @@ export async function statusCommand() {
55
55
  // LLM Provider Info
56
56
  console.log(chalk.yellow('\n┌─ LLM Provider ─────────────────────────┐\n'));
57
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';
58
+ const providerIcon = getProviderIcon(provider);
59
+ const providerName = getProviderName(provider);
62
60
  console.log(` ${chalk.gray('Provider:')} ${providerIcon} ${chalk.white(providerName)}`);
63
61
  if (config.llmModel) {
64
62
  console.log(` ${chalk.gray('Model:')} ${chalk.white(config.llmModel)}`);
@@ -71,8 +69,8 @@ export async function statusCommand() {
71
69
  const ollamaOk = await checkOllamaAvailable(config.llmBaseUrl);
72
70
  console.log(` ${chalk.gray('Status:')} ${ollamaOk ? chalk.green('✓ Connected') : chalk.red('✗ Not reachable')}`);
73
71
  }
74
- else if (provider === 'grok') {
75
- const hasRuntimeKey = Boolean(process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim());
72
+ else if (provider !== 'openai-compatible') {
73
+ const hasRuntimeKey = hasRuntimeProviderKey(provider);
76
74
  const hasStoredKey = Boolean(config.apiKey?.trim() && config.apiKey !== '__ENV__');
77
75
  const source = hasRuntimeKey ? 'Runtime env' : hasStoredKey ? 'Config file' : 'Missing';
78
76
  console.log(` ${chalk.gray('API Key:')} ${hasRuntimeKey || hasStoredKey ? chalk.green(`✓ ${source}`) : chalk.red('✗ Missing')}`);
@@ -102,3 +100,46 @@ export async function statusCommand() {
102
100
  console.log(` ${chalk.cyan('npx terminus-agent doctor')} ${chalk.gray('Run readiness checks')}`);
103
101
  console.log();
104
102
  }
103
+ function getProviderIcon(provider) {
104
+ if (provider === 'grok')
105
+ return '🌐';
106
+ if (provider === 'openai')
107
+ return '🧠';
108
+ if (provider === 'anthropic')
109
+ return '🧩';
110
+ if (provider === 'google')
111
+ return '🔷';
112
+ if (provider === 'ollama')
113
+ return '🦙';
114
+ return '🔧';
115
+ }
116
+ function getProviderName(provider) {
117
+ if (provider === 'grok')
118
+ return 'Grok API (xAI Cloud)';
119
+ if (provider === 'openai')
120
+ return 'OpenAI API (ChatGPT)';
121
+ if (provider === 'anthropic')
122
+ return 'Claude API (Anthropic)';
123
+ if (provider === 'google')
124
+ return 'Gemini API (Google)';
125
+ if (provider === 'ollama')
126
+ return 'Ollama (Local)';
127
+ return 'OpenAI-Compatible';
128
+ }
129
+ function hasRuntimeProviderKey(provider) {
130
+ if (provider === 'grok') {
131
+ return Boolean(process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim());
132
+ }
133
+ if (provider === 'openai') {
134
+ return Boolean(process.env.TERMINUS_OPENAI_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim());
135
+ }
136
+ if (provider === 'anthropic') {
137
+ return Boolean(process.env.TERMINUS_ANTHROPIC_API_KEY?.trim() || process.env.ANTHROPIC_API_KEY?.trim());
138
+ }
139
+ if (provider === 'google') {
140
+ return Boolean(process.env.TERMINUS_GOOGLE_API_KEY?.trim()
141
+ || process.env.GOOGLE_API_KEY?.trim()
142
+ || process.env.GEMINI_API_KEY?.trim());
143
+ }
144
+ return false;
145
+ }
@@ -1,4 +1,5 @@
1
1
  export type NetworkProfile = 'local' | 'testnet' | 'mainnet';
2
+ export type LlmProvider = 'grok' | 'openai' | 'anthropic' | 'google' | 'ollama' | 'openai-compatible';
2
3
  export interface AgentConfig {
3
4
  agentType: string;
4
5
  wallet: string;
@@ -6,7 +7,7 @@ export interface AgentConfig {
6
7
  apiKey: string;
7
8
  controlPlaneUrl: string;
8
9
  nodeId: string;
9
- llmProvider?: 'grok' | 'ollama' | 'openai-compatible';
10
+ llmProvider?: LlmProvider;
10
11
  llmBaseUrl?: string;
11
12
  llmModel?: string;
12
13
  networkProfile?: NetworkProfile;
@@ -50,11 +50,11 @@ export function validateConfig(config) {
50
50
  errors.push('nodeId is required');
51
51
  }
52
52
  const provider = config.llmProvider || 'grok';
53
- if (provider === 'grok') {
53
+ if (requiresApiKey(provider)) {
54
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());
55
+ const hasRuntimeKey = hasRuntimeProviderKey(provider);
56
56
  if (!hasStoredKey && !hasRuntimeKey) {
57
- errors.push('grok provider requires apiKey in config or TERMINUS_GROK_API_KEY/XAI_API_KEY env');
57
+ errors.push(`${provider} provider requires apiKey in config or runtime env`);
58
58
  }
59
59
  }
60
60
  if (provider === 'openai-compatible' && !config.llmBaseUrl) {
@@ -71,7 +71,7 @@ export function saveConfig(config) {
71
71
  }
72
72
  securePermissions(CONFIG_DIR, DIR_MODE);
73
73
  const provider = config.llmProvider || 'grok';
74
- const normalizedApiKey = provider === 'grok'
74
+ const normalizedApiKey = requiresApiKey(provider)
75
75
  ? (config.apiKey || '__ENV__')
76
76
  : (config.apiKey || '');
77
77
  const normalizedProfile = normalizeNetworkProfile(config.networkProfile);
@@ -105,7 +105,12 @@ function normalizeConfig(raw) {
105
105
  return null;
106
106
  const value = raw;
107
107
  const llmProvider = value.llmProvider;
108
- const provider = llmProvider === 'ollama' || llmProvider === 'openai-compatible' || llmProvider === 'grok'
108
+ const provider = llmProvider === 'grok'
109
+ || llmProvider === 'openai'
110
+ || llmProvider === 'anthropic'
111
+ || llmProvider === 'google'
112
+ || llmProvider === 'ollama'
113
+ || llmProvider === 'openai-compatible'
109
114
  ? llmProvider
110
115
  : 'grok';
111
116
  const networkProfile = normalizeNetworkProfile(value.networkProfile);
@@ -146,3 +151,23 @@ function normalizeNetworkProfile(value) {
146
151
  return value;
147
152
  return 'local';
148
153
  }
154
+ function requiresApiKey(provider) {
155
+ return provider === 'grok' || provider === 'openai' || provider === 'anthropic' || provider === 'google';
156
+ }
157
+ function hasRuntimeProviderKey(provider) {
158
+ if (provider === 'grok') {
159
+ return Boolean(process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim());
160
+ }
161
+ if (provider === 'openai') {
162
+ return Boolean(process.env.TERMINUS_OPENAI_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim());
163
+ }
164
+ if (provider === 'anthropic') {
165
+ return Boolean(process.env.TERMINUS_ANTHROPIC_API_KEY?.trim() || process.env.ANTHROPIC_API_KEY?.trim());
166
+ }
167
+ if (provider === 'google') {
168
+ return Boolean(process.env.TERMINUS_GOOGLE_API_KEY?.trim()
169
+ || process.env.GOOGLE_API_KEY?.trim()
170
+ || process.env.GEMINI_API_KEY?.trim());
171
+ }
172
+ return false;
173
+ }
package/dist/index.js CHANGED
@@ -19,13 +19,13 @@ async function withCliErrorHandling(task) {
19
19
  program
20
20
  .name('terminus-agent')
21
21
  .description('Standalone agent runner for Terminus network')
22
- .version('0.1.0');
22
+ .version('0.1.3');
23
23
  program
24
24
  .command('init')
25
25
  .description('Initialize agent configuration')
26
26
  .option('--agent-type <id>', 'Agent type id (example: travel-planner)')
27
27
  .option('--wallet <address>', 'Wallet address for payouts')
28
- .option('--llm-provider <provider>', 'grok | ollama | openai-compatible')
28
+ .option('--llm-provider <provider>', 'grok | openai | anthropic | google | ollama | openai-compatible')
29
29
  .option('--api-key <key>', 'API key for provider')
30
30
  .option('--llm-base-url <url>', 'Provider base URL')
31
31
  .option('--llm-model <name>', 'LLM model name')
@@ -7,8 +7,9 @@ export interface LLMResponse {
7
7
  model: string;
8
8
  tokensUsed?: number;
9
9
  }
10
+ export type LLMProviderType = 'grok' | 'openai' | 'anthropic' | 'google' | 'ollama' | 'openai-compatible';
10
11
  export interface LLMProviderConfig {
11
- provider: 'grok' | 'ollama' | 'openai-compatible';
12
+ provider: LLMProviderType;
12
13
  apiKey?: string;
13
14
  baseUrl?: string;
14
15
  model?: string;
@@ -30,6 +31,36 @@ export declare class GrokProvider implements LLMProvider {
30
31
  temperature?: number;
31
32
  }): Promise<LLMResponse>;
32
33
  }
34
+ export declare class OpenAIProvider implements LLMProvider {
35
+ name: string;
36
+ private apiKey;
37
+ private model;
38
+ constructor(apiKey: string, model?: string);
39
+ chat(messages: LLMMessage[], options?: {
40
+ maxTokens?: number;
41
+ temperature?: number;
42
+ }): Promise<LLMResponse>;
43
+ }
44
+ export declare class AnthropicProvider implements LLMProvider {
45
+ name: string;
46
+ private apiKey;
47
+ private model;
48
+ constructor(apiKey: string, model?: string);
49
+ chat(messages: LLMMessage[], options?: {
50
+ maxTokens?: number;
51
+ temperature?: number;
52
+ }): Promise<LLMResponse>;
53
+ }
54
+ export declare class GoogleProvider implements LLMProvider {
55
+ name: string;
56
+ private apiKey;
57
+ private model;
58
+ constructor(apiKey: string, model?: string);
59
+ chat(messages: LLMMessage[], options?: {
60
+ maxTokens?: number;
61
+ temperature?: number;
62
+ }): Promise<LLMResponse>;
63
+ }
33
64
  export declare class OllamaProvider implements LLMProvider {
34
65
  name: string;
35
66
  private baseUrl;
@@ -2,7 +2,8 @@
2
2
  // TERMINUS AGENT - LLM Provider Interface
3
3
  // =============================================================================
4
4
  // Abstraction layer for different LLM backends.
5
- // Supports: xAI Grok (API), Ollama (local), OpenAI-compatible (local/cloud)
5
+ // Supports: xAI Grok, OpenAI, Anthropic Claude, Google Gemini, Ollama,
6
+ // and OpenAI-compatible endpoints.
6
7
  // =============================================================================
7
8
  // =============================================================================
8
9
  // xAI Grok Provider
@@ -20,7 +21,7 @@ export class GrokProvider {
20
21
  method: 'POST',
21
22
  headers: {
22
23
  'Content-Type': 'application/json',
23
- 'Authorization': `Bearer ${this.apiKey}`,
24
+ Authorization: `Bearer ${this.apiKey}`,
24
25
  },
25
26
  body: JSON.stringify({
26
27
  model: this.model,
@@ -42,6 +43,139 @@ export class GrokProvider {
42
43
  }
43
44
  }
44
45
  // =============================================================================
46
+ // OpenAI Provider
47
+ // =============================================================================
48
+ export class OpenAIProvider {
49
+ name = 'openai';
50
+ apiKey;
51
+ model;
52
+ constructor(apiKey, model = 'gpt-4o-mini') {
53
+ this.apiKey = apiKey;
54
+ this.model = model;
55
+ }
56
+ async chat(messages, options) {
57
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
58
+ method: 'POST',
59
+ headers: {
60
+ 'Content-Type': 'application/json',
61
+ Authorization: `Bearer ${this.apiKey}`,
62
+ },
63
+ body: JSON.stringify({
64
+ model: this.model,
65
+ messages,
66
+ max_tokens: options?.maxTokens ?? 1024,
67
+ temperature: options?.temperature ?? 0.7,
68
+ }),
69
+ });
70
+ if (!response.ok) {
71
+ const error = await response.text();
72
+ throw new Error(`OpenAI API error: ${response.status} - ${error}`);
73
+ }
74
+ const data = await response.json();
75
+ return {
76
+ content: extractOpenAIContent(data.choices[0]?.message?.content),
77
+ model: this.model,
78
+ tokensUsed: data.usage?.total_tokens,
79
+ };
80
+ }
81
+ }
82
+ // =============================================================================
83
+ // Anthropic Claude Provider
84
+ // =============================================================================
85
+ export class AnthropicProvider {
86
+ name = 'anthropic';
87
+ apiKey;
88
+ model;
89
+ constructor(apiKey, model = 'claude-3-5-haiku-latest') {
90
+ this.apiKey = apiKey;
91
+ this.model = model;
92
+ }
93
+ async chat(messages, options) {
94
+ const normalized = toAnthropicMessages(messages);
95
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
96
+ method: 'POST',
97
+ headers: {
98
+ 'Content-Type': 'application/json',
99
+ 'x-api-key': this.apiKey,
100
+ 'anthropic-version': '2023-06-01',
101
+ },
102
+ body: JSON.stringify({
103
+ model: this.model,
104
+ max_tokens: options?.maxTokens ?? 1024,
105
+ temperature: options?.temperature ?? 0.7,
106
+ system: normalized.systemPrompt || undefined,
107
+ messages: normalized.messages,
108
+ }),
109
+ });
110
+ if (!response.ok) {
111
+ const error = await response.text();
112
+ throw new Error(`Anthropic API error: ${response.status} - ${error}`);
113
+ }
114
+ const data = await response.json();
115
+ const content = data.content
116
+ .filter((part) => part.type === 'text' && typeof part.text === 'string')
117
+ .map((part) => part.text)
118
+ .join('\n')
119
+ .trim();
120
+ const inputTokens = data.usage?.input_tokens ?? 0;
121
+ const outputTokens = data.usage?.output_tokens ?? 0;
122
+ return {
123
+ content,
124
+ model: this.model,
125
+ tokensUsed: inputTokens + outputTokens,
126
+ };
127
+ }
128
+ }
129
+ // =============================================================================
130
+ // Google Gemini Provider
131
+ // =============================================================================
132
+ export class GoogleProvider {
133
+ name = 'google';
134
+ apiKey;
135
+ model;
136
+ constructor(apiKey, model = 'gemini-2.0-flash') {
137
+ this.apiKey = apiKey;
138
+ this.model = model;
139
+ }
140
+ async chat(messages, options) {
141
+ const normalized = toGoogleMessages(messages);
142
+ const encodedModel = encodeURIComponent(this.model);
143
+ const payload = {
144
+ contents: normalized.messages,
145
+ generationConfig: {
146
+ maxOutputTokens: options?.maxTokens ?? 1024,
147
+ temperature: options?.temperature ?? 0.7,
148
+ },
149
+ };
150
+ if (normalized.systemPrompt) {
151
+ payload.systemInstruction = {
152
+ parts: [{ text: normalized.systemPrompt }],
153
+ };
154
+ }
155
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${encodedModel}:generateContent?key=${this.apiKey}`, {
156
+ method: 'POST',
157
+ headers: {
158
+ 'Content-Type': 'application/json',
159
+ },
160
+ body: JSON.stringify(payload),
161
+ });
162
+ if (!response.ok) {
163
+ const error = await response.text();
164
+ throw new Error(`Google Gemini API error: ${response.status} - ${error}`);
165
+ }
166
+ const data = await response.json();
167
+ const content = data.candidates?.[0]?.content?.parts
168
+ ?.map((part) => part.text || '')
169
+ .join('\n')
170
+ .trim() || '';
171
+ return {
172
+ content,
173
+ model: this.model,
174
+ tokensUsed: data.usageMetadata?.totalTokenCount,
175
+ };
176
+ }
177
+ }
178
+ // =============================================================================
45
179
  // Ollama Provider (Local LLM)
46
180
  // =============================================================================
47
181
  export class OllamaProvider {
@@ -49,11 +183,10 @@ export class OllamaProvider {
49
183
  baseUrl;
50
184
  model;
51
185
  constructor(baseUrl = 'http://localhost:11434', model = 'llama3') {
52
- this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
186
+ this.baseUrl = baseUrl.replace(/\/$/, '');
53
187
  this.model = model;
54
188
  }
55
189
  async chat(messages, options) {
56
- // Ollama uses /api/chat endpoint
57
190
  const response = await fetch(`${this.baseUrl}/api/chat`, {
58
191
  method: 'POST',
59
192
  headers: {
@@ -129,14 +262,34 @@ export class OpenAICompatibleProvider {
129
262
  // =============================================================================
130
263
  export function createLLMProvider(config) {
131
264
  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);
265
+ case 'grok': {
266
+ const apiKey = resolveProviderApiKey('grok', config.apiKey);
267
+ if (!apiKey) {
268
+ throw new Error('Grok provider requires a key in config or TERMINUS_GROK_API_KEY/XAI_API_KEY env');
269
+ }
270
+ return new GrokProvider(apiKey, config.model);
271
+ }
272
+ case 'openai': {
273
+ const apiKey = resolveProviderApiKey('openai', config.apiKey);
274
+ if (!apiKey) {
275
+ throw new Error('OpenAI provider requires a key in config or TERMINUS_OPENAI_API_KEY/OPENAI_API_KEY env');
139
276
  }
277
+ return new OpenAIProvider(apiKey, config.model || 'gpt-4o-mini');
278
+ }
279
+ case 'anthropic': {
280
+ const apiKey = resolveProviderApiKey('anthropic', config.apiKey);
281
+ if (!apiKey) {
282
+ throw new Error('Anthropic provider requires a key in config or TERMINUS_ANTHROPIC_API_KEY/ANTHROPIC_API_KEY env');
283
+ }
284
+ return new AnthropicProvider(apiKey, config.model || 'claude-3-5-haiku-latest');
285
+ }
286
+ case 'google': {
287
+ const apiKey = resolveProviderApiKey('google', config.apiKey);
288
+ if (!apiKey) {
289
+ throw new Error('Google provider requires a key in config or TERMINUS_GOOGLE_API_KEY/GOOGLE_API_KEY/GEMINI_API_KEY env');
290
+ }
291
+ return new GoogleProvider(apiKey, config.model || 'gemini-2.0-flash');
292
+ }
140
293
  case 'ollama':
141
294
  return new OllamaProvider(config.baseUrl || 'http://localhost:11434', config.model || 'llama3');
142
295
  case 'openai-compatible':
@@ -149,7 +302,7 @@ export function createLLMProvider(config) {
149
302
  }
150
303
  }
151
304
  // =============================================================================
152
- // Helper to detect Ollama availability
305
+ // Provider helpers
153
306
  // =============================================================================
154
307
  export async function checkOllamaAvailable(baseUrl = 'http://localhost:11434') {
155
308
  try {
@@ -166,16 +319,80 @@ export async function listOllamaModels(baseUrl = 'http://localhost:11434') {
166
319
  if (!response.ok)
167
320
  return [];
168
321
  const data = await response.json();
169
- return data.models?.map(m => m.name) || [];
322
+ return data.models?.map((m) => m.name) || [];
170
323
  }
171
324
  catch {
172
325
  return [];
173
326
  }
174
327
  }
175
- function resolveGrokApiKey(configApiKey) {
328
+ function resolveProviderApiKey(provider, configApiKey) {
176
329
  const key = configApiKey?.trim();
177
330
  if (key && key !== '__ENV__')
178
331
  return key;
179
- const runtimeKey = process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim();
180
- return runtimeKey || undefined;
332
+ if (provider === 'grok') {
333
+ return process.env.TERMINUS_GROK_API_KEY?.trim() || process.env.XAI_API_KEY?.trim() || undefined;
334
+ }
335
+ if (provider === 'openai') {
336
+ return process.env.TERMINUS_OPENAI_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim() || undefined;
337
+ }
338
+ if (provider === 'anthropic') {
339
+ return process.env.TERMINUS_ANTHROPIC_API_KEY?.trim() || process.env.ANTHROPIC_API_KEY?.trim() || undefined;
340
+ }
341
+ return (process.env.TERMINUS_GOOGLE_API_KEY?.trim()
342
+ || process.env.GOOGLE_API_KEY?.trim()
343
+ || process.env.GEMINI_API_KEY?.trim()
344
+ || undefined);
345
+ }
346
+ function extractOpenAIContent(content) {
347
+ if (!content)
348
+ return '';
349
+ if (typeof content === 'string')
350
+ return content;
351
+ return content
352
+ .filter((part) => part.type === 'text' && typeof part.text === 'string')
353
+ .map((part) => part.text)
354
+ .join('\n')
355
+ .trim();
356
+ }
357
+ function toAnthropicMessages(messages) {
358
+ const systemChunks = [];
359
+ const converted = [];
360
+ for (const message of messages) {
361
+ if (message.role === 'system') {
362
+ systemChunks.push(message.content);
363
+ continue;
364
+ }
365
+ converted.push({
366
+ role: message.role,
367
+ content: message.content,
368
+ });
369
+ }
370
+ if (converted.length === 0) {
371
+ converted.push({ role: 'user', content: 'Continue.' });
372
+ }
373
+ return {
374
+ systemPrompt: systemChunks.join('\n\n').trim(),
375
+ messages: converted,
376
+ };
377
+ }
378
+ function toGoogleMessages(messages) {
379
+ const systemChunks = [];
380
+ const converted = [];
381
+ for (const message of messages) {
382
+ if (message.role === 'system') {
383
+ systemChunks.push(message.content);
384
+ continue;
385
+ }
386
+ converted.push({
387
+ role: message.role === 'assistant' ? 'model' : 'user',
388
+ parts: [{ text: message.content }],
389
+ });
390
+ }
391
+ if (converted.length === 0) {
392
+ converted.push({ role: 'user', parts: [{ text: 'Continue.' }] });
393
+ }
394
+ return {
395
+ systemPrompt: systemChunks.join('\n\n').trim(),
396
+ messages: converted,
397
+ };
181
398
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terminusagents/agents",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Standalone agent runner for Terminus network",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",