jobarbiter 0.3.2 → 0.3.4
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 +7 -7
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/detect-tools.d.ts +1 -1
- package/dist/lib/detect-tools.js +12 -12
- package/dist/lib/observe.d.ts +1 -1
- package/dist/lib/observe.js +4 -4
- package/dist/lib/onboard.js +211 -76
- package/dist/lib/providers.d.ts +48 -0
- package/dist/lib/providers.js +157 -0
- package/package.json +1 -1
- package/src/index.ts +7 -7
- package/src/lib/config.ts +2 -0
- package/src/lib/detect-tools.ts +13 -13
- package/src/lib/observe.ts +4 -4
- package/src/lib/onboard.ts +228 -75
- package/src/lib/providers.ts +205 -0
package/src/lib/onboard.ts
CHANGED
|
@@ -20,6 +20,12 @@ import {
|
|
|
20
20
|
type DetectedTool,
|
|
21
21
|
type ToolCategory,
|
|
22
22
|
} from "./detect-tools.js";
|
|
23
|
+
import {
|
|
24
|
+
loadProviderKeys,
|
|
25
|
+
saveProviderKey,
|
|
26
|
+
validateProviderKey,
|
|
27
|
+
getSupportedProviders,
|
|
28
|
+
} from "./providers.js";
|
|
23
29
|
|
|
24
30
|
// ── ANSI Colors ────────────────────────────────────────────────────────
|
|
25
31
|
|
|
@@ -139,13 +145,44 @@ interface OnboardState {
|
|
|
139
145
|
export async function runOnboardWizard(opts: { force?: boolean; baseUrl?: string }): Promise<void> {
|
|
140
146
|
const baseUrl = opts.baseUrl || "https://jobarbiter-api-production.up.railway.app";
|
|
141
147
|
|
|
142
|
-
// Check for existing config
|
|
148
|
+
// Check for existing config — resume if onboarding incomplete
|
|
143
149
|
const existingConfig = loadConfig();
|
|
144
150
|
if (existingConfig && !opts.force) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
if (existingConfig.onboardingComplete) {
|
|
152
|
+
console.log(`\n${sym.check} ${c.success("You're already onboarded!")}`);
|
|
153
|
+
console.log(`\n Run ${c.highlight("jobarbiter status")} to check your account.`);
|
|
154
|
+
console.log(` Run ${c.highlight("jobarbiter onboard --force")} to start fresh.\n`);
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
// Onboarding incomplete — resume
|
|
158
|
+
const resumeStep = (existingConfig.onboardingStep ?? 1) + 1;
|
|
159
|
+
console.log(`\n${sym.rocket} ${c.bold("Resuming onboarding")} from step ${resumeStep}/7\n`);
|
|
160
|
+
console.log(c.dim(` Account: ${existingConfig.userType} | API key configured`));
|
|
161
|
+
console.log(c.dim(` Run ${c.highlight("jobarbiter onboard --force")} to start over.\n`));
|
|
162
|
+
|
|
163
|
+
const prompt = new Prompt();
|
|
164
|
+
const state: Partial<OnboardState> = {
|
|
165
|
+
baseUrl,
|
|
166
|
+
apiKey: existingConfig.apiKey,
|
|
167
|
+
userType: existingConfig.userType as "worker" | "employer",
|
|
168
|
+
userId: "",
|
|
169
|
+
email: "",
|
|
170
|
+
};
|
|
171
|
+
try {
|
|
172
|
+
if (existingConfig.userType === "worker" || existingConfig.userType === "seeker") {
|
|
173
|
+
await runWorkerFlow(prompt, state as OnboardState, resumeStep);
|
|
174
|
+
} else {
|
|
175
|
+
await runEmployerFlow(prompt, state as OnboardState);
|
|
176
|
+
}
|
|
177
|
+
prompt.close();
|
|
178
|
+
} catch (err) {
|
|
179
|
+
prompt.close();
|
|
180
|
+
if (err instanceof Error) {
|
|
181
|
+
console.log(`\n${sym.cross} ${c.error(err.message)}`);
|
|
182
|
+
}
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
149
186
|
}
|
|
150
187
|
|
|
151
188
|
const prompt = new Prompt();
|
|
@@ -163,11 +200,13 @@ export async function runOnboardWizard(opts: { force?: boolean; baseUrl?: string
|
|
|
163
200
|
state.apiKey = apiKey;
|
|
164
201
|
state.userId = userId;
|
|
165
202
|
|
|
166
|
-
// Save config immediately after verification
|
|
203
|
+
// Save config immediately after verification (with step progress)
|
|
167
204
|
saveConfig({
|
|
168
205
|
apiKey,
|
|
169
206
|
baseUrl,
|
|
170
207
|
userType,
|
|
208
|
+
onboardingStep: 1,
|
|
209
|
+
onboardingComplete: false,
|
|
171
210
|
});
|
|
172
211
|
|
|
173
212
|
if (userType === "worker") {
|
|
@@ -224,9 +263,9 @@ async function handleEmailVerification(
|
|
|
224
263
|
baseUrl: string,
|
|
225
264
|
userType: "worker" | "employer"
|
|
226
265
|
): Promise<{ email: string; apiKey: string; userId: string }> {
|
|
227
|
-
// Workers: 1) Account, 2) Tool Detection, 3)
|
|
228
|
-
// Employers: 1) Account, 2) (skip verification), 3) Company, 4) Domain, 5) What You Need, 6) Done
|
|
229
|
-
const totalSteps = 6;
|
|
266
|
+
// Workers: 1) Account, 2) Tool Detection, 3) AI Accounts, 4) Domains, 5) GitHub, 6) LinkedIn, 7) Done
|
|
267
|
+
// Employers: 1) Account, 2) (skip verification), 3) Company, 4) Domain, 5) What You Need, 6) Done (stays at 6)
|
|
268
|
+
const totalSteps = userType === "employer" ? 6 : 7;
|
|
230
269
|
|
|
231
270
|
console.log(`\n${sym.email} ${c.bold(`Step 1/${totalSteps} — Create Your Account`)}\n`);
|
|
232
271
|
|
|
@@ -306,85 +345,108 @@ async function handleEmailVerification(
|
|
|
306
345
|
|
|
307
346
|
// ── Worker Flow ────────────────────────────────────────────────────────
|
|
308
347
|
|
|
309
|
-
async function runWorkerFlow(prompt: Prompt, state: OnboardState): Promise<void> {
|
|
348
|
+
async function runWorkerFlow(prompt: Prompt, state: OnboardState, startStep = 2): Promise<void> {
|
|
310
349
|
const config: Config = {
|
|
311
350
|
apiKey: state.apiKey,
|
|
312
351
|
baseUrl: state.baseUrl,
|
|
313
352
|
userType: "worker",
|
|
314
353
|
};
|
|
315
354
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// Step 3: Domains
|
|
321
|
-
console.log(`${sym.target} ${c.bold("Step 3/6 — Your Domains")}\n`);
|
|
322
|
-
console.log(`What domains do you work in? ${c.dim("(comma-separated)")}`);
|
|
323
|
-
console.log(c.dim("Examples: full-stack dev, data engineering, trading, content creation\n"));
|
|
324
|
-
const domainsInput = await prompt.question(`${sym.arrow} `);
|
|
325
|
-
const domains = domainsInput.split(",").map(s => s.trim()).filter(Boolean);
|
|
326
|
-
state.domains = domains;
|
|
355
|
+
const saveProgress = (step: number) => {
|
|
356
|
+
saveConfig({ ...config, onboardingStep: step });
|
|
357
|
+
};
|
|
327
358
|
|
|
328
|
-
//
|
|
329
|
-
|
|
359
|
+
// Step 2: Auto-detect AI Tools
|
|
360
|
+
if (startStep <= 2) {
|
|
361
|
+
const detectedToolsResult = await runToolDetectionStep(prompt, config);
|
|
362
|
+
state.tools = detectedToolsResult.tools;
|
|
363
|
+
saveProgress(2);
|
|
364
|
+
}
|
|
330
365
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
primary: state.tools,
|
|
336
|
-
},
|
|
337
|
-
});
|
|
338
|
-
console.log(`${sym.check} Profile saved\n`);
|
|
339
|
-
} catch (err) {
|
|
340
|
-
console.log(`${sym.warning} ${c.warning("Could not save profile details — you can update later with 'jobarbiter profile create'")}\n`);
|
|
366
|
+
// Step 3: Connect AI Accounts (optional)
|
|
367
|
+
if (startStep <= 3) {
|
|
368
|
+
await runConnectAIAccountsStep(prompt);
|
|
369
|
+
saveProgress(3);
|
|
341
370
|
}
|
|
342
371
|
|
|
343
|
-
// Step 4:
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
372
|
+
// Step 4: Domains
|
|
373
|
+
if (startStep <= 4) {
|
|
374
|
+
console.log(`${sym.target} ${c.bold("Step 4/7 — Your Domains")}\n`);
|
|
375
|
+
console.log(`What domains do you work in? ${c.dim("(comma-separated)")}`);
|
|
376
|
+
console.log(c.dim("Examples: full-stack dev, data engineering, trading, content creation\n"));
|
|
377
|
+
const domainsInput = await prompt.question(`${sym.arrow} `);
|
|
378
|
+
const domains = domainsInput.split(",").map(s => s.trim()).filter(Boolean);
|
|
379
|
+
state.domains = domains;
|
|
380
|
+
|
|
381
|
+
// Create/update profile
|
|
382
|
+
console.log(c.dim("\nSaving profile..."));
|
|
347
383
|
|
|
348
|
-
const githubUsername = await prompt.question(`GitHub username ${c.dim("(press Enter to skip)")}: `);
|
|
349
|
-
|
|
350
|
-
if (githubUsername) {
|
|
351
|
-
console.log(c.dim("\nConnecting GitHub..."));
|
|
352
384
|
try {
|
|
353
|
-
await api(config, "POST", "/v1/
|
|
354
|
-
|
|
355
|
-
|
|
385
|
+
await api(config, "POST", "/v1/profile", {
|
|
386
|
+
domains,
|
|
387
|
+
tools: {
|
|
388
|
+
primary: state.tools,
|
|
389
|
+
},
|
|
356
390
|
});
|
|
357
|
-
console.log(`${sym.check}
|
|
358
|
-
state.githubUsername = githubUsername;
|
|
391
|
+
console.log(`${sym.check} Profile saved\n`);
|
|
359
392
|
} catch (err) {
|
|
360
|
-
console.log(`${sym.warning} ${c.warning("Could not
|
|
393
|
+
console.log(`${sym.warning} ${c.warning("Could not save profile details — you can update later with 'jobarbiter profile create'")}\n`);
|
|
361
394
|
}
|
|
362
|
-
|
|
363
|
-
console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter git connect'")}\n`);
|
|
395
|
+
saveProgress(4);
|
|
364
396
|
}
|
|
365
397
|
|
|
366
|
-
// Step 5: Connect
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
398
|
+
// Step 5: Connect GitHub (optional)
|
|
399
|
+
if (startStep <= 5) {
|
|
400
|
+
console.log(`${sym.link} ${c.bold("Step 5/7 — Connect GitHub")} ${c.dim("(optional)")}\n`);
|
|
401
|
+
console.log(`Connecting your GitHub lets us analyze your AI-assisted work patterns.`);
|
|
402
|
+
console.log(`This significantly boosts your proficiency score.\n`);
|
|
370
403
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
404
|
+
const githubUsername = await prompt.question(`GitHub username ${c.dim("(press Enter to skip)")}: `);
|
|
405
|
+
|
|
406
|
+
if (githubUsername) {
|
|
407
|
+
console.log(c.dim("\nConnecting GitHub..."));
|
|
408
|
+
try {
|
|
409
|
+
await api(config, "POST", "/v1/attestations/git/connect", {
|
|
410
|
+
provider: "github",
|
|
411
|
+
username: githubUsername,
|
|
412
|
+
});
|
|
413
|
+
console.log(`${sym.check} GitHub connected: ${c.highlight(githubUsername)}\n`);
|
|
414
|
+
state.githubUsername = githubUsername;
|
|
415
|
+
} catch (err) {
|
|
416
|
+
console.log(`${sym.warning} ${c.warning("Could not connect GitHub — you can try later with 'jobarbiter git connect'")}\n`);
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter git connect'")}\n`);
|
|
382
420
|
}
|
|
383
|
-
|
|
384
|
-
console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter identity linkedin <url>'")}\n`);
|
|
421
|
+
saveProgress(5);
|
|
385
422
|
}
|
|
386
423
|
|
|
387
|
-
// Step 6:
|
|
424
|
+
// Step 6: Connect LinkedIn (optional)
|
|
425
|
+
if (startStep <= 6) {
|
|
426
|
+
console.log(`${sym.link} ${c.bold("Step 6/7 — Connect LinkedIn")} ${c.dim("(optional)")}\n`);
|
|
427
|
+
console.log(`Your LinkedIn profile strengthens identity verification.`);
|
|
428
|
+
console.log(c.dim("We never post on your behalf or access your connections.\n"));
|
|
429
|
+
|
|
430
|
+
const linkedinUrl = await prompt.question(`LinkedIn URL ${c.dim("(press Enter to skip)")}: `);
|
|
431
|
+
|
|
432
|
+
if (linkedinUrl) {
|
|
433
|
+
console.log(c.dim("\nSubmitting for verification..."));
|
|
434
|
+
try {
|
|
435
|
+
await api(config, "POST", "/v1/verification/linkedin", {
|
|
436
|
+
linkedinUrl: linkedinUrl.trim(),
|
|
437
|
+
});
|
|
438
|
+
console.log(`${sym.check} LinkedIn submitted for verification\n`);
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.log(`${sym.warning} ${c.warning("Could not submit LinkedIn — you can try later with 'jobarbiter identity linkedin <url>'")}\n`);
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter identity linkedin <url>'")}\n`);
|
|
444
|
+
}
|
|
445
|
+
saveProgress(6);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Step 7: Done!
|
|
449
|
+
saveConfig({ ...config, onboardingComplete: true, onboardingStep: 7 });
|
|
388
450
|
showWorkerCompletion(state);
|
|
389
451
|
}
|
|
390
452
|
|
|
@@ -394,15 +456,15 @@ async function runToolDetectionStep(
|
|
|
394
456
|
prompt: Prompt,
|
|
395
457
|
config: Config,
|
|
396
458
|
): Promise<{ tools: string[] }> {
|
|
397
|
-
console.log(`🔍 ${c.bold("Step 2/
|
|
459
|
+
console.log(`🔍 ${c.bold("Step 2/7 — Detecting AI Tools")}\n`);
|
|
398
460
|
console.log(c.dim(" Scanning your machine...\n"));
|
|
399
461
|
|
|
400
462
|
const allTools = detectAllTools();
|
|
401
463
|
const installed = allTools.filter((t) => t.installed);
|
|
402
|
-
const notInstalled = allTools.filter((t) => !t.installed && t.category === "
|
|
464
|
+
const notInstalled = allTools.filter((t) => !t.installed && t.category === "ai-agent");
|
|
403
465
|
|
|
404
466
|
// Group by category
|
|
405
|
-
const codingAgents = installed.filter((t) => t.category === "
|
|
467
|
+
const codingAgents = installed.filter((t) => t.category === "ai-agent");
|
|
406
468
|
const chatTools = installed.filter((t) => t.category === "chat");
|
|
407
469
|
const orchestration = installed.filter((t) => t.category === "orchestration");
|
|
408
470
|
const apiProviders = installed.filter((t) => t.category === "api-provider");
|
|
@@ -416,7 +478,7 @@ async function runToolDetectionStep(
|
|
|
416
478
|
|
|
417
479
|
console.log(` ${c.bold("Found:")}`);
|
|
418
480
|
|
|
419
|
-
// Show
|
|
481
|
+
// Show AI agents with observer status
|
|
420
482
|
for (const tool of codingAgents) {
|
|
421
483
|
const display = formatToolDisplay(tool);
|
|
422
484
|
if (tool.observerAvailable) {
|
|
@@ -441,7 +503,7 @@ async function runToolDetectionStep(
|
|
|
441
503
|
console.log(` ${sym.check} ${tool.name} ${c.dim("configured")}`);
|
|
442
504
|
}
|
|
443
505
|
|
|
444
|
-
// Show not-detected
|
|
506
|
+
// Show not-detected AI agents
|
|
445
507
|
if (notInstalled.length > 0) {
|
|
446
508
|
console.log(`\n ${c.dim("Not detected (install to track):")}`);
|
|
447
509
|
for (const tool of notInstalled.slice(0, 5)) {
|
|
@@ -455,12 +517,12 @@ async function runToolDetectionStep(
|
|
|
455
517
|
// Collect tool names for profile
|
|
456
518
|
const toolNames = installed.map((t) => t.name);
|
|
457
519
|
|
|
458
|
-
// Observer installation for
|
|
520
|
+
// Observer installation for AI agents
|
|
459
521
|
const needsObserver = codingAgents.filter((t) => t.observerAvailable && !t.observerActive);
|
|
460
522
|
|
|
461
523
|
if (needsObserver.length > 0) {
|
|
462
524
|
console.log(`\n ${c.bold("Observers")}`);
|
|
463
|
-
console.log(` JobArbiter observes your
|
|
525
|
+
console.log(` JobArbiter observes your AI sessions to build your`);
|
|
464
526
|
console.log(` proficiency profile. ${c.bold("No code or prompts leave your machine")} —`);
|
|
465
527
|
console.log(` only aggregate scores (tool usage, session counts, token volume).\n`);
|
|
466
528
|
console.log(c.dim(` Data stored locally: ~/.config/jobarbiter/observer/observations.json`));
|
|
@@ -517,8 +579,99 @@ async function runToolDetectionStep(
|
|
|
517
579
|
return { tools: toolNames };
|
|
518
580
|
}
|
|
519
581
|
|
|
582
|
+
// ── Connect AI Accounts Step ───────────────────────────────────────────
|
|
583
|
+
|
|
584
|
+
async function runConnectAIAccountsStep(prompt: Prompt): Promise<void> {
|
|
585
|
+
console.log(`${sym.link} ${c.bold("Step 3/7 — Connect AI Accounts")} ${c.dim("(optional)")}\n`);
|
|
586
|
+
|
|
587
|
+
console.log(` Connecting your AI provider accounts lets us analyze usage depth,`);
|
|
588
|
+
console.log(` not just tool presence. The more we can see, the stronger your`);
|
|
589
|
+
console.log(` verified proficiency profile.\n`);
|
|
590
|
+
|
|
591
|
+
console.log(` ${c.bold("What we can pull:")}`);
|
|
592
|
+
console.log(` ${sym.bullet} Token usage volume and patterns ${c.dim("(how much you use AI)")}`);
|
|
593
|
+
console.log(` ${sym.bullet} Model preferences ${c.dim("(which models you reach for)")}`);
|
|
594
|
+
console.log(` ${sym.bullet} Session frequency and consistency over time`);
|
|
595
|
+
console.log(` ${sym.bullet} API spend patterns ${c.dim("(demonstrates serious usage)")}\n`);
|
|
596
|
+
|
|
597
|
+
console.log(` ${c.bold("What we NEVER access:")}`);
|
|
598
|
+
console.log(` ${sym.bullet} Your conversation content`);
|
|
599
|
+
console.log(` ${sym.bullet} Prompts or responses`);
|
|
600
|
+
console.log(` ${sym.bullet} Any proprietary or sensitive data\n`);
|
|
601
|
+
|
|
602
|
+
// Check for already connected providers
|
|
603
|
+
const existingProviders = loadProviderKeys();
|
|
604
|
+
if (existingProviders.length > 0) {
|
|
605
|
+
console.log(` ${c.bold("Already connected:")}`);
|
|
606
|
+
for (const p of existingProviders) {
|
|
607
|
+
console.log(` ${sym.check} ${p.provider}`);
|
|
608
|
+
}
|
|
609
|
+
console.log();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Show available connections
|
|
613
|
+
let continueConnecting = true;
|
|
614
|
+
|
|
615
|
+
while (continueConnecting) {
|
|
616
|
+
console.log(` ${c.bold("Available connections:")}\n`);
|
|
617
|
+
console.log(` ${c.highlight("1.")} Anthropic API key — Pull Claude usage stats`);
|
|
618
|
+
console.log(` ${c.highlight("2.")} OpenAI API key — Pull GPT/ChatGPT usage stats`);
|
|
619
|
+
console.log(` ${c.highlight("3.")} Skip for now\n`);
|
|
620
|
+
console.log(c.dim(` You can connect accounts later with 'jobarbiter tokens connect'\n`));
|
|
621
|
+
|
|
622
|
+
const choice = await prompt.question(` Your choice ${c.dim("[1/2/3]")}: `);
|
|
623
|
+
|
|
624
|
+
if (choice === "3" || choice.toLowerCase() === "skip" || choice === "") {
|
|
625
|
+
console.log(`\n${c.dim(" Skipped — you can connect providers later with 'jobarbiter tokens connect'")}\n`);
|
|
626
|
+
continueConnecting = false;
|
|
627
|
+
} else if (choice === "1") {
|
|
628
|
+
await connectProvider(prompt, "anthropic", "Anthropic");
|
|
629
|
+
// Ask if they want to connect another
|
|
630
|
+
continueConnecting = await prompt.confirm(`\n Connect another provider?`, false);
|
|
631
|
+
console.log();
|
|
632
|
+
} else if (choice === "2") {
|
|
633
|
+
await connectProvider(prompt, "openai", "OpenAI");
|
|
634
|
+
// Ask if they want to connect another
|
|
635
|
+
continueConnecting = await prompt.confirm(`\n Connect another provider?`, false);
|
|
636
|
+
console.log();
|
|
637
|
+
} else {
|
|
638
|
+
console.log(c.error(" Please enter 1, 2, or 3"));
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function connectProvider(prompt: Prompt, providerId: string, providerName: string): Promise<void> {
|
|
644
|
+
console.log(`\n ${sym.lock} ${c.bold("Privacy Notice")}`);
|
|
645
|
+
console.log(` ${c.dim("─".repeat(50))}`);
|
|
646
|
+
console.log(` Your API key is stored ${c.bold("locally only")} at:`);
|
|
647
|
+
console.log(` ${c.dim("~/.config/jobarbiter/providers.json")}\n`);
|
|
648
|
+
console.log(` ${sym.bullet} Keys are ${c.bold("NEVER")} sent to JobArbiter's servers`);
|
|
649
|
+
console.log(` ${sym.bullet} Only aggregate stats (token counts, model usage) are submitted`);
|
|
650
|
+
console.log(` ${sym.bullet} Revoke anytime: ${c.highlight(`jobarbiter tokens disconnect ${providerId}`)}`);
|
|
651
|
+
console.log(` ${c.dim("─".repeat(50))}\n`);
|
|
652
|
+
|
|
653
|
+
const apiKey = await prompt.question(` ${providerName} API key: `);
|
|
654
|
+
|
|
655
|
+
if (!apiKey || apiKey.trim() === "") {
|
|
656
|
+
console.log(` ${c.dim("Skipped")}`);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
console.log(c.dim("\n Validating API key..."));
|
|
661
|
+
|
|
662
|
+
const result = await validateProviderKey(providerId, apiKey.trim());
|
|
663
|
+
|
|
664
|
+
if (result.valid) {
|
|
665
|
+
saveProviderKey(providerId, apiKey.trim());
|
|
666
|
+
console.log(` ${sym.check} ${c.success(`${providerName} connected`)} — ${result.summary}`);
|
|
667
|
+
} else {
|
|
668
|
+
console.log(` ${sym.cross} ${c.error(`Invalid key: ${result.error}`)}`);
|
|
669
|
+
console.log(` ${c.dim("You can try again later with 'jobarbiter tokens connect'")}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
520
673
|
function showWorkerCompletion(state: OnboardState): void {
|
|
521
|
-
console.log(`${sym.done} ${c.bold("Step
|
|
674
|
+
console.log(`${sym.done} ${c.bold("Step 7/7 — You're In!")}\n`);
|
|
522
675
|
console.log(`Your profile is live. Here's what happens next:\n`);
|
|
523
676
|
|
|
524
677
|
console.log(` 📊 Your proficiency score builds automatically from:`);
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Key Management Module
|
|
3
|
+
*
|
|
4
|
+
* Securely manages API keys for AI providers (Anthropic, OpenAI, etc.)
|
|
5
|
+
* Keys are stored locally ONLY and never sent to JobArbiter's servers.
|
|
6
|
+
* Only aggregate usage stats are submitted for proficiency scoring.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
|
|
13
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface ProviderConfig {
|
|
16
|
+
provider: string; // "anthropic" | "openai" | "google"
|
|
17
|
+
apiKey: string; // stored locally only
|
|
18
|
+
connectedAt: string; // ISO timestamp
|
|
19
|
+
lastSync?: string; // last time usage data was pulled
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProvidersFile {
|
|
23
|
+
version: number;
|
|
24
|
+
providers: ProviderConfig[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ValidationResult {
|
|
28
|
+
valid: boolean;
|
|
29
|
+
error?: string;
|
|
30
|
+
summary?: string; // e.g., "1.2M tokens used this month"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Paths ──────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
const CONFIG_DIR = join(homedir(), ".config", "jobarbiter");
|
|
36
|
+
const PROVIDERS_FILE = join(CONFIG_DIR, "providers.json");
|
|
37
|
+
|
|
38
|
+
export function getProvidersPath(): string {
|
|
39
|
+
return PROVIDERS_FILE;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Load / Save ────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export function loadProviderKeys(): ProviderConfig[] {
|
|
45
|
+
if (!existsSync(PROVIDERS_FILE)) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const raw = readFileSync(PROVIDERS_FILE, "utf-8");
|
|
51
|
+
const data = JSON.parse(raw) as ProvidersFile;
|
|
52
|
+
return data.providers || [];
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function saveProviderKey(provider: string, apiKey: string): void {
|
|
59
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
60
|
+
|
|
61
|
+
const existing = loadProviderKeys();
|
|
62
|
+
|
|
63
|
+
// Remove any existing entry for this provider
|
|
64
|
+
const filtered = existing.filter((p) => p.provider !== provider);
|
|
65
|
+
|
|
66
|
+
// Add new entry
|
|
67
|
+
filtered.push({
|
|
68
|
+
provider,
|
|
69
|
+
apiKey,
|
|
70
|
+
connectedAt: new Date().toISOString(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const data: ProvidersFile = {
|
|
74
|
+
version: 1,
|
|
75
|
+
providers: filtered,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
writeFileSync(PROVIDERS_FILE, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function removeProviderKey(provider: string): boolean {
|
|
82
|
+
const existing = loadProviderKeys();
|
|
83
|
+
const filtered = existing.filter((p) => p.provider !== provider);
|
|
84
|
+
|
|
85
|
+
if (filtered.length === existing.length) {
|
|
86
|
+
return false; // Provider not found
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const data: ProvidersFile = {
|
|
90
|
+
version: 1,
|
|
91
|
+
providers: filtered,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
writeFileSync(PROVIDERS_FILE, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getProviderKey(provider: string): ProviderConfig | null {
|
|
99
|
+
const providers = loadProviderKeys();
|
|
100
|
+
return providers.find((p) => p.provider === provider) || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Validation ─────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate an Anthropic API key by making a test API call.
|
|
107
|
+
* Returns validation result with usage summary if available.
|
|
108
|
+
*/
|
|
109
|
+
export async function validateAnthropicKey(apiKey: string): Promise<ValidationResult> {
|
|
110
|
+
try {
|
|
111
|
+
const response = await fetch("https://api.anthropic.com/v1/models", {
|
|
112
|
+
method: "GET",
|
|
113
|
+
headers: {
|
|
114
|
+
"x-api-key": apiKey,
|
|
115
|
+
"anthropic-version": "2023-06-01",
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (response.status === 401) {
|
|
120
|
+
return { valid: false, error: "Invalid API key" };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (response.status === 403) {
|
|
124
|
+
return { valid: false, error: "API key doesn't have required permissions" };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
return { valid: false, error: `API error: ${response.status}` };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Key is valid - we can't get usage from this endpoint,
|
|
132
|
+
// but we can confirm the key works
|
|
133
|
+
return {
|
|
134
|
+
valid: true,
|
|
135
|
+
summary: "API key validated",
|
|
136
|
+
};
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (err instanceof Error) {
|
|
139
|
+
return { valid: false, error: `Connection error: ${err.message}` };
|
|
140
|
+
}
|
|
141
|
+
return { valid: false, error: "Unknown error" };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Validate an OpenAI API key by making a test API call.
|
|
147
|
+
* Returns validation result with usage summary if available.
|
|
148
|
+
*/
|
|
149
|
+
export async function validateOpenAIKey(apiKey: string): Promise<ValidationResult> {
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetch("https://api.openai.com/v1/models", {
|
|
152
|
+
method: "GET",
|
|
153
|
+
headers: {
|
|
154
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (response.status === 401) {
|
|
159
|
+
return { valid: false, error: "Invalid API key" };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (response.status === 403) {
|
|
163
|
+
return { valid: false, error: "API key doesn't have required permissions" };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
return { valid: false, error: `API error: ${response.status}` };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Key is valid
|
|
171
|
+
return {
|
|
172
|
+
valid: true,
|
|
173
|
+
summary: "API key validated",
|
|
174
|
+
};
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (err instanceof Error) {
|
|
177
|
+
return { valid: false, error: `Connection error: ${err.message}` };
|
|
178
|
+
}
|
|
179
|
+
return { valid: false, error: "Unknown error" };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get list of supported providers.
|
|
185
|
+
*/
|
|
186
|
+
export function getSupportedProviders(): Array<{ id: string; name: string }> {
|
|
187
|
+
return [
|
|
188
|
+
{ id: "anthropic", name: "Anthropic" },
|
|
189
|
+
{ id: "openai", name: "OpenAI" },
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Validate a key for any supported provider.
|
|
195
|
+
*/
|
|
196
|
+
export async function validateProviderKey(provider: string, apiKey: string): Promise<ValidationResult> {
|
|
197
|
+
switch (provider) {
|
|
198
|
+
case "anthropic":
|
|
199
|
+
return validateAnthropicKey(apiKey);
|
|
200
|
+
case "openai":
|
|
201
|
+
return validateOpenAIKey(apiKey);
|
|
202
|
+
default:
|
|
203
|
+
return { valid: false, error: `Unknown provider: ${provider}` };
|
|
204
|
+
}
|
|
205
|
+
}
|