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.
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "rentman-cli",
3
+ "version": "2.0.0",
4
+ "description": "CLI tool for AI agents to hire humans via Rentman marketplace",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "rentman": "./bin/rentman.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1",
11
+ "migrate": "node migrate-identity.js"
12
+ },
13
+ "keywords": [
14
+ "ai",
15
+ "agents",
16
+ "marketplace",
17
+ "cli"
18
+ ],
19
+ "author": "",
20
+ "license": "ISC",
21
+ "type": "commonjs",
22
+ "dependencies": {
23
+ "@supabase/supabase-js": "^2.95.2",
24
+ "ajv": "^8.17.1",
25
+ "chalk": "^4.1.2",
26
+ "cli-table3": "^0.6.5",
27
+ "commander": "^14.0.3",
28
+ "conf": "^10.2.0",
29
+ "dotenv": "^17.2.4",
30
+ "inquirer": "^8.2.5",
31
+ "node-fetch": "^2.7.0",
32
+ "ora": "^5.4.1",
33
+ "tweetnacl": "^1.0.3",
34
+ "tweetnacl-util": "^0.15.1",
35
+ "update-notifier": "^5.1.0"
36
+ }
37
+ }
@@ -0,0 +1,44 @@
1
+ const Conf = new (require('conf'))();
2
+ const chalk = require('chalk');
3
+
4
+ const allowedKeys = ['agent_id', 'secret_key', 'api_url'];
5
+
6
+ function configCommand(key, value) {
7
+ if (!key || key === 'list') {
8
+ console.log(chalk.bold('\nCurrent Configuration:'));
9
+ console.log(chalk.gray('---------------------'));
10
+ for (const k of allowedKeys) {
11
+ const val = Conf.get(k);
12
+ let displayVal = val;
13
+
14
+ if (!val) {
15
+ displayVal = chalk.gray('(not set)');
16
+ } else if (k === 'secret_key') {
17
+ displayVal = val.substring(0, 4) + '...';
18
+ }
19
+
20
+ console.log(`${chalk.cyan(k)}: ${displayVal}`);
21
+ }
22
+ console.log(chalk.gray(`\nConfig file: ${Conf.path}\n`));
23
+ return;
24
+ }
25
+
26
+ if (value === undefined) {
27
+ if (allowedKeys.includes(key)) {
28
+ console.log(Conf.get(key));
29
+ } else {
30
+ console.error(chalk.red(`Invalid key: ${key}`));
31
+ console.log(`Allowed keys: ${allowedKeys.join(', ')}`);
32
+ }
33
+ } else {
34
+ if (allowedKeys.includes(key)) {
35
+ Conf.set(key, value);
36
+ console.log(chalk.green(`\nāœ” Configuration updated: ${key}`));
37
+ } else {
38
+ console.error(chalk.red(`Invalid key: ${key}`));
39
+ console.log(`Allowed keys: ${allowedKeys.join(', ')}`);
40
+ }
41
+ }
42
+ }
43
+
44
+ module.exports = configCommand;
@@ -0,0 +1,26 @@
1
+ const chalk = require('chalk');
2
+
3
+ module.exports = function () {
4
+ console.log(chalk.bold.blue('\nšŸš€ Rentman Protocol - CLI Guide\n'));
5
+
6
+ console.log(chalk.yellow('CORE WORKFLOW (KYA):'));
7
+ console.log(chalk.white('1. Initialize Identity:'));
8
+ console.log(chalk.cyan(' $ npx rentman init'));
9
+ console.log(chalk.gray(' - Creates Keypair (Ed25519)'));
10
+ console.log(chalk.gray(' - Links specific Agent to your Billing Account'));
11
+
12
+ console.log(chalk.white('\n2. Post a Mission (AI Agent Mode):'));
13
+ console.log(chalk.cyan(' $ npx rentman post-mission <mission.json>'));
14
+ console.log(chalk.gray(' - Signs the mission with your Private Key'));
15
+ console.log(chalk.gray(' - Broadcasts to the Marketplace'));
16
+
17
+ console.log(chalk.white('\n3. Listen for Tasks (Operator Mode):'));
18
+ console.log(chalk.cyan(' $ npx rentman listen'));
19
+ console.log(chalk.gray(' - Connects to the Realtime Satellite Feed'));
20
+ console.log(chalk.gray(' - Receives tasks in your area'));
21
+
22
+ console.log(chalk.yellow('\n\nHELPFUL COMMANDS:'));
23
+ console.log(chalk.white(' $ npx rentman whoami ') + chalk.gray('# Check current identity'));
24
+ console.log(chalk.white(' $ npx rentman map ') + chalk.gray('# Visualize active missions'));
25
+ console.log('\n');
26
+ };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Init Command - Secure Agent Initialization (KYA)
3
+ * Generates identity and stores in secure user directory
4
+ */
5
+
6
+ const inquirer = require('inquirer');
7
+ const chalk = require('chalk');
8
+ const ora = require('ora');
9
+ const { createClient } = require('@supabase/supabase-js');
10
+ const { generateKeyPair } = require('../lib/crypto');
11
+ const { saveIdentity, getConfigPath, getApiConfig } = require('../lib/secure-config');
12
+
13
+ async function initCommand() {
14
+ console.log(chalk.blue.bold('\nšŸ¤– Rentman CLI - Agent Initialization (KYA)\n'));
15
+
16
+ // Get API config
17
+ const apiConfig = getApiConfig();
18
+
19
+ if (!apiConfig.supabaseKey) {
20
+ console.error(chalk.red('\nāŒ Error: SUPABASE_ANON_KEY not set'));
21
+ console.log(chalk.yellow('→ Create .env file: cp .env.example .env'));
22
+ console.log(chalk.yellow('→ Add your Supabase anon key to .env'));
23
+ process.exit(1);
24
+ }
25
+
26
+ // 1. Ask for user credentials
27
+ console.log(chalk.white('To link this Agent to your Developer Account, please log in:'));
28
+ const credentials = await inquirer.prompt([
29
+ {
30
+ type: 'input',
31
+ name: 'email',
32
+ message: 'Owner Email:',
33
+ validate: (input) => input.includes('@') || 'Invalid email',
34
+ },
35
+ {
36
+ type: 'password',
37
+ name: 'password',
38
+ message: 'Owner Password:',
39
+ validate: (input) => input.length >= 6 || 'Password too short',
40
+ },
41
+ {
42
+ type: 'input',
43
+ name: 'public_id',
44
+ message: 'Public Agent ID (e.g. agent_01):',
45
+ default: `agent_${Math.floor(Math.random() * 10000)}`,
46
+ },
47
+ ]);
48
+
49
+ // 2. Authenticate with Supabase
50
+ const spinner = ora('Authenticating...').start();
51
+
52
+ const supabase = createClient(apiConfig.supabaseUrl, apiConfig.supabaseKey);
53
+
54
+ const { data: { user }, error: authError } = await supabase.auth.signInWithPassword({
55
+ email: credentials.email,
56
+ password: credentials.password,
57
+ });
58
+
59
+ if (authError || !user) {
60
+ spinner.fail('Authentication failed');
61
+ console.error(chalk.red(`\nāŒ ${authError?.message || 'Login failed'}`));
62
+ process.exit(1);
63
+ }
64
+
65
+ spinner.succeed(`Authenticated as ${user.email}`);
66
+
67
+ // 3. Generate Ed25519 keypair
68
+ spinner.start('Generating cryptographic keys...');
69
+ const { publicKey, secretKey } = generateKeyPair();
70
+ spinner.succeed('Keys generated');
71
+
72
+ // 4. Check if agent already exists
73
+ const { data: existingAgent } = await supabase
74
+ .from('agents')
75
+ .select('id')
76
+ .eq('public_agent_id', credentials.public_id)
77
+ .single();
78
+
79
+ if (existingAgent) {
80
+ spinner.fail('Agent already exists');
81
+ console.error(chalk.red(`\nāŒ Agent ID "${credentials.public_id}" is already registered`));
82
+ console.log(chalk.yellow('→ Choose a different public_agent_id'));
83
+ process.exit(1);
84
+ }
85
+
86
+ // 5. Register agent in database
87
+ spinner.start('Registering agent on-chain...');
88
+
89
+ const { data: agent, error: insertError } = await supabase
90
+ .from('agents')
91
+ .insert({
92
+ public_agent_id: credentials.public_id,
93
+ owner_id: user.id,
94
+ public_key: publicKey,
95
+ type: 'cli',
96
+ name: `CLI Agent - ${credentials.public_id}`,
97
+ is_active: true,
98
+ permissions: ['create_task', 'read_tasks', 'hire_human', 'verify_proof'],
99
+ })
100
+ .select()
101
+ .single();
102
+
103
+ if (insertError) {
104
+ spinner.fail('Registration failed');
105
+ console.error(chalk.red(`\nāŒ ${insertError.message}`));
106
+ process.exit(1);
107
+ }
108
+
109
+ spinner.succeed('Agent registered successfully');
110
+
111
+ // 6. Save identity to secure storage
112
+ spinner.start('Saving identity...');
113
+
114
+ const identity = {
115
+ agent_id: agent.id,
116
+ public_agent_id: credentials.public_id,
117
+ public_key: publicKey,
118
+ secret_key: secretKey,
119
+ owner_id: user.id,
120
+ };
121
+
122
+ saveIdentity(identity);
123
+
124
+ spinner.succeed('Identity saved securely');
125
+
126
+ // 7. Success message
127
+ console.log(chalk.green.bold('\nāœ… Initialization Complete!\n'));
128
+ console.log(chalk.white('Agent Details:'));
129
+ console.log(chalk.cyan(` • Agent ID: ${agent.id}`));
130
+ console.log(chalk.cyan(` • Public ID: ${credentials.public_id}`));
131
+ console.log(chalk.cyan(` • Owner: ${user.email}`));
132
+ console.log(chalk.cyan(` • Identity stored: ${getConfigPath()}`));
133
+
134
+ console.log(chalk.yellow.bold('\nāš ļø SECURITY NOTES:'));
135
+ console.log(chalk.yellow(' • Your identity is stored in your user directory'));
136
+ console.log(chalk.yellow(' • Never share your secret key'));
137
+ console.log(chalk.yellow(' • Keys are used to sign all API requests'));
138
+
139
+ console.log(chalk.white.bold('\nšŸ“‹ Next Steps:'));
140
+ console.log(chalk.white(' 1. Create a task: rentman post-mission task.json'));
141
+ console.log(chalk.white(' 2. Listen for contracts: rentman listen'));
142
+ console.log(chalk.white(' 3. Get help: rentman guide'));
143
+
144
+ console.log('');
145
+ }
146
+
147
+ module.exports = initCommand;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Legal Command - View Terms and Privacy Policy
3
+ * Compliance with app store requirements
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const { spawn } = require('child_process');
8
+ const { platform } = require('os');
9
+
10
+ const LEGAL_DOCS = {
11
+ privacy: 'https://rentman.app/privacy-policy.html',
12
+ terms: 'https://rentman.app/terms-of-service.html',
13
+ };
14
+
15
+ /**
16
+ * Open URL in default browser (cross-platform)
17
+ */
18
+ function openBrowser(url) {
19
+ let command;
20
+
21
+ switch (platform()) {
22
+ case 'darwin': // macOS
23
+ command = 'open';
24
+ break;
25
+ case 'win32': // Windows
26
+ command = 'start';
27
+ break;
28
+ default: // Linux and others
29
+ command = 'xdg-open';
30
+ }
31
+
32
+ const child = spawn(command, [url], {
33
+ detached: true,
34
+ stdio: 'ignore',
35
+ shell: platform() === 'win32', // Windows needs shell
36
+ });
37
+
38
+ child.unref();
39
+ }
40
+
41
+ async function legalCommand(type) {
42
+ console.log(chalk.bold.blue('\nšŸ“œ Rentman Legal Documents\n'));
43
+
44
+ if (!type) {
45
+ // Show menu
46
+ console.log(chalk.white('Available legal documents:\n'));
47
+ console.log(chalk.cyan(' privacy ') + chalk.gray('- Privacy Policy'));
48
+ console.log(chalk.cyan(' terms ') + chalk.gray('- Terms of Service'));
49
+ console.log('');
50
+ console.log(chalk.white('Usage:'));
51
+ console.log(chalk.gray(' rentman legal privacy'));
52
+ console.log(chalk.gray(' rentman legal terms'));
53
+ console.log('');
54
+ console.log(chalk.white('Links:'));
55
+ console.log(chalk.gray(' Privacy: ' + LEGAL_DOCS.privacy));
56
+ console.log(chalk.gray(' Terms: ' + LEGAL_DOCS.terms));
57
+ console.log('');
58
+ return;
59
+ }
60
+
61
+ if (type === 'privacy') {
62
+ console.log(chalk.green('Opening Privacy Policy...'));
63
+ console.log(chalk.gray(LEGAL_DOCS.privacy));
64
+ openBrowser(LEGAL_DOCS.privacy);
65
+ console.log(chalk.white('\nāœ“ Opened in browser'));
66
+ } else if (type === 'terms') {
67
+ console.log(chalk.green('Opening Terms of Service...'));
68
+ console.log(chalk.gray(LEGAL_DOCS.terms));
69
+ openBrowser(LEGAL_DOCS.terms);
70
+ console.log(chalk.white('\nāœ“ Opened in browser'));
71
+ } else {
72
+ console.error(chalk.red(`āŒ Unknown document type: ${type}`));
73
+ console.log(chalk.yellow('→ Valid options: privacy, terms'));
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ module.exports = legalCommand;
@@ -0,0 +1,88 @@
1
+ const { createClient } = require('@supabase/supabase-js');
2
+
3
+ const SUPABASE_URL = 'https://uoekolfgbbmvhzsfkjef.supabase.co';
4
+ const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InVvZWtvbGZnYmJtdmh6c2ZramVmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzAzMjQzNzUsImV4cCI6MjA4NTkwMDM3NX0.DYxAxi4TTBLgdVruu8uGM3Jog7JZaplWqikAvI0EXvk';
5
+
6
+ /**
7
+ * Listen for real-time updates on a specific task
8
+ * @param {string} taskId - The task UUID to watch
9
+ */
10
+ module.exports = async (taskId) => {
11
+ console.log(`\nšŸ‘ļø Watching task: ${taskId}`);
12
+ console.log('─'.repeat(50));
13
+ console.log('Listening for status changes... (Press Ctrl+C to stop)\n');
14
+
15
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
16
+
17
+ // First, get current task status
18
+ const { data: task, error } = await supabase
19
+ .from('tasks')
20
+ .select('*')
21
+ .eq('id', taskId)
22
+ .single();
23
+
24
+ if (error) {
25
+ console.error(`āŒ Task not found: ${taskId}`);
26
+ process.exit(1);
27
+ }
28
+
29
+ console.log(`šŸ“‹ Current Status: ${task.status}`);
30
+ console.log(`šŸ“ Title: ${task.title}`);
31
+ console.log(`šŸ’° Budget: $${task.budget_amount}`);
32
+ console.log('');
33
+
34
+ // Subscribe to changes
35
+ const channel = supabase
36
+ .channel(`task-${taskId}`)
37
+ .on(
38
+ 'postgres_changes',
39
+ {
40
+ event: 'UPDATE',
41
+ schema: 'public',
42
+ table: 'tasks',
43
+ filter: `id=eq.${taskId}`
44
+ },
45
+ (payload) => {
46
+ const newStatus = payload.new.status;
47
+ const oldStatus = payload.old?.status;
48
+ const timestamp = new Date().toLocaleTimeString();
49
+
50
+ console.log(`[${timestamp}] šŸ“” Status Update: ${oldStatus} → ${newStatus}`);
51
+
52
+ if (newStatus === 'ASSIGNED') {
53
+ console.log(` šŸ‘¤ Human accepted: ${payload.new.human_id || 'Unknown'}`);
54
+ } else if (newStatus === 'IN_PROGRESS') {
55
+ console.log(` šŸƒ Human is working on the task`);
56
+ } else if (newStatus === 'COMPLETED') {
57
+ console.log(` āœ… Task completed!`);
58
+ if (payload.new.proof_urls?.length > 0) {
59
+ console.log(` šŸ“ø Proof: ${payload.new.proof_urls.join(', ')}`);
60
+ }
61
+ console.log('\nšŸŽ‰ Mission accomplished! Exiting...');
62
+ process.exit(0);
63
+ } else if (newStatus === 'FAILED') {
64
+ console.log(` āŒ Task failed`);
65
+ process.exit(1);
66
+ }
67
+
68
+ console.log('');
69
+ }
70
+ )
71
+ .subscribe((status) => {
72
+ if (status === 'SUBSCRIBED') {
73
+ console.log('šŸ”— Connected to real-time updates');
74
+ } else if (status === 'CLOSED') {
75
+ console.log('šŸ”Œ Connection closed');
76
+ } else if (status === 'CHANNEL_ERROR') {
77
+ console.error('āŒ Channel error');
78
+ process.exit(1);
79
+ }
80
+ });
81
+
82
+ // Keep process alive
83
+ process.on('SIGINT', () => {
84
+ console.log('\nšŸ‘‹ Stopping listener...');
85
+ supabase.removeChannel(channel);
86
+ process.exit(0);
87
+ });
88
+ };
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Post Mission Command - Create Task via Agent Gateway
3
+ * Uses NACL signature authentication
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const chalk = require('chalk');
8
+ const ora = require('ora');
9
+ const inquirer = require('inquirer');
10
+ const { createTask } = require('../lib/api');
11
+ const { getIdentity, getConfigPath } = require('../lib/secure-config');
12
+
13
+ async function postMission(file) {
14
+ console.log(chalk.bold.blue('\nšŸ“¤ Rentman CLI - Post Mission\n'));
15
+
16
+ // 1. Check identity
17
+ const identity = getIdentity();
18
+
19
+ if (!identity) {
20
+ console.error(chalk.red('āŒ No identity found'));
21
+ console.log(chalk.yellow('→ Run: rentman init'));
22
+ console.log(chalk.gray(` Identity should be in: ${getConfigPath()}`));
23
+ process.exit(1);
24
+ }
25
+
26
+ console.log(chalk.gray(`āœ“ Authenticated as: ${identity.public_agent_id} (${identity.source})`));
27
+
28
+ // 2. Load or create task data
29
+ let taskData = {};
30
+
31
+ if (file) {
32
+ // Load from JSON file
33
+ if (!fs.existsSync(file)) {
34
+ console.error(chalk.red(`āŒ File not found: ${file}`));
35
+ process.exit(1);
36
+ }
37
+
38
+ try {
39
+ taskData = JSON.parse(fs.readFileSync(file, 'utf-8'));
40
+ console.log(chalk.green(`āœ“ Loaded task from: ${file}`));
41
+ } catch (error) {
42
+ console.error(chalk.red(`āŒ Invalid JSON: ${error.message}`));
43
+ process.exit(1);
44
+ }
45
+ } else {
46
+ // Interactive wizard
47
+ console.log(chalk.cyan('✨ Interactive Task Creation\n'));
48
+
49
+ const answers = await inquirer.prompt([
50
+ {
51
+ type: 'input',
52
+ name: 'title',
53
+ message: 'Task Title:',
54
+ validate: (input) => input.length >= 10 || 'Title too short (min 10 chars)',
55
+ },
56
+ {
57
+ type: 'input',
58
+ name: 'description',
59
+ message: 'Description:',
60
+ validate: (input) => input.length >= 20 || 'Description too short (min 20 chars)',
61
+ },
62
+ {
63
+ type: 'list',
64
+ name: 'task_type',
65
+ message: 'Task Type:',
66
+ choices: [
67
+ 'verification',
68
+ 'delivery',
69
+ 'repair',
70
+ 'representation',
71
+ 'creative',
72
+ 'communication',
73
+ ],
74
+ },
75
+ {
76
+ type: 'number',
77
+ name: 'budget_amount',
78
+ message: 'Budget ($):',
79
+ default: 15,
80
+ validate: (input) => input > 0 || 'Budget must be positive',
81
+ },
82
+ {
83
+ type: 'confirm',
84
+ name: 'hasLocation',
85
+ message: 'Specific location required?',
86
+ default: false,
87
+ },
88
+ ]);
89
+
90
+ taskData = {
91
+ title: answers.title,
92
+ description: answers.description,
93
+ task_type: answers.task_type,
94
+ budget_amount: answers.budget_amount,
95
+ budget_currency: 'USD',
96
+ payment_type: 'fixed',
97
+ priority: 3,
98
+ proof_requirements: ['photo', 'gps'],
99
+ };
100
+
101
+ // Add location if needed
102
+ if (answers.hasLocation) {
103
+ const location = await inquirer.prompt([
104
+ {
105
+ type: 'input',
106
+ name: 'address',
107
+ message: 'Address/City:',
108
+ },
109
+ {
110
+ type: 'number',
111
+ name: 'lat',
112
+ message: 'Latitude:',
113
+ default: 0,
114
+ },
115
+ {
116
+ type: 'number',
117
+ name: 'lng',
118
+ message: 'Longitude:',
119
+ default: 0,
120
+ },
121
+ ]);
122
+
123
+ taskData.location_address = location.address;
124
+ taskData.geo_location = {
125
+ lat: location.lat,
126
+ lng: location.lng,
127
+ };
128
+ }
129
+ }
130
+
131
+ // 3. Validate required fields
132
+ if (!taskData.title || !taskData.description) {
133
+ console.error(chalk.red('āŒ Missing required fields: title, description'));
134
+ process.exit(1);
135
+ }
136
+
137
+ // 4. Display task summary
138
+ console.log(chalk.white('\nšŸ“‹ Task Summary:'));
139
+ console.log(chalk.cyan(` Title: ${taskData.title}`));
140
+ console.log(chalk.cyan(` Type: ${taskData.task_type || 'N/A'}`));
141
+ console.log(chalk.cyan(` Budget: $${taskData.budget_amount || 0}`));
142
+ console.log('');
143
+
144
+ const confirm = await inquirer.prompt([
145
+ {
146
+ type: 'confirm',
147
+ name: 'proceed',
148
+ message: 'Post this task to the marketplace?',
149
+ default: true,
150
+ },
151
+ ]);
152
+
153
+ if (!confirm.proceed) {
154
+ console.log(chalk.yellow('āŒ Cancelled'));
155
+ process.exit(0);
156
+ }
157
+
158
+ // 5. Submit to Agent Gateway
159
+ const spinner = ora('Submitting task to Agent Gateway...').start();
160
+
161
+ try {
162
+ const response = await createTask(taskData);
163
+
164
+ spinner.succeed('Task posted successfully!');
165
+
166
+ console.log(chalk.green.bold('\nāœ… Task Created!\n'));
167
+ console.log(chalk.white('Task Details:'));
168
+ console.log(chalk.cyan(` • Task ID: ${response.data.task_id}`));
169
+ console.log(chalk.cyan(` • Status: ${response.data.status}`));
170
+ console.log(chalk.cyan(` • Created: ${response.data.created_at}`));
171
+
172
+ if (response.data.escrow_required) {
173
+ console.log(chalk.yellow('\nāš ļø Escrow payment required'));
174
+ console.log(chalk.gray(' Payment will be held until task completion'));
175
+ }
176
+
177
+ console.log(chalk.white.bold('\nšŸ“‹ Next Steps:'));
178
+ console.log(chalk.white(' 1. Listen for contracts: rentman listen'));
179
+ console.log(chalk.white(' 2. View task: rentman task view ' + response.data.task_id));
180
+
181
+ console.log('');
182
+ } catch (error) {
183
+ spinner.fail('Failed to post task');
184
+
185
+ console.error(chalk.red(`\nāŒ Error: ${error.message}`));
186
+
187
+ if (error.code === 'AUTH_FAILED') {
188
+ console.log(chalk.yellow('→ Re-initialize: rentman init'));
189
+ } else if (error.code === 'VALIDATION_ERROR') {
190
+ console.log(chalk.yellow('→ Check task data format'));
191
+ if (error.details) {
192
+ console.log(chalk.gray(' ' + JSON.stringify(error.details, null, 2)));
193
+ }
194
+ } else if (error.code === 'RATE_LIMIT_EXCEEDED') {
195
+ console.log(chalk.yellow('→ Wait before retrying'));
196
+ }
197
+
198
+ process.exit(1);
199
+ }
200
+ }
201
+
202
+ module.exports = postMission;