jobarbiter 0.3.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/dist/index.js ADDED
@@ -0,0 +1,816 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { saveConfig, requireConfig, getConfigPath } from "./lib/config.js";
4
+ import { api, apiUnauthenticated, ApiError } from "./lib/api.js";
5
+ import { output, outputList, success, error, setJsonMode } from "./lib/output.js";
6
+ const program = new Command();
7
+ program
8
+ .name("jobarbiter")
9
+ .description("CLI for JobArbiter — the first AI Proficiency Marketplace")
10
+ .version("0.3.0")
11
+ .option("--json", "Output JSON (machine-readable)")
12
+ .hook("preAction", (cmd) => {
13
+ const opts = cmd.opts();
14
+ if (opts.json)
15
+ setJsonMode(true);
16
+ });
17
+ // ============================================================
18
+ // register
19
+ // ============================================================
20
+ program
21
+ .command("register")
22
+ .description("Register a new account and save API key")
23
+ .requiredOption("--email <email>", "Email address")
24
+ .requiredOption("--type <type>", "Account type: worker or employer")
25
+ .option("--base-url <url>", "API base URL", "https://jobarbiter-api-production.up.railway.app")
26
+ .action(async (opts) => {
27
+ try {
28
+ const userType = opts.type === "seeker" ? "worker" : opts.type;
29
+ if (!["worker", "employer"].includes(userType)) {
30
+ error("Type must be 'worker' or 'employer'");
31
+ process.exit(1);
32
+ }
33
+ // Step 1: Register and request verification code
34
+ const registerData = await apiUnauthenticated(opts.baseUrl, "POST", "/v1/auth/register", {
35
+ email: opts.email,
36
+ userType,
37
+ });
38
+ success(`Verification code sent to ${opts.email} (expires in 15 minutes)`);
39
+ // Step 2: Prompt for verification code
40
+ const code = await promptForCode();
41
+ if (!code) {
42
+ error("Verification cancelled.");
43
+ process.exit(1);
44
+ }
45
+ // Step 3: Verify the code
46
+ const verifyData = await apiUnauthenticated(opts.baseUrl, "POST", "/v1/auth/verify", {
47
+ email: opts.email,
48
+ code: code.trim(),
49
+ });
50
+ // Step 4: Save config with API key
51
+ saveConfig({
52
+ apiKey: verifyData.apiKey,
53
+ baseUrl: opts.baseUrl,
54
+ userType: userType,
55
+ });
56
+ success(`Email verified! API key saved to ${getConfigPath()}`);
57
+ console.log(` Key: ${verifyData.apiKey.slice(0, 20)}... (save this — shown only once)`);
58
+ output({ id: verifyData.id, email: opts.email, userType });
59
+ }
60
+ catch (e) {
61
+ handleError(e);
62
+ }
63
+ });
64
+ // ============================================================
65
+ // verify (standalone verification for existing registrations)
66
+ // ============================================================
67
+ program
68
+ .command("verify")
69
+ .description("Verify email with code (if registration was interrupted)")
70
+ .requiredOption("--email <email>", "Email address")
71
+ .option("--code <code>", "6-digit verification code")
72
+ .option("--base-url <url>", "API base URL", "https://jobarbiter-api-production.up.railway.app")
73
+ .action(async (opts) => {
74
+ try {
75
+ let code = opts.code;
76
+ if (!code) {
77
+ code = await promptForCode();
78
+ if (!code) {
79
+ error("Verification cancelled.");
80
+ process.exit(1);
81
+ }
82
+ }
83
+ const data = await apiUnauthenticated(opts.baseUrl, "POST", "/v1/auth/verify", {
84
+ email: opts.email,
85
+ code: code.trim(),
86
+ });
87
+ // Determine user type from response or default
88
+ const userType = data.userType || "worker";
89
+ saveConfig({
90
+ apiKey: data.apiKey,
91
+ baseUrl: opts.baseUrl,
92
+ userType: userType,
93
+ });
94
+ success(`Email verified! API key saved to ${getConfigPath()}`);
95
+ console.log(` Key: ${data.apiKey.slice(0, 20)}... (save this — shown only once)`);
96
+ output({ id: data.id, email: opts.email, userType });
97
+ }
98
+ catch (e) {
99
+ handleError(e);
100
+ }
101
+ });
102
+ // ============================================================
103
+ // resend-code
104
+ // ============================================================
105
+ program
106
+ .command("resend-code")
107
+ .description("Resend verification code to email")
108
+ .requiredOption("--email <email>", "Email address")
109
+ .option("--base-url <url>", "API base URL", "https://jobarbiter-api-production.up.railway.app")
110
+ .action(async (opts) => {
111
+ try {
112
+ const data = await apiUnauthenticated(opts.baseUrl, "POST", "/v1/auth/resend-code", {
113
+ email: opts.email,
114
+ });
115
+ success("If this email is registered and unverified, a new code has been sent.");
116
+ console.log(" Use: jobarbiter verify --email " + opts.email);
117
+ }
118
+ catch (e) {
119
+ handleError(e);
120
+ }
121
+ });
122
+ /**
123
+ * Prompt user for verification code via stdin
124
+ */
125
+ async function promptForCode() {
126
+ const readline = await import("node:readline");
127
+ const rl = readline.createInterface({
128
+ input: process.stdin,
129
+ output: process.stdout,
130
+ });
131
+ return new Promise((resolve) => {
132
+ rl.question("\nEnter verification code: ", (answer) => {
133
+ rl.close();
134
+ resolve(answer || null);
135
+ });
136
+ });
137
+ }
138
+ // ============================================================
139
+ // status
140
+ // ============================================================
141
+ program
142
+ .command("status")
143
+ .description("Check connection and account status")
144
+ .action(async () => {
145
+ try {
146
+ const config = requireConfig();
147
+ const health = await api(config, "GET", "/health");
148
+ const profile = await api(config, "GET", "/v1/profile").catch(() => null);
149
+ output({
150
+ connected: true,
151
+ service: health.service,
152
+ version: health.version,
153
+ userType: config.userType,
154
+ baseUrl: config.baseUrl,
155
+ hasProfile: !!profile,
156
+ compositeScore: profile?.compositeScore || null,
157
+ primaryTrack: profile?.primaryTrack || null,
158
+ configPath: getConfigPath(),
159
+ });
160
+ }
161
+ catch (e) {
162
+ handleError(e);
163
+ }
164
+ });
165
+ // ============================================================
166
+ // profile (worker commands)
167
+ // ============================================================
168
+ const profile = program.command("profile").description("Manage your AI proficiency profile");
169
+ profile
170
+ .command("show")
171
+ .description("Show your current proficiency profile")
172
+ .action(async () => {
173
+ try {
174
+ const config = requireConfig();
175
+ const data = await api(config, "GET", "/v1/profile");
176
+ output(data);
177
+ }
178
+ catch (e) {
179
+ handleError(e);
180
+ }
181
+ });
182
+ profile
183
+ .command("create")
184
+ .description("Create or update your proficiency profile")
185
+ .option("--bio <text>", "Professional bio / summary")
186
+ .option("--domains <list>", "Comma-separated domains: software-engineering,data-analytics,content,operations")
187
+ .option("--tools <json>", 'Tools JSON: {"models":["claude","gpt-4"],"agents":["cursor","claude-code"]}')
188
+ .option("--compensation-min <n>", "Minimum compensation", parseInt)
189
+ .option("--compensation-max <n>", "Maximum compensation", parseInt)
190
+ .option("--currency <code>", "Compensation currency", "USD")
191
+ .option("--open-to <types>", "Comma-separated: full-time,fractional,project,advisory")
192
+ .option("--remote <pref>", "Remote preference: remote|hybrid|onsite|any")
193
+ .option("--actively-seeking", "Mark as actively seeking opportunities")
194
+ .option("--not-seeking", "Mark as not actively seeking")
195
+ .action(async (opts) => {
196
+ try {
197
+ const config = requireConfig();
198
+ const body = {};
199
+ if (opts.bio)
200
+ body.bio = opts.bio;
201
+ if (opts.domains)
202
+ body.domains = opts.domains.split(",").map((s) => s.trim());
203
+ if (opts.tools)
204
+ body.tools = JSON.parse(opts.tools);
205
+ if (opts.compensationMin)
206
+ body.compensationMin = opts.compensationMin;
207
+ if (opts.compensationMax)
208
+ body.compensationMax = opts.compensationMax;
209
+ if (opts.currency)
210
+ body.compensationCurrency = opts.currency;
211
+ if (opts.openTo)
212
+ body.openTo = opts.openTo.split(",").map((s) => s.trim());
213
+ if (opts.remote)
214
+ body.remotePreference = opts.remote;
215
+ if (opts.activelySeeking)
216
+ body.activelySeeking = true;
217
+ if (opts.notSeeking)
218
+ body.activelySeeking = false;
219
+ const data = await api(config, "POST", "/v1/profile", body);
220
+ success("Profile created/updated. Embedding generated for matching.");
221
+ output(data);
222
+ }
223
+ catch (e) {
224
+ handleError(e);
225
+ }
226
+ });
227
+ profile
228
+ .command("score")
229
+ .description("Show detailed proficiency score breakdown (6 dimensions)")
230
+ .action(async () => {
231
+ try {
232
+ const config = requireConfig();
233
+ const data = await api(config, "GET", "/v1/profile/scores");
234
+ output(data);
235
+ }
236
+ catch (e) {
237
+ handleError(e);
238
+ }
239
+ });
240
+ profile
241
+ .command("delete")
242
+ .description("Delete your profile (GDPR)")
243
+ .action(async () => {
244
+ try {
245
+ const config = requireConfig();
246
+ const data = await api(config, "DELETE", "/v1/profile");
247
+ success("Profile and all associated data deleted.");
248
+ output(data);
249
+ }
250
+ catch (e) {
251
+ handleError(e);
252
+ }
253
+ });
254
+ // ============================================================
255
+ // git (connect GitHub for analysis)
256
+ // ============================================================
257
+ const git = program.command("git").description("Connect and analyze git repositories");
258
+ git
259
+ .command("connect")
260
+ .description("Connect a GitHub/GitLab account for AI-assisted contribution analysis")
261
+ .requiredOption("--provider <provider>", "Provider: github|gitlab|bitbucket", "github")
262
+ .requiredOption("--username <username>", "Git username")
263
+ .option("--token <token>", "Access token (or set GITHUB_TOKEN env)")
264
+ .action(async (opts) => {
265
+ try {
266
+ const config = requireConfig();
267
+ const token = opts.token || process.env.GITHUB_TOKEN || process.env.GITLAB_TOKEN;
268
+ const data = await api(config, "POST", "/v1/attestations/git/connect", {
269
+ provider: opts.provider,
270
+ username: opts.username,
271
+ accessToken: token,
272
+ });
273
+ success(`Git connected: ${opts.provider}/${opts.username}. Analysis queued.`);
274
+ output(data);
275
+ }
276
+ catch (e) {
277
+ handleError(e);
278
+ }
279
+ });
280
+ git
281
+ .command("analysis")
282
+ .description("Show git analysis results (AI-assisted contribution detection)")
283
+ .action(async () => {
284
+ try {
285
+ const config = requireConfig();
286
+ const data = await api(config, "GET", "/v1/attestations/git/analysis");
287
+ output(data);
288
+ }
289
+ catch (e) {
290
+ handleError(e);
291
+ }
292
+ });
293
+ // ============================================================
294
+ // attest (agent attestation)
295
+ // ============================================================
296
+ program
297
+ .command("attest")
298
+ .description("Submit an agent attestation for AI proficiency")
299
+ .requiredOption("--agent <name>", "Agent identifier (e.g., openclaw, cursor, claude-code)")
300
+ .option("--version <version>", "Agent version")
301
+ .option("--start <date>", "Observation start date (ISO 8601)")
302
+ .option("--end <date>", "Observation end date (ISO 8601)")
303
+ .option("--hours <n>", "Total observation hours", parseFloat)
304
+ .option("--type <type>", "Attestation type: behavioral|capability|history", "behavioral")
305
+ .requiredOption("--capabilities <json>", 'Capabilities JSON: [{"skill":"multi-agent-orchestration","level":"advanced","confidence":0.85,"evidence":"..."}]')
306
+ .option("--patterns <json>", 'Patterns JSON: {"orchestrationComplexity":4,"toolDiversity":6,"outputVelocity":0.85,"qualitySignals":0.8}')
307
+ .option("--signature <sig>", "Cryptographic signature")
308
+ .action(async (opts) => {
309
+ try {
310
+ const config = requireConfig();
311
+ const observationPeriod = {
312
+ start: opts.start || new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString(),
313
+ end: opts.end || new Date().toISOString(),
314
+ totalHours: opts.hours || 100,
315
+ };
316
+ const capabilities = JSON.parse(opts.capabilities);
317
+ const patterns = opts.patterns ? JSON.parse(opts.patterns) : {
318
+ orchestrationComplexity: 3,
319
+ toolDiversity: 4,
320
+ outputVelocity: 0.7,
321
+ qualitySignals: 0.7,
322
+ };
323
+ const data = await api(config, "POST", "/v1/attestations", {
324
+ agentIdentifier: opts.agent,
325
+ agentVersion: opts.version,
326
+ observationPeriod,
327
+ attestationType: opts.type,
328
+ capabilities,
329
+ patterns,
330
+ signature: opts.signature,
331
+ });
332
+ success("Agent attestation submitted. Proficiency scores updated.");
333
+ output(data);
334
+ }
335
+ catch (e) {
336
+ handleError(e);
337
+ }
338
+ });
339
+ // ============================================================
340
+ // credentials (on-chain proficiency credentials)
341
+ // ============================================================
342
+ const credentials = program.command("credentials").description("Manage on-chain proficiency credentials");
343
+ credentials
344
+ .command("list")
345
+ .description("List your minted credentials")
346
+ .action(async () => {
347
+ try {
348
+ const config = requireConfig();
349
+ const data = await api(config, "GET", "/v1/credentials");
350
+ outputList((data.credentials || []), "Credentials");
351
+ }
352
+ catch (e) {
353
+ handleError(e);
354
+ }
355
+ });
356
+ credentials
357
+ .command("mint")
358
+ .description("Mint your proficiency as an on-chain credential ($25 via x402)")
359
+ .option("--type <type>", "Credential type: proficiency|track|milestone", "proficiency")
360
+ .action(async () => {
361
+ try {
362
+ const config = requireConfig();
363
+ const data = await api(config, "POST", "/v1/credentials/mint", {
364
+ credentialType: "proficiency",
365
+ });
366
+ success("Credential minted! Immutable record created on Base chain.");
367
+ output(data);
368
+ }
369
+ catch (e) {
370
+ handleError(e);
371
+ }
372
+ });
373
+ credentials
374
+ .command("show <id>")
375
+ .description("Show credential details")
376
+ .action(async (id) => {
377
+ try {
378
+ const config = requireConfig();
379
+ const data = await api(config, "GET", `/v1/credentials/${id}`);
380
+ output(data);
381
+ }
382
+ catch (e) {
383
+ handleError(e);
384
+ }
385
+ });
386
+ // ============================================================
387
+ // opportunities (worker: browse; employer: manage)
388
+ // ============================================================
389
+ const opportunities = program.command("opportunities").description("Browse or manage opportunities");
390
+ opportunities
391
+ .command("list")
392
+ .description("List matched opportunities (workers) or your posted opportunities (employers)")
393
+ .action(async () => {
394
+ try {
395
+ const config = requireConfig();
396
+ if (config.userType === "employer") {
397
+ const data = await api(config, "GET", "/v1/opportunities");
398
+ outputList((data.opportunities || []), "Your Opportunities");
399
+ }
400
+ else {
401
+ // Workers see matches
402
+ const data = await api(config, "GET", "/v1/matches");
403
+ const matches = (data.matches || []);
404
+ outputList(matches.map((m) => ({
405
+ matchId: m.matchId,
406
+ score: m.score,
407
+ recommendation: m.recommendation,
408
+ status: m.status,
409
+ title: m.opportunity?.title,
410
+ type: m.opportunity?.opportunityType,
411
+ compensation: formatCompensation(m.opportunity?.compensation),
412
+ remote: m.opportunity?.context?.remote,
413
+ })), "Matched Opportunities");
414
+ }
415
+ }
416
+ catch (e) {
417
+ handleError(e);
418
+ }
419
+ });
420
+ opportunities
421
+ .command("show <id>")
422
+ .description("Show opportunity or match details")
423
+ .action(async (id) => {
424
+ try {
425
+ const config = requireConfig();
426
+ if (config.userType === "employer") {
427
+ const data = await api(config, "GET", `/v1/opportunities/${id}`);
428
+ output(data);
429
+ }
430
+ else {
431
+ // Workers view match details
432
+ const data = await api(config, "GET", `/v1/matches/${id}`);
433
+ output(data);
434
+ }
435
+ }
436
+ catch (e) {
437
+ handleError(e);
438
+ }
439
+ });
440
+ opportunities
441
+ .command("create")
442
+ .description("Create a new opportunity (employers only)")
443
+ .requiredOption("--title <title>", "Opportunity title")
444
+ .requiredOption("--description <desc>", "What this person will do")
445
+ .option("--type <type>", "Type: full-time|fractional|project|advisory|trial", "full-time")
446
+ .option("--track <track>", "Primary track required: orchestrator|systemsBuilder|domainTranslator")
447
+ .option("--min-score <n>", "Minimum proficiency score (0-1000)", parseInt)
448
+ .option("--required-tools <list>", "Comma-separated required tools")
449
+ .option("--preferred-tools <list>", "Comma-separated preferred tools")
450
+ .option("--history-min <months>", "Minimum history depth (e.g., 12-months)")
451
+ .option("--compensation-min <n>", "Min compensation", parseInt)
452
+ .option("--compensation-max <n>", "Max compensation", parseInt)
453
+ .option("--currency <code>", "Currency", "USD")
454
+ .option("--structure <type>", "Compensation structure: salary|hourly|project|equity")
455
+ .option("--team-size <n>", "Team size", parseInt)
456
+ .option("--token-budget <level>", "Token budget: low|medium|high|unlimited")
457
+ .option("--autonomy <level>", "Autonomy level: low|medium|high")
458
+ .option("--remote", "Remote position", true)
459
+ .option("--onsite", "Onsite position")
460
+ .option("--location <loc>", "Location/timezone")
461
+ .action(async (opts) => {
462
+ try {
463
+ const config = requireConfig();
464
+ if (config.userType !== "employer") {
465
+ error("Only employers can create opportunities. Register with --type employer.");
466
+ process.exit(1);
467
+ }
468
+ const requirements = {};
469
+ if (opts.track)
470
+ requirements.primaryTrack = opts.track;
471
+ if (opts.minScore)
472
+ requirements.minimumScore = opts.minScore;
473
+ if (opts.requiredTools || opts.preferredTools) {
474
+ requirements.toolFluency = {
475
+ required: opts.requiredTools ? opts.requiredTools.split(",").map((s) => s.trim()) : [],
476
+ preferred: opts.preferredTools ? opts.preferredTools.split(",").map((s) => s.trim()) : [],
477
+ };
478
+ }
479
+ if (opts.historyMin) {
480
+ requirements.historyDepth = { minimum: opts.historyMin };
481
+ }
482
+ const body = {
483
+ title: opts.title,
484
+ description: opts.description,
485
+ opportunityType: opts.type,
486
+ requirements,
487
+ compensationMin: opts.compensationMin,
488
+ compensationMax: opts.compensationMax,
489
+ compensationCurrency: opts.currency,
490
+ compensationStructure: opts.structure,
491
+ teamSize: opts.teamSize,
492
+ tokenBudget: opts.tokenBudget,
493
+ autonomy: opts.autonomy,
494
+ remote: opts.onsite ? false : true,
495
+ location: opts.location,
496
+ };
497
+ const data = await api(config, "POST", "/v1/opportunities", body);
498
+ success(`Opportunity created: "${opts.title}". Now matching against proficiency profiles.`);
499
+ output(data);
500
+ }
501
+ catch (e) {
502
+ handleError(e);
503
+ }
504
+ });
505
+ opportunities
506
+ .command("update <id>")
507
+ .description("Update an opportunity (employers only)")
508
+ .option("--title <title>", "New title")
509
+ .option("--description <desc>", "New description")
510
+ .option("--status <status>", "Status: active|paused|filled|closed")
511
+ .action(async (id, opts) => {
512
+ try {
513
+ const config = requireConfig();
514
+ const body = {};
515
+ if (opts.title)
516
+ body.title = opts.title;
517
+ if (opts.description)
518
+ body.description = opts.description;
519
+ if (opts.status)
520
+ body.status = opts.status;
521
+ const data = await api(config, "PUT", `/v1/opportunities/${id}`, body);
522
+ success("Opportunity updated.");
523
+ output(data);
524
+ }
525
+ catch (e) {
526
+ handleError(e);
527
+ }
528
+ });
529
+ opportunities
530
+ .command("close <id>")
531
+ .description("Close an opportunity (employers only)")
532
+ .action(async (id) => {
533
+ try {
534
+ const config = requireConfig();
535
+ const data = await api(config, "DELETE", `/v1/opportunities/${id}`);
536
+ success("Opportunity closed.");
537
+ output(data);
538
+ }
539
+ catch (e) {
540
+ handleError(e);
541
+ }
542
+ });
543
+ opportunities
544
+ .command("matches <id>")
545
+ .description("Get matched candidate profiles for an opportunity ($50 via x402, employers only)")
546
+ .action(async (id) => {
547
+ try {
548
+ const config = requireConfig();
549
+ const data = await api(config, "GET", `/v1/opportunities/${id}/matches`);
550
+ outputList((data.matches || []), "Matched Candidates");
551
+ }
552
+ catch (e) {
553
+ handleError(e);
554
+ }
555
+ });
556
+ // ============================================================
557
+ // interest (express/decline interest in matches)
558
+ // ============================================================
559
+ const interest = program.command("interest").description("Express or decline interest in matches");
560
+ interest
561
+ .command("express <matchId>")
562
+ .description("Express interest in a match")
563
+ .action(async (matchId) => {
564
+ try {
565
+ const config = requireConfig();
566
+ const data = await api(config, "POST", `/v1/matches/${matchId}/interest`);
567
+ if (data.mutualInterest) {
568
+ success("MUTUAL INTEREST! Both sides are interested. You can now create an introduction.");
569
+ }
570
+ else {
571
+ success(`Interest expressed. Status: ${data.status}`);
572
+ }
573
+ output(data);
574
+ }
575
+ catch (e) {
576
+ handleError(e);
577
+ }
578
+ });
579
+ interest
580
+ .command("decline <matchId>")
581
+ .description("Decline a match")
582
+ .option("--reason <reason>", "Reason for declining")
583
+ .action(async (matchId, opts) => {
584
+ try {
585
+ const config = requireConfig();
586
+ const body = {};
587
+ if (opts.reason)
588
+ body.reason = opts.reason;
589
+ const data = await api(config, "POST", `/v1/matches/${matchId}/decline`, body);
590
+ success("Match declined.");
591
+ output(data);
592
+ }
593
+ catch (e) {
594
+ handleError(e);
595
+ }
596
+ });
597
+ // ============================================================
598
+ // search (employer search for candidates)
599
+ // ============================================================
600
+ program
601
+ .command("search")
602
+ .description("Search for candidates by proficiency criteria ($50 via x402, employers only)")
603
+ .option("--track <track>", "Primary track: orchestrator|systemsBuilder|domainTranslator")
604
+ .option("--min-score <n>", "Minimum composite score", parseInt)
605
+ .option("--tools <list>", "Comma-separated tools required")
606
+ .option("--history-min <months>", "Minimum history depth in months", parseInt)
607
+ .action(async (opts) => {
608
+ try {
609
+ const config = requireConfig();
610
+ // Note: This uses the opportunities matches endpoint
611
+ // A dedicated search endpoint would be POST /v1/employer/search
612
+ // For now, guide users to create an opportunity and view matches
613
+ error("Direct search not yet implemented. Create an opportunity to find matching candidates:");
614
+ console.log(" jobarbiter opportunities create --title 'Search' --description 'Looking for...' --track orchestrator --min-score 600");
615
+ console.log(" jobarbiter opportunities matches <id>");
616
+ }
617
+ catch (e) {
618
+ handleError(e);
619
+ }
620
+ });
621
+ // ============================================================
622
+ // unlock (employer unlocks full profile)
623
+ // ============================================================
624
+ program
625
+ .command("unlock <matchId>")
626
+ .description("Unlock a candidate's full profile ($250 via x402, employers only)")
627
+ .action(async (matchId) => {
628
+ try {
629
+ const config = requireConfig();
630
+ const data = await api(config, "GET", `/v1/matches/${matchId}/full-profile`);
631
+ success("Full profile unlocked.");
632
+ output(data);
633
+ }
634
+ catch (e) {
635
+ handleError(e);
636
+ }
637
+ });
638
+ // ============================================================
639
+ // introduce (create introduction on mutual interest)
640
+ // ============================================================
641
+ program
642
+ .command("introduce <matchId>")
643
+ .description("Create an introduction on mutual interest ($2,500 via x402)")
644
+ .action(async (matchId) => {
645
+ try {
646
+ const config = requireConfig();
647
+ const data = await api(config, "POST", "/v1/introductions", {
648
+ matchId,
649
+ });
650
+ success("Introduction created! Contact information exchanged.");
651
+ output(data);
652
+ }
653
+ catch (e) {
654
+ handleError(e);
655
+ }
656
+ });
657
+ // ============================================================
658
+ // introductions (list/view introductions)
659
+ // ============================================================
660
+ const intro = program.command("intro").description("Manage introductions");
661
+ intro
662
+ .command("list")
663
+ .description("List your introductions")
664
+ .action(async () => {
665
+ try {
666
+ const config = requireConfig();
667
+ const data = await api(config, "GET", "/v1/introductions");
668
+ outputList((data.introductions || []), "Introductions");
669
+ }
670
+ catch (e) {
671
+ handleError(e);
672
+ }
673
+ });
674
+ intro
675
+ .command("show <id>")
676
+ .description("Show introduction details")
677
+ .action(async (id) => {
678
+ try {
679
+ const config = requireConfig();
680
+ const data = await api(config, "GET", `/v1/introductions/${id}`);
681
+ output(data);
682
+ }
683
+ catch (e) {
684
+ handleError(e);
685
+ }
686
+ });
687
+ // ============================================================
688
+ // webhook
689
+ // ============================================================
690
+ program
691
+ .command("webhook <url>")
692
+ .description("Set webhook URL for real-time notifications")
693
+ .action(async (url) => {
694
+ try {
695
+ const config = requireConfig();
696
+ const data = await api(config, "PATCH", "/v1/auth/webhook", {
697
+ webhookUrl: url,
698
+ });
699
+ success(`Webhook set: ${url}`);
700
+ output(data);
701
+ }
702
+ catch (e) {
703
+ handleError(e);
704
+ }
705
+ });
706
+ // ============================================================
707
+ // verify
708
+ // ============================================================
709
+ const verify = program.command("verify").description("Identity verification");
710
+ verify
711
+ .command("linkedin <url>")
712
+ .description("Submit LinkedIn profile for verification")
713
+ .action(async (url) => {
714
+ try {
715
+ const config = requireConfig();
716
+ const data = await api(config, "POST", "/v1/verification/linkedin", {
717
+ linkedinUrl: url,
718
+ });
719
+ success("LinkedIn verification queued.");
720
+ output(data);
721
+ }
722
+ catch (e) {
723
+ handleError(e);
724
+ }
725
+ });
726
+ verify
727
+ .command("github <username>")
728
+ .description("Submit GitHub username for verification")
729
+ .action(async (username) => {
730
+ try {
731
+ const config = requireConfig();
732
+ const data = await api(config, "POST", "/v1/verification/github", {
733
+ githubUsername: username,
734
+ });
735
+ success("GitHub verification queued.");
736
+ output(data);
737
+ }
738
+ catch (e) {
739
+ handleError(e);
740
+ }
741
+ });
742
+ // ============================================================
743
+ // tokens (sync token usage)
744
+ // ============================================================
745
+ program
746
+ .command("tokens")
747
+ .description("Sync token usage data from AI providers")
748
+ .requiredOption("--provider <provider>", "Provider: anthropic|openai|google")
749
+ .requiredOption("--start <date>", "Period start (YYYY-MM-DD)")
750
+ .requiredOption("--end <date>", "Period end (YYYY-MM-DD)")
751
+ .option("--input-tokens <n>", "Input tokens", parseInt)
752
+ .option("--output-tokens <n>", "Output tokens", parseInt)
753
+ .option("--total-tokens <n>", "Total tokens", parseInt)
754
+ .option("--cost <n>", "Estimated cost USD", parseFloat)
755
+ .option("--models <json>", "Model usage breakdown JSON")
756
+ .action(async (opts) => {
757
+ try {
758
+ const config = requireConfig();
759
+ const data = await api(config, "POST", "/v1/attestations/tokens/sync", {
760
+ provider: opts.provider,
761
+ periodStart: opts.start,
762
+ periodEnd: opts.end,
763
+ inputTokens: opts.inputTokens,
764
+ outputTokens: opts.outputTokens,
765
+ totalTokens: opts.totalTokens,
766
+ estimatedCostUsd: opts.cost,
767
+ modelUsage: opts.models ? JSON.parse(opts.models) : undefined,
768
+ });
769
+ success("Token usage synced. Proficiency scores updated.");
770
+ output(data);
771
+ }
772
+ catch (e) {
773
+ handleError(e);
774
+ }
775
+ });
776
+ // ============================================================
777
+ // Helpers
778
+ // ============================================================
779
+ function handleError(e) {
780
+ if (e instanceof ApiError) {
781
+ if (e.status === 402) {
782
+ error(`Payment required: ${e.body.error || "x402 USDC payment needed"}. Configure an x402-compatible wallet.`);
783
+ }
784
+ else if (e.status === 404) {
785
+ error(`Not found: ${e.body.error || "Resource does not exist"}`);
786
+ }
787
+ else if (e.status === 403) {
788
+ error(`Access denied: ${e.body.error || "You don't have permission for this action"}`);
789
+ }
790
+ else {
791
+ error(`${e.status}: ${e.message}`);
792
+ }
793
+ }
794
+ else if (e instanceof Error) {
795
+ error(e.message);
796
+ }
797
+ else {
798
+ error(String(e));
799
+ }
800
+ process.exit(1);
801
+ }
802
+ function formatCompensation(comp) {
803
+ if (!comp)
804
+ return "Not listed";
805
+ const min = comp.min;
806
+ const max = comp.max;
807
+ const cur = comp.currency || "USD";
808
+ if (min && max)
809
+ return `${cur} ${min.toLocaleString()}-${max.toLocaleString()}`;
810
+ if (min)
811
+ return `${cur} ${min.toLocaleString()}+`;
812
+ if (max)
813
+ return `${cur} up to ${max.toLocaleString()}`;
814
+ return "Not listed";
815
+ }
816
+ program.parse();