@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,33 @@
1
+ import { AgentConfig } from '../config/store.js';
2
+ export declare class AgentClient {
3
+ private ws;
4
+ private config;
5
+ private executor;
6
+ private heartbeatInterval;
7
+ private reconnectAttempts;
8
+ private activeJobs;
9
+ private authChallenge;
10
+ private authenticated;
11
+ private manualDisconnect;
12
+ private connectResolve;
13
+ private connectReject;
14
+ private authAckTimeout;
15
+ private readonly authAckTimeoutMs;
16
+ constructor(config: AgentConfig);
17
+ connect(): Promise<void>;
18
+ disconnect(): void;
19
+ private getRuntimePrivateKey;
20
+ private getSystemMetrics;
21
+ private sendAuthFromChallenge;
22
+ private handleMessage;
23
+ private handleAuthAck;
24
+ private handleAgentJob;
25
+ private startHeartbeat;
26
+ private stopHeartbeat;
27
+ private scheduleReconnect;
28
+ private send;
29
+ private generateTraceId;
30
+ private resolvePendingConnect;
31
+ private rejectPendingConnect;
32
+ private clearAuthTimeout;
33
+ }
@@ -0,0 +1,389 @@
1
+ // =============================================================================
2
+ // TERMINUS AGENT - WebSocket Client
3
+ // =============================================================================
4
+ import os from 'os';
5
+ import WebSocket from 'ws';
6
+ import chalk from 'chalk';
7
+ import { ethers } from 'ethers';
8
+ import { AgentExecutor } from '../agent/executor.js';
9
+ function isRecord(value) {
10
+ return typeof value === 'object' && value !== null;
11
+ }
12
+ function parseIncomingMessage(raw) {
13
+ let parsed;
14
+ try {
15
+ parsed = JSON.parse(raw);
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ if (!isRecord(parsed) || typeof parsed.type !== 'string')
21
+ return null;
22
+ const payload = isRecord(parsed.payload) ? parsed.payload : {};
23
+ switch (parsed.type) {
24
+ case 'AUTH_CHALLENGE':
25
+ if (typeof payload.challengeId !== 'string' ||
26
+ typeof payload.challengeMessage !== 'string' ||
27
+ typeof payload.expiresAt !== 'number' ||
28
+ typeof payload.requireWallet !== 'boolean' ||
29
+ typeof payload.requireNft !== 'boolean' ||
30
+ typeof payload.allowSharedSecret !== 'boolean') {
31
+ return null;
32
+ }
33
+ return parsed;
34
+ case 'AUTH_ACK':
35
+ if (typeof payload.success !== 'boolean' ||
36
+ (payload.message !== undefined && typeof payload.message !== 'string') ||
37
+ (payload.heartbeatInterval !== undefined && typeof payload.heartbeatInterval !== 'number')) {
38
+ return null;
39
+ }
40
+ return parsed;
41
+ case 'AGENT_JOB':
42
+ if (typeof payload.jobId !== 'string' ||
43
+ typeof payload.agentType !== 'string' ||
44
+ typeof payload.userQuery !== 'string') {
45
+ return null;
46
+ }
47
+ return parsed;
48
+ case 'HEARTBEAT_ACK':
49
+ return parsed;
50
+ case 'ERROR':
51
+ if (payload.message !== undefined && typeof payload.message !== 'string') {
52
+ return null;
53
+ }
54
+ return parsed;
55
+ default:
56
+ return null;
57
+ }
58
+ }
59
+ export class AgentClient {
60
+ ws = null;
61
+ config;
62
+ executor;
63
+ heartbeatInterval = null;
64
+ reconnectAttempts = 0;
65
+ activeJobs = 0;
66
+ authChallenge = null;
67
+ authenticated = false;
68
+ manualDisconnect = false;
69
+ connectResolve = null;
70
+ connectReject = null;
71
+ authAckTimeout = null;
72
+ authAckTimeoutMs = parsePositiveInt(process.env.TERMINUS_AUTH_ACK_TIMEOUT_MS, 15000);
73
+ constructor(config) {
74
+ this.config = config;
75
+ this.executor = new AgentExecutor(config);
76
+ }
77
+ async connect() {
78
+ return new Promise((resolve, reject) => {
79
+ const requireWss = process.env.REQUIRE_WSS === 'true';
80
+ if (requireWss && !this.config.controlPlaneUrl.startsWith('wss://')) {
81
+ reject(new Error('REQUIRE_WSS=true but control plane URL is not wss://'));
82
+ return;
83
+ }
84
+ if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
85
+ if (this.authenticated) {
86
+ resolve();
87
+ return;
88
+ }
89
+ reject(new Error('Connection is already in progress'));
90
+ return;
91
+ }
92
+ this.connectResolve = resolve;
93
+ this.connectReject = reject;
94
+ this.authenticated = false;
95
+ this.manualDisconnect = false;
96
+ this.clearAuthTimeout();
97
+ console.log(chalk.gray(`🔌 Connecting to ${this.config.controlPlaneUrl}...`));
98
+ this.ws = new WebSocket(this.config.controlPlaneUrl);
99
+ this.ws.on('open', () => {
100
+ console.log(chalk.green('✅ Connected! Waiting AUTH_CHALLENGE...'));
101
+ this.reconnectAttempts = 0;
102
+ this.authAckTimeout = setTimeout(() => {
103
+ if (!this.authenticated) {
104
+ this.rejectPendingConnect(new Error('Authentication timeout waiting for AUTH_ACK'));
105
+ this.disconnect();
106
+ }
107
+ }, this.authAckTimeoutMs);
108
+ });
109
+ this.ws.on('message', (data) => {
110
+ this.handleMessage(data.toString());
111
+ });
112
+ this.ws.on('close', () => {
113
+ console.log(chalk.yellow('❌ Disconnected from Control Plane'));
114
+ this.stopHeartbeat();
115
+ this.authChallenge = null;
116
+ this.clearAuthTimeout();
117
+ const wasAuthenticated = this.authenticated;
118
+ this.authenticated = false;
119
+ this.ws = null;
120
+ if (!wasAuthenticated) {
121
+ this.rejectPendingConnect(new Error('Connection closed before authentication completed'));
122
+ }
123
+ if (!this.manualDisconnect) {
124
+ this.scheduleReconnect();
125
+ }
126
+ });
127
+ this.ws.on('error', (err) => {
128
+ console.log(chalk.red(`Socket error: ${err.message}`));
129
+ if (!this.authenticated) {
130
+ this.rejectPendingConnect(new Error(`Socket error: ${err.message}`));
131
+ }
132
+ });
133
+ });
134
+ }
135
+ disconnect() {
136
+ this.manualDisconnect = true;
137
+ this.stopHeartbeat();
138
+ this.clearAuthTimeout();
139
+ if (this.ws) {
140
+ this.ws.close();
141
+ this.ws = null;
142
+ }
143
+ }
144
+ getRuntimePrivateKey() {
145
+ const envPk = process.env.TERMINUS_WALLET_PRIVATE_KEY?.trim();
146
+ if (envPk)
147
+ return envPk;
148
+ return this.config.privateKey;
149
+ }
150
+ getSystemMetrics() {
151
+ const cpuCount = Math.max(os.cpus().length, 1);
152
+ const load = os.loadavg()[0];
153
+ const cpuUsage = Math.max(0, Math.min(100, Math.round((load / cpuCount) * 100)));
154
+ const totalMemory = os.totalmem();
155
+ const usedMemory = totalMemory - os.freemem();
156
+ const memoryUsage = Math.max(0, Math.min(100, Math.round((usedMemory / totalMemory) * 100)));
157
+ return { cpuUsage, memoryUsage };
158
+ }
159
+ async sendAuthFromChallenge(challenge) {
160
+ const privateKey = this.getRuntimePrivateKey();
161
+ let challengeSignature;
162
+ let wallet = this.config.wallet;
163
+ if (privateKey) {
164
+ try {
165
+ const normalizedPk = privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`;
166
+ const signer = new ethers.Wallet(normalizedPk);
167
+ wallet = signer.address;
168
+ challengeSignature = await signer.signMessage(challenge.challengeMessage);
169
+ console.log(chalk.green('🔐 Challenge signature created'));
170
+ if (wallet.toLowerCase() !== this.config.wallet.toLowerCase()) {
171
+ if (process.env.ALLOW_WALLET_MISMATCH === 'true') {
172
+ console.log(chalk.yellow(`⚠ Config wallet ${this.config.wallet} differs from signer wallet ${wallet}; signer wallet will be used.`));
173
+ }
174
+ else {
175
+ console.log(chalk.red('❌ Config wallet does not match TERMINUS_WALLET_PRIVATE_KEY signer wallet.'));
176
+ console.log(chalk.gray(' Fix by running `npx terminus-agent init` with the signer wallet.'));
177
+ this.rejectPendingConnect(new Error('Wallet mismatch between config and private key'));
178
+ this.disconnect();
179
+ return;
180
+ }
181
+ }
182
+ }
183
+ catch (err) {
184
+ console.log(chalk.red(`❌ Failed to sign challenge: ${err.message}`));
185
+ }
186
+ }
187
+ const devSecret = process.env.NODE_SECRET?.trim();
188
+ const allowDevSharedSecret = process.env.ALLOW_DEV_SHARED_SECRET === 'true';
189
+ const canUseDevSecret = allowDevSharedSecret && challenge.allowSharedSecret && Boolean(devSecret);
190
+ const secret = challengeSignature ? undefined : (canUseDevSecret ? devSecret : undefined);
191
+ if (challenge.requireWallet && !wallet) {
192
+ console.log(chalk.red('❌ Wallet is required by control plane but not configured.'));
193
+ this.rejectPendingConnect(new Error('Wallet is required by control plane but not configured'));
194
+ this.disconnect();
195
+ return;
196
+ }
197
+ if (challenge.requireWallet && !challengeSignature && !secret) {
198
+ console.log(chalk.red('❌ Missing TERMINUS_WALLET_PRIVATE_KEY for challenge-signature auth.'));
199
+ this.rejectPendingConnect(new Error('Missing challenge signature and shared-secret fallback'));
200
+ this.disconnect();
201
+ return;
202
+ }
203
+ const totalMemoryGB = Math.round((os.totalmem() / 1024 / 1024 / 1024) * 100) / 100;
204
+ const message = {
205
+ type: 'AUTH',
206
+ traceId: this.generateTraceId(),
207
+ timestamp: Date.now(),
208
+ payload: {
209
+ nodeId: this.config.nodeId,
210
+ capabilities: ['agent-execution', 'llm-inference'],
211
+ agentTypes: [this.config.agentType],
212
+ wallet,
213
+ challengeId: challenge.challengeId,
214
+ challengeMessage: challenge.challengeMessage,
215
+ challengeSignature,
216
+ specs: {
217
+ os: process.platform,
218
+ arch: process.arch,
219
+ cpuCores: os.cpus().length,
220
+ totalMemoryGB,
221
+ nodeVersion: process.version,
222
+ },
223
+ secret,
224
+ version: '0.2.0',
225
+ },
226
+ };
227
+ this.send(message);
228
+ }
229
+ handleMessage(raw) {
230
+ const message = parseIncomingMessage(raw);
231
+ if (!message) {
232
+ console.log(chalk.red('Failed to parse or validate incoming message'));
233
+ return;
234
+ }
235
+ switch (message.type) {
236
+ case 'AUTH_CHALLENGE':
237
+ this.authChallenge = message.payload;
238
+ if (Date.now() > message.payload.expiresAt) {
239
+ console.log(chalk.red('❌ Received expired AUTH_CHALLENGE'));
240
+ this.rejectPendingConnect(new Error('Received expired AUTH_CHALLENGE'));
241
+ this.disconnect();
242
+ return;
243
+ }
244
+ void this.sendAuthFromChallenge(message.payload);
245
+ break;
246
+ case 'AUTH_ACK':
247
+ this.handleAuthAck(message);
248
+ break;
249
+ case 'HEARTBEAT_ACK':
250
+ break;
251
+ case 'AGENT_JOB':
252
+ void this.handleAgentJob(message);
253
+ break;
254
+ case 'ERROR':
255
+ console.log(chalk.red(`❌ Error: ${message.payload.message || 'Unknown error'}`));
256
+ break;
257
+ }
258
+ }
259
+ handleAuthAck(message) {
260
+ if (message.payload.success) {
261
+ this.authenticated = true;
262
+ this.clearAuthTimeout();
263
+ this.resolvePendingConnect();
264
+ console.log(chalk.green(`🎉 Authenticated as ${this.config.nodeId}`));
265
+ this.startHeartbeat(message.payload.heartbeatInterval || 5000);
266
+ return;
267
+ }
268
+ console.log(chalk.red(`❌ Auth failed: ${message.payload.message || 'Unknown reason'}`));
269
+ this.rejectPendingConnect(new Error(message.payload.message || 'Authentication failed'));
270
+ this.disconnect();
271
+ }
272
+ async handleAgentJob(message) {
273
+ const { jobId, userQuery } = message.payload;
274
+ console.log(chalk.cyan(`📥 Job ${jobId} received (${message.payload.agentType})`));
275
+ this.activeJobs++;
276
+ try {
277
+ const startTime = Date.now();
278
+ const result = await this.executor.execute(userQuery);
279
+ const durationMs = Date.now() - startTime;
280
+ console.log(chalk.green(`✅ Job ${jobId} complete (${durationMs}ms)`));
281
+ this.send({
282
+ type: 'AGENT_JOB_RESULT',
283
+ traceId: message.traceId,
284
+ timestamp: Date.now(),
285
+ payload: {
286
+ jobId,
287
+ success: true,
288
+ response: result.response,
289
+ toolsUsed: result.toolsUsed,
290
+ metrics: {
291
+ executionTimeMs: durationMs,
292
+ },
293
+ },
294
+ });
295
+ }
296
+ catch (err) {
297
+ const error = err;
298
+ console.log(chalk.red(`❌ Job ${jobId} failed: ${error.message}`));
299
+ this.send({
300
+ type: 'AGENT_JOB_RESULT',
301
+ traceId: message.traceId,
302
+ timestamp: Date.now(),
303
+ payload: {
304
+ jobId,
305
+ success: false,
306
+ response: '',
307
+ error: {
308
+ code: 'EXECUTION_ERROR',
309
+ message: error.message,
310
+ },
311
+ },
312
+ });
313
+ }
314
+ finally {
315
+ this.activeJobs--;
316
+ }
317
+ }
318
+ startHeartbeat(intervalMs) {
319
+ console.log(chalk.gray(`⏱️ Starting heartbeat every ${intervalMs}ms`));
320
+ this.heartbeatInterval = setInterval(() => {
321
+ const status = this.activeJobs > 0 ? 'BUSY' : 'IDLE';
322
+ const metrics = this.getSystemMetrics();
323
+ const message = {
324
+ type: 'HEARTBEAT',
325
+ traceId: this.generateTraceId(),
326
+ timestamp: Date.now(),
327
+ payload: {
328
+ status,
329
+ cpuUsage: metrics.cpuUsage,
330
+ memoryUsage: metrics.memoryUsage,
331
+ activeJobs: this.activeJobs,
332
+ },
333
+ };
334
+ this.send(message);
335
+ }, intervalMs);
336
+ }
337
+ stopHeartbeat() {
338
+ if (this.heartbeatInterval) {
339
+ clearInterval(this.heartbeatInterval);
340
+ this.heartbeatInterval = null;
341
+ }
342
+ }
343
+ scheduleReconnect() {
344
+ this.reconnectAttempts++;
345
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 30000);
346
+ console.log(chalk.gray(`🔄 Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})...`));
347
+ setTimeout(() => {
348
+ void this.connect().catch(() => {
349
+ // A failed reconnect will trigger close/error flow and next retry.
350
+ });
351
+ }, delay);
352
+ }
353
+ send(message) {
354
+ if (this.ws?.readyState === WebSocket.OPEN) {
355
+ this.ws.send(JSON.stringify(message));
356
+ }
357
+ }
358
+ generateTraceId() {
359
+ return `trace-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
360
+ }
361
+ resolvePendingConnect() {
362
+ if (!this.connectResolve)
363
+ return;
364
+ const resolve = this.connectResolve;
365
+ this.connectResolve = null;
366
+ this.connectReject = null;
367
+ resolve();
368
+ }
369
+ rejectPendingConnect(error) {
370
+ if (!this.connectReject)
371
+ return;
372
+ const reject = this.connectReject;
373
+ this.connectResolve = null;
374
+ this.connectReject = null;
375
+ reject(error);
376
+ }
377
+ clearAuthTimeout() {
378
+ if (!this.authAckTimeout)
379
+ return;
380
+ clearTimeout(this.authAckTimeout);
381
+ this.authAckTimeout = null;
382
+ }
383
+ }
384
+ function parsePositiveInt(raw, fallback) {
385
+ if (!raw)
386
+ return fallback;
387
+ const value = Number.parseInt(raw, 10);
388
+ return Number.isFinite(value) && value > 0 ? value : fallback;
389
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@terminusagents/agents",
3
+ "version": "0.1.0",
4
+ "description": "Standalone agent runner for Terminus network",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "files": [
8
+ "bin",
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "bin": {
13
+ "terminus-agent": "bin/terminus-agent.js"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "clean": "rm -rf dist",
20
+ "build": "tsc",
21
+ "prepare": "npm run build",
22
+ "prepack": "npm run build",
23
+ "dev": "tsx src/index.ts",
24
+ "start": "node dist/index.js"
25
+ },
26
+ "dependencies": {
27
+ "chalk": "^5.3.0",
28
+ "commander": "^12.1.0",
29
+ "dotenv": "^16.4.5",
30
+ "ethers": "^6.16.0",
31
+ "inquirer": "^9.2.23",
32
+ "ws": "^8.18.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/inquirer": "^9.0.7",
36
+ "@types/node": "^20.11.0",
37
+ "@types/ws": "^8.5.10",
38
+ "tsx": "^4.7.0",
39
+ "typescript": "^5.4.0"
40
+ },
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ },
44
+ "keywords": [
45
+ "terminus",
46
+ "agent",
47
+ "ai",
48
+ "distributed",
49
+ "x402",
50
+ "operator-node",
51
+ "terminus-agents"
52
+ ],
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/Terminusagents/agents.git"
56
+ },
57
+ "bugs": {
58
+ "url": "https://github.com/Terminusagents/agents/issues"
59
+ },
60
+ "homepage": "https://github.com/Terminusagents/agents",
61
+ "author": "Terminusagents <terminusai@gmail.com>",
62
+ "license": "MIT"
63
+ }