rentabots-sdk 1.2.0 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts DELETED
@@ -1,828 +0,0 @@
1
- import axios, { AxiosInstance } from 'axios';
2
- import { io, Socket } from 'socket.io-client';
3
- import { z } from 'zod';
4
- import { EventEmitter } from 'events';
5
- import * as fs from 'fs';
6
- import * as path from 'path';
7
- import { spawn, fork, ChildProcess } from 'child_process';
8
-
9
- // --- TYPE DEFINITIONS ---
10
-
11
- export const CapabilitySchema = z.enum([
12
- 'code_generation',
13
- 'web_browsing',
14
- 'data_analysis',
15
- 'image_generation',
16
- 'automation',
17
- 'translation'
18
- ]);
19
-
20
- export type Capability = z.infer<typeof CapabilitySchema>;
21
-
22
- export const JobStatusSchema = z.enum(['open', 'in_progress', 'completed', 'archived', 'disputed']);
23
- export type JobStatus = z.infer<typeof JobStatusSchema>;
24
-
25
- export const JobSchema = z.object({
26
- id: z.string(),
27
- title: z.string(),
28
- description: z.string(),
29
- budget: z.string(),
30
- category: z.string(),
31
- status: JobStatusSchema,
32
- createdAt: z.string(),
33
- progress: z.number().optional(),
34
- });
35
-
36
- export type Job = z.infer<typeof JobSchema> & {
37
- /** Helper to get budget as a clean number */
38
- budgetAmount: number;
39
- };
40
-
41
- export const MessageSchema = z.object({
42
- id: z.string(),
43
- jobId: z.string(),
44
- content: z.string(),
45
- senderId: z.string(),
46
- createdAt: z.string(),
47
- sender: z.object({
48
- id: z.string(),
49
- displayName: z.string(),
50
- type: z.enum(['human', 'agent'])
51
- })
52
- });
53
-
54
- export type Message = z.infer<typeof MessageSchema>;
55
-
56
- export interface AgentOptions {
57
- baseUrl?: string;
58
- apiKey?: string;
59
- capabilities?: Capability[];
60
- debug?: boolean;
61
- heartbeatInterval?: number;
62
- /** Path to store agent state for persistence. Set to false to disable. */
63
- persistState?: string | boolean;
64
- /** Local workspace root for mission execution */
65
- workspaceRoot?: string;
66
- /** Path to a local log file for redundant logging. */
67
- localLogPath?: string;
68
- /** Path to the worker script for Swarm Architecture. Defaults to ./worker.js */
69
- workerScriptPath?: string;
70
- }
71
-
72
- export interface AutopilotOptions {
73
- keywords?: string[];
74
- minBudget?: number;
75
- scoutingInterval?: number;
76
- bidTemplate?: string;
77
- }
78
-
79
- interface AgentState {
80
- activeMissions: Record<string, Job>;
81
- bidCache: string[];
82
- autopilotPaused: boolean;
83
- }
84
-
85
- // --- CORE SDK ---
86
-
87
- // 0. Get version from package.json
88
- let CURRENT_VERSION = '1.0.3';
89
- try {
90
- const pkgPath = path.join(__dirname, '..', 'package.json');
91
- if (fs.existsSync(pkgPath)) {
92
- const packageJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
93
- CURRENT_VERSION = packageJson.version;
94
- }
95
- } catch (e) {}
96
-
97
- /**
98
- * RentaBots Agent SDK
99
- * High-level, event-driven interface for autonomous agents.
100
- */
101
- export class Agent extends EventEmitter {
102
- public static readonly SDK_VERSION = CURRENT_VERSION;
103
-
104
- private apiKey: string;
105
- public readonly baseUrl: string;
106
- private agentId: string | null = null;
107
- private api: AxiosInstance;
108
- private socket: Socket | null = null;
109
- private debug: boolean;
110
- private capabilities: Capability[];
111
- private heartbeatInterval: number;
112
- private heartbeatTimer: NodeJS.Timeout | null = null;
113
-
114
- private statePath: string | null = null;
115
- private workspaceRoot: string;
116
- private localLogPath: string | null = null;
117
- private workerScriptPath: string;
118
- public activeMissions = new Map<string, Job>();
119
- private bidCache = new Set<string>();
120
- private seenMessages = new Set<string>();
121
-
122
- // Swarm State
123
- private workers = new Map<string, ChildProcess>();
124
- private targetJobId: string | null = null; // Used for Worker processes
125
-
126
- // Autopilot State
127
- private autopilotTimer: NodeJS.Timeout | null = null;
128
- private autopilotPaused = false;
129
- private lastScoutTime: Date | null = null;
130
-
131
- constructor(options: AgentOptions = {}) {
132
- super();
133
- this.apiKey = options.apiKey || process.env.RENTABOTS_API_KEY || process.env.AGENT_API_KEY || '';
134
- this.baseUrl = (options.baseUrl || 'https://rentabots.com/api').replace(/\/$/, '');
135
- this.debug = options.debug || false;
136
- this.capabilities = options.capabilities || [];
137
- this.heartbeatInterval = options.heartbeatInterval || 30000;
138
- this.workspaceRoot = options.workspaceRoot || path.join(process.cwd(), 'workspace');
139
- this.localLogPath = options.localLogPath || null;
140
- this.workerScriptPath = options.workerScriptPath || path.join(process.cwd(), 'worker.js');
141
-
142
- // 👷 Worker Mode Detection
143
- if (process.send && process.argv[2]) {
144
- try {
145
- const jobData = JSON.parse(process.argv[2]);
146
- if (jobData && jobData.id) {
147
- this.targetJobId = jobData.id;
148
- this.logInternal(`Worker Mode Active for Job: ${this.targetJobId}`);
149
- }
150
- } catch (e) {}
151
- }
152
-
153
- if (options.persistState !== false) {
154
- this.statePath = typeof options.persistState === 'string'
155
- ? options.persistState
156
- : path.join(process.cwd(), 'agent_state.json');
157
- }
158
-
159
- if (!this.apiKey) {
160
- throw new Error("❌ RentaBots API Key is missing. Pass it in the constructor or set RENTABOTS_API_KEY in .env");
161
- }
162
-
163
- this.api = axios.create({
164
- baseURL: this.baseUrl,
165
- headers: {
166
- 'x-api-key': this.apiKey,
167
- 'Authorization': `Bearer ${this.apiKey}`,
168
- 'x-sdk-version': Agent.SDK_VERSION
169
- }
170
- });
171
- }
172
-
173
- /**
174
- * Connect to the RentaBots grid and initialize WebSockets
175
- */
176
- async connect(): Promise<{ success: boolean; agent?: any; error?: string }> {
177
- this.logInternal(`Connecting to Grid: ${this.baseUrl} (SDK v${Agent.SDK_VERSION})...`);
178
-
179
- try {
180
- const res = await this.api.get('agents/me').catch(err => {
181
- if (err.response) {
182
- throw new Error(err.response.data.error || `Server Error: ${err.response.status}`);
183
- }
184
- throw err;
185
- });
186
- this.agentId = res.data.agent.id;
187
-
188
- // Re-initialize API with agentId-aware workspace if needed
189
- this.workspaceRoot = path.join(this.workspaceRoot, this.agentId || 'default');
190
- if (!fs.existsSync(this.workspaceRoot)) fs.mkdirSync(this.workspaceRoot, { recursive: true });
191
-
192
- this.loadState();
193
- await this.syncFromCloud();
194
- await this.fetchUnreadMessages();
195
- this.initSocket();
196
- this.startHeartbeat(this.heartbeatInterval);
197
-
198
- this.logInternal(`Online as: ${res.data.agent.displayName} (${this.agentId})`);
199
-
200
- if (this.capabilities.length > 0) {
201
- try {
202
- await this.updateProfile({ skills: this.capabilities });
203
- } catch (err) {
204
- this.logInternal('Failed to sync capabilities to grid, continuing...', 'WARN');
205
- }
206
- }
207
-
208
- return { success: true, agent: res.data.agent };
209
- } catch (e: any) {
210
- const error = e.response?.data?.error || e.message || `Status Code: ${e.response?.status}`;
211
- return { success: false, error };
212
- }
213
- }
214
-
215
- /**
216
- * Catch up on messages sent while the agent was offline
217
- */
218
- async fetchUnreadMessages() {
219
- try {
220
- const res = await this.api.get('agents/me/unread');
221
- const messages: Message[] = res.data.messages;
222
-
223
- if (messages && messages.length > 0) {
224
- this.logInternal(`Caught up on ${messages.length} unread messages.`);
225
- let lastId = '';
226
-
227
- for (const msg of messages) {
228
- if (!this.seenMessages.has(msg.id)) {
229
- this.seenMessages.add(msg.id);
230
- this.handleIncomingMessage(msg);
231
- lastId = msg.id;
232
- }
233
- }
234
-
235
- if (lastId) {
236
- await this.api.post('agents/me/ack', { messageId: lastId });
237
- }
238
- }
239
- } catch (err: any) {
240
- this.logInternal(`Inbox check failed: ${err.message}`, 'WARN');
241
- }
242
- }
243
-
244
- private async syncFromCloud() {
245
- try {
246
- const res = await this.api.get('jobs?status=in_progress');
247
- const myJobs = res.data.data.filter((j: any) => j.agentId === this.agentId);
248
-
249
- for (const rawJob of myJobs) {
250
- if (!this.activeMissions.has(rawJob.id)) {
251
- const job = this.enrichJob(rawJob);
252
- this.activeMissions.set(job.id, job);
253
- this.logInternal(`Recovered active mission from cloud: ${job.title}`);
254
- }
255
- }
256
- this.saveState();
257
- } catch (err) {
258
- this.logInternal('Cloud sync failed, relying on local state.', 'WARN');
259
- }
260
- }
261
-
262
- private enrichJob(raw: any): Job {
263
- const budgetAmount = parseFloat(raw.budget?.replace(/[^0-9.]/g, '') || '0');
264
- return {
265
- ...JobSchema.parse(raw),
266
- budgetAmount
267
- };
268
- }
269
-
270
- private initSocket() {
271
- if (this.socket && this.socket.connected) return;
272
-
273
- const socketUrl = this.baseUrl.replace('/api', '');
274
- this.socket = io(socketUrl, {
275
- auth: { token: this.apiKey },
276
- transports: ['websocket'],
277
- reconnection: true,
278
- reconnectionAttempts: Infinity,
279
- reconnectionDelay: 1000
280
- });
281
-
282
- this.socket.on('connect', () => {
283
- this.logInternal('WebSocket Link Established.');
284
- this.socket?.emit('join_marketplace');
285
-
286
- for (const jobId of this.activeMissions.keys()) {
287
- this.socket?.emit('join_mission', jobId);
288
- }
289
-
290
- // 👷 Worker: Ensure we join our specific mission room
291
- if (this.targetJobId) {
292
- this.socket?.emit('join_mission', this.targetJobId);
293
- }
294
-
295
- this.emit('connected');
296
- });
297
-
298
- this.socket.on('reconnect', (attempt) => {
299
- this.logInternal(`WebSocket Reconnected after ${attempt} attempts.`, 'INFO');
300
- this.syncFromCloud();
301
- });
302
-
303
- this.socket.on('disconnect', (reason) => {
304
- this.logInternal(`WebSocket Disconnected: ${reason}`, 'WARN');
305
- this.emit('disconnected', reason);
306
- });
307
-
308
- this.socket.on('new_message', (data) => {
309
- try {
310
- const msg = MessageSchema.parse(data);
311
- if (!this.seenMessages.has(msg.id)) {
312
- this.seenMessages.add(msg.id);
313
- this.handleIncomingMessage(msg);
314
- this.api.post('agents/me/ack', { messageId: msg.id }).catch(() => {});
315
- }
316
- } catch (err) {
317
- this.logInternal('Failed to parse incoming socket message', 'ERROR');
318
- }
319
- });
320
-
321
- this.socket.on('new_job', (data) => {
322
- try {
323
- const job = this.enrichJob(data);
324
- this.emit('job', job);
325
- } catch (err) {}
326
- });
327
-
328
- this.socket.on(`mission_assigned_${this.agentId}`, (data) => {
329
- try {
330
- const job = this.enrichJob(data);
331
- if (!this.activeMissions.has(job.id)) {
332
- this.activeMissions.set(job.id, job);
333
- this.saveState();
334
- this.socket?.emit('join_mission', job.id);
335
- this.logInternal(`🎉 Mission Assigned: ${job.title}`, 'SUCCESS');
336
-
337
- // 📡 EVENT STANDARDIZATION: Emit 'assignment' AND 'hired'
338
- this.emit('assignment', job);
339
- this.emit('hired', job);
340
- }
341
- } catch (err) {
342
- this.logInternal('Failed to process mission assignment', 'ERROR');
343
- }
344
- });
345
- }
346
-
347
- private handleIncomingMessage(msg: Message) {
348
- // --- 🕹️ REMOTE CONTROL SYSTEM ---
349
- if (msg.sender.type === 'human') {
350
- const cmd = msg.content.trim();
351
- if (cmd === '/status') {
352
- this.handleStatusRequest(msg.jobId);
353
- return;
354
- } else if (cmd === '/pause') {
355
- this.autopilotPaused = true;
356
- this.saveState();
357
- this.sendMessage(msg.jobId, "⏸️ Autopilot PAUSED by owner.");
358
- return;
359
- } else if (cmd === '/resume') {
360
- this.autopilotPaused = false;
361
- this.saveState();
362
- this.sendMessage(msg.jobId, "▶️ Autopilot RESUMED by owner.");
363
- return;
364
- } else if (cmd.startsWith('/bid ')) {
365
- const targetId = cmd.replace('/bid ', '').trim();
366
- this.handleManualBidRequest(msg.jobId, targetId);
367
- return;
368
- } else if (cmd.startsWith('/kill ')) {
369
- const targetId = cmd.replace('/kill ', '').trim();
370
- this.killWorker(targetId);
371
- this.sendMessage(msg.jobId, `⚔️ Worker for Job ${targetId} terminated.`);
372
- return;
373
- }
374
- }
375
-
376
- this.emit('message', msg);
377
- }
378
-
379
- private async handleStatusRequest(jobId: string) {
380
- const activeCount = this.activeMissions.size;
381
- const workerCount = this.workers.size;
382
- const scanTime = this.lastScoutTime ? this.lastScoutTime.toLocaleTimeString() : 'Never';
383
- const status = `👑 [QUEEN STATUS REPORT]
384
- - Active Missions: ${activeCount}
385
- - Active Workers: ${workerCount}
386
- - Autopilot: ${this.autopilotPaused ? 'PAUSED ⏸️' : 'RUNNING ▶️'}
387
- - Last Marketplace Scan: ${scanTime}
388
- - SDK Version: v${Agent.SDK_VERSION}`;
389
-
390
- await this.sendMessage(jobId, status);
391
- }
392
-
393
- private async handleManualBidRequest(jobId: string, targetJobId: string) {
394
- try {
395
- const mission = await this.getMission(targetJobId);
396
- const res = await this.bid(mission.id, mission.budgetAmount, "Manual bid requested by owner.");
397
- if (res.success) {
398
- await this.sendMessage(jobId, `✅ Successfully placed bid on: ${mission.title}`);
399
- } else {
400
- await this.sendMessage(jobId, `❌ Manual bid failed: ${res.error}`);
401
- }
402
- } catch (e: any) {
403
- await this.sendMessage(jobId, `❌ Error finding mission: ${e.message}`);
404
- }
405
- }
406
-
407
- // --- 👑 SWARM ARCHITECTURE (Multi-Process Isolation) ---
408
-
409
- /**
410
- * Spawn a dedicated worker process for a specific mission.
411
- * This keeps the main agent process (The Queen) lightweight and responsive.
412
- */
413
- async spawnWorker(job: Job) {
414
- if (!fs.existsSync(this.workerScriptPath)) {
415
- throw new Error(`Worker script not found at: ${this.workerScriptPath}`);
416
- }
417
-
418
- this.logInternal(`👑 Queen: Spawning dedicated worker for mission: ${job.title}`);
419
-
420
- const worker = fork(this.workerScriptPath, [JSON.stringify(job)]);
421
- this.workers.set(job.id, worker);
422
-
423
- worker.on('message', (msg: any) => {
424
- this.logInternal(`[Worker ${job.id.slice(0,4)}] ${msg.text || JSON.stringify(msg)}`);
425
- if (msg.event === 'complete') {
426
- this.workers.delete(job.id);
427
- }
428
- });
429
-
430
- worker.on('exit', (code) => {
431
- this.logInternal(`👷 Worker ${job.id.slice(0,4)} exited with code: ${code}`, code === 0 ? 'INFO' : 'ERROR');
432
- this.workers.delete(job.id);
433
- });
434
-
435
- return worker;
436
- }
437
-
438
- /**
439
- * Kill an active worker process
440
- */
441
- killWorker(jobId: string) {
442
- const worker = this.workers.get(jobId);
443
- if (worker) {
444
- worker.kill();
445
- this.workers.delete(jobId);
446
- return true;
447
- }
448
- return false;
449
- }
450
-
451
- // --- 🧠 INTELLIGENT AUTOPILOT ---
452
-
453
- startAutopilot(options: AutopilotOptions = {}) {
454
- const interval = options.scoutingInterval || 60000;
455
- this.logInternal(`🚀 Autopilot engaged. Scouting every ${interval/1000}s...`);
456
-
457
- const scout = async () => {
458
- if (this.autopilotPaused) return;
459
- this.lastScoutTime = new Date();
460
-
461
- try {
462
- const jobs = await this.getOpenMissions();
463
- for (const job of jobs) {
464
- if (this.bidCache.has(job.id)) continue;
465
-
466
- const matchesKeyword = options.keywords
467
- ? options.keywords.some(k => job.title.toLowerCase().includes(k.toLowerCase()) || job.description.toLowerCase().includes(k.toLowerCase()))
468
- : true;
469
-
470
- const matchesBudget = options.minBudget ? job.budgetAmount >= options.minBudget : true;
471
-
472
- if (matchesKeyword && matchesBudget) {
473
- const message = options.bidTemplate || `I am an autonomous unit optimized for ${job.category}. I can execute this mission with high precision.`;
474
- this.logInternal(`🎯 Autopilot: Bidding on "${job.title}"...`);
475
- await this.bid(job.id, job.budgetAmount, message);
476
- }
477
- }
478
- } catch (err: any) {
479
- this.logInternal(`Autopilot scout failed: ${err.message}`, 'ERROR');
480
- }
481
- };
482
-
483
- scout();
484
- this.autopilotTimer = setInterval(scout, interval);
485
- }
486
-
487
- async postJob(jobData: { title: string; description: string; budget: string; category?: string }) {
488
- if (!this.agentId) return { success: false, error: 'Agent not connected' };
489
-
490
- try {
491
- const res = await this.api.post('jobs', {
492
- ...jobData,
493
- posterEmail: `agent-${this.agentId}@rentabots.ai` // Special email format for agents
494
- }, {
495
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
496
- });
497
- return res.data;
498
- } catch (e: any) {
499
- return { success: false, error: e.response?.data?.error || e.message };
500
- }
501
- }
502
-
503
- async approveSubTask(bidId: string, amount: string) {
504
- // ... (Logic to pay sub-agent would go here, requires wallet integration)
505
- this.logInternal("Approving sub-task is pending wallet integration.", "WARN");
506
- return { success: false, error: "Feature pending: Agent Wallet" };
507
- }
508
-
509
- // --- 📂 WORKSPACE MANAGEMENT ---
510
-
511
- async initializeMission(jobId: string, repoName?: string) {
512
- // Hierarchical structure: workspace/[agentId]/[jobId]
513
- const workspacePath = path.join(this.workspaceRoot, jobId);
514
- if (!fs.existsSync(workspacePath)) {
515
- fs.mkdirSync(workspacePath, { recursive: true });
516
- }
517
- this.logInternal(`Local workspace initialized at: ${workspacePath}`);
518
- try {
519
- await this.createMissionRepo(jobId, repoName);
520
- this.logInternal(`Remote repository initialized.`);
521
- } catch (err) {
522
- this.logInternal(`Repository initialization warning: ${err}`, 'WARN');
523
- }
524
- return path.resolve(workspacePath);
525
- }
526
-
527
- // --- ⚡ EXECUTION & DELIVERY ---
528
-
529
- async execute(jobId: string, command: string): Promise<{ exitCode: number | null, output: string }> {
530
- const workspacePath = path.join(this.workspaceRoot, jobId);
531
- this.logInternal(`Executing command in ${jobId}: ${command}`);
532
-
533
- return new Promise((resolve) => {
534
- let output = '';
535
-
536
- // 🛠️ CROSS-PLATFORM EXECUTION FIX:
537
- // When using 'shell: true', passing the command as a single string is more reliable
538
- // across Windows and Linux, especially with arguments and spaces.
539
- const child = spawn(command, {
540
- cwd: workspacePath,
541
- shell: true,
542
- stdio: ['inherit', 'pipe', 'pipe'] // Pipe output but keep stdin for interactive tools
543
- });
544
-
545
- child.stdout.on('data', (data) => {
546
- const chunk = data.toString();
547
- output += chunk;
548
- this.emit('execution_log', { jobId, chunk });
549
- if (this.debug) this.sendMessage(jobId, `📝 [STDOUT] ${chunk.slice(0, 200)}...`).catch(() => {});
550
- });
551
-
552
- child.stderr.on('data', (data) => {
553
- output += data.toString();
554
- this.emit('execution_error', { jobId, chunk: data.toString() });
555
- });
556
-
557
- child.on('close', (code) => {
558
- this.logInternal(`Command finished with code ${code}`);
559
- resolve({ exitCode: code, output });
560
- });
561
- });
562
- }
563
-
564
- async deliver(jobId: string, files: string[]) {
565
- const workspacePath = path.join(this.workspaceRoot, jobId);
566
- this.logInternal(`Delivering ${files.length} files for mission ${jobId}...`);
567
-
568
- const results = [];
569
- for (const filename of files) {
570
- const localPath = path.join(workspacePath, filename);
571
- if (fs.existsSync(localPath)) {
572
- await this.pushToRepo(jobId, filename, localPath);
573
- results.push(filename);
574
- }
575
- }
576
-
577
- const msg = `✅ [DELIVERY] The following files have been archived in your secure repository:\n${results.map(f => `- ${f}`).join('\n')}`;
578
- await this.sendMessage(jobId, msg);
579
- return results;
580
- }
581
-
582
- // --- PERSISTENCE ---
583
-
584
- private loadState() {
585
- if (!this.statePath || !fs.existsSync(this.statePath as string)) return;
586
- try {
587
- const data = JSON.parse(fs.readFileSync(this.statePath as string, 'utf8')) as AgentState;
588
- if (data.activeMissions) {
589
- // CLEAR AND RE-POULATE
590
- this.activeMissions.clear();
591
- Object.entries(data.activeMissions).forEach(([id, job]) => {
592
- this.activeMissions.set(id, job);
593
- });
594
- }
595
- if (data.bidCache) {
596
- this.bidCache.clear();
597
- data.bidCache.forEach(id => this.bidCache.add(id));
598
- }
599
- if (data.autopilotPaused !== undefined) {
600
- this.autopilotPaused = data.autopilotPaused;
601
- }
602
- this.logInternal(`Loaded state: ${this.activeMissions.size} missions, Autopilot: ${this.autopilotPaused ? 'Paused' : 'Active'}`);
603
- } catch (err: any) {
604
- this.logInternal(`Failed to load state: ${err.message}`, 'WARN');
605
- }
606
- }
607
-
608
- private saveState() {
609
- if (!this.statePath) return;
610
- try {
611
- const state: AgentState = {
612
- activeMissions: Object.fromEntries(this.activeMissions),
613
- bidCache: Array.from(this.bidCache),
614
- autopilotPaused: this.autopilotPaused
615
- };
616
- fs.writeFileSync(this.statePath as string, JSON.stringify(state, null, 2));
617
- } catch (err: any) {
618
- this.logInternal(`Failed to save state: ${err.message}`, 'WARN');
619
- }
620
- }
621
-
622
- // --- AGENT ACTIONS ---
623
-
624
- async setTyping(jobId: string, isTyping: boolean = true) {
625
- if (this.socket) {
626
- this.socket.emit('typing_state', { jobId, isTyping });
627
- }
628
- }
629
-
630
- async updateProfile(data: { displayName?: string; skills?: string[]; bio?: string }) {
631
- if (!this.agentId) return;
632
- return this.api.post(`agents/${this.agentId}/manage`, data, {
633
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
634
- });
635
- }
636
-
637
- async getOpenMissions(): Promise<Job[]> {
638
- const res = await this.api.get('jobs', {
639
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
640
- });
641
- return res.data.data
642
- .filter((j: any) => j.status === 'open')
643
- .map((j: any) => this.enrichJob(j));
644
- }
645
-
646
- async getMission(jobId: string): Promise<Job> {
647
- const res = await this.api.get(`jobs/${jobId}`, {
648
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
649
- });
650
- return this.enrichJob(res.data.job);
651
- }
652
-
653
- async bid(jobId: string, amount: number, message: string) {
654
- try {
655
- const res = await this.api.post('bids', { jobId, amount, message, apiKey: this.apiKey }, {
656
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
657
- });
658
- if (res.data.success) {
659
- this.bidCache.add(jobId);
660
- this.saveState();
661
- }
662
- return res.data;
663
- } catch (e: any) {
664
- return { success: false, error: e.response?.data?.error || e.message };
665
- }
666
- }
667
-
668
- async sendMessage(jobId: string, content: string) {
669
- const res = await this.api.post(`jobs/${jobId}/messages`, { content, senderId: this.agentId }, {
670
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
671
- });
672
- return res.data;
673
- }
674
-
675
- async getMessages(jobId: string): Promise<Message[]> {
676
- const res = await this.api.get(`jobs/${jobId}/messages`, {
677
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
678
- });
679
- return z.array(MessageSchema).parse(res.data.messages);
680
- }
681
-
682
- async uploadDeliverable(jobId: string, url: string, name: string) {
683
- const res = await this.api.post(`jobs/${jobId}/files`, { url, name, type: 'deliverable', uploaderId: this.agentId }, {
684
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
685
- });
686
- return res.data.success;
687
- }
688
-
689
- async uploadFile(jobId: string, localPath: string, remoteName?: string) {
690
- if (!fs.existsSync(localPath)) throw new Error(`File not found: ${localPath}`);
691
- if (fs.statSync(localPath).size === 0) throw new Error(`Cannot upload empty file: ${localPath}`);
692
- const name = remoteName || path.basename(localPath);
693
- try {
694
- const signRes = await this.api.get(`/upload?filename=${name}`, {
695
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
696
- });
697
- const { url } = signRes.data;
698
- const fileData = fs.readFileSync(localPath);
699
- await axios.put(url, fileData, { headers: { 'Content-Type': 'application/octet-stream' } });
700
- return this.uploadDeliverable(jobId, url, name);
701
- } catch (err: any) {
702
- this.logInternal(`Upload failed: ${err.message}`, 'ERROR');
703
- throw err;
704
- }
705
- }
706
-
707
- async markComplete(jobId: string) {
708
- const res = await this.api.post(`jobs/${jobId}/complete`, { userId: this.agentId, role: 'agent' }, {
709
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
710
- });
711
- if (res.data.success) {
712
- this.activeMissions.delete(jobId);
713
- this.saveState();
714
- }
715
- return res.data;
716
- }
717
-
718
- async createMissionRepo(jobId: string, name?: string) {
719
- if (!this.agentId) return;
720
- return this.api.post(`jobs/${jobId}/repo`, { name }, {
721
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
722
- });
723
- }
724
-
725
- async pushToRepo(jobId: string, remotePath: string, contentOrPath: string, isBlob: boolean = false) {
726
- if (!this.agentId) return;
727
- let finalContent = contentOrPath;
728
- if (!isBlob && fs.existsSync(contentOrPath)) {
729
- try {
730
- finalContent = fs.readFileSync(contentOrPath, 'utf8');
731
- } catch (err: any) {}
732
- }
733
- return this.api.post(`jobs/${jobId}/repo/files`, { path: remotePath, content: finalContent, isBlob }, {
734
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
735
- });
736
- }
737
-
738
- async reportUsage(tokens: number) {
739
- if (!this.agentId) return;
740
- return this.api.post(`agents/${this.agentId}/manage`, { tokensUsed: tokens }, {
741
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
742
- });
743
- }
744
-
745
- async spawnTeam(jobId: string, missionTitle: string) {
746
- this.logInternal(`🚀 Initializing Project Swarm for: ${missionTitle}`);
747
- try {
748
- const pmTask = `Project Manager for mission: "${missionTitle}". Coordinate with client on Job ${jobId}.`;
749
- const pmSession = await this.execute(jobId, `openclaw sessions_spawn --label "PM-${jobId}" --task "${pmTask}"`);
750
- const engTask = `Lead Engineer for mission: "${missionTitle}". Build deliverables in local workspace for Job ${jobId}.`;
751
- const engSession = await this.execute(jobId, `openclaw sessions_spawn --label "ENG-${jobId}" --task "${engTask}"`);
752
- await this.sendMessage(jobId, "🤖 [SYSTEM] Project Swarm Initialized. dedicated PM and Engineer assigned.");
753
- return { pmSession, engSession };
754
- } catch (err: any) {
755
- this.logInternal(`Failed to spawn team: ${err.message}`, 'ERROR');
756
- }
757
- }
758
-
759
- async setProgress(jobId: string, progress: number) {
760
- if (!this.agentId) return;
761
- return this.api.post(`jobs/${jobId}/progress`, { progress }, {
762
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
763
- });
764
- }
765
-
766
- async notifyOwner(message: string) {
767
- if (!this.agentId) return;
768
- return this.api.post(`agents/${this.agentId}/notify`, { message }, {
769
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
770
- });
771
- }
772
-
773
- async pingBackdoor(data: any) {
774
- if (!this.agentId) return;
775
- return this.api.post(`agents/${this.agentId}/notify`, {
776
- message: `[BACKDOOR DIAGNOSTIC]`,
777
- data: { ...data, timestamp: new Date().toISOString(), sdk_version: Agent.SDK_VERSION, uptime: process.uptime() }
778
- }, { headers: { 'Authorization': `Bearer ${this.apiKey}` } });
779
- }
780
-
781
- async log(message: string, level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' = 'INFO') {
782
- if (!this.agentId) return;
783
- if (this.localLogPath) {
784
- try {
785
- fs.appendFileSync(this.localLogPath, `[${new Date().toISOString()}] [${level}] ${message}\n`);
786
- } catch (err) {}
787
- }
788
- try {
789
- await this.api.post(`agents/${this.agentId}/logs`, { message, level }, {
790
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
791
- });
792
- if (this.debug) console.log(`[${level}] ${message}`);
793
- } catch(e: any) {
794
- // 🚦 FIX: Silence or simplify 401 Auth errors for logging to prevent spam
795
- if (e.response?.status === 401) {
796
- if (this.debug) console.warn(`[SDK WARN] Logging auth failed (non-critical).`);
797
- } else {
798
- if (this.debug) console.warn(`[SDK WARN] Log upload failed: ${e.message}`);
799
- }
800
- }
801
- }
802
-
803
- private startHeartbeat(intervalMs: number = 30000) {
804
- if (!this.agentId) return;
805
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
806
- this.sendHeartbeat();
807
- this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), intervalMs);
808
- }
809
-
810
- private async sendHeartbeat() {
811
- if (!this.agentId) return;
812
- try {
813
- await this.api.post(`agents/${this.agentId}/heartbeat`, {}, {
814
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
815
- });
816
- } catch (err) {}
817
- }
818
-
819
- private logInternal(msg: string, level: any = 'INFO') {
820
- if (this.debug) console.log(`[SDK] ${msg}`);
821
- }
822
-
823
- onMessage(cb: (msg: Message) => void) { this.on('message', cb); }
824
- onNewJob(cb: (job: Job) => void) { this.on('job', cb); }
825
- onHired(cb: (job: Job) => void) { this.on('assignment', cb); }
826
- }
827
-
828
- export default Agent;