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 CHANGED
@@ -743,9 +743,9 @@ program
743
743
  }
744
744
  });
745
745
  // ============================================================
746
- // observe (manage coding agent observers)
746
+ // observe (manage AI agent observers)
747
747
  // ============================================================
748
- const observe = program.command("observe").description("Manage coding agent proficiency observers");
748
+ const observe = program.command("observe").description("Manage AI agent proficiency observers");
749
749
  observe
750
750
  .command("status")
751
751
  .description("Show observer status and accumulated data")
@@ -754,7 +754,7 @@ observe
754
754
  const agents = detectAgents();
755
755
  const status = getObservationStatus();
756
756
  const detected = agents.filter((a) => a.installed);
757
- console.log("\nšŸ” Coding Agent Observers\n");
757
+ console.log("\nšŸ” AI Agent Observers\n");
758
758
  console.log(" Agents:");
759
759
  for (const agent of agents) {
760
760
  if (!agent.installed) {
@@ -795,7 +795,7 @@ observe
795
795
  });
796
796
  observe
797
797
  .command("install")
798
- .description("Install observers for detected coding agents")
798
+ .description("Install observers for detected AI agents")
799
799
  .option("--agent <id>", "Install for specific agent (claude-code, cursor, opencode, codex, gemini)")
800
800
  .option("--all", "Install for all detected agents")
801
801
  .action(async (opts) => {
@@ -803,7 +803,7 @@ observe
803
803
  const agents = detectAgents();
804
804
  const detected = agents.filter((a) => a.installed);
805
805
  if (detected.length === 0) {
806
- error("No coding agents detected on this system.");
806
+ error("No AI agents detected on this system.");
807
807
  console.log(" Supported: Claude Code, Cursor, OpenCode, Codex CLI, Gemini CLI");
808
808
  process.exit(1);
809
809
  }
@@ -841,7 +841,7 @@ observe
841
841
  });
842
842
  observe
843
843
  .command("remove")
844
- .description("Remove observers from coding agents")
844
+ .description("Remove observers from AI agents")
845
845
  .option("--agent <id>", "Remove from specific agent")
846
846
  .option("--all", "Remove from all agents")
847
847
  .action(async (opts) => {
@@ -880,7 +880,7 @@ observe
880
880
  const status = getObservationStatus();
881
881
  if (!status.hasData) {
882
882
  console.log("\nNo observation data collected yet.");
883
- console.log("Use your coding agents normally — data accumulates automatically.\n");
883
+ console.log("Use your AI agents normally — data accumulates automatically.\n");
884
884
  process.exit(0);
885
885
  }
886
886
  console.log("\nšŸ“Š Observation Data Review\n");
@@ -2,6 +2,8 @@ export interface Config {
2
2
  apiKey: string;
3
3
  baseUrl: string;
4
4
  userType: "worker" | "employer" | "seeker" | "poster";
5
+ onboardingComplete?: boolean;
6
+ onboardingStep?: number;
5
7
  }
6
8
  export declare function getConfigPath(): string;
7
9
  export declare function loadConfig(): Config | null;
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Never logs or stores API key values — only checks for existence.
9
9
  */
10
- export type ToolCategory = "coding-agent" | "chat" | "orchestration" | "api-provider";
10
+ export type ToolCategory = "ai-agent" | "chat" | "orchestration" | "api-provider";
11
11
  export interface DetectedTool {
12
12
  id: string;
13
13
  name: string;
@@ -13,11 +13,11 @@ import { homedir, platform } from "node:os";
13
13
  import { execSync } from "node:child_process";
14
14
  // ── Tool Definitions ───────────────────────────────────────────────────
15
15
  const TOOL_DEFINITIONS = [
16
- // AI Coding Agents
16
+ // AI AI Agents
17
17
  {
18
18
  id: "claude-code",
19
19
  name: "Claude Code",
20
- category: "coding-agent",
20
+ category: "ai-agent",
21
21
  binary: "claude",
22
22
  configDir: join(homedir(), ".claude"),
23
23
  observerAvailable: true,
@@ -25,7 +25,7 @@ const TOOL_DEFINITIONS = [
25
25
  {
26
26
  id: "cursor",
27
27
  name: "Cursor",
28
- category: "coding-agent",
28
+ category: "ai-agent",
29
29
  binary: "cursor",
30
30
  configDir: join(homedir(), ".cursor"),
31
31
  macApp: "/Applications/Cursor.app",
@@ -34,7 +34,7 @@ const TOOL_DEFINITIONS = [
34
34
  {
35
35
  id: "github-copilot",
36
36
  name: "GitHub Copilot",
37
- category: "coding-agent",
37
+ category: "ai-agent",
38
38
  configDir: join(homedir(), ".config", "github-copilot"),
39
39
  vscodeExtension: "github.copilot",
40
40
  cursorExtension: "github.copilot",
@@ -43,7 +43,7 @@ const TOOL_DEFINITIONS = [
43
43
  {
44
44
  id: "codex",
45
45
  name: "Codex CLI",
46
- category: "coding-agent",
46
+ category: "ai-agent",
47
47
  binary: "codex",
48
48
  configDir: join(homedir(), ".codex"),
49
49
  observerAvailable: true,
@@ -51,7 +51,7 @@ const TOOL_DEFINITIONS = [
51
51
  {
52
52
  id: "opencode",
53
53
  name: "OpenCode",
54
- category: "coding-agent",
54
+ category: "ai-agent",
55
55
  binary: "opencode",
56
56
  configDir: join(homedir(), ".config", "opencode"),
57
57
  observerAvailable: true,
@@ -59,7 +59,7 @@ const TOOL_DEFINITIONS = [
59
59
  {
60
60
  id: "aider",
61
61
  name: "Aider",
62
- category: "coding-agent",
62
+ category: "ai-agent",
63
63
  binary: "aider",
64
64
  configDir: join(homedir(), ".aider"),
65
65
  pipPackage: "aider-chat",
@@ -68,7 +68,7 @@ const TOOL_DEFINITIONS = [
68
68
  {
69
69
  id: "continue",
70
70
  name: "Continue",
71
- category: "coding-agent",
71
+ category: "ai-agent",
72
72
  vscodeExtension: "continue.continue",
73
73
  cursorExtension: "continue.continue",
74
74
  observerAvailable: false,
@@ -76,7 +76,7 @@ const TOOL_DEFINITIONS = [
76
76
  {
77
77
  id: "cline",
78
78
  name: "Cline",
79
- category: "coding-agent",
79
+ category: "ai-agent",
80
80
  vscodeExtension: "saoudrizwan.claude-dev",
81
81
  cursorExtension: "saoudrizwan.claude-dev",
82
82
  observerAvailable: false,
@@ -84,7 +84,7 @@ const TOOL_DEFINITIONS = [
84
84
  {
85
85
  id: "windsurf",
86
86
  name: "Windsurf",
87
- category: "coding-agent",
87
+ category: "ai-agent",
88
88
  binary: "windsurf",
89
89
  macApp: "/Applications/Windsurf.app",
90
90
  observerAvailable: false,
@@ -92,7 +92,7 @@ const TOOL_DEFINITIONS = [
92
92
  {
93
93
  id: "letta",
94
94
  name: "Letta Code",
95
- category: "coding-agent",
95
+ category: "ai-agent",
96
96
  binary: "letta",
97
97
  configDir: join(homedir(), ".letta"),
98
98
  pipPackage: "letta",
@@ -101,7 +101,7 @@ const TOOL_DEFINITIONS = [
101
101
  {
102
102
  id: "gemini",
103
103
  name: "Gemini CLI",
104
- category: "coding-agent",
104
+ category: "ai-agent",
105
105
  binary: "gemini",
106
106
  configDir: join(homedir(), ".gemini"),
107
107
  observerAvailable: true,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JobArbiter Observer — Hook installer for coding agent CLIs
2
+ * JobArbiter Observer — Hook installer for AI agent CLIs
3
3
  *
4
4
  * Installs observation hooks that extract proficiency signals from
5
5
  * session transcripts. Uses detect-tools.ts for agent detection.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JobArbiter Observer — Hook installer for coding agent CLIs
2
+ * JobArbiter Observer — Hook installer for AI agent CLIs
3
3
  *
4
4
  * Installs observation hooks that extract proficiency signals from
5
5
  * session transcripts. Uses detect-tools.ts for agent detection.
@@ -64,7 +64,7 @@ function ensureObserverDirs() {
64
64
  }
65
65
  // ── Core Observer Script ───────────────────────────────────────────────
66
66
  /**
67
- * The universal observer script. Runs as a hook in any coding agent.
67
+ * The universal observer script. Runs as a hook in any AI agent.
68
68
  * Reads session transcript data from stdin (JSON), extracts proficiency
69
69
  * signals, and appends them to the local observations file.
70
70
  *
@@ -75,7 +75,7 @@ function getObserverScript() {
75
75
  return `#!/usr/bin/env node
76
76
  /**
77
77
  * JobArbiter Observer Hook
78
- * Extracts proficiency signals from coding agent sessions.
78
+ * Extracts proficiency signals from AI agent sessions.
79
79
  *
80
80
  * Reads JSON from stdin, writes observations to:
81
81
  * ~/.config/jobarbiter/observer/observations.json
@@ -105,7 +105,7 @@ process.stdin.on("end", () => {
105
105
  const observation = extractSignals(data);
106
106
  if (observation) appendObservation(observation);
107
107
  } catch (err) {
108
- // Silent failure — never block the coding agent
108
+ // Silent failure — never block the AI agent
109
109
  fs.appendFileSync(
110
110
  path.join(os.homedir(), ".config", "jobarbiter", "observer", "errors.log"),
111
111
  \`[\${new Date().toISOString()}] \${err.message}\\n\`
@@ -12,6 +12,7 @@ import { loadConfig, saveConfig, getConfigPath } from "./config.js";
12
12
  import { apiUnauthenticated, api, ApiError } from "./api.js";
13
13
  import { installObservers } from "./observe.js";
14
14
  import { detectAllTools, formatToolDisplay, } from "./detect-tools.js";
15
+ import { loadProviderKeys, saveProviderKey, validateProviderKey, } from "./providers.js";
15
16
  // ── ANSI Colors ────────────────────────────────────────────────────────
16
17
  const colors = {
17
18
  reset: "\x1b[0m",
@@ -92,13 +93,45 @@ class Prompt {
92
93
  // ── Main Wizard ────────────────────────────────────────────────────────
93
94
  export async function runOnboardWizard(opts) {
94
95
  const baseUrl = opts.baseUrl || "https://jobarbiter-api-production.up.railway.app";
95
- // Check for existing config
96
+ // Check for existing config — resume if onboarding incomplete
96
97
  const existingConfig = loadConfig();
97
98
  if (existingConfig && !opts.force) {
98
- console.log(`\n${sym.warning} ${c.warning("You already have a JobArbiter account configured.")}`);
99
- console.log(`\n Run ${c.highlight("jobarbiter status")} to check your account.`);
100
- console.log(` Run ${c.highlight("jobarbiter onboard --force")} to start fresh.\n`);
101
- process.exit(0);
99
+ if (existingConfig.onboardingComplete) {
100
+ console.log(`\n${sym.check} ${c.success("You're already onboarded!")}`);
101
+ console.log(`\n Run ${c.highlight("jobarbiter status")} to check your account.`);
102
+ console.log(` Run ${c.highlight("jobarbiter onboard --force")} to start fresh.\n`);
103
+ process.exit(0);
104
+ }
105
+ // Onboarding incomplete — resume
106
+ const resumeStep = (existingConfig.onboardingStep ?? 1) + 1;
107
+ console.log(`\n${sym.rocket} ${c.bold("Resuming onboarding")} from step ${resumeStep}/7\n`);
108
+ console.log(c.dim(` Account: ${existingConfig.userType} | API key configured`));
109
+ console.log(c.dim(` Run ${c.highlight("jobarbiter onboard --force")} to start over.\n`));
110
+ const prompt = new Prompt();
111
+ const state = {
112
+ baseUrl,
113
+ apiKey: existingConfig.apiKey,
114
+ userType: existingConfig.userType,
115
+ userId: "",
116
+ email: "",
117
+ };
118
+ try {
119
+ if (existingConfig.userType === "worker" || existingConfig.userType === "seeker") {
120
+ await runWorkerFlow(prompt, state, resumeStep);
121
+ }
122
+ else {
123
+ await runEmployerFlow(prompt, state);
124
+ }
125
+ prompt.close();
126
+ }
127
+ catch (err) {
128
+ prompt.close();
129
+ if (err instanceof Error) {
130
+ console.log(`\n${sym.cross} ${c.error(err.message)}`);
131
+ }
132
+ process.exit(1);
133
+ }
134
+ return;
102
135
  }
103
136
  const prompt = new Prompt();
104
137
  const state = { baseUrl };
@@ -112,11 +145,13 @@ export async function runOnboardWizard(opts) {
112
145
  state.email = email;
113
146
  state.apiKey = apiKey;
114
147
  state.userId = userId;
115
- // Save config immediately after verification
148
+ // Save config immediately after verification (with step progress)
116
149
  saveConfig({
117
150
  apiKey,
118
151
  baseUrl,
119
152
  userType,
153
+ onboardingStep: 1,
154
+ onboardingComplete: false,
120
155
  });
121
156
  if (userType === "worker") {
122
157
  await runWorkerFlow(prompt, state);
@@ -165,9 +200,9 @@ async function selectUserType(prompt) {
165
200
  }
166
201
  // ── Email & Verification ───────────────────────────────────────────────
167
202
  async function handleEmailVerification(prompt, baseUrl, userType) {
168
- // Workers: 1) Account, 2) Tool Detection, 3) Domains, 4) GitHub, 5) LinkedIn, 6) Done
169
- // Employers: 1) Account, 2) (skip verification), 3) Company, 4) Domain, 5) What You Need, 6) Done
170
- const totalSteps = 6;
203
+ // Workers: 1) Account, 2) Tool Detection, 3) AI Accounts, 4) Domains, 5) GitHub, 6) LinkedIn, 7) Done
204
+ // Employers: 1) Account, 2) (skip verification), 3) Company, 4) Domain, 5) What You Need, 6) Done (stays at 6)
205
+ const totalSteps = userType === "employer" ? 6 : 7;
171
206
  console.log(`\n${sym.email} ${c.bold(`Step 1/${totalSteps} — Create Your Account`)}\n`);
172
207
  // Get email
173
208
  let email;
@@ -237,90 +272,111 @@ async function handleEmailVerification(prompt, baseUrl, userType) {
237
272
  return { email, apiKey, userId };
238
273
  }
239
274
  // ── Worker Flow ────────────────────────────────────────────────────────
240
- async function runWorkerFlow(prompt, state) {
275
+ async function runWorkerFlow(prompt, state, startStep = 2) {
241
276
  const config = {
242
277
  apiKey: state.apiKey,
243
278
  baseUrl: state.baseUrl,
244
279
  userType: "worker",
245
280
  };
281
+ const saveProgress = (step) => {
282
+ saveConfig({ ...config, onboardingStep: step });
283
+ };
246
284
  // Step 2: Auto-detect AI Tools
247
- const detectedToolsResult = await runToolDetectionStep(prompt, config);
248
- state.tools = detectedToolsResult.tools;
249
- // Step 3: Domains
250
- console.log(`${sym.target} ${c.bold("Step 3/6 — Your Domains")}\n`);
251
- console.log(`What domains do you work in? ${c.dim("(comma-separated)")}`);
252
- console.log(c.dim("Examples: full-stack dev, data engineering, trading, content creation\n"));
253
- const domainsInput = await prompt.question(`${sym.arrow} `);
254
- const domains = domainsInput.split(",").map(s => s.trim()).filter(Boolean);
255
- state.domains = domains;
256
- // Create/update profile
257
- console.log(c.dim("\nSaving profile..."));
258
- try {
259
- await api(config, "POST", "/v1/profile", {
260
- domains,
261
- tools: {
262
- primary: state.tools,
263
- },
264
- });
265
- console.log(`${sym.check} Profile saved\n`);
266
- }
267
- catch (err) {
268
- console.log(`${sym.warning} ${c.warning("Could not save profile details — you can update later with 'jobarbiter profile create'")}\n`);
269
- }
270
- // Step 4: Connect GitHub (optional)
271
- console.log(`${sym.link} ${c.bold("Step 4/6 — Connect GitHub")} ${c.dim("(optional)")}\n`);
272
- console.log(`Connecting your GitHub lets us analyze your AI-assisted work patterns.`);
273
- console.log(`This significantly boosts your proficiency score.\n`);
274
- const githubUsername = await prompt.question(`GitHub username ${c.dim("(press Enter to skip)")}: `);
275
- if (githubUsername) {
276
- console.log(c.dim("\nConnecting GitHub..."));
285
+ if (startStep <= 2) {
286
+ const detectedToolsResult = await runToolDetectionStep(prompt, config);
287
+ state.tools = detectedToolsResult.tools;
288
+ saveProgress(2);
289
+ }
290
+ // Step 3: Connect AI Accounts (optional)
291
+ if (startStep <= 3) {
292
+ await runConnectAIAccountsStep(prompt);
293
+ saveProgress(3);
294
+ }
295
+ // Step 4: Domains
296
+ if (startStep <= 4) {
297
+ console.log(`${sym.target} ${c.bold("Step 4/7 — Your Domains")}\n`);
298
+ console.log(`What domains do you work in? ${c.dim("(comma-separated)")}`);
299
+ console.log(c.dim("Examples: full-stack dev, data engineering, trading, content creation\n"));
300
+ const domainsInput = await prompt.question(`${sym.arrow} `);
301
+ const domains = domainsInput.split(",").map(s => s.trim()).filter(Boolean);
302
+ state.domains = domains;
303
+ // Create/update profile
304
+ console.log(c.dim("\nSaving profile..."));
277
305
  try {
278
- await api(config, "POST", "/v1/attestations/git/connect", {
279
- provider: "github",
280
- username: githubUsername,
306
+ await api(config, "POST", "/v1/profile", {
307
+ domains,
308
+ tools: {
309
+ primary: state.tools,
310
+ },
281
311
  });
282
- console.log(`${sym.check} GitHub connected: ${c.highlight(githubUsername)}\n`);
283
- state.githubUsername = githubUsername;
312
+ console.log(`${sym.check} Profile saved\n`);
284
313
  }
285
314
  catch (err) {
286
- console.log(`${sym.warning} ${c.warning("Could not connect GitHub — you can try later with 'jobarbiter git connect'")}\n`);
315
+ console.log(`${sym.warning} ${c.warning("Could not save profile details — you can update later with 'jobarbiter profile create'")}\n`);
287
316
  }
288
- }
289
- else {
290
- console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter git connect'")}\n`);
291
- }
292
- // Step 5: Connect LinkedIn (optional)
293
- console.log(`${sym.link} ${c.bold("Step 5/6 — Connect LinkedIn")} ${c.dim("(optional)")}\n`);
294
- console.log(`Your LinkedIn profile strengthens identity verification.`);
295
- console.log(c.dim("We never post on your behalf or access your connections.\n"));
296
- const linkedinUrl = await prompt.question(`LinkedIn URL ${c.dim("(press Enter to skip)")}: `);
297
- if (linkedinUrl) {
298
- console.log(c.dim("\nSubmitting for verification..."));
299
- try {
300
- await api(config, "POST", "/v1/verification/linkedin", {
301
- linkedinUrl: linkedinUrl.trim(),
302
- });
303
- console.log(`${sym.check} LinkedIn submitted for verification\n`);
317
+ saveProgress(4);
318
+ }
319
+ // Step 5: Connect GitHub (optional)
320
+ if (startStep <= 5) {
321
+ console.log(`${sym.link} ${c.bold("Step 5/7 — Connect GitHub")} ${c.dim("(optional)")}\n`);
322
+ console.log(`Connecting your GitHub lets us analyze your AI-assisted work patterns.`);
323
+ console.log(`This significantly boosts your proficiency score.\n`);
324
+ const githubUsername = await prompt.question(`GitHub username ${c.dim("(press Enter to skip)")}: `);
325
+ if (githubUsername) {
326
+ console.log(c.dim("\nConnecting GitHub..."));
327
+ try {
328
+ await api(config, "POST", "/v1/attestations/git/connect", {
329
+ provider: "github",
330
+ username: githubUsername,
331
+ });
332
+ console.log(`${sym.check} GitHub connected: ${c.highlight(githubUsername)}\n`);
333
+ state.githubUsername = githubUsername;
334
+ }
335
+ catch (err) {
336
+ console.log(`${sym.warning} ${c.warning("Could not connect GitHub — you can try later with 'jobarbiter git connect'")}\n`);
337
+ }
304
338
  }
305
- catch (err) {
306
- console.log(`${sym.warning} ${c.warning("Could not submit LinkedIn — you can try later with 'jobarbiter identity linkedin <url>'")}\n`);
339
+ else {
340
+ console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter git connect'")}\n`);
307
341
  }
342
+ saveProgress(5);
343
+ }
344
+ // Step 6: Connect LinkedIn (optional)
345
+ if (startStep <= 6) {
346
+ console.log(`${sym.link} ${c.bold("Step 6/7 — Connect LinkedIn")} ${c.dim("(optional)")}\n`);
347
+ console.log(`Your LinkedIn profile strengthens identity verification.`);
348
+ console.log(c.dim("We never post on your behalf or access your connections.\n"));
349
+ const linkedinUrl = await prompt.question(`LinkedIn URL ${c.dim("(press Enter to skip)")}: `);
350
+ if (linkedinUrl) {
351
+ console.log(c.dim("\nSubmitting for verification..."));
352
+ try {
353
+ await api(config, "POST", "/v1/verification/linkedin", {
354
+ linkedinUrl: linkedinUrl.trim(),
355
+ });
356
+ console.log(`${sym.check} LinkedIn submitted for verification\n`);
357
+ }
358
+ catch (err) {
359
+ console.log(`${sym.warning} ${c.warning("Could not submit LinkedIn — you can try later with 'jobarbiter identity linkedin <url>'")}\n`);
360
+ }
361
+ }
362
+ else {
363
+ console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter identity linkedin <url>'")}\n`);
364
+ }
365
+ saveProgress(6);
308
366
  }
309
- else {
310
- console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter identity linkedin <url>'")}\n`);
311
- }
312
- // Step 6: Done!
367
+ // Step 7: Done!
368
+ saveConfig({ ...config, onboardingComplete: true, onboardingStep: 7 });
313
369
  showWorkerCompletion(state);
314
370
  }
315
371
  // ── Tool Detection Step ────────────────────────────────────────────────
316
372
  async function runToolDetectionStep(prompt, config) {
317
- console.log(`šŸ” ${c.bold("Step 2/6 — Detecting AI Tools")}\n`);
373
+ console.log(`šŸ” ${c.bold("Step 2/7 — Detecting AI Tools")}\n`);
318
374
  console.log(c.dim(" Scanning your machine...\n"));
319
375
  const allTools = detectAllTools();
320
376
  const installed = allTools.filter((t) => t.installed);
321
- const notInstalled = allTools.filter((t) => !t.installed && t.category === "coding-agent");
377
+ const notInstalled = allTools.filter((t) => !t.installed && t.category === "ai-agent");
322
378
  // Group by category
323
- const codingAgents = installed.filter((t) => t.category === "coding-agent");
379
+ const codingAgents = installed.filter((t) => t.category === "ai-agent");
324
380
  const chatTools = installed.filter((t) => t.category === "chat");
325
381
  const orchestration = installed.filter((t) => t.category === "orchestration");
326
382
  const apiProviders = installed.filter((t) => t.category === "api-provider");
@@ -331,7 +387,7 @@ async function runToolDetectionStep(prompt, config) {
331
387
  return { tools: [] };
332
388
  }
333
389
  console.log(` ${c.bold("Found:")}`);
334
- // Show coding agents with observer status
390
+ // Show AI agents with observer status
335
391
  for (const tool of codingAgents) {
336
392
  const display = formatToolDisplay(tool);
337
393
  if (tool.observerAvailable) {
@@ -356,7 +412,7 @@ async function runToolDetectionStep(prompt, config) {
356
412
  for (const tool of apiProviders) {
357
413
  console.log(` ${sym.check} ${tool.name} ${c.dim("configured")}`);
358
414
  }
359
- // Show not-detected coding agents
415
+ // Show not-detected AI agents
360
416
  if (notInstalled.length > 0) {
361
417
  console.log(`\n ${c.dim("Not detected (install to track):")}`);
362
418
  for (const tool of notInstalled.slice(0, 5)) {
@@ -368,11 +424,11 @@ async function runToolDetectionStep(prompt, config) {
368
424
  }
369
425
  // Collect tool names for profile
370
426
  const toolNames = installed.map((t) => t.name);
371
- // Observer installation for coding agents
427
+ // Observer installation for AI agents
372
428
  const needsObserver = codingAgents.filter((t) => t.observerAvailable && !t.observerActive);
373
429
  if (needsObserver.length > 0) {
374
430
  console.log(`\n ${c.bold("Observers")}`);
375
- console.log(` JobArbiter observes your coding sessions to build your`);
431
+ console.log(` JobArbiter observes your AI sessions to build your`);
376
432
  console.log(` proficiency profile. ${c.bold("No code or prompts leave your machine")} —`);
377
433
  console.log(` only aggregate scores (tool usage, session counts, token volume).\n`);
378
434
  console.log(c.dim(` Data stored locally: ~/.config/jobarbiter/observer/observations.json`));
@@ -423,8 +479,87 @@ async function runToolDetectionStep(prompt, config) {
423
479
  }
424
480
  return { tools: toolNames };
425
481
  }
482
+ // ── Connect AI Accounts Step ───────────────────────────────────────────
483
+ async function runConnectAIAccountsStep(prompt) {
484
+ console.log(`${sym.link} ${c.bold("Step 3/7 — Connect AI Accounts")} ${c.dim("(optional)")}\n`);
485
+ console.log(` Connecting your AI provider accounts lets us analyze usage depth,`);
486
+ console.log(` not just tool presence. The more we can see, the stronger your`);
487
+ console.log(` verified proficiency profile.\n`);
488
+ console.log(` ${c.bold("What we can pull:")}`);
489
+ console.log(` ${sym.bullet} Token usage volume and patterns ${c.dim("(how much you use AI)")}`);
490
+ console.log(` ${sym.bullet} Model preferences ${c.dim("(which models you reach for)")}`);
491
+ console.log(` ${sym.bullet} Session frequency and consistency over time`);
492
+ console.log(` ${sym.bullet} API spend patterns ${c.dim("(demonstrates serious usage)")}\n`);
493
+ console.log(` ${c.bold("What we NEVER access:")}`);
494
+ console.log(` ${sym.bullet} Your conversation content`);
495
+ console.log(` ${sym.bullet} Prompts or responses`);
496
+ console.log(` ${sym.bullet} Any proprietary or sensitive data\n`);
497
+ // Check for already connected providers
498
+ const existingProviders = loadProviderKeys();
499
+ if (existingProviders.length > 0) {
500
+ console.log(` ${c.bold("Already connected:")}`);
501
+ for (const p of existingProviders) {
502
+ console.log(` ${sym.check} ${p.provider}`);
503
+ }
504
+ console.log();
505
+ }
506
+ // Show available connections
507
+ let continueConnecting = true;
508
+ while (continueConnecting) {
509
+ console.log(` ${c.bold("Available connections:")}\n`);
510
+ console.log(` ${c.highlight("1.")} Anthropic API key — Pull Claude usage stats`);
511
+ console.log(` ${c.highlight("2.")} OpenAI API key — Pull GPT/ChatGPT usage stats`);
512
+ console.log(` ${c.highlight("3.")} Skip for now\n`);
513
+ console.log(c.dim(` You can connect accounts later with 'jobarbiter tokens connect'\n`));
514
+ const choice = await prompt.question(` Your choice ${c.dim("[1/2/3]")}: `);
515
+ if (choice === "3" || choice.toLowerCase() === "skip" || choice === "") {
516
+ console.log(`\n${c.dim(" Skipped — you can connect providers later with 'jobarbiter tokens connect'")}\n`);
517
+ continueConnecting = false;
518
+ }
519
+ else if (choice === "1") {
520
+ await connectProvider(prompt, "anthropic", "Anthropic");
521
+ // Ask if they want to connect another
522
+ continueConnecting = await prompt.confirm(`\n Connect another provider?`, false);
523
+ console.log();
524
+ }
525
+ else if (choice === "2") {
526
+ await connectProvider(prompt, "openai", "OpenAI");
527
+ // Ask if they want to connect another
528
+ continueConnecting = await prompt.confirm(`\n Connect another provider?`, false);
529
+ console.log();
530
+ }
531
+ else {
532
+ console.log(c.error(" Please enter 1, 2, or 3"));
533
+ }
534
+ }
535
+ }
536
+ async function connectProvider(prompt, providerId, providerName) {
537
+ console.log(`\n ${sym.lock} ${c.bold("Privacy Notice")}`);
538
+ console.log(` ${c.dim("─".repeat(50))}`);
539
+ console.log(` Your API key is stored ${c.bold("locally only")} at:`);
540
+ console.log(` ${c.dim("~/.config/jobarbiter/providers.json")}\n`);
541
+ console.log(` ${sym.bullet} Keys are ${c.bold("NEVER")} sent to JobArbiter's servers`);
542
+ console.log(` ${sym.bullet} Only aggregate stats (token counts, model usage) are submitted`);
543
+ console.log(` ${sym.bullet} Revoke anytime: ${c.highlight(`jobarbiter tokens disconnect ${providerId}`)}`);
544
+ console.log(` ${c.dim("─".repeat(50))}\n`);
545
+ const apiKey = await prompt.question(` ${providerName} API key: `);
546
+ if (!apiKey || apiKey.trim() === "") {
547
+ console.log(` ${c.dim("Skipped")}`);
548
+ return;
549
+ }
550
+ console.log(c.dim("\n Validating API key..."));
551
+ const result = await validateProviderKey(providerId, apiKey.trim());
552
+ if (result.valid) {
553
+ saveProviderKey(providerId, apiKey.trim());
554
+ console.log(` ${sym.check} ${c.success(`${providerName} connected`)} — ${result.summary}`);
555
+ }
556
+ else {
557
+ console.log(` ${sym.cross} ${c.error(`Invalid key: ${result.error}`)}`);
558
+ console.log(` ${c.dim("You can try again later with 'jobarbiter tokens connect'")}`);
559
+ }
560
+ }
426
561
  function showWorkerCompletion(state) {
427
- console.log(`${sym.done} ${c.bold("Step 6/6 — You're In!")}\n`);
562
+ console.log(`${sym.done} ${c.bold("Step 7/7 — You're In!")}\n`);
428
563
  console.log(`Your profile is live. Here's what happens next:\n`);
429
564
  console.log(` šŸ“Š Your proficiency score builds automatically from:`);
430
565
  console.log(` ${sym.bullet} Coding agent observation ${c.dim("(biggest factor — 35%)")}`);
@@ -0,0 +1,48 @@
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
+ export interface ProviderConfig {
9
+ provider: string;
10
+ apiKey: string;
11
+ connectedAt: string;
12
+ lastSync?: string;
13
+ }
14
+ export interface ProvidersFile {
15
+ version: number;
16
+ providers: ProviderConfig[];
17
+ }
18
+ export interface ValidationResult {
19
+ valid: boolean;
20
+ error?: string;
21
+ summary?: string;
22
+ }
23
+ export declare function getProvidersPath(): string;
24
+ export declare function loadProviderKeys(): ProviderConfig[];
25
+ export declare function saveProviderKey(provider: string, apiKey: string): void;
26
+ export declare function removeProviderKey(provider: string): boolean;
27
+ export declare function getProviderKey(provider: string): ProviderConfig | null;
28
+ /**
29
+ * Validate an Anthropic API key by making a test API call.
30
+ * Returns validation result with usage summary if available.
31
+ */
32
+ export declare function validateAnthropicKey(apiKey: string): Promise<ValidationResult>;
33
+ /**
34
+ * Validate an OpenAI API key by making a test API call.
35
+ * Returns validation result with usage summary if available.
36
+ */
37
+ export declare function validateOpenAIKey(apiKey: string): Promise<ValidationResult>;
38
+ /**
39
+ * Get list of supported providers.
40
+ */
41
+ export declare function getSupportedProviders(): Array<{
42
+ id: string;
43
+ name: string;
44
+ }>;
45
+ /**
46
+ * Validate a key for any supported provider.
47
+ */
48
+ export declare function validateProviderKey(provider: string, apiKey: string): Promise<ValidationResult>;