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