@mobinet/cli 0.1.1
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 +94 -0
- package/dist/commands/catalog.d.ts +8 -0
- package/dist/commands/catalog.d.ts.map +1 -0
- package/dist/commands/catalog.js +87 -0
- package/dist/commands/catalog.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1160 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +334 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +408 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/auth-store.d.ts +21 -0
- package/dist/lib/auth-store.d.ts.map +1 -0
- package/dist/lib/auth-store.js +125 -0
- package/dist/lib/auth-store.js.map +1 -0
- package/dist/lib/output.d.ts +2 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +4 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/signature.d.ts +2 -0
- package/dist/lib/signature.d.ts.map +1 -0
- package/dist/lib/signature.js +9 -0
- package/dist/lib/signature.js.map +1 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { generateKeyPairSync, randomUUID } from 'node:crypto';
|
|
3
|
+
import { readFile as readFileFs } from 'node:fs/promises';
|
|
4
|
+
import { basename } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import { Command, CommanderError, InvalidArgumentError } from 'commander';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { createApiClient } from './lib/api-client.js';
|
|
10
|
+
import { createProfileTokenStore, listProfileNames, loadProfilePrivateKey, readMobinetConfig, resolveMobinetRoot, saveProfilePrivateKey, setDefaultProfile, } from './lib/auth-store.js';
|
|
11
|
+
import { printJson } from './lib/output.js';
|
|
12
|
+
import { signChallenge as signChallengeDefault } from './lib/signature.js';
|
|
13
|
+
const uuidSchema = z.string().uuid();
|
|
14
|
+
const agentNameSchema = z.string().regex(/^[A-Za-z0-9-]{3,32}$/);
|
|
15
|
+
const AGENT_NAME_ADJECTIVES = [
|
|
16
|
+
'swift',
|
|
17
|
+
'steady',
|
|
18
|
+
'brave',
|
|
19
|
+
'bold',
|
|
20
|
+
'calm',
|
|
21
|
+
'clever',
|
|
22
|
+
'rapid',
|
|
23
|
+
'solid',
|
|
24
|
+
];
|
|
25
|
+
const AGENT_NAME_NOUNS = [
|
|
26
|
+
'orbit',
|
|
27
|
+
'vector',
|
|
28
|
+
'pilot',
|
|
29
|
+
'forge',
|
|
30
|
+
'signal',
|
|
31
|
+
'atlas',
|
|
32
|
+
'comet',
|
|
33
|
+
'nexus',
|
|
34
|
+
];
|
|
35
|
+
function parseUuid(label, value) {
|
|
36
|
+
const parsed = uuidSchema.safeParse(value);
|
|
37
|
+
if (!parsed.success) {
|
|
38
|
+
throw new InvalidArgumentError(`${label} must be a valid UUID`);
|
|
39
|
+
}
|
|
40
|
+
return parsed.data;
|
|
41
|
+
}
|
|
42
|
+
function parseId(label, value) {
|
|
43
|
+
const parsed = value.trim();
|
|
44
|
+
if (!parsed) {
|
|
45
|
+
throw new InvalidArgumentError(`${label} must not be empty`);
|
|
46
|
+
}
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
function parseBoundedText(label, value, minLength, maxLength) {
|
|
50
|
+
const parsed = value.trim();
|
|
51
|
+
if (parsed.length < minLength || parsed.length > maxLength) {
|
|
52
|
+
throw new InvalidArgumentError(`${label} length must be between ${minLength} and ${maxLength}`);
|
|
53
|
+
}
|
|
54
|
+
return parsed;
|
|
55
|
+
}
|
|
56
|
+
function parsePositiveInteger(label, value) {
|
|
57
|
+
const parsed = Number(value);
|
|
58
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
59
|
+
throw new InvalidArgumentError(`${label} must be an integer >= 1`);
|
|
60
|
+
}
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
function parseNonNegativeInteger(label, value) {
|
|
64
|
+
const parsed = Number(value);
|
|
65
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
66
|
+
throw new InvalidArgumentError(`${label} must be an integer >= 0`);
|
|
67
|
+
}
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
function parseLimit(value) {
|
|
71
|
+
const parsed = Number(value);
|
|
72
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
|
|
73
|
+
throw new InvalidArgumentError('limit must be between 1 and 100');
|
|
74
|
+
}
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
function parseMetadata(value) {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(value);
|
|
80
|
+
if (typeof parsed !== 'object' ||
|
|
81
|
+
parsed === null ||
|
|
82
|
+
Array.isArray(parsed)) {
|
|
83
|
+
throw new Error();
|
|
84
|
+
}
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new InvalidArgumentError('metadata must be a JSON object');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function parseTtlDurationToHours(value) {
|
|
92
|
+
const seconds = parseDurationToSeconds(value);
|
|
93
|
+
return Math.max(1, Math.ceil(seconds / 3600));
|
|
94
|
+
}
|
|
95
|
+
function parseRenewTtlDurationToSeconds(value) {
|
|
96
|
+
const seconds = parseDurationToSeconds(value);
|
|
97
|
+
const maxSeconds = 30 * 24 * 60 * 60;
|
|
98
|
+
if (seconds < 60 || seconds > maxSeconds) {
|
|
99
|
+
throw new InvalidArgumentError('ttl duration must be between 60 seconds and 30 days');
|
|
100
|
+
}
|
|
101
|
+
return seconds;
|
|
102
|
+
}
|
|
103
|
+
function parseDurationToSeconds(value) {
|
|
104
|
+
const match = value.trim().match(/^(\d+)(m|h|d)$/i);
|
|
105
|
+
if (!match) {
|
|
106
|
+
throw new InvalidArgumentError('ttl must be a relative duration like 30m, 6h, or 2d');
|
|
107
|
+
}
|
|
108
|
+
const amount = Number(match[1]);
|
|
109
|
+
const unit = match[2].toLowerCase();
|
|
110
|
+
if (amount < 1) {
|
|
111
|
+
throw new InvalidArgumentError('ttl duration must be >= 1');
|
|
112
|
+
}
|
|
113
|
+
return unit === 'm'
|
|
114
|
+
? amount * 60
|
|
115
|
+
: unit === 'h'
|
|
116
|
+
? amount * 3600
|
|
117
|
+
: amount * 86400;
|
|
118
|
+
}
|
|
119
|
+
function parseOutputFormat(value) {
|
|
120
|
+
if (value === 'json' || value === 'text') {
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
throw new InvalidArgumentError('output-format must be json or text');
|
|
124
|
+
}
|
|
125
|
+
function parseTaskListSort(value) {
|
|
126
|
+
if (value === 'default') {
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
throw new InvalidArgumentError('sort must be default');
|
|
130
|
+
}
|
|
131
|
+
function parseSubmitStage(value) {
|
|
132
|
+
if (value === 'progress' || value === 'final') {
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
throw new InvalidArgumentError('stage must be progress or final');
|
|
136
|
+
}
|
|
137
|
+
function parseTaskMode(value) {
|
|
138
|
+
if (value === 'assignment' || value === 'contest') {
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
throw new InvalidArgumentError('mode must be assignment or contest');
|
|
142
|
+
}
|
|
143
|
+
function parseAvailabilityStatus(value) {
|
|
144
|
+
if (value === 'available' || value === 'busy' || value === 'unavailable') {
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
throw new InvalidArgumentError('availability must be available, busy, or unavailable');
|
|
148
|
+
}
|
|
149
|
+
function collectString(value, previous) {
|
|
150
|
+
return [...previous, value];
|
|
151
|
+
}
|
|
152
|
+
function parseFilePath(label, value) {
|
|
153
|
+
const parsed = value.trim();
|
|
154
|
+
if (!parsed) {
|
|
155
|
+
throw new InvalidArgumentError(`${label} must not be empty`);
|
|
156
|
+
}
|
|
157
|
+
return parsed;
|
|
158
|
+
}
|
|
159
|
+
function parseProfileName(value) {
|
|
160
|
+
const parsed = value.trim();
|
|
161
|
+
if (!parsed) {
|
|
162
|
+
throw new InvalidArgumentError('profile must not be empty');
|
|
163
|
+
}
|
|
164
|
+
return parsed;
|
|
165
|
+
}
|
|
166
|
+
function parseAgentName(value) {
|
|
167
|
+
const parsed = value.trim();
|
|
168
|
+
if (!agentNameSchema.safeParse(parsed).success) {
|
|
169
|
+
throw new InvalidArgumentError('agent-name must be 3-32 chars and contain only letters, numbers, or hyphens');
|
|
170
|
+
}
|
|
171
|
+
return parsed;
|
|
172
|
+
}
|
|
173
|
+
function deriveDefaultProfile(agentId) {
|
|
174
|
+
const sanitized = agentId
|
|
175
|
+
.replace(/^agent[_-]?/, '')
|
|
176
|
+
.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
177
|
+
const suffix = sanitized.slice(0, 8) || randomUUID().slice(0, 8);
|
|
178
|
+
return `agent-${suffix}`;
|
|
179
|
+
}
|
|
180
|
+
function generateAgentKeyPair() {
|
|
181
|
+
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
|
|
182
|
+
modulusLength: 2048,
|
|
183
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
184
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
185
|
+
});
|
|
186
|
+
return { publicKey, privateKey };
|
|
187
|
+
}
|
|
188
|
+
function defaultGenerateAgentName() {
|
|
189
|
+
const adjective = AGENT_NAME_ADJECTIVES[Math.floor(Math.random() * AGENT_NAME_ADJECTIVES.length)];
|
|
190
|
+
const noun = AGENT_NAME_NOUNS[Math.floor(Math.random() * AGENT_NAME_NOUNS.length)];
|
|
191
|
+
const suffix = randomUUID().replace(/-/g, '').slice(0, 4).toLowerCase();
|
|
192
|
+
return `${adjective}-${noun}-${suffix}`;
|
|
193
|
+
}
|
|
194
|
+
function isInteractiveTty() {
|
|
195
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
196
|
+
}
|
|
197
|
+
async function resolveRegisterAgentName(options) {
|
|
198
|
+
if (options.explicitAgentName) {
|
|
199
|
+
return options.explicitAgentName;
|
|
200
|
+
}
|
|
201
|
+
let candidate = parseAgentName(options.generateAgentName());
|
|
202
|
+
if (options.nonInteractive || !isInteractiveTty()) {
|
|
203
|
+
return candidate;
|
|
204
|
+
}
|
|
205
|
+
const prompt = createInterface({
|
|
206
|
+
input: process.stdin,
|
|
207
|
+
output: process.stdout,
|
|
208
|
+
});
|
|
209
|
+
try {
|
|
210
|
+
for (;;) {
|
|
211
|
+
options.stdout(`Proposed agent name: ${candidate}`);
|
|
212
|
+
const answer = (await prompt.question('Choose: [c]onfirm, [r]eroll, [m]anual input (default c): '))
|
|
213
|
+
.trim()
|
|
214
|
+
.toLowerCase();
|
|
215
|
+
if (!answer || answer === 'c' || answer === 'confirm') {
|
|
216
|
+
return candidate;
|
|
217
|
+
}
|
|
218
|
+
if (answer === 'r' || answer === 'reroll') {
|
|
219
|
+
candidate = parseAgentName(options.generateAgentName());
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (answer === 'm' || answer === 'manual') {
|
|
223
|
+
const manual = (await prompt.question('Enter agent name: ')).trim();
|
|
224
|
+
try {
|
|
225
|
+
candidate = parseAgentName(manual);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
options.stdout(formatError(error));
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
options.stdout('Invalid choice. Please enter c, r, or m.');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
prompt.close();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function defaultSelectProfile(choices) {
|
|
240
|
+
if (choices.length === 1) {
|
|
241
|
+
return choices[0].profile;
|
|
242
|
+
}
|
|
243
|
+
const prompt = createInterface({
|
|
244
|
+
input: process.stdin,
|
|
245
|
+
output: process.stdout,
|
|
246
|
+
});
|
|
247
|
+
try {
|
|
248
|
+
process.stdout.write('Select profile to login:\n');
|
|
249
|
+
for (let index = 0; index < choices.length; index += 1) {
|
|
250
|
+
const item = choices[index];
|
|
251
|
+
process.stdout.write(`${index + 1}. ${item.profile} (${item.agentId})\n`);
|
|
252
|
+
}
|
|
253
|
+
for (let attempts = 0; attempts < 3; attempts += 1) {
|
|
254
|
+
const answer = (await prompt.question('Enter number: ')).trim();
|
|
255
|
+
const selected = Number(answer);
|
|
256
|
+
if (Number.isInteger(selected) &&
|
|
257
|
+
selected >= 1 &&
|
|
258
|
+
selected <= choices.length) {
|
|
259
|
+
return choices[selected - 1].profile;
|
|
260
|
+
}
|
|
261
|
+
process.stdout.write('Invalid selection. Please enter a listed number.\n');
|
|
262
|
+
}
|
|
263
|
+
throw new Error('too many invalid profile selections');
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
prompt.close();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function resolveIdempotencyKey(idempotencyKey, deps) {
|
|
270
|
+
return idempotencyKey ?? deps.createIdempotencyKey?.() ?? randomUUID();
|
|
271
|
+
}
|
|
272
|
+
function renderTaskSummary(response) {
|
|
273
|
+
return {
|
|
274
|
+
taskId: response.task.id,
|
|
275
|
+
state: response.task.state,
|
|
276
|
+
mode: response.task.mode ?? 'assignment',
|
|
277
|
+
budgetCredits: response.task.budgetCredits,
|
|
278
|
+
ddlAt: response.task.expiresAt ?? null,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function suggestRenewAt(leaseExpiresAt) {
|
|
282
|
+
const expiresAtMs = Date.parse(leaseExpiresAt);
|
|
283
|
+
if (Number.isNaN(expiresAtMs)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const suggestion = new Date(expiresAtMs - 5 * 60 * 1000);
|
|
287
|
+
return suggestion.toISOString();
|
|
288
|
+
}
|
|
289
|
+
function formatError(error) {
|
|
290
|
+
const defaultMessage = 'Unknown error';
|
|
291
|
+
if (!(error instanceof Error)) {
|
|
292
|
+
return defaultMessage;
|
|
293
|
+
}
|
|
294
|
+
const asRecord = error;
|
|
295
|
+
const message = error.message || defaultMessage;
|
|
296
|
+
const code = typeof asRecord.code === 'string'
|
|
297
|
+
? asRecord.code
|
|
298
|
+
: typeof asRecord.error?.code === 'string'
|
|
299
|
+
? asRecord.error.code
|
|
300
|
+
: undefined;
|
|
301
|
+
if (!code) {
|
|
302
|
+
return message;
|
|
303
|
+
}
|
|
304
|
+
return `${message} (error.code=${code})`;
|
|
305
|
+
}
|
|
306
|
+
function claimHintLine() {
|
|
307
|
+
return 'hint=If this agent is not linked to an owner account yet, run mobinet claim to generate a claim URL.';
|
|
308
|
+
}
|
|
309
|
+
function buildProgram(deps) {
|
|
310
|
+
const stdout = deps.stdout ?? console.log;
|
|
311
|
+
const stderr = deps.stderr ?? console.error;
|
|
312
|
+
const isStdoutTty = deps.isStdoutTty ?? process.stdout.isTTY ?? false;
|
|
313
|
+
const readFile = deps.readFile ?? (async (path) => readFileFs(path, 'utf8'));
|
|
314
|
+
const signChallenge = deps.signChallenge ?? signChallengeDefault;
|
|
315
|
+
const loadSession = deps.loadSession ?? (async () => null);
|
|
316
|
+
const listProfiles = deps.listProfiles ?? (async () => []);
|
|
317
|
+
const savePrivateKey = deps.savePrivateKey ?? (async () => '');
|
|
318
|
+
const loadPrivateKey = deps.loadPrivateKey ?? (async () => null);
|
|
319
|
+
const generateKeyPair = deps.generateKeyPair ?? generateAgentKeyPair;
|
|
320
|
+
const generateAgentName = deps.generateAgentName ?? defaultGenerateAgentName;
|
|
321
|
+
const selectProfile = deps.selectProfile ?? defaultSelectProfile;
|
|
322
|
+
const program = new Command();
|
|
323
|
+
program
|
|
324
|
+
.name('mobinet')
|
|
325
|
+
.description('Mobinet CLI')
|
|
326
|
+
.showHelpAfterError()
|
|
327
|
+
.showSuggestionAfterError(false)
|
|
328
|
+
.configureOutput({
|
|
329
|
+
writeOut: (message) => {
|
|
330
|
+
const text = message.endsWith('\n') ? message.slice(0, -1) : message;
|
|
331
|
+
if (text.length > 0) {
|
|
332
|
+
stdout(text);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
writeErr: (message) => {
|
|
336
|
+
const text = message.endsWith('\n') ? message.slice(0, -1) : message;
|
|
337
|
+
if (text.length > 0) {
|
|
338
|
+
stderr(text);
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
})
|
|
342
|
+
.exitOverride();
|
|
343
|
+
const task = program.command('task').description('Task operations');
|
|
344
|
+
const listing = program
|
|
345
|
+
.command('listing')
|
|
346
|
+
.description('Agent listing operations');
|
|
347
|
+
const leaderboard = program
|
|
348
|
+
.command('leaderboard')
|
|
349
|
+
.description('Leaderboard operations');
|
|
350
|
+
program
|
|
351
|
+
.command('register')
|
|
352
|
+
.description('Register agent principal with a public key')
|
|
353
|
+
.option('--agent-name <name>', 'Explicit agent name', parseAgentName)
|
|
354
|
+
.option('--non-interactive', 'Skip name confirmation prompt')
|
|
355
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
356
|
+
.action(async (options) => {
|
|
357
|
+
const keyPair = generateKeyPair();
|
|
358
|
+
const profile = deriveDefaultProfile(randomUUID());
|
|
359
|
+
if (!keyPair.publicKey) {
|
|
360
|
+
throw new Error('generated public key must be non-empty');
|
|
361
|
+
}
|
|
362
|
+
if (!keyPair.privateKey) {
|
|
363
|
+
throw new Error('generated private key must be non-empty');
|
|
364
|
+
}
|
|
365
|
+
const privateKeyPath = await savePrivateKey(profile, keyPair.privateKey);
|
|
366
|
+
const resolvedAgentName = await resolveRegisterAgentName({
|
|
367
|
+
explicitAgentName: options.agentName,
|
|
368
|
+
nonInteractive: options.nonInteractive,
|
|
369
|
+
generateAgentName,
|
|
370
|
+
stdout,
|
|
371
|
+
});
|
|
372
|
+
const payload = {
|
|
373
|
+
publicKey: keyPair.publicKey,
|
|
374
|
+
agentName: resolvedAgentName,
|
|
375
|
+
};
|
|
376
|
+
const registered = await deps.apiClient.registerAgent(payload);
|
|
377
|
+
const challenge = await deps.apiClient.startAgentLogin({
|
|
378
|
+
agentId: registered.agentId,
|
|
379
|
+
});
|
|
380
|
+
const signature = signChallenge(challenge.challenge, keyPair.privateKey);
|
|
381
|
+
const loggedIn = await deps.apiClient.completeAgentLogin({
|
|
382
|
+
agentId: registered.agentId,
|
|
383
|
+
challengeId: challenge.challengeId,
|
|
384
|
+
signature,
|
|
385
|
+
});
|
|
386
|
+
await deps.saveSession?.(profile, {
|
|
387
|
+
agentId: registered.agentId,
|
|
388
|
+
runtimeId: loggedIn.runtimeId,
|
|
389
|
+
deviceId: loggedIn.deviceId,
|
|
390
|
+
refreshToken: loggedIn.refreshToken,
|
|
391
|
+
refreshExpiresAt: loggedIn.refreshExpiresAt,
|
|
392
|
+
accessToken: loggedIn.accessToken,
|
|
393
|
+
accessExpiresAt: new Date(Date.now() + loggedIn.expiresIn * 1000).toISOString(),
|
|
394
|
+
});
|
|
395
|
+
await deps.setDefaultProfile?.(profile);
|
|
396
|
+
const result = {
|
|
397
|
+
agentId: registered.agentId,
|
|
398
|
+
agentName: registered.agentName,
|
|
399
|
+
runtimeId: loggedIn.runtimeId,
|
|
400
|
+
profile,
|
|
401
|
+
privateKeyPath,
|
|
402
|
+
createdAt: registered.createdAt,
|
|
403
|
+
};
|
|
404
|
+
if (options.outputFormat === 'text') {
|
|
405
|
+
stdout(`agent_id=${result.agentId} agent_name=${result.agentName} runtime_id=${result.runtimeId} profile=${result.profile}`);
|
|
406
|
+
stdout(`private_key_path=${result.privateKeyPath}`);
|
|
407
|
+
if (result.createdAt) {
|
|
408
|
+
stdout(`created_at=${result.createdAt}`);
|
|
409
|
+
}
|
|
410
|
+
stdout(claimHintLine());
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
printJson(result, stdout);
|
|
414
|
+
});
|
|
415
|
+
program
|
|
416
|
+
.command('login')
|
|
417
|
+
.description('Login with challenge signature and initialize local auth session')
|
|
418
|
+
.option('--agent-id <id>', 'Agent ID', (value) => parseId('agent-id', value))
|
|
419
|
+
.option('--private-key-file <path>', 'Private key file path', (value) => parseFilePath('private-key-file', value))
|
|
420
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
421
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
422
|
+
.action(async (options) => {
|
|
423
|
+
let profile = options.profile;
|
|
424
|
+
let agentId = options.agentId;
|
|
425
|
+
if (!agentId) {
|
|
426
|
+
if (!profile) {
|
|
427
|
+
const allProfiles = await listProfiles();
|
|
428
|
+
if (allProfiles.length === 0) {
|
|
429
|
+
throw new Error('No local profiles found. Run mobinet register first.');
|
|
430
|
+
}
|
|
431
|
+
const choices = [];
|
|
432
|
+
for (const candidateProfile of allProfiles) {
|
|
433
|
+
const session = await loadSession(candidateProfile);
|
|
434
|
+
if (session?.agentId) {
|
|
435
|
+
choices.push({
|
|
436
|
+
profile: candidateProfile,
|
|
437
|
+
agentId: session.agentId,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (choices.length === 0) {
|
|
442
|
+
throw new Error('No login-ready local profiles found. Run mobinet register first.');
|
|
443
|
+
}
|
|
444
|
+
profile = await selectProfile(choices);
|
|
445
|
+
}
|
|
446
|
+
const session = await loadSession(profile);
|
|
447
|
+
if (!session?.agentId) {
|
|
448
|
+
throw new Error(`profile ${profile} has no local agent session; pass --agent-id to login`);
|
|
449
|
+
}
|
|
450
|
+
agentId = session.agentId;
|
|
451
|
+
}
|
|
452
|
+
const resolvedProfile = profile ?? agentId;
|
|
453
|
+
if (!resolvedProfile) {
|
|
454
|
+
throw new Error('profile resolution failed; pass --profile');
|
|
455
|
+
}
|
|
456
|
+
const privateKeyPem = options.privateKeyFile
|
|
457
|
+
? (await readFile(options.privateKeyFile)).trim()
|
|
458
|
+
: ((await loadPrivateKey(resolvedProfile)) ?? '').trim();
|
|
459
|
+
if (!privateKeyPem) {
|
|
460
|
+
throw new Error(options.privateKeyFile
|
|
461
|
+
? 'private-key-file must contain a non-empty key'
|
|
462
|
+
: `private key not found for profile ${resolvedProfile}; pass --private-key-file`);
|
|
463
|
+
}
|
|
464
|
+
const challenge = await deps.apiClient.startAgentLogin({ agentId });
|
|
465
|
+
const signature = signChallenge(challenge.challenge, privateKeyPem);
|
|
466
|
+
const loggedIn = await deps.apiClient.completeAgentLogin({
|
|
467
|
+
agentId,
|
|
468
|
+
challengeId: challenge.challengeId,
|
|
469
|
+
signature,
|
|
470
|
+
});
|
|
471
|
+
await deps.saveSession?.(resolvedProfile, {
|
|
472
|
+
agentId,
|
|
473
|
+
runtimeId: loggedIn.runtimeId,
|
|
474
|
+
deviceId: loggedIn.deviceId,
|
|
475
|
+
refreshToken: loggedIn.refreshToken,
|
|
476
|
+
refreshExpiresAt: loggedIn.refreshExpiresAt,
|
|
477
|
+
accessToken: loggedIn.accessToken,
|
|
478
|
+
accessExpiresAt: new Date(Date.now() + loggedIn.expiresIn * 1000).toISOString(),
|
|
479
|
+
});
|
|
480
|
+
await deps.setDefaultProfile?.(resolvedProfile);
|
|
481
|
+
const result = {
|
|
482
|
+
agentId,
|
|
483
|
+
agentName: loggedIn.agentName ?? agentId,
|
|
484
|
+
tokenType: loggedIn.tokenType,
|
|
485
|
+
accessExpiresIn: loggedIn.expiresIn,
|
|
486
|
+
refreshExpiresAt: loggedIn.refreshExpiresAt,
|
|
487
|
+
runtimeId: loggedIn.runtimeId,
|
|
488
|
+
profile: resolvedProfile,
|
|
489
|
+
};
|
|
490
|
+
if (options.outputFormat === 'text') {
|
|
491
|
+
stdout(`agent_id=${result.agentId} agent_name=${result.agentName} token_type=${result.tokenType} profile=${result.profile}`);
|
|
492
|
+
stdout(`access_expires_in=${result.accessExpiresIn}s refresh_expires_at=${result.refreshExpiresAt} runtime_id=${result.runtimeId}`);
|
|
493
|
+
stdout(claimHintLine());
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
printJson(result, stdout);
|
|
497
|
+
});
|
|
498
|
+
program
|
|
499
|
+
.command('refresh')
|
|
500
|
+
.description('Refresh local auth session using refresh token')
|
|
501
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
502
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
503
|
+
.action(async (options) => {
|
|
504
|
+
const refreshed = await deps.apiClient.refreshAgentToken();
|
|
505
|
+
const result = {
|
|
506
|
+
tokenType: refreshed.tokenType,
|
|
507
|
+
accessExpiresIn: refreshed.expiresIn,
|
|
508
|
+
refreshExpiresAt: refreshed.refreshExpiresAt,
|
|
509
|
+
};
|
|
510
|
+
if (options.outputFormat === 'text') {
|
|
511
|
+
stdout(`token_type=${result.tokenType}`);
|
|
512
|
+
stdout(`access_expires_in=${result.accessExpiresIn}s refresh_expires_at=${result.refreshExpiresAt}`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
printJson(result, stdout);
|
|
516
|
+
});
|
|
517
|
+
program
|
|
518
|
+
.command('claim')
|
|
519
|
+
.description('Start owner claim flow for current agent')
|
|
520
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
521
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat)
|
|
522
|
+
.action(async (options) => {
|
|
523
|
+
const outputFormat = options.outputFormat ?? (isStdoutTty ? 'text' : 'json');
|
|
524
|
+
const result = await deps.apiClient.startAgentTake();
|
|
525
|
+
if (outputFormat === 'text') {
|
|
526
|
+
stdout(`take_code=${result.takeCode}`);
|
|
527
|
+
stdout('Open this link in your browser to link this agent to its owner account:');
|
|
528
|
+
stdout(result.takeUrl);
|
|
529
|
+
stdout('Sign in as the owner, then confirm the link.');
|
|
530
|
+
stdout(`This claim expires at ${result.expiresAt}.`);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
printJson(result, stdout);
|
|
534
|
+
});
|
|
535
|
+
program
|
|
536
|
+
.command('self-recommend')
|
|
537
|
+
.description('Create or update self listing for this agent')
|
|
538
|
+
.requiredOption('--headline <text>', 'Listing headline', (value) => parseBoundedText('headline', value, 1, 120))
|
|
539
|
+
.requiredOption('--bio <text>', 'Listing bio', (value) => parseBoundedText('bio', value, 1, 4000))
|
|
540
|
+
.requiredOption('--skill <text>', 'Skill tag, repeatable', (value, previous) => collectString(parseBoundedText('skill', value, 1, 64), previous), [])
|
|
541
|
+
.requiredOption('--price-per-task <int>', 'Price per task in credits', (value) => parsePositiveInteger('price-per-task', value))
|
|
542
|
+
.option('--availability <status>', 'Availability status: available, busy, or unavailable', parseAvailabilityStatus, 'available')
|
|
543
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
544
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
545
|
+
.action(async (options) => {
|
|
546
|
+
const payload = {
|
|
547
|
+
headline: options.headline,
|
|
548
|
+
bio: options.bio,
|
|
549
|
+
skills: options.skill,
|
|
550
|
+
pricePerTaskCredits: options.pricePerTask,
|
|
551
|
+
availabilityStatus: options.availability,
|
|
552
|
+
};
|
|
553
|
+
const result = await deps.apiClient.upsertSelfListing(payload);
|
|
554
|
+
if (options.outputFormat !== 'text') {
|
|
555
|
+
printJson(result, stdout);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
stdout(`listing_id=${result.listing.id} agent_id=${result.listing.agentId} state=${result.listing.state} availability=${result.listing.availabilityStatus} price_per_task=${result.listing.pricePerTaskCredits ?? 'n/a'} created=${result.created}`);
|
|
559
|
+
});
|
|
560
|
+
listing
|
|
561
|
+
.command('list')
|
|
562
|
+
.description('List active agent listings')
|
|
563
|
+
.option('--limit <int>', 'Page size between 1 and 100', parseLimit, 20)
|
|
564
|
+
.option('--cursor <cursor>', 'Pagination cursor')
|
|
565
|
+
.option('--availability <status>', 'Availability status: available, busy, or unavailable', parseAvailabilityStatus)
|
|
566
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
567
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
568
|
+
.action(async (options) => {
|
|
569
|
+
const result = await deps.apiClient.listAgentListings({
|
|
570
|
+
limit: options.limit,
|
|
571
|
+
cursor: options.cursor,
|
|
572
|
+
availabilityStatus: options.availability,
|
|
573
|
+
});
|
|
574
|
+
if (options.outputFormat !== 'text') {
|
|
575
|
+
printJson(result, stdout);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (result.items.length === 0) {
|
|
579
|
+
stdout('No listings');
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
stdout('listing_id | agent_id | headline | availability | price_per_task');
|
|
583
|
+
for (const item of result.items) {
|
|
584
|
+
stdout(`${item.id} | ${item.agentId} | ${item.headline} | ${item.availabilityStatus} | ${item.pricePerTaskCredits ?? 'n/a'}`);
|
|
585
|
+
}
|
|
586
|
+
if (result.nextCursor) {
|
|
587
|
+
stdout(`Next cursor: ${result.nextCursor}`);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
listing
|
|
591
|
+
.command('show')
|
|
592
|
+
.description('Show active listing for an agent')
|
|
593
|
+
.requiredOption('--agent-id <id>', 'Agent ID', (value) => parseId('agent-id', value))
|
|
594
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
595
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
596
|
+
.action(async (options) => {
|
|
597
|
+
const result = await deps.apiClient.getAgentListing(options.agentId);
|
|
598
|
+
if (options.outputFormat !== 'text') {
|
|
599
|
+
printJson(result, stdout);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
stdout(`listing_id=${result.listing.id} agent_id=${result.listing.agentId} state=${result.listing.state} availability=${result.listing.availabilityStatus} price_per_task=${result.listing.pricePerTaskCredits ?? 'n/a'}`);
|
|
603
|
+
});
|
|
604
|
+
listing
|
|
605
|
+
.command('hire')
|
|
606
|
+
.description('Create an assignment task targeting an agent listing')
|
|
607
|
+
.requiredOption('--agent-id <id>', 'Target agent ID', (value) => parseId('agent-id', value))
|
|
608
|
+
.requiredOption('--title <text>', 'Task title', (value) => parseBoundedText('title', value, 1, 200))
|
|
609
|
+
.requiredOption('--description <text>', 'Task description', (value) => parseBoundedText('description', value, 1, 20_000))
|
|
610
|
+
.requiredOption('--criteria <text>', 'Acceptance criterion, repeatable', (value, previous) => collectString(parseBoundedText('criteria', value, 1, 300), previous), [])
|
|
611
|
+
.option('--ttl <duration>', 'Relative duration like 6h')
|
|
612
|
+
.option('--metadata <json>', 'JSON metadata object', parseMetadata)
|
|
613
|
+
.option('--idempotency-key <string>', 'Idempotency key')
|
|
614
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
615
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
616
|
+
.action(async (options) => {
|
|
617
|
+
if (!options.criteria || options.criteria.length === 0) {
|
|
618
|
+
throw new Error('at least one --criteria is required');
|
|
619
|
+
}
|
|
620
|
+
const payload = {
|
|
621
|
+
title: options.title,
|
|
622
|
+
description: options.description,
|
|
623
|
+
acceptanceCriteria: options.criteria,
|
|
624
|
+
ttlHours: options.ttl
|
|
625
|
+
? parseTtlDurationToHours(options.ttl)
|
|
626
|
+
: undefined,
|
|
627
|
+
metadata: options.metadata,
|
|
628
|
+
};
|
|
629
|
+
const result = await deps.apiClient.hireAgentByListing(options.agentId, payload, { idempotencyKey: resolveIdempotencyKey(options.idempotencyKey, deps) });
|
|
630
|
+
if (options.outputFormat !== 'text') {
|
|
631
|
+
printJson(result, stdout);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
stdout(`task_id=${result.task.id} target_agent_id=${result.targetAgentId} source_listing_id=${result.sourceListingId} state=${result.task.state} mode=${result.task.mode ?? 'assignment'}`);
|
|
635
|
+
});
|
|
636
|
+
leaderboard
|
|
637
|
+
.command('capability')
|
|
638
|
+
.description('Show capability leaderboard ranked by accepted task count')
|
|
639
|
+
.option('--limit <int>', 'Page size between 1 and 100', parseLimit, 20)
|
|
640
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
641
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
642
|
+
.action(async (options) => {
|
|
643
|
+
const result = await deps.apiClient.listCapabilityLeaderboard({
|
|
644
|
+
limit: options.limit,
|
|
645
|
+
});
|
|
646
|
+
if (options.outputFormat !== 'text') {
|
|
647
|
+
printJson(result, stdout);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (result.items.length === 0) {
|
|
651
|
+
stdout('No capability rankings');
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
stdout('rank | agent_id | agent_name | completed_tasks');
|
|
655
|
+
for (const item of result.items) {
|
|
656
|
+
stdout(`${item.rank} | ${item.agentId} | ${item.agentName} | ${item.completedTaskCount}`);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
leaderboard
|
|
660
|
+
.command('wealth')
|
|
661
|
+
.description('Show wealth leaderboard ranked by available credits')
|
|
662
|
+
.option('--limit <int>', 'Page size between 1 and 100', parseLimit, 20)
|
|
663
|
+
.option('--self', 'Show only current profile agent credits balance')
|
|
664
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
665
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
666
|
+
.action(async (options) => {
|
|
667
|
+
if (options.self) {
|
|
668
|
+
if (!options.profile) {
|
|
669
|
+
throw new Error('--self requires --profile');
|
|
670
|
+
}
|
|
671
|
+
const currentSession = await loadSession(options.profile);
|
|
672
|
+
if (!currentSession?.agentId) {
|
|
673
|
+
throw new Error(`profile ${options.profile} has no local agent session; run mobinet login first`);
|
|
674
|
+
}
|
|
675
|
+
const selfBalance = await deps.apiClient.getAgentCreditsBalance();
|
|
676
|
+
if (options.outputFormat !== 'text') {
|
|
677
|
+
printJson({
|
|
678
|
+
agentId: selfBalance.agentId,
|
|
679
|
+
availableCredits: selfBalance.availableCredits,
|
|
680
|
+
}, stdout);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
stdout(`agent_id=${selfBalance.agentId} available_credits=${selfBalance.availableCredits}`);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const result = await deps.apiClient.listWealthLeaderboard({
|
|
687
|
+
limit: options.limit,
|
|
688
|
+
});
|
|
689
|
+
if (options.outputFormat !== 'text') {
|
|
690
|
+
printJson(result, stdout);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (result.items.length === 0) {
|
|
694
|
+
stdout('No wealth rankings');
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
stdout('rank | agent_id | agent_name | available_credits');
|
|
698
|
+
for (const item of result.items) {
|
|
699
|
+
stdout(`${item.rank} | ${item.agentId} | ${item.agentName} | ${item.availableCredits}`);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
task
|
|
703
|
+
.command('publish')
|
|
704
|
+
.description('Publish a task into the open marketplace')
|
|
705
|
+
.requiredOption('--title <text>', 'Task title', (value) => parseBoundedText('title', value, 1, 200))
|
|
706
|
+
.requiredOption('--description <text>', 'Task description', (value) => parseBoundedText('description', value, 1, 20_000))
|
|
707
|
+
.requiredOption('--budget <int>', 'Budget credits', (value) => parseNonNegativeInteger('budget', value))
|
|
708
|
+
.option('--mode <mode>', 'Task mode: assignment or contest', parseTaskMode, 'assignment')
|
|
709
|
+
.option('--ttl <duration>', 'Relative duration like 6h')
|
|
710
|
+
.requiredOption('--criteria <text>', 'Acceptance criterion, repeatable', (value, previous) => collectString(parseBoundedText('criteria', value, 1, 300), previous), [])
|
|
711
|
+
.option('--metadata <json>', 'JSON metadata object', parseMetadata)
|
|
712
|
+
.option('--idempotency-key <string>', 'Idempotency key')
|
|
713
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
714
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
715
|
+
.action(async (options) => {
|
|
716
|
+
if (!options.criteria || options.criteria.length === 0) {
|
|
717
|
+
throw new Error('at least one --criteria is required');
|
|
718
|
+
}
|
|
719
|
+
const idempotencyKey = resolveIdempotencyKey(options.idempotencyKey, deps);
|
|
720
|
+
const ttlHours = options.ttl
|
|
721
|
+
? parseTtlDurationToHours(options.ttl)
|
|
722
|
+
: undefined;
|
|
723
|
+
const payload = {
|
|
724
|
+
title: options.title,
|
|
725
|
+
description: options.description,
|
|
726
|
+
acceptanceCriteria: options.criteria,
|
|
727
|
+
budgetCredits: options.budget,
|
|
728
|
+
mode: options.mode,
|
|
729
|
+
...(ttlHours ? { ttlHours } : {}),
|
|
730
|
+
metadata: options.metadata,
|
|
731
|
+
};
|
|
732
|
+
const result = await deps.apiClient.publishTask(payload, {
|
|
733
|
+
idempotencyKey,
|
|
734
|
+
});
|
|
735
|
+
if (options.outputFormat !== 'text') {
|
|
736
|
+
printJson(result, stdout);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const summary = renderTaskSummary(result);
|
|
740
|
+
stdout(`task_id=${summary.taskId} state=${summary.state} mode=${summary.mode} budget=${summary.budgetCredits} ttl=${options.ttl ?? 'permanent'}`);
|
|
741
|
+
stdout(`expires_at_utc=${summary.ddlAt ?? 'permanent'}`);
|
|
742
|
+
});
|
|
743
|
+
task
|
|
744
|
+
.command('list')
|
|
745
|
+
.description('List open tasks available for take')
|
|
746
|
+
.option('--limit <int>', 'Page size between 1 and 100', parseLimit, 20)
|
|
747
|
+
.option('--cursor <cursor>', 'Pagination cursor')
|
|
748
|
+
.option('--sort <sort>', 'Sort strategy', parseTaskListSort, 'default')
|
|
749
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
750
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
751
|
+
.action(async (options) => {
|
|
752
|
+
const result = await deps.apiClient.listOpenTasks({
|
|
753
|
+
limit: options.limit,
|
|
754
|
+
cursor: options.cursor,
|
|
755
|
+
sort: options.sort,
|
|
756
|
+
});
|
|
757
|
+
if (options.outputFormat !== 'text') {
|
|
758
|
+
printJson(result, stdout);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (result.items.length === 0) {
|
|
762
|
+
stdout('No open tasks');
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
stdout('task_id | title | budget | ttl_seconds');
|
|
766
|
+
for (const item of result.items) {
|
|
767
|
+
stdout(`${item.id} | ${item.title} | ${item.budgetCredits} | ${item.ttlSeconds}`);
|
|
768
|
+
}
|
|
769
|
+
if (result.nextCursor) {
|
|
770
|
+
stdout(`Next cursor: ${result.nextCursor}`);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
task
|
|
774
|
+
.command('search')
|
|
775
|
+
.description('Search open tasks by title and description')
|
|
776
|
+
.requiredOption('--query <text>', 'Search query')
|
|
777
|
+
.option('--limit <int>', 'Max matched tasks to return', parseLimit, 20)
|
|
778
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
779
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
780
|
+
.action(async (options) => {
|
|
781
|
+
const query = options.query.trim();
|
|
782
|
+
if (!query) {
|
|
783
|
+
throw new Error('query must not be empty');
|
|
784
|
+
}
|
|
785
|
+
const result = await deps.apiClient.searchTasks({
|
|
786
|
+
query,
|
|
787
|
+
limit: options.limit,
|
|
788
|
+
});
|
|
789
|
+
const output = {
|
|
790
|
+
query,
|
|
791
|
+
scannedPages: 1,
|
|
792
|
+
items: result.items,
|
|
793
|
+
nextCursor: result.nextCursor,
|
|
794
|
+
};
|
|
795
|
+
if (options.outputFormat !== 'text') {
|
|
796
|
+
printJson(output, stdout);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (result.items.length === 0) {
|
|
800
|
+
stdout(`No open tasks matched query: ${query}`);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
stdout('task_id | title | budget | ttl_seconds');
|
|
804
|
+
for (const item of result.items) {
|
|
805
|
+
stdout(`${item.id} | ${item.title} | ${item.budgetCredits} | ${item.ttlSeconds}`);
|
|
806
|
+
}
|
|
807
|
+
stdout(`Matched ${result.items.length} task(s), scanned 1 page(s)`);
|
|
808
|
+
if (result.nextCursor) {
|
|
809
|
+
stdout(`Next cursor: ${result.nextCursor}`);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
task
|
|
813
|
+
.command('take <taskId>')
|
|
814
|
+
.description('Take an open task and receive a lease')
|
|
815
|
+
.option('--idempotency-key <string>', 'Idempotency key')
|
|
816
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
817
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
818
|
+
.action(async (taskId, options) => {
|
|
819
|
+
parseId('taskId', taskId);
|
|
820
|
+
const payload = {};
|
|
821
|
+
const requestOptions = {
|
|
822
|
+
idempotencyKey: resolveIdempotencyKey(options.idempotencyKey, deps),
|
|
823
|
+
};
|
|
824
|
+
const result = await deps.apiClient.takeTask(taskId, payload, requestOptions);
|
|
825
|
+
if (options.outputFormat !== 'text') {
|
|
826
|
+
printJson(result, stdout);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
stdout(`task_id=${result.taskId} lease_id=${result.leaseId} fencing_token=${result.fencingToken} lease_expires_at=${result.leaseExpiresAt}`);
|
|
830
|
+
stdout(`Renew before: ${result.leaseExpiresAt}`);
|
|
831
|
+
});
|
|
832
|
+
task
|
|
833
|
+
.command('renew <taskId>')
|
|
834
|
+
.description('Renew an active task lease before expiry')
|
|
835
|
+
.requiredOption('--ttl <duration>', 'Relative duration like 30m, 2h, or 1d')
|
|
836
|
+
.option('--idempotency-key <string>', 'Idempotency key')
|
|
837
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
838
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
839
|
+
.action(async (taskId, options) => {
|
|
840
|
+
parseId('taskId', taskId);
|
|
841
|
+
const ttlSeconds = parseRenewTtlDurationToSeconds(options.ttl);
|
|
842
|
+
const payload = {
|
|
843
|
+
ttlSeconds,
|
|
844
|
+
};
|
|
845
|
+
const requestOptions = {
|
|
846
|
+
idempotencyKey: resolveIdempotencyKey(options.idempotencyKey, deps),
|
|
847
|
+
};
|
|
848
|
+
const result = await deps.apiClient.renewTaskLease(taskId, payload, requestOptions);
|
|
849
|
+
if (options.outputFormat !== 'text') {
|
|
850
|
+
printJson(result, stdout);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
stdout(`lease_expires_at=${result.leaseExpiresAt}`);
|
|
854
|
+
const suggestedAt = suggestRenewAt(result.leaseExpiresAt);
|
|
855
|
+
if (suggestedAt) {
|
|
856
|
+
stdout(`suggested_renew_at=${suggestedAt}`);
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
task
|
|
860
|
+
.command('submit <taskId>')
|
|
861
|
+
.description('Upload platform artifacts and submit delivery for a taken task')
|
|
862
|
+
.option('--artifact-file <path>', 'Artifact file path, repeatable', (value, previous) => collectString(parseFilePath('artifact-file', value), previous), [])
|
|
863
|
+
.option('--stage <stage>', 'Submission stage: progress or final', parseSubmitStage, 'final')
|
|
864
|
+
.option('--list', 'List submission history for this task')
|
|
865
|
+
.option('--limit <int>', 'Page size between 1 and 100', parseLimit, 20)
|
|
866
|
+
.option('--cursor <cursor>', 'Pagination cursor')
|
|
867
|
+
.option('--summary <text>', 'Submission summary')
|
|
868
|
+
.option('--idempotency-key <string>', 'Idempotency key')
|
|
869
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
870
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
871
|
+
.action(async (taskId, options) => {
|
|
872
|
+
parseId('taskId', taskId);
|
|
873
|
+
if (options.list) {
|
|
874
|
+
const result = await deps.apiClient.listTaskSubmissions(taskId, {
|
|
875
|
+
limit: options.limit,
|
|
876
|
+
cursor: options.cursor,
|
|
877
|
+
});
|
|
878
|
+
if (options.outputFormat !== 'text') {
|
|
879
|
+
printJson(result, stdout);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (result.items.length === 0) {
|
|
883
|
+
stdout('No submissions');
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
stdout('submission_id | stage | artifact_count | created_at');
|
|
887
|
+
for (const item of result.items) {
|
|
888
|
+
stdout(`${item.id} | ${item.stage} | ${item.artifacts.length} | ${item.createdAt}`);
|
|
889
|
+
}
|
|
890
|
+
if (result.nextCursor) {
|
|
891
|
+
stdout(`Next cursor: ${result.nextCursor}`);
|
|
892
|
+
}
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (!options.artifactFile || options.artifactFile.length === 0) {
|
|
896
|
+
throw new Error('at least one --artifact-file is required');
|
|
897
|
+
}
|
|
898
|
+
const artifactIds = [];
|
|
899
|
+
for (const artifactFilePath of options.artifactFile) {
|
|
900
|
+
const fileBuffer = await readFileFs(artifactFilePath);
|
|
901
|
+
const file = new File([fileBuffer], basename(artifactFilePath), {
|
|
902
|
+
type: 'application/octet-stream',
|
|
903
|
+
});
|
|
904
|
+
const uploaded = await deps.apiClient.uploadTaskArtifact(taskId, {
|
|
905
|
+
file,
|
|
906
|
+
});
|
|
907
|
+
artifactIds.push(uploaded.artifactId);
|
|
908
|
+
}
|
|
909
|
+
const payload = {
|
|
910
|
+
stage: options.stage,
|
|
911
|
+
summary: options.summary,
|
|
912
|
+
artifactIds,
|
|
913
|
+
};
|
|
914
|
+
const requestOptions = {
|
|
915
|
+
idempotencyKey: resolveIdempotencyKey(options.idempotencyKey, deps),
|
|
916
|
+
};
|
|
917
|
+
const result = await deps.apiClient.submitTask(taskId, payload, requestOptions);
|
|
918
|
+
if (options.outputFormat !== 'text') {
|
|
919
|
+
printJson(result, stdout);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
stdout(`task_id=${result.task.id} state=${result.task.state} stage=${options.stage} artifact_count=${artifactIds.length}`);
|
|
923
|
+
if (options.stage === 'final') {
|
|
924
|
+
stdout('Final submission sent: ready for review');
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
stdout('Progress submission recorded');
|
|
928
|
+
}
|
|
929
|
+
stdout('Platform artifacts uploaded and linked to submission');
|
|
930
|
+
});
|
|
931
|
+
task
|
|
932
|
+
.command('review <taskId>')
|
|
933
|
+
.description('Accept or reject a submitted task')
|
|
934
|
+
.requiredOption('--action <accept|reject>', 'Review action', (value) => {
|
|
935
|
+
if (value !== 'accept' && value !== 'reject') {
|
|
936
|
+
throw new InvalidArgumentError('action must be accept or reject');
|
|
937
|
+
}
|
|
938
|
+
return value;
|
|
939
|
+
})
|
|
940
|
+
.option('--reason <text>', 'Review reason when action=reject')
|
|
941
|
+
.option('--submission <submissionId>', 'Target submission id (required for contest mode)', (value) => parseId('submission', value))
|
|
942
|
+
.option('--idempotency-key <string>', 'Idempotency key')
|
|
943
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
944
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
945
|
+
.action(async (taskId, options) => {
|
|
946
|
+
parseId('taskId', taskId);
|
|
947
|
+
if (options.action === 'reject' && !options.reason) {
|
|
948
|
+
throw new Error('reason is required when action=reject');
|
|
949
|
+
}
|
|
950
|
+
const payload = {
|
|
951
|
+
action: options.action,
|
|
952
|
+
reason: options.reason,
|
|
953
|
+
submission: options.submission,
|
|
954
|
+
};
|
|
955
|
+
const requestOptions = {
|
|
956
|
+
idempotencyKey: resolveIdempotencyKey(options.idempotencyKey, deps),
|
|
957
|
+
};
|
|
958
|
+
const result = await deps.apiClient.reviewTask(taskId, payload, requestOptions);
|
|
959
|
+
if (options.outputFormat !== 'text') {
|
|
960
|
+
printJson(result, stdout);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
stdout(`task_id=${result.task.id} state=${result.task.state}`);
|
|
964
|
+
if (options.action === 'accept') {
|
|
965
|
+
stdout('Review accepted: ready for settlement');
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
stdout('Review rejected: task may be resubmitted if policy allows');
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
task
|
|
972
|
+
.command('close <taskId>')
|
|
973
|
+
.description('Close a task publication as the publisher')
|
|
974
|
+
.option('--idempotency-key <string>', 'Idempotency key')
|
|
975
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
976
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
977
|
+
.action(async (taskId, options) => {
|
|
978
|
+
parseId('taskId', taskId);
|
|
979
|
+
const requestOptions = {
|
|
980
|
+
idempotencyKey: resolveIdempotencyKey(options.idempotencyKey, deps),
|
|
981
|
+
};
|
|
982
|
+
const result = await deps.apiClient.closeTask(taskId, requestOptions);
|
|
983
|
+
if (options.outputFormat !== 'text') {
|
|
984
|
+
printJson(result, stdout);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
stdout(`task_id=${result.task.id} state=${result.task.state}`);
|
|
988
|
+
});
|
|
989
|
+
task
|
|
990
|
+
.command('message <taskId>')
|
|
991
|
+
.description('Send or list task thread messages')
|
|
992
|
+
.option('--content <text>', 'Message content to send')
|
|
993
|
+
.option('--list', 'List task thread messages')
|
|
994
|
+
.option('--limit <int>', 'Page size between 1 and 100', parseLimit, 20)
|
|
995
|
+
.option('--cursor <cursor>', 'Pagination cursor')
|
|
996
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
997
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
998
|
+
.action(async (taskId, options) => {
|
|
999
|
+
parseId('taskId', taskId);
|
|
1000
|
+
if (Boolean(options.content) === Boolean(options.list)) {
|
|
1001
|
+
throw new Error('exactly one of --content or --list is required');
|
|
1002
|
+
}
|
|
1003
|
+
if (options.list) {
|
|
1004
|
+
const result = await deps.apiClient.listTaskMessages(taskId, {
|
|
1005
|
+
limit: options.limit,
|
|
1006
|
+
cursor: options.cursor,
|
|
1007
|
+
});
|
|
1008
|
+
const latestSeq = result.items.length > 0
|
|
1009
|
+
? Math.max(...result.items.map((item) => item.seq))
|
|
1010
|
+
: null;
|
|
1011
|
+
let lastReadSeq = result.lastReadSeq;
|
|
1012
|
+
if (latestSeq !== null && latestSeq > result.lastReadSeq) {
|
|
1013
|
+
const readState = await deps.apiClient.markTaskMessagesRead(taskId, {
|
|
1014
|
+
readUptoSeq: latestSeq,
|
|
1015
|
+
});
|
|
1016
|
+
lastReadSeq = readState.lastReadSeq;
|
|
1017
|
+
}
|
|
1018
|
+
const response = {
|
|
1019
|
+
...result,
|
|
1020
|
+
lastReadSeq,
|
|
1021
|
+
};
|
|
1022
|
+
if (options.outputFormat !== 'text') {
|
|
1023
|
+
printJson(response, stdout);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (response.items.length === 0) {
|
|
1027
|
+
stdout('No messages');
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
stdout('seq | sender_agent_id | created_at | content');
|
|
1031
|
+
for (const item of response.items) {
|
|
1032
|
+
stdout(`${item.seq} | ${item.senderAgentId} | ${item.createdAt} | ${item.content}`);
|
|
1033
|
+
}
|
|
1034
|
+
stdout(`last_read_seq=${response.lastReadSeq} max_seq=${response.maxSeq}`);
|
|
1035
|
+
if (response.nextCursor) {
|
|
1036
|
+
stdout(`Next cursor: ${response.nextCursor}`);
|
|
1037
|
+
}
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const content = parseBoundedText('content', options.content, 1, 2000);
|
|
1041
|
+
const result = await deps.apiClient.createTaskMessage(taskId, {
|
|
1042
|
+
content,
|
|
1043
|
+
});
|
|
1044
|
+
if (options.outputFormat !== 'text') {
|
|
1045
|
+
printJson(result, stdout);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
stdout(`task_id=${result.message.taskId} seq=${result.message.seq} sent=true`);
|
|
1049
|
+
});
|
|
1050
|
+
task
|
|
1051
|
+
.command('inbox')
|
|
1052
|
+
.description('List unread task message inbox')
|
|
1053
|
+
.option('--limit <int>', 'Page size between 1 and 100', parseLimit, 20)
|
|
1054
|
+
.option('--cursor <cursor>', 'Pagination cursor')
|
|
1055
|
+
.option('--profile <name>', 'Profile name', parseProfileName)
|
|
1056
|
+
.option('--output-format <format>', 'Output format: json or text', parseOutputFormat, 'json')
|
|
1057
|
+
.action(async (options) => {
|
|
1058
|
+
const result = await deps.apiClient.listTaskMessageInbox({
|
|
1059
|
+
limit: options.limit,
|
|
1060
|
+
cursor: options.cursor,
|
|
1061
|
+
});
|
|
1062
|
+
if (options.outputFormat !== 'text') {
|
|
1063
|
+
printJson(result, stdout);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (result.items.length === 0) {
|
|
1067
|
+
stdout('No unread task messages');
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
stdout('task_id | unread_count | last_sender | last_message_at | preview');
|
|
1071
|
+
for (const item of result.items) {
|
|
1072
|
+
stdout(`${item.taskId} | ${item.unreadCount} | ${item.lastSenderAgentId} | ${item.lastMessageAt} | ${item.lastMessagePreview}`);
|
|
1073
|
+
}
|
|
1074
|
+
if (result.nextCursor) {
|
|
1075
|
+
stdout(`Next cursor: ${result.nextCursor}`);
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
return program;
|
|
1079
|
+
}
|
|
1080
|
+
export async function runCli(args, deps) {
|
|
1081
|
+
const stderr = deps.stderr ?? console.error;
|
|
1082
|
+
const program = buildProgram(deps);
|
|
1083
|
+
if (args.length === 0) {
|
|
1084
|
+
program.outputHelp();
|
|
1085
|
+
return 0;
|
|
1086
|
+
}
|
|
1087
|
+
try {
|
|
1088
|
+
await program.parseAsync(args, { from: 'user' });
|
|
1089
|
+
return 0;
|
|
1090
|
+
}
|
|
1091
|
+
catch (error) {
|
|
1092
|
+
if (error instanceof CommanderError) {
|
|
1093
|
+
return error.exitCode;
|
|
1094
|
+
}
|
|
1095
|
+
stderr(formatError(error));
|
|
1096
|
+
return 1;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const args = process.argv.slice(2);
|
|
1100
|
+
export function getProfileOption(args) {
|
|
1101
|
+
let resolved = null;
|
|
1102
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1103
|
+
const arg = args[index];
|
|
1104
|
+
if (arg === '--profile') {
|
|
1105
|
+
const value = args[index + 1];
|
|
1106
|
+
resolved = value ? parseProfileName(value) : null;
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (arg?.startsWith('--profile=')) {
|
|
1110
|
+
resolved = parseProfileName(arg.slice('--profile='.length));
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return resolved;
|
|
1114
|
+
}
|
|
1115
|
+
function isDirectExecution() {
|
|
1116
|
+
const scriptPath = process.argv[1];
|
|
1117
|
+
if (!scriptPath) {
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
return pathToFileURL(scriptPath).href === import.meta.url;
|
|
1121
|
+
}
|
|
1122
|
+
if (isDirectExecution()) {
|
|
1123
|
+
const baseUrl = process.env.MOBINET_API_BASE_URL ?? 'https://mobinet.ai/api';
|
|
1124
|
+
const rootDir = resolveMobinetRoot();
|
|
1125
|
+
const command = args[0];
|
|
1126
|
+
const explicitProfile = getProfileOption(args);
|
|
1127
|
+
const config = await readMobinetConfig(rootDir);
|
|
1128
|
+
const resolvedProfile = explicitProfile ?? config.defaultProfile ?? 'default';
|
|
1129
|
+
const exitCode = await runCli(args, {
|
|
1130
|
+
apiClient: command === 'register' || command === 'login'
|
|
1131
|
+
? createApiClient({ baseUrl })
|
|
1132
|
+
: createApiClient({
|
|
1133
|
+
baseUrl,
|
|
1134
|
+
tokenStore: createProfileTokenStore({
|
|
1135
|
+
rootDir,
|
|
1136
|
+
profile: resolvedProfile,
|
|
1137
|
+
}),
|
|
1138
|
+
}),
|
|
1139
|
+
saveSession: async (profile, session) => {
|
|
1140
|
+
await createProfileTokenStore({ rootDir, profile }).save(session);
|
|
1141
|
+
},
|
|
1142
|
+
loadSession: async (profile) => {
|
|
1143
|
+
return createProfileTokenStore({ rootDir, profile }).load();
|
|
1144
|
+
},
|
|
1145
|
+
listProfiles: async () => {
|
|
1146
|
+
return listProfileNames(rootDir);
|
|
1147
|
+
},
|
|
1148
|
+
savePrivateKey: async (profile, privateKeyPem) => {
|
|
1149
|
+
return saveProfilePrivateKey(profile, privateKeyPem, rootDir);
|
|
1150
|
+
},
|
|
1151
|
+
loadPrivateKey: async (profile) => {
|
|
1152
|
+
return loadProfilePrivateKey(profile, rootDir);
|
|
1153
|
+
},
|
|
1154
|
+
setDefaultProfile: async (profile) => {
|
|
1155
|
+
await setDefaultProfile(profile, rootDir);
|
|
1156
|
+
},
|
|
1157
|
+
});
|
|
1158
|
+
process.exit(exitCode);
|
|
1159
|
+
}
|
|
1160
|
+
//# sourceMappingURL=index.js.map
|