jobarbiter 0.3.1 → 0.3.3

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.
@@ -11,7 +11,15 @@
11
11
  import * as readline from "node:readline";
12
12
  import { loadConfig, saveConfig, getConfigPath, type Config } from "./config.js";
13
13
  import { apiUnauthenticated, api, ApiError } from "./api.js";
14
- import { detectAgents, installObservers, type DetectedAgent } from "./observe.js";
14
+ import { installObservers } from "./observe.js";
15
+ import {
16
+ detectAllTools,
17
+ getInstalledTools,
18
+ getToolsNeedingObserver,
19
+ formatToolDisplay,
20
+ type DetectedTool,
21
+ type ToolCategory,
22
+ } from "./detect-tools.js";
15
23
 
16
24
  // ── ANSI Colors ────────────────────────────────────────────────────────
17
25
 
@@ -131,13 +139,44 @@ interface OnboardState {
131
139
  export async function runOnboardWizard(opts: { force?: boolean; baseUrl?: string }): Promise<void> {
132
140
  const baseUrl = opts.baseUrl || "https://jobarbiter-api-production.up.railway.app";
133
141
 
134
- // Check for existing config
142
+ // Check for existing config — resume if onboarding incomplete
135
143
  const existingConfig = loadConfig();
136
144
  if (existingConfig && !opts.force) {
137
- console.log(`\n${sym.warning} ${c.warning("You already have a JobArbiter account configured.")}`);
138
- console.log(`\n Run ${c.highlight("jobarbiter status")} to check your account.`);
139
- console.log(` Run ${c.highlight("jobarbiter onboard --force")} to start fresh.\n`);
140
- process.exit(0);
145
+ if (existingConfig.onboardingComplete) {
146
+ console.log(`\n${sym.check} ${c.success("You're already onboarded!")}`);
147
+ console.log(`\n Run ${c.highlight("jobarbiter status")} to check your account.`);
148
+ console.log(` Run ${c.highlight("jobarbiter onboard --force")} to start fresh.\n`);
149
+ process.exit(0);
150
+ }
151
+ // Onboarding incomplete — resume
152
+ const resumeStep = (existingConfig.onboardingStep ?? 1) + 1;
153
+ console.log(`\n${sym.rocket} ${c.bold("Resuming onboarding")} from step ${resumeStep}/6\n`);
154
+ console.log(c.dim(` Account: ${existingConfig.userType} | API key configured`));
155
+ console.log(c.dim(` Run ${c.highlight("jobarbiter onboard --force")} to start over.\n`));
156
+
157
+ const prompt = new Prompt();
158
+ const state: Partial<OnboardState> = {
159
+ baseUrl,
160
+ apiKey: existingConfig.apiKey,
161
+ userType: existingConfig.userType as "worker" | "employer",
162
+ userId: "",
163
+ email: "",
164
+ };
165
+ try {
166
+ if (existingConfig.userType === "worker" || existingConfig.userType === "seeker") {
167
+ await runWorkerFlow(prompt, state as OnboardState, resumeStep);
168
+ } else {
169
+ await runEmployerFlow(prompt, state as OnboardState);
170
+ }
171
+ prompt.close();
172
+ } catch (err) {
173
+ prompt.close();
174
+ if (err instanceof Error) {
175
+ console.log(`\n${sym.cross} ${c.error(err.message)}`);
176
+ }
177
+ process.exit(1);
178
+ }
179
+ return;
141
180
  }
142
181
 
143
182
  const prompt = new Prompt();
@@ -155,11 +194,13 @@ export async function runOnboardWizard(opts: { force?: boolean; baseUrl?: string
155
194
  state.apiKey = apiKey;
156
195
  state.userId = userId;
157
196
 
158
- // Save config immediately after verification
197
+ // Save config immediately after verification (with step progress)
159
198
  saveConfig({
160
199
  apiKey,
161
200
  baseUrl,
162
201
  userType,
202
+ onboardingStep: 1,
203
+ onboardingComplete: false,
163
204
  });
164
205
 
165
206
  if (userType === "worker") {
@@ -216,7 +257,9 @@ async function handleEmailVerification(
216
257
  baseUrl: string,
217
258
  userType: "worker" | "employer"
218
259
  ): Promise<{ email: string; apiKey: string; userId: string }> {
219
- const totalSteps = userType === "worker" ? 5 : 6;
260
+ // Workers: 1) Account, 2) Tool Detection, 3) Domains, 4) GitHub, 5) LinkedIn, 6) Done
261
+ // Employers: 1) Account, 2) (skip verification), 3) Company, 4) Domain, 5) What You Need, 6) Done
262
+ const totalSteps = 6;
220
263
 
221
264
  console.log(`\n${sym.email} ${c.bold(`Step 1/${totalSteps} — Create Your Account`)}\n`);
222
265
 
@@ -296,189 +339,232 @@ async function handleEmailVerification(
296
339
 
297
340
  // ── Worker Flow ────────────────────────────────────────────────────────
298
341
 
299
- async function runWorkerFlow(prompt: Prompt, state: OnboardState): Promise<void> {
342
+ async function runWorkerFlow(prompt: Prompt, state: OnboardState, startStep = 2): Promise<void> {
300
343
  const config: Config = {
301
344
  apiKey: state.apiKey,
302
345
  baseUrl: state.baseUrl,
303
346
  userType: "worker",
304
347
  };
305
348
 
306
- // Step 2: Profile Setup (tools + domains)
307
- console.log(`${sym.tools} ${c.bold("Step 2/6 Your AI Stack")}\n`);
308
-
309
- console.log(`What AI tools do you use? ${c.dim("(comma-separated)")}`);
310
- console.log(c.dim("Examples: Claude Code, Cursor, OpenClaw, ChatGPT, Copilot, Midjourney\n"));
311
- const toolsInput = await prompt.question(`${sym.arrow} `);
312
- const tools = toolsInput.split(",").map(s => s.trim()).filter(Boolean);
313
- state.tools = tools;
314
-
315
- console.log(`\nWhat domains do you work in? ${c.dim("(comma-separated)")}`);
316
- console.log(c.dim("Examples: full-stack dev, data engineering, trading, content creation\n"));
317
- const domainsInput = await prompt.question(`${sym.arrow} `);
318
- const domains = domainsInput.split(",").map(s => s.trim()).filter(Boolean);
319
- state.domains = domains;
320
-
321
- // Create/update profile
322
- console.log(c.dim("\nSaving profile..."));
349
+ const saveProgress = (step: number) => {
350
+ saveConfig({ ...config, onboardingStep: step });
351
+ };
323
352
 
324
- try {
325
- await api(config, "POST", "/v1/profile", {
326
- domains,
327
- tools: {
328
- primary: tools,
329
- },
330
- });
331
- console.log(`${sym.check} Profile saved\n`);
332
- } catch (err) {
333
- console.log(`${sym.warning} ${c.warning("Could not save profile details — you can update later with 'jobarbiter profile create'")}\n`);
353
+ // Step 2: Auto-detect AI Tools
354
+ if (startStep <= 2) {
355
+ const detectedToolsResult = await runToolDetectionStep(prompt, config);
356
+ state.tools = detectedToolsResult.tools;
357
+ saveProgress(2);
334
358
  }
335
359
 
336
- // Step 3: Install Coding Agent Observers
337
- await runObserverStep(prompt, state, 3, 6);
360
+ // Step 3: Domains
361
+ if (startStep <= 3) {
362
+ console.log(`${sym.target} ${c.bold("Step 3/6 — Your Domains")}\n`);
363
+ console.log(`What domains do you work in? ${c.dim("(comma-separated)")}`);
364
+ console.log(c.dim("Examples: full-stack dev, data engineering, trading, content creation\n"));
365
+ const domainsInput = await prompt.question(`${sym.arrow} `);
366
+ const domains = domainsInput.split(",").map(s => s.trim()).filter(Boolean);
367
+ state.domains = domains;
338
368
 
339
- // Step 4: Connect GitHub (optional)
340
- console.log(`${sym.link} ${c.bold("Step 4/6 — Connect GitHub")} ${c.dim("(optional)")}\n`);
341
- console.log(`Connecting your GitHub lets us analyze your AI-assisted work patterns.`);
342
- console.log(`This significantly boosts your proficiency score.\n`);
369
+ // Create/update profile
370
+ console.log(c.dim("\nSaving profile..."));
343
371
 
344
- const githubUsername = await prompt.question(`GitHub username ${c.dim("(press Enter to skip)")}: `);
345
-
346
- if (githubUsername) {
347
- console.log(c.dim("\nConnecting GitHub..."));
348
372
  try {
349
- await api(config, "POST", "/v1/attestations/git/connect", {
350
- provider: "github",
351
- username: githubUsername,
373
+ await api(config, "POST", "/v1/profile", {
374
+ domains,
375
+ tools: {
376
+ primary: state.tools,
377
+ },
352
378
  });
353
- console.log(`${sym.check} GitHub connected: ${c.highlight(githubUsername)}\n`);
354
- state.githubUsername = githubUsername;
379
+ console.log(`${sym.check} Profile saved\n`);
355
380
  } catch (err) {
356
- console.log(`${sym.warning} ${c.warning("Could not connect GitHub — you can try later with 'jobarbiter git connect'")}\n`);
381
+ console.log(`${sym.warning} ${c.warning("Could not save profile details — you can update later with 'jobarbiter profile create'")}\n`);
357
382
  }
358
- } else {
359
- console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter git connect'")}\n`);
383
+ saveProgress(3);
384
+ }
385
+
386
+ // Step 4: Connect GitHub (optional)
387
+ if (startStep <= 4) {
388
+ console.log(`${sym.link} ${c.bold("Step 4/6 — Connect GitHub")} ${c.dim("(optional)")}\n`);
389
+ console.log(`Connecting your GitHub lets us analyze your AI-assisted work patterns.`);
390
+ console.log(`This significantly boosts your proficiency score.\n`);
391
+
392
+ const githubUsername = await prompt.question(`GitHub username ${c.dim("(press Enter to skip)")}: `);
393
+
394
+ if (githubUsername) {
395
+ console.log(c.dim("\nConnecting GitHub..."));
396
+ try {
397
+ await api(config, "POST", "/v1/attestations/git/connect", {
398
+ provider: "github",
399
+ username: githubUsername,
400
+ });
401
+ console.log(`${sym.check} GitHub connected: ${c.highlight(githubUsername)}\n`);
402
+ state.githubUsername = githubUsername;
403
+ } catch (err) {
404
+ console.log(`${sym.warning} ${c.warning("Could not connect GitHub — you can try later with 'jobarbiter git connect'")}\n`);
405
+ }
406
+ } else {
407
+ console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter git connect'")}\n`);
408
+ }
409
+ saveProgress(4);
360
410
  }
361
411
 
362
412
  // Step 5: Connect LinkedIn (optional)
363
- console.log(`${sym.link} ${c.bold("Step 5/6 — Connect LinkedIn")} ${c.dim("(optional)")}\n`);
364
- console.log(`Your LinkedIn profile strengthens identity verification.`);
365
- console.log(c.dim("We never post on your behalf or access your connections.\n"));
413
+ if (startStep <= 5) {
414
+ console.log(`${sym.link} ${c.bold("Step 5/6 Connect LinkedIn")} ${c.dim("(optional)")}\n`);
415
+ console.log(`Your LinkedIn profile strengthens identity verification.`);
416
+ console.log(c.dim("We never post on your behalf or access your connections.\n"));
366
417
 
367
- const linkedinUrl = await prompt.question(`LinkedIn URL ${c.dim("(press Enter to skip)")}: `);
368
-
369
- if (linkedinUrl) {
370
- console.log(c.dim("\nSubmitting for verification..."));
371
- try {
372
- await api(config, "POST", "/v1/verification/linkedin", {
373
- linkedinUrl: linkedinUrl.trim(),
374
- });
375
- console.log(`${sym.check} LinkedIn submitted for verification\n`);
376
- } catch (err) {
377
- console.log(`${sym.warning} ${c.warning("Could not submit LinkedIn — you can try later with 'jobarbiter identity linkedin <url>'")}\n`);
418
+ const linkedinUrl = await prompt.question(`LinkedIn URL ${c.dim("(press Enter to skip)")}: `);
419
+
420
+ if (linkedinUrl) {
421
+ console.log(c.dim("\nSubmitting for verification..."));
422
+ try {
423
+ await api(config, "POST", "/v1/verification/linkedin", {
424
+ linkedinUrl: linkedinUrl.trim(),
425
+ });
426
+ console.log(`${sym.check} LinkedIn submitted for verification\n`);
427
+ } catch (err) {
428
+ console.log(`${sym.warning} ${c.warning("Could not submit LinkedIn — you can try later with 'jobarbiter identity linkedin <url>'")}\n`);
429
+ }
430
+ } else {
431
+ console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter identity linkedin <url>'")}\n`);
378
432
  }
379
- } else {
380
- console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter identity linkedin <url>'")}\n`);
433
+ saveProgress(5);
381
434
  }
382
435
 
383
436
  // Step 6: Done!
437
+ saveConfig({ ...config, onboardingComplete: true, onboardingStep: 6 });
384
438
  showWorkerCompletion(state);
385
439
  }
386
440
 
387
- // ── Observer Installation Step ─────────────────────────────────────────
441
+ // ── Tool Detection Step ────────────────────────────────────────────────
388
442
 
389
- async function runObserverStep(
443
+ async function runToolDetectionStep(
390
444
  prompt: Prompt,
391
- state: OnboardState,
392
- stepNum: number,
393
- totalSteps: number,
394
- ): Promise<void> {
395
- console.log(`🔍 ${c.bold(`Step ${stepNum}/${totalSteps} — Coding Agent Observers`)}\n`);
396
- console.log(c.dim("Scanning for coding agents...\n"));
397
-
398
- const agents = detectAgents();
399
- const detected = agents.filter((a) => a.installed);
400
- const notDetected = agents.filter((a) => !a.installed);
401
-
402
- // Show results
403
- if (detected.length === 0) {
404
- console.log(` ${c.dim("No coding agents detected on this system.")}\n`);
405
- console.log(c.dim(" You can install observers later with 'jobarbiter observe install'.\n"));
406
- return;
445
+ config: Config,
446
+ ): Promise<{ tools: string[] }> {
447
+ console.log(`🔍 ${c.bold("Step 2/6 — Detecting AI Tools")}\n`);
448
+ console.log(c.dim(" Scanning your machine...\n"));
449
+
450
+ const allTools = detectAllTools();
451
+ const installed = allTools.filter((t) => t.installed);
452
+ const notInstalled = allTools.filter((t) => !t.installed && t.category === "coding-agent");
453
+
454
+ // Group by category
455
+ const codingAgents = installed.filter((t) => t.category === "coding-agent");
456
+ const chatTools = installed.filter((t) => t.category === "chat");
457
+ const orchestration = installed.filter((t) => t.category === "orchestration");
458
+ const apiProviders = installed.filter((t) => t.category === "api-provider");
459
+
460
+ // Display found tools
461
+ if (installed.length === 0) {
462
+ console.log(` ${c.dim("No AI tools detected on this system.")}\n`);
463
+ console.log(c.dim(" You can add tools later with 'jobarbiter observe install'.\n"));
464
+ return { tools: [] };
407
465
  }
408
466
 
409
- console.log(` Found on your system:`);
410
- for (const agent of detected) {
411
- if (agent.hookInstalled) {
412
- console.log(` ${sym.check} ${agent.name} ${c.dim("(observer already installed)")}`);
467
+ console.log(` ${c.bold("Found:")}`);
468
+
469
+ // Show coding agents with observer status
470
+ for (const tool of codingAgents) {
471
+ const display = formatToolDisplay(tool);
472
+ if (tool.observerAvailable) {
473
+ if (tool.observerActive) {
474
+ console.log(` ${sym.check} ${display} ${c.dim("(observer active)")}`);
475
+ } else {
476
+ console.log(` ${sym.check} ${display} ${c.success("(observer available)")}`);
477
+ }
413
478
  } else {
414
- console.log(` ${c.success("")} ${agent.name}`);
479
+ console.log(` ${sym.check} ${display} ${c.dim("(detected)")}`);
415
480
  }
416
481
  }
417
- for (const agent of notDetected) {
418
- console.log(` ${c.dim("⬚")} ${agent.name} ${c.dim("(not found)")}`);
482
+
483
+ // Show other tools
484
+ for (const tool of chatTools) {
485
+ console.log(` ${sym.check} ${formatToolDisplay(tool)} ${c.dim("(detected)")}`);
486
+ }
487
+ for (const tool of orchestration) {
488
+ console.log(` ${sym.check} ${formatToolDisplay(tool)} ${c.dim("(detected)")}`);
489
+ }
490
+ for (const tool of apiProviders) {
491
+ console.log(` ${sym.check} ${tool.name} ${c.dim("configured")}`);
419
492
  }
420
493
 
421
- // Filter to agents that need installation
422
- const needsInstall = detected.filter((a) => !a.hookInstalled);
423
-
424
- if (needsInstall.length === 0) {
425
- console.log(`\n ${c.dim("All detected agents already have observers installed.")}\n`);
426
- return;
494
+ // Show not-detected coding agents
495
+ if (notInstalled.length > 0) {
496
+ console.log(`\n ${c.dim("Not detected (install to track):")}`);
497
+ for (const tool of notInstalled.slice(0, 5)) {
498
+ console.log(` ${c.dim("")} ${tool.name}`);
499
+ }
500
+ if (notInstalled.length > 5) {
501
+ console.log(` ${c.dim(`... and ${notInstalled.length - 5} more`)}`);
502
+ }
427
503
  }
428
504
 
429
- console.log(`\n JobArbiter observes your coding sessions to build your`);
430
- console.log(` proficiency profile. ${c.bold("No code or prompts leave your machine")} —`);
431
- console.log(` only aggregate scores (tool usage, session counts, token volume).\n`);
432
- console.log(c.dim(` Data stored locally: ~/.config/jobarbiter/observer/observations.json`));
433
- console.log(c.dim(` Review anytime: jobarbiter observe status\n`));
505
+ // Collect tool names for profile
506
+ const toolNames = installed.map((t) => t.name);
434
507
 
435
- const installAll = await prompt.confirm(
436
- ` Install observers for all ${needsInstall.length} detected agent${needsInstall.length > 1 ? "s" : ""}?`,
437
- );
508
+ // Observer installation for coding agents
509
+ const needsObserver = codingAgents.filter((t) => t.observerAvailable && !t.observerActive);
438
510
 
439
- let toInstall: string[];
511
+ if (needsObserver.length > 0) {
512
+ console.log(`\n ${c.bold("Observers")}`);
513
+ console.log(` JobArbiter observes your coding sessions to build your`);
514
+ console.log(` proficiency profile. ${c.bold("No code or prompts leave your machine")} —`);
515
+ console.log(` only aggregate scores (tool usage, session counts, token volume).\n`);
516
+ console.log(c.dim(` Data stored locally: ~/.config/jobarbiter/observer/observations.json`));
517
+ console.log(c.dim(` Review anytime: jobarbiter observe status\n`));
440
518
 
441
- if (installAll) {
442
- toInstall = needsInstall.map((a) => a.id);
443
- } else {
444
- // Let user select individually
445
- console.log(`\n Select which agents to observe:\n`);
446
- const selections: Record<string, boolean> = {};
447
-
448
- for (const agent of needsInstall) {
449
- selections[agent.id] = await prompt.confirm(` ${agent.name}?`, true);
450
- }
451
-
452
- toInstall = Object.entries(selections)
453
- .filter(([, v]) => v)
454
- .map(([k]) => k);
519
+ const observerNames = needsObserver.map((t) => t.name).join(", ");
520
+ const installAll = await prompt.confirm(
521
+ ` Install observers for detected tools? (${observerNames})`,
522
+ );
455
523
 
456
- if (toInstall.length === 0) {
457
- console.log(`\n ${c.dim("No observers installed. You can add them later with 'jobarbiter observe install'.")}\n`);
458
- return;
459
- }
460
- }
524
+ if (installAll) {
525
+ const toInstall = needsObserver.map((t) => t.id);
526
+ console.log(c.dim("\n Installing observers..."));
527
+ const result = installObservers(toInstall);
461
528
 
462
- // Install
463
- console.log(c.dim("\n Installing observers..."));
464
- const result = installObservers(toInstall);
529
+ for (const name of result.installed) {
530
+ console.log(` ${sym.check} ${name}`);
531
+ }
532
+ for (const name of result.skipped) {
533
+ console.log(` ${c.dim("—")} ${name} ${c.dim("(already installed)")}`);
534
+ }
535
+ for (const { agent, error: errMsg } of result.errors) {
536
+ console.log(` ${sym.cross} ${agent}: ${c.error(errMsg)}`);
537
+ }
465
538
 
466
- for (const name of result.installed) {
467
- console.log(` ${sym.check} ${name}`);
468
- }
469
- for (const name of result.skipped) {
470
- console.log(` ${c.dim("—")} ${name} ${c.dim("(already installed)")}`);
471
- }
472
- for (const { agent, error: errMsg } of result.errors) {
473
- console.log(` ${sym.cross} ${agent}: ${c.error(errMsg)}`);
539
+ if (result.installed.length > 0) {
540
+ console.log(`\n ${sym.check} ${c.success(`${result.installed.length} observer${result.installed.length > 1 ? "s" : ""} installed!`)}`);
541
+ console.log(c.dim(` Your proficiency profile will start building automatically.\n`));
542
+ }
543
+ } else {
544
+ console.log(c.dim("\n Skipped — you can install observers later with 'jobarbiter observe install'.\n"));
545
+ }
546
+ } else if (codingAgents.length > 0) {
547
+ const hasActiveObservers = codingAgents.some((t) => t.observerActive);
548
+ if (hasActiveObservers) {
549
+ console.log(`\n ${c.dim("All detected agents already have observers installed.")}\n`);
550
+ } else {
551
+ console.log();
552
+ }
474
553
  }
475
554
 
476
- if (result.installed.length > 0) {
477
- console.log(`\n ${sym.check} ${c.success(`${result.installed.length} observer${result.installed.length > 1 ? "s" : ""} installed!`)}`);
478
- console.log(c.dim(` Your proficiency profile will start building automatically.\n`));
555
+ // "Did we miss anything?" prompt
556
+ console.log(` ${c.dim("Did we miss anything?")}`);
557
+ const additionalTools = await prompt.question(` Other AI tools you use ${c.dim("(comma-separated, or press Enter)")}: `);
558
+
559
+ if (additionalTools.trim()) {
560
+ const additional = additionalTools.split(",").map((s) => s.trim()).filter(Boolean);
561
+ toolNames.push(...additional);
562
+ console.log(` ${sym.check} Added: ${additional.join(", ")}\n`);
479
563
  } else {
480
564
  console.log();
481
565
  }
566
+
567
+ return { tools: toolNames };
482
568
  }
483
569
 
484
570
  function showWorkerCompletion(state: OnboardState): void {