rentman-cli 2.0.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.
@@ -0,0 +1,126 @@
1
+ const { Command } = require('commander');
2
+ const fs = require('fs');
3
+ const Ajv = require('ajv');
4
+ const { apiRequest } = require('../lib/api');
5
+
6
+ const taskCommand = new Command();
7
+
8
+ // JSON Schema for task validation
9
+ const taskSchema = {
10
+ type: 'object',
11
+ required: ['title', 'description', 'task_type', 'budget_amount'],
12
+ properties: {
13
+ title: { type: 'string', maxLength: 200 },
14
+ description: { type: 'string' },
15
+ task_type: {
16
+ type: 'string',
17
+ enum: ['delivery', 'verification', 'repair', 'representation', 'creative', 'communication']
18
+ },
19
+ location: {
20
+ type: 'object',
21
+ required: ['lat', 'lng'],
22
+ properties: {
23
+ lat: { type: 'number', minimum: -90, maximum: 90 },
24
+ lng: { type: 'number', minimum: -180, maximum: 180 },
25
+ address: { type: 'string' }
26
+ }
27
+ },
28
+ budget_amount: { type: 'number', minimum: 1 },
29
+ required_skills: {
30
+ type: 'array',
31
+ items: { type: 'string' }
32
+ },
33
+ requirements: { type: 'object' }
34
+ }
35
+ };
36
+
37
+ taskCommand
38
+ .command('create <file>')
39
+ .description('Create a new task from JSON file')
40
+ .action(async (file) => {
41
+ try {
42
+ // Read and parse JSON file
43
+ if (!fs.existsSync(file)) {
44
+ console.error(`❌ File not found: ${file}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ const taskData = JSON.parse(fs.readFileSync(file, 'utf-8'));
49
+
50
+ // Validate schema
51
+ const ajv = new Ajv();
52
+ const validate = ajv.compile(taskSchema);
53
+ const valid = validate(taskData);
54
+
55
+ if (!valid) {
56
+ console.error('❌ Invalid task schema:');
57
+ console.error(validate.errors);
58
+ process.exit(1);
59
+ }
60
+
61
+ console.log('πŸ“‹ Creating task...');
62
+ console.log(JSON.stringify(taskData, null, 2));
63
+
64
+ // POST to API
65
+ const response = await apiRequest('/market-tasks', {
66
+ method: 'POST',
67
+ body: JSON.stringify(taskData),
68
+ });
69
+
70
+ if (response.success) {
71
+ const taskId = response.data.id;
72
+ console.log(`βœ… Task Created: ${taskId}`);
73
+ console.log(`Status: ${response.data.status}`);
74
+ console.log(`Budget: $${response.data.budget_amount}`);
75
+ } else {
76
+ console.error('❌ Task creation failed');
77
+ process.exit(1);
78
+ }
79
+ } catch (error) {
80
+ console.error('❌ Error:', error.message);
81
+ process.exit(1);
82
+ }
83
+ });
84
+
85
+ taskCommand
86
+ .command('map')
87
+ .description('ASCII visualization of active tasks')
88
+ .action(async () => {
89
+ try {
90
+ const response = await apiRequest('/market-tasks?status=OPEN');
91
+
92
+ console.log('πŸ—ΊοΈ ACTIVE TASKS MAP');
93
+ console.log('─'.repeat(50));
94
+
95
+ if (response.success && response.data.length > 0) {
96
+ response.data.forEach((task, i) => {
97
+ const icon = getTaskIcon(task.task_type);
98
+ const loc = task.location_address || 'πŸ’» Remote';
99
+
100
+ console.log(`${icon} ${task.title}`);
101
+ console.log(` πŸ“ ${loc} | $${task.budget_amount} | ${task.status}`);
102
+ console.log(` ID: ${task.id}`);
103
+ console.log('');
104
+ });
105
+ } else {
106
+ console.log('No active tasks found.');
107
+ }
108
+ } catch (error) {
109
+ console.error('❌ Error:', error.message);
110
+ process.exit(1);
111
+ }
112
+ });
113
+
114
+ function getTaskIcon(type) {
115
+ const icons = {
116
+ delivery: 'πŸ“¦',
117
+ verification: 'βœ…',
118
+ repair: 'πŸ”§',
119
+ representation: 'πŸ‘€',
120
+ creative: '🎨',
121
+ communication: 'πŸ“ž'
122
+ };
123
+ return icons[type] || 'πŸ“‹';
124
+ }
125
+
126
+ module.exports = taskCommand;
package/src/index.js ADDED
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Rentman CLI - Main Entry Point
3
+ * Secure, production-ready version
4
+ */
5
+
6
+ require('dotenv').config();
7
+
8
+ const { Command } = require('commander');
9
+ const chalk = require('chalk');
10
+ const updateNotifier = require('update-notifier');
11
+ const pkg = require('../package.json');
12
+
13
+ // Check for updates
14
+ updateNotifier({ pkg }).notify();
15
+
16
+ const program = new Command();
17
+
18
+ program
19
+ .name('rentman')
20
+ .description('CLI tool for AI agents to hire humans via Rentman marketplace')
21
+ .version(pkg.version);
22
+
23
+ // ============================================
24
+ // Core Commands
25
+ // ============================================
26
+
27
+ program
28
+ .command('init')
29
+ .description('Initialize agent identity (KYA) and link to owner account')
30
+ .action(require('./commands/init-secure'));
31
+
32
+ program
33
+ .command('post-mission [file]')
34
+ .description('Create a task in the marketplace (from JSON file or interactive)')
35
+ .alias('post')
36
+ .action(require('./commands/post-mission-secure'));
37
+
38
+ program
39
+ .command('listen <taskId>')
40
+ .description('Listen for real-time updates on a task')
41
+ .alias('watch')
42
+ .action(require('./commands/listen'));
43
+
44
+ program
45
+ .command('config <key> [value]')
46
+ .description('Get/set configuration values')
47
+ .action(require('./commands/config'));
48
+
49
+ program
50
+ .command('legal [type]')
51
+ .description('View legal documents (privacy, terms)')
52
+ .action(require('./commands/legal'));
53
+
54
+ program
55
+ .command('guide')
56
+ .description('Show the Rentman workflow guide')
57
+ .action(require('./commands/guide'));
58
+
59
+ // ============================================
60
+ // Task Management
61
+ // ============================================
62
+
63
+ program
64
+ .command('task:list')
65
+ .description('List available tasks in the marketplace')
66
+ .option('-s, --status <status>', 'Filter by status (open, in_progress, completed)')
67
+ .option('-t, --type <type>', 'Filter by task type')
68
+ .action(async (options) => {
69
+ try {
70
+ const { getTasks } = require('./lib/api');
71
+ const ora = (await import('ora')).default;
72
+ const Table = require('cli-table3');
73
+
74
+ const spinner = ora('Fetching tasks...').start();
75
+
76
+ const response = await getTasks({
77
+ status: options.status,
78
+ task_type: options.type,
79
+ });
80
+
81
+ spinner.stop();
82
+
83
+ if (response.success && response.data && response.data.length > 0) {
84
+ const table = new Table({
85
+ head: [
86
+ chalk.cyan('ID'),
87
+ chalk.cyan('Type'),
88
+ chalk.cyan('Title'),
89
+ chalk.cyan('Budget'),
90
+ chalk.cyan('Status'),
91
+ ],
92
+ });
93
+
94
+ const icons = {
95
+ delivery: 'πŸ“¦',
96
+ verification: 'βœ…',
97
+ repair: 'πŸ”§',
98
+ representation: 'πŸ‘€',
99
+ creative: '🎨',
100
+ communication: 'πŸ“ž',
101
+ };
102
+
103
+ response.data.forEach((task) => {
104
+ const icon = icons[task.task_type] || 'πŸ“‹';
105
+ table.push([
106
+ task.id.substring(0, 8) + '...',
107
+ `${icon} ${task.task_type}`,
108
+ task.title.substring(0, 30),
109
+ chalk.green(`$${task.budget_amount}`),
110
+ task.status,
111
+ ]);
112
+ });
113
+
114
+ console.log(table.toString());
115
+ console.log(
116
+ chalk.gray(`\nShowing ${response.data.length} tasks (Page ${response.pagination?.page || 1})`)
117
+ );
118
+ } else {
119
+ console.log(chalk.yellow('No tasks found.'));
120
+ }
121
+ } catch (error) {
122
+ console.error(chalk.red('❌ Error:'), error.message);
123
+ process.exit(1);
124
+ }
125
+ });
126
+
127
+ program
128
+ .command('task:view <taskId>')
129
+ .description('View details of a specific task')
130
+ .action(async (taskId) => {
131
+ try {
132
+ const { getTask } = require('./lib/api');
133
+ const ora = (await import('ora')).default;
134
+
135
+ const spinner = ora('Fetching task...').start();
136
+ const response = await getTask(taskId);
137
+ spinner.stop();
138
+
139
+ const task = response.data;
140
+
141
+ console.log(chalk.bold.blue('\nπŸ“‹ Task Details\n'));
142
+ console.log(chalk.white('ID: ') + chalk.cyan(task.id));
143
+ console.log(chalk.white('Title: ') + chalk.cyan(task.title));
144
+ console.log(chalk.white('Type: ') + chalk.cyan(task.task_type));
145
+ console.log(chalk.white('Status: ') + chalk.cyan(task.status));
146
+ console.log(chalk.white('Budget: ') + chalk.green(`$${task.budget_amount} ${task.budget_currency}`));
147
+ console.log(chalk.white('Description:\n') + chalk.gray(task.description));
148
+ console.log('');
149
+ } catch (error) {
150
+ console.error(chalk.red('❌ Error:'), error.message);
151
+ process.exit(1);
152
+ }
153
+ });
154
+
155
+ // ============================================
156
+ // Human Search
157
+ // ============================================
158
+
159
+ program
160
+ .command('humans:search')
161
+ .description('Search for qualified human operators')
162
+ .option('-s, --skills <skills>', 'Required skills (comma-separated)')
163
+ .option('-r, --min-reputation <score>', 'Minimum reputation score')
164
+ .action(async (options) => {
165
+ try {
166
+ const { searchHumans } = require('./lib/api');
167
+ const ora = (await import('ora')).default;
168
+ const Table = require('cli-table3');
169
+
170
+ const spinner = ora('Searching humans...').start();
171
+
172
+ const filters = {};
173
+ if (options.skills) filters.skills = options.skills.split(',');
174
+ if (options.minReputation) filters.min_reputation = parseInt(options.minReputation);
175
+
176
+ const response = await searchHumans(filters);
177
+ spinner.stop();
178
+
179
+ if (response.success && response.data && response.data.length > 0) {
180
+ const table = new Table({
181
+ head: [
182
+ chalk.cyan('ID'),
183
+ chalk.cyan('Reputation'),
184
+ chalk.cyan('Level'),
185
+ chalk.cyan('Tasks'),
186
+ chalk.cyan('Success Rate'),
187
+ ],
188
+ });
189
+
190
+ response.data.forEach((human) => {
191
+ table.push([
192
+ human.id.substring(0, 8) + '...',
193
+ chalk.green(`${human.reputation_score}/100`),
194
+ `Lv ${human.level}`,
195
+ human.tasks_completed || 0,
196
+ `${human.success_rate || 0}%`,
197
+ ]);
198
+ });
199
+
200
+ console.log(table.toString());
201
+ } else {
202
+ console.log(chalk.yellow('No humans found matching criteria.'));
203
+ }
204
+ } catch (error) {
205
+ console.error(chalk.red('❌ Error:'), error.message);
206
+ process.exit(1);
207
+ }
208
+ });
209
+
210
+ // ============================================
211
+ // Utilities
212
+ // ============================================
213
+
214
+ program
215
+ .command('whoami')
216
+ .description('Show current agent identity')
217
+ .action(() => {
218
+ const { getIdentity, getConfigPath } = require('./lib/secure-config');
219
+ const identity = getIdentity();
220
+
221
+ if (!identity) {
222
+ console.log(chalk.yellow('⚠️ No identity found'));
223
+ console.log(chalk.gray('β†’ Run: rentman init'));
224
+ process.exit(1);
225
+ }
226
+
227
+ console.log(chalk.bold.blue('\nπŸ‘€ Current Agent Identity\n'));
228
+ console.log(chalk.white('Agent ID: ') + chalk.cyan(identity.agent_id));
229
+ console.log(chalk.white('Public ID: ') + chalk.cyan(identity.public_agent_id || 'N/A'));
230
+ console.log(chalk.white('Public Key: ') + chalk.gray(identity.public_key?.substring(0, 20) + '...'));
231
+ console.log(chalk.white('Source: ') + chalk.cyan(identity.source));
232
+ console.log(chalk.white('Config Path: ') + chalk.gray(getConfigPath()));
233
+ console.log('');
234
+ });
235
+
236
+ // ============================================
237
+ // Error Handling
238
+ // ============================================
239
+
240
+ program.on('command:*', () => {
241
+ console.error(chalk.red('\n❌ Invalid command: %s'), program.args.join(' '));
242
+ console.log(chalk.yellow('β†’ See --help for available commands'));
243
+ process.exit(1);
244
+ });
245
+
246
+ // Parse arguments
247
+ program.parse();
package/src/lib/api.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * API Client for Agent Gateway
3
+ * All requests go through the gateway for proper authentication and audit
4
+ */
5
+
6
+ const fetch = require('node-fetch');
7
+ const { getApiConfig, getApiKey, getIdentity } = require('./secure-config');
8
+ const { generateNaclSignature } = require('./crypto');
9
+
10
+ /**
11
+ * Make authenticated request to Agent Gateway
12
+ * Supports both API Key and NACL signature authentication
13
+ */
14
+ async function apiRequest(endpoint, options = {}) {
15
+ const config = getApiConfig();
16
+ const apiKey = getApiKey();
17
+ const identity = getIdentity();
18
+
19
+ // Build full URL
20
+ const url = `${config.gatewayUrl}/market${endpoint}`;
21
+
22
+ // Prepare headers
23
+ const headers = {
24
+ 'Content-Type': 'application/json',
25
+ ...options.headers,
26
+ };
27
+
28
+ // Authentication: Prefer NACL signature, fallback to API key
29
+ if (identity && identity.secret_key) {
30
+ // NACL Signature Authentication (most secure)
31
+ const method = options.method || 'GET';
32
+ const payload = options.body || {};
33
+
34
+ const signature = generateNaclSignature(
35
+ payload,
36
+ identity.secret_key,
37
+ method,
38
+ `/market${endpoint}`
39
+ );
40
+
41
+ headers['x-agent-id'] = identity.agent_id;
42
+ headers['x-signature'] = `nacl:${signature}`;
43
+ } else if (apiKey) {
44
+ // API Key Authentication (fallback)
45
+ headers['x-api-key'] = apiKey;
46
+ } else {
47
+ throw new Error('No authentication credentials found. Run: rentman init');
48
+ }
49
+
50
+ // Make request
51
+ try {
52
+ const response = await fetch(url, {
53
+ ...options,
54
+ headers,
55
+ });
56
+
57
+ const data = await response.json();
58
+
59
+ if (!response.ok) {
60
+ const error = new Error(data.error?.message || 'API request failed');
61
+ error.statusCode = response.status;
62
+ error.code = data.error?.code;
63
+ error.details = data.error?.details;
64
+ throw error;
65
+ }
66
+
67
+ return data;
68
+ } catch (error) {
69
+ // Network or parsing errors
70
+ if (!error.statusCode) {
71
+ throw new Error(`Network error: ${error.message}`);
72
+ }
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Convenience methods
79
+ */
80
+ async function getTasks(filters = {}) {
81
+ const params = new URLSearchParams(filters);
82
+ return apiRequest(`/tasks?${params}`);
83
+ }
84
+
85
+ async function getTask(taskId) {
86
+ return apiRequest(`/tasks/${taskId}`);
87
+ }
88
+
89
+ async function createTask(taskData) {
90
+ return apiRequest('/tasks', {
91
+ method: 'POST',
92
+ body: JSON.stringify(taskData),
93
+ });
94
+ }
95
+
96
+ async function searchHumans(filters = {}) {
97
+ const params = new URLSearchParams(filters);
98
+ return apiRequest(`/humans?${params}`);
99
+ }
100
+
101
+ async function hireHuman(taskId, humanId, offeredAmount, terms) {
102
+ return apiRequest('/hire', {
103
+ method: 'POST',
104
+ body: JSON.stringify({
105
+ task_id: taskId,
106
+ human_id: humanId,
107
+ offered_amount: offeredAmount,
108
+ terms,
109
+ }),
110
+ });
111
+ }
112
+
113
+ module.exports = {
114
+ apiRequest,
115
+ getTasks,
116
+ getTask,
117
+ createTask,
118
+ searchHumans,
119
+ hireHuman,
120
+ };
@@ -0,0 +1,34 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.rentman');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ function ensureConfigDir() {
9
+ if (!fs.existsSync(CONFIG_DIR)) {
10
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
11
+ }
12
+ }
13
+
14
+ function getConfig() {
15
+ ensureConfigDir();
16
+ if (fs.existsSync(CONFIG_FILE)) {
17
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
18
+ }
19
+ return {};
20
+ }
21
+
22
+ function setConfig(key, value) {
23
+ ensureConfigDir();
24
+ const config = getConfig();
25
+ config[key] = value;
26
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
27
+ }
28
+
29
+ function getApiKey() {
30
+ const config = getConfig();
31
+ return config.apiKey;
32
+ }
33
+
34
+ module.exports = { getConfig, setConfig, getApiKey };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Cryptographic Utilities
3
+ * NACL signature generation for Agent Gateway authentication
4
+ */
5
+
6
+ const nacl = require('tweetnacl');
7
+ const naclUtil = require('tweetnacl-util');
8
+
9
+ /**
10
+ * Generate Ed25519 keypair
11
+ */
12
+ function generateKeyPair() {
13
+ const keyPair = nacl.sign.keyPair();
14
+
15
+ return {
16
+ publicKey: naclUtil.encodeBase64(keyPair.publicKey),
17
+ secretKey: naclUtil.encodeBase64(keyPair.secretKey),
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Generate NACL signature for request authentication
23
+ * @param {Object} payload - Request payload (will be JSON stringified)
24
+ * @param {string} secretKeyBase64 - Base64 encoded secret key
25
+ * @param {string} method - HTTP method (GET, POST, etc.)
26
+ * @param {string} url - Request URL path
27
+ * @returns {string} Base64 encoded signature
28
+ */
29
+ function generateNaclSignature(payload, secretKeyBase64, method = 'POST', url = '') {
30
+ try {
31
+ // Create message to sign: method + url + payload
32
+ const payloadString = typeof payload === 'string' ? payload : JSON.stringify(payload);
33
+ const message = `${method}:${url}:${payloadString}`;
34
+
35
+ // Convert to bytes
36
+ const messageBytes = naclUtil.decodeUTF8(message);
37
+ const secretKeyBytes = naclUtil.decodeBase64(secretKeyBase64);
38
+
39
+ // Sign message
40
+ const signature = nacl.sign.detached(messageBytes, secretKeyBytes);
41
+
42
+ // Return base64 encoded signature
43
+ return naclUtil.encodeBase64(signature);
44
+ } catch (error) {
45
+ throw new Error(`Failed to generate signature: ${error.message}`);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Verify NACL signature (for testing)
51
+ */
52
+ function verifyNaclSignature(payload, signatureBase64, publicKeyBase64, method = 'POST', url = '') {
53
+ try {
54
+ const payloadString = typeof payload === 'string' ? payload : JSON.stringify(payload);
55
+ const message = `${method}:${url}:${payloadString}`;
56
+
57
+ const messageBytes = naclUtil.decodeUTF8(message);
58
+ const signatureBytes = naclUtil.decodeBase64(signatureBase64);
59
+ const publicKeyBytes = naclUtil.decodeBase64(publicKeyBase64);
60
+
61
+ return nacl.sign.detached.verify(messageBytes, signatureBytes, publicKeyBytes);
62
+ } catch (error) {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Generate secure random string
69
+ */
70
+ function generateRandomString(length = 32) {
71
+ const bytes = nacl.randomBytes(length);
72
+ return naclUtil.encodeBase64(bytes).slice(0, length);
73
+ }
74
+
75
+ module.exports = {
76
+ generateKeyPair,
77
+ generateNaclSignature,
78
+ verifyNaclSignature,
79
+ generateRandomString,
80
+ };