jobarbiter 0.3.3 ā 0.3.5
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/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 +114 -28
- 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 +1 -1
- package/src/lib/detect-tools.ts +13 -13
- package/src/lib/observe.ts +4 -4
- package/src/lib/onboard.ts +132 -28
- package/src/lib/providers.ts +205 -0
package/dist/index.js
CHANGED
|
@@ -743,9 +743,9 @@ program
|
|
|
743
743
|
}
|
|
744
744
|
});
|
|
745
745
|
// ============================================================
|
|
746
|
-
// observe (manage
|
|
746
|
+
// observe (manage AI agent observers)
|
|
747
747
|
// ============================================================
|
|
748
|
-
const observe = program.command("observe").description("Manage
|
|
748
|
+
const observe = program.command("observe").description("Manage AI tool 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š
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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");
|
|
@@ -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 = "
|
|
10
|
+
export type ToolCategory = "ai-agent" | "chat" | "orchestration" | "api-provider";
|
|
11
11
|
export interface DetectedTool {
|
|
12
12
|
id: string;
|
|
13
13
|
name: string;
|
package/dist/lib/detect-tools.js
CHANGED
|
@@ -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
|
|
16
|
+
// AI AI Agents
|
|
17
17
|
{
|
|
18
18
|
id: "claude-code",
|
|
19
19
|
name: "Claude Code",
|
|
20
|
-
category: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
104
|
+
category: "ai-agent",
|
|
105
105
|
binary: "gemini",
|
|
106
106
|
configDir: join(homedir(), ".gemini"),
|
|
107
107
|
observerAvailable: true,
|
package/dist/lib/observe.d.ts
CHANGED
package/dist/lib/observe.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JobArbiter Observer ā Hook installer for
|
|
2
|
+
* JobArbiter Observer ā Hook installer for AI tools
|
|
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
|
|
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
|
|
78
|
+
* Extracts proficiency signals from AI tool 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
|
|
108
|
+
// Silent failure ā never block the AI tool
|
|
109
109
|
fs.appendFileSync(
|
|
110
110
|
path.join(os.homedir(), ".config", "jobarbiter", "observer", "errors.log"),
|
|
111
111
|
\`[\${new Date().toISOString()}] \${err.message}\\n\`
|
package/dist/lib/onboard.js
CHANGED
|
@@ -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",
|
|
@@ -103,7 +104,7 @@ export async function runOnboardWizard(opts) {
|
|
|
103
104
|
}
|
|
104
105
|
// Onboarding incomplete ā resume
|
|
105
106
|
const resumeStep = (existingConfig.onboardingStep ?? 1) + 1;
|
|
106
|
-
console.log(`\n${sym.rocket} ${c.bold("Resuming onboarding")} from step ${resumeStep}/
|
|
107
|
+
console.log(`\n${sym.rocket} ${c.bold("Resuming onboarding")} from step ${resumeStep}/7\n`);
|
|
107
108
|
console.log(c.dim(` Account: ${existingConfig.userType} | API key configured`));
|
|
108
109
|
console.log(c.dim(` Run ${c.highlight("jobarbiter onboard --force")} to start over.\n`));
|
|
109
110
|
const prompt = new Prompt();
|
|
@@ -199,9 +200,9 @@ async function selectUserType(prompt) {
|
|
|
199
200
|
}
|
|
200
201
|
// āā Email & Verification āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
201
202
|
async function handleEmailVerification(prompt, baseUrl, userType) {
|
|
202
|
-
// Workers: 1) Account, 2) Tool Detection, 3)
|
|
203
|
-
// Employers: 1) Account, 2) (skip verification), 3) Company, 4) Domain, 5) What You Need, 6) Done
|
|
204
|
-
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;
|
|
205
206
|
console.log(`\n${sym.email} ${c.bold(`Step 1/${totalSteps} ā Create Your Account`)}\n`);
|
|
206
207
|
// Get email
|
|
207
208
|
let email;
|
|
@@ -286,9 +287,14 @@ async function runWorkerFlow(prompt, state, startStep = 2) {
|
|
|
286
287
|
state.tools = detectedToolsResult.tools;
|
|
287
288
|
saveProgress(2);
|
|
288
289
|
}
|
|
289
|
-
// Step 3:
|
|
290
|
+
// Step 3: Connect AI Accounts (optional)
|
|
290
291
|
if (startStep <= 3) {
|
|
291
|
-
|
|
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`);
|
|
292
298
|
console.log(`What domains do you work in? ${c.dim("(comma-separated)")}`);
|
|
293
299
|
console.log(c.dim("Examples: full-stack dev, data engineering, trading, content creation\n"));
|
|
294
300
|
const domainsInput = await prompt.question(`${sym.arrow} `);
|
|
@@ -308,11 +314,11 @@ async function runWorkerFlow(prompt, state, startStep = 2) {
|
|
|
308
314
|
catch (err) {
|
|
309
315
|
console.log(`${sym.warning} ${c.warning("Could not save profile details ā you can update later with 'jobarbiter profile create'")}\n`);
|
|
310
316
|
}
|
|
311
|
-
saveProgress(
|
|
317
|
+
saveProgress(4);
|
|
312
318
|
}
|
|
313
|
-
// Step
|
|
314
|
-
if (startStep <=
|
|
315
|
-
console.log(`${sym.link} ${c.bold("Step
|
|
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`);
|
|
316
322
|
console.log(`Connecting your GitHub lets us analyze your AI-assisted work patterns.`);
|
|
317
323
|
console.log(`This significantly boosts your proficiency score.\n`);
|
|
318
324
|
const githubUsername = await prompt.question(`GitHub username ${c.dim("(press Enter to skip)")}: `);
|
|
@@ -333,11 +339,11 @@ async function runWorkerFlow(prompt, state, startStep = 2) {
|
|
|
333
339
|
else {
|
|
334
340
|
console.log(`${c.dim("Skipped ā you can connect later with 'jobarbiter git connect'")}\n`);
|
|
335
341
|
}
|
|
336
|
-
saveProgress(
|
|
342
|
+
saveProgress(5);
|
|
337
343
|
}
|
|
338
|
-
// Step
|
|
339
|
-
if (startStep <=
|
|
340
|
-
console.log(`${sym.link} ${c.bold("Step
|
|
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`);
|
|
341
347
|
console.log(`Your LinkedIn profile strengthens identity verification.`);
|
|
342
348
|
console.log(c.dim("We never post on your behalf or access your connections.\n"));
|
|
343
349
|
const linkedinUrl = await prompt.question(`LinkedIn URL ${c.dim("(press Enter to skip)")}: `);
|
|
@@ -356,21 +362,21 @@ async function runWorkerFlow(prompt, state, startStep = 2) {
|
|
|
356
362
|
else {
|
|
357
363
|
console.log(`${c.dim("Skipped ā you can connect later with 'jobarbiter identity linkedin <url>'")}\n`);
|
|
358
364
|
}
|
|
359
|
-
saveProgress(
|
|
365
|
+
saveProgress(6);
|
|
360
366
|
}
|
|
361
|
-
// Step
|
|
362
|
-
saveConfig({ ...config, onboardingComplete: true, onboardingStep:
|
|
367
|
+
// Step 7: Done!
|
|
368
|
+
saveConfig({ ...config, onboardingComplete: true, onboardingStep: 7 });
|
|
363
369
|
showWorkerCompletion(state);
|
|
364
370
|
}
|
|
365
371
|
// āā Tool Detection Step āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
366
372
|
async function runToolDetectionStep(prompt, config) {
|
|
367
|
-
console.log(`š ${c.bold("Step 2/
|
|
373
|
+
console.log(`š ${c.bold("Step 2/7 ā Detecting AI Tools")}\n`);
|
|
368
374
|
console.log(c.dim(" Scanning your machine...\n"));
|
|
369
375
|
const allTools = detectAllTools();
|
|
370
376
|
const installed = allTools.filter((t) => t.installed);
|
|
371
|
-
const notInstalled = allTools.filter((t) => !t.installed && t.category === "
|
|
377
|
+
const notInstalled = allTools.filter((t) => !t.installed && t.category === "ai-agent");
|
|
372
378
|
// Group by category
|
|
373
|
-
const codingAgents = installed.filter((t) => t.category === "
|
|
379
|
+
const codingAgents = installed.filter((t) => t.category === "ai-agent");
|
|
374
380
|
const chatTools = installed.filter((t) => t.category === "chat");
|
|
375
381
|
const orchestration = installed.filter((t) => t.category === "orchestration");
|
|
376
382
|
const apiProviders = installed.filter((t) => t.category === "api-provider");
|
|
@@ -381,7 +387,7 @@ async function runToolDetectionStep(prompt, config) {
|
|
|
381
387
|
return { tools: [] };
|
|
382
388
|
}
|
|
383
389
|
console.log(` ${c.bold("Found:")}`);
|
|
384
|
-
// Show
|
|
390
|
+
// Show AI agents with observer status
|
|
385
391
|
for (const tool of codingAgents) {
|
|
386
392
|
const display = formatToolDisplay(tool);
|
|
387
393
|
if (tool.observerAvailable) {
|
|
@@ -406,7 +412,7 @@ async function runToolDetectionStep(prompt, config) {
|
|
|
406
412
|
for (const tool of apiProviders) {
|
|
407
413
|
console.log(` ${sym.check} ${tool.name} ${c.dim("configured")}`);
|
|
408
414
|
}
|
|
409
|
-
// Show not-detected
|
|
415
|
+
// Show not-detected AI agents
|
|
410
416
|
if (notInstalled.length > 0) {
|
|
411
417
|
console.log(`\n ${c.dim("Not detected (install to track):")}`);
|
|
412
418
|
for (const tool of notInstalled.slice(0, 5)) {
|
|
@@ -418,11 +424,11 @@ async function runToolDetectionStep(prompt, config) {
|
|
|
418
424
|
}
|
|
419
425
|
// Collect tool names for profile
|
|
420
426
|
const toolNames = installed.map((t) => t.name);
|
|
421
|
-
// Observer installation for
|
|
427
|
+
// Observer installation for AI agents
|
|
422
428
|
const needsObserver = codingAgents.filter((t) => t.observerAvailable && !t.observerActive);
|
|
423
429
|
if (needsObserver.length > 0) {
|
|
424
430
|
console.log(`\n ${c.bold("Observers")}`);
|
|
425
|
-
console.log(` JobArbiter observes your
|
|
431
|
+
console.log(` JobArbiter observes your AI sessions to build your`);
|
|
426
432
|
console.log(` proficiency profile. ${c.bold("No code or prompts leave your machine")} ā`);
|
|
427
433
|
console.log(` only aggregate scores (tool usage, session counts, token volume).\n`);
|
|
428
434
|
console.log(c.dim(` Data stored locally: ~/.config/jobarbiter/observer/observations.json`));
|
|
@@ -473,20 +479,100 @@ async function runToolDetectionStep(prompt, config) {
|
|
|
473
479
|
}
|
|
474
480
|
return { tools: toolNames };
|
|
475
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
|
+
}
|
|
476
561
|
function showWorkerCompletion(state) {
|
|
477
|
-
console.log(`${sym.done} ${c.bold("Step
|
|
562
|
+
console.log(`${sym.done} ${c.bold("Step 7/7 ā You're In!")}\n`);
|
|
478
563
|
console.log(`Your profile is live. Here's what happens next:\n`);
|
|
479
564
|
console.log(` š Your proficiency score builds automatically from:`);
|
|
480
|
-
console.log(` ${sym.bullet}
|
|
565
|
+
console.log(` ${sym.bullet} AI tool observation ${c.dim("(biggest factor ā 35%)")}`);
|
|
481
566
|
console.log(` ${sym.bullet} GitHub contribution analysis ${c.dim("(20%)")}`);
|
|
482
567
|
console.log(` ${sym.bullet} Token consumption patterns ${c.dim("(15%)")}`);
|
|
483
568
|
console.log(` ${sym.bullet} Tool diversity & fluency ${c.dim("(15%)")}`);
|
|
484
569
|
console.log(` ${sym.bullet} Outcome verification ${c.dim("(15%)")}\n`);
|
|
485
570
|
console.log(` šÆ Your proficiency ${c.bold("track")} (Orchestrator, Systems Builder, or`);
|
|
486
571
|
console.log(` Domain Translator) is determined automatically as we observe`);
|
|
487
|
-
console.log(` how you work
|
|
572
|
+
console.log(` how you work ā whether that's coding, research, automation,`);
|
|
573
|
+
console.log(` content creation, or anything else. No need to self-assess.\n`);
|
|
488
574
|
console.log(` š¤ For deeper attestation, install the ${c.highlight("jobarbiter-proficiency")}`);
|
|
489
|
-
console.log(` skill in your AI
|
|
575
|
+
console.log(` skill in your AI tools (OpenClaw, Claude Code, etc.)\n`);
|
|
490
576
|
console.log(` ${c.bold("Useful commands:")}`);
|
|
491
577
|
console.log(` ${c.highlight("jobarbiter profile score")} ā Check your proficiency score`);
|
|
492
578
|
console.log(` ${c.highlight("jobarbiter observe status")} ā See collected observation data`);
|
|
@@ -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>;
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
// āā Paths āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
12
|
+
const CONFIG_DIR = join(homedir(), ".config", "jobarbiter");
|
|
13
|
+
const PROVIDERS_FILE = join(CONFIG_DIR, "providers.json");
|
|
14
|
+
export function getProvidersPath() {
|
|
15
|
+
return PROVIDERS_FILE;
|
|
16
|
+
}
|
|
17
|
+
// āā Load / Save āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
18
|
+
export function loadProviderKeys() {
|
|
19
|
+
if (!existsSync(PROVIDERS_FILE)) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const raw = readFileSync(PROVIDERS_FILE, "utf-8");
|
|
24
|
+
const data = JSON.parse(raw);
|
|
25
|
+
return data.providers || [];
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function saveProviderKey(provider, apiKey) {
|
|
32
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
33
|
+
const existing = loadProviderKeys();
|
|
34
|
+
// Remove any existing entry for this provider
|
|
35
|
+
const filtered = existing.filter((p) => p.provider !== provider);
|
|
36
|
+
// Add new entry
|
|
37
|
+
filtered.push({
|
|
38
|
+
provider,
|
|
39
|
+
apiKey,
|
|
40
|
+
connectedAt: new Date().toISOString(),
|
|
41
|
+
});
|
|
42
|
+
const data = {
|
|
43
|
+
version: 1,
|
|
44
|
+
providers: filtered,
|
|
45
|
+
};
|
|
46
|
+
writeFileSync(PROVIDERS_FILE, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
47
|
+
}
|
|
48
|
+
export function removeProviderKey(provider) {
|
|
49
|
+
const existing = loadProviderKeys();
|
|
50
|
+
const filtered = existing.filter((p) => p.provider !== provider);
|
|
51
|
+
if (filtered.length === existing.length) {
|
|
52
|
+
return false; // Provider not found
|
|
53
|
+
}
|
|
54
|
+
const data = {
|
|
55
|
+
version: 1,
|
|
56
|
+
providers: filtered,
|
|
57
|
+
};
|
|
58
|
+
writeFileSync(PROVIDERS_FILE, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
export function getProviderKey(provider) {
|
|
62
|
+
const providers = loadProviderKeys();
|
|
63
|
+
return providers.find((p) => p.provider === provider) || null;
|
|
64
|
+
}
|
|
65
|
+
// āā Validation āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
66
|
+
/**
|
|
67
|
+
* Validate an Anthropic API key by making a test API call.
|
|
68
|
+
* Returns validation result with usage summary if available.
|
|
69
|
+
*/
|
|
70
|
+
export async function validateAnthropicKey(apiKey) {
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch("https://api.anthropic.com/v1/models", {
|
|
73
|
+
method: "GET",
|
|
74
|
+
headers: {
|
|
75
|
+
"x-api-key": apiKey,
|
|
76
|
+
"anthropic-version": "2023-06-01",
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
if (response.status === 401) {
|
|
80
|
+
return { valid: false, error: "Invalid API key" };
|
|
81
|
+
}
|
|
82
|
+
if (response.status === 403) {
|
|
83
|
+
return { valid: false, error: "API key doesn't have required permissions" };
|
|
84
|
+
}
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
return { valid: false, error: `API error: ${response.status}` };
|
|
87
|
+
}
|
|
88
|
+
// Key is valid - we can't get usage from this endpoint,
|
|
89
|
+
// but we can confirm the key works
|
|
90
|
+
return {
|
|
91
|
+
valid: true,
|
|
92
|
+
summary: "API key validated",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (err instanceof Error) {
|
|
97
|
+
return { valid: false, error: `Connection error: ${err.message}` };
|
|
98
|
+
}
|
|
99
|
+
return { valid: false, error: "Unknown error" };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Validate an OpenAI API key by making a test API call.
|
|
104
|
+
* Returns validation result with usage summary if available.
|
|
105
|
+
*/
|
|
106
|
+
export async function validateOpenAIKey(apiKey) {
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch("https://api.openai.com/v1/models", {
|
|
109
|
+
method: "GET",
|
|
110
|
+
headers: {
|
|
111
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
if (response.status === 401) {
|
|
115
|
+
return { valid: false, error: "Invalid API key" };
|
|
116
|
+
}
|
|
117
|
+
if (response.status === 403) {
|
|
118
|
+
return { valid: false, error: "API key doesn't have required permissions" };
|
|
119
|
+
}
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
return { valid: false, error: `API error: ${response.status}` };
|
|
122
|
+
}
|
|
123
|
+
// Key is valid
|
|
124
|
+
return {
|
|
125
|
+
valid: true,
|
|
126
|
+
summary: "API key validated",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
if (err instanceof Error) {
|
|
131
|
+
return { valid: false, error: `Connection error: ${err.message}` };
|
|
132
|
+
}
|
|
133
|
+
return { valid: false, error: "Unknown error" };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get list of supported providers.
|
|
138
|
+
*/
|
|
139
|
+
export function getSupportedProviders() {
|
|
140
|
+
return [
|
|
141
|
+
{ id: "anthropic", name: "Anthropic" },
|
|
142
|
+
{ id: "openai", name: "OpenAI" },
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Validate a key for any supported provider.
|
|
147
|
+
*/
|
|
148
|
+
export async function validateProviderKey(provider, apiKey) {
|
|
149
|
+
switch (provider) {
|
|
150
|
+
case "anthropic":
|
|
151
|
+
return validateAnthropicKey(apiKey);
|
|
152
|
+
case "openai":
|
|
153
|
+
return validateOpenAIKey(apiKey);
|
|
154
|
+
default:
|
|
155
|
+
return { valid: false, error: `Unknown provider: ${provider}` };
|
|
156
|
+
}
|
|
157
|
+
}
|