jobarbiter 0.3.3 ā 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/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 +110 -25
- 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 +128 -25
- 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 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š
|
|
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 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
|
|
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 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
|
|
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\`
|
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,8 +479,87 @@ 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
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>;
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -781,10 +781,10 @@ program
|
|
|
781
781
|
});
|
|
782
782
|
|
|
783
783
|
// ============================================================
|
|
784
|
-
// observe (manage
|
|
784
|
+
// observe (manage AI agent observers)
|
|
785
785
|
// ============================================================
|
|
786
786
|
|
|
787
|
-
const observe = program.command("observe").description("Manage
|
|
787
|
+
const observe = program.command("observe").description("Manage AI tool proficiency observers");
|
|
788
788
|
|
|
789
789
|
observe
|
|
790
790
|
.command("status")
|
|
@@ -796,7 +796,7 @@ observe
|
|
|
796
796
|
|
|
797
797
|
const detected = agents.filter((a) => a.installed);
|
|
798
798
|
|
|
799
|
-
console.log("\nš
|
|
799
|
+
console.log("\nš AI Agent Observers\n");
|
|
800
800
|
console.log(" Agents:");
|
|
801
801
|
for (const agent of agents) {
|
|
802
802
|
if (!agent.installed) {
|
|
@@ -839,7 +839,7 @@ observe
|
|
|
839
839
|
|
|
840
840
|
observe
|
|
841
841
|
.command("install")
|
|
842
|
-
.description("Install observers for detected
|
|
842
|
+
.description("Install observers for detected AI agents")
|
|
843
843
|
.option("--agent <id>", "Install for specific agent (claude-code, cursor, opencode, codex, gemini)")
|
|
844
844
|
.option("--all", "Install for all detected agents")
|
|
845
845
|
.action(async (opts) => {
|
|
@@ -848,7 +848,7 @@ observe
|
|
|
848
848
|
const detected = agents.filter((a) => a.installed);
|
|
849
849
|
|
|
850
850
|
if (detected.length === 0) {
|
|
851
|
-
error("No
|
|
851
|
+
error("No AI agents detected on this system.");
|
|
852
852
|
console.log(" Supported: Claude Code, Cursor, OpenCode, Codex CLI, Gemini CLI");
|
|
853
853
|
process.exit(1);
|
|
854
854
|
}
|
|
@@ -890,7 +890,7 @@ observe
|
|
|
890
890
|
|
|
891
891
|
observe
|
|
892
892
|
.command("remove")
|
|
893
|
-
.description("Remove observers from
|
|
893
|
+
.description("Remove observers from AI agents")
|
|
894
894
|
.option("--agent <id>", "Remove from specific agent")
|
|
895
895
|
.option("--all", "Remove from all agents")
|
|
896
896
|
.action(async (opts) => {
|
|
@@ -935,7 +935,7 @@ observe
|
|
|
935
935
|
|
|
936
936
|
if (!status.hasData) {
|
|
937
937
|
console.log("\nNo observation data collected yet.");
|
|
938
|
-
console.log("Use your
|
|
938
|
+
console.log("Use your AI agents normally ā data accumulates automatically.\n");
|
|
939
939
|
process.exit(0);
|
|
940
940
|
}
|
|
941
941
|
|
package/src/lib/config.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface Config {
|
|
|
7
7
|
baseUrl: string;
|
|
8
8
|
userType: "worker" | "employer" | "seeker" | "poster";
|
|
9
9
|
onboardingComplete?: boolean;
|
|
10
|
-
onboardingStep?: number; // last completed step (1-
|
|
10
|
+
onboardingStep?: number; // last completed step (1-7)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const CONFIG_DIR = join(homedir(), ".config", "jobarbiter");
|
package/src/lib/detect-tools.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { execSync } from "node:child_process";
|
|
|
15
15
|
|
|
16
16
|
// āā Types āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
17
17
|
|
|
18
|
-
export type ToolCategory = "
|
|
18
|
+
export type ToolCategory = "ai-agent" | "chat" | "orchestration" | "api-provider";
|
|
19
19
|
|
|
20
20
|
export interface DetectedTool {
|
|
21
21
|
id: string;
|
|
@@ -46,11 +46,11 @@ interface ToolDefinition {
|
|
|
46
46
|
// āā Tool Definitions āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
47
47
|
|
|
48
48
|
const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
49
|
-
// AI
|
|
49
|
+
// AI AI Agents
|
|
50
50
|
{
|
|
51
51
|
id: "claude-code",
|
|
52
52
|
name: "Claude Code",
|
|
53
|
-
category: "
|
|
53
|
+
category: "ai-agent",
|
|
54
54
|
binary: "claude",
|
|
55
55
|
configDir: join(homedir(), ".claude"),
|
|
56
56
|
observerAvailable: true,
|
|
@@ -58,7 +58,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
58
58
|
{
|
|
59
59
|
id: "cursor",
|
|
60
60
|
name: "Cursor",
|
|
61
|
-
category: "
|
|
61
|
+
category: "ai-agent",
|
|
62
62
|
binary: "cursor",
|
|
63
63
|
configDir: join(homedir(), ".cursor"),
|
|
64
64
|
macApp: "/Applications/Cursor.app",
|
|
@@ -67,7 +67,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
67
67
|
{
|
|
68
68
|
id: "github-copilot",
|
|
69
69
|
name: "GitHub Copilot",
|
|
70
|
-
category: "
|
|
70
|
+
category: "ai-agent",
|
|
71
71
|
configDir: join(homedir(), ".config", "github-copilot"),
|
|
72
72
|
vscodeExtension: "github.copilot",
|
|
73
73
|
cursorExtension: "github.copilot",
|
|
@@ -76,7 +76,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
76
76
|
{
|
|
77
77
|
id: "codex",
|
|
78
78
|
name: "Codex CLI",
|
|
79
|
-
category: "
|
|
79
|
+
category: "ai-agent",
|
|
80
80
|
binary: "codex",
|
|
81
81
|
configDir: join(homedir(), ".codex"),
|
|
82
82
|
observerAvailable: true,
|
|
@@ -84,7 +84,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
84
84
|
{
|
|
85
85
|
id: "opencode",
|
|
86
86
|
name: "OpenCode",
|
|
87
|
-
category: "
|
|
87
|
+
category: "ai-agent",
|
|
88
88
|
binary: "opencode",
|
|
89
89
|
configDir: join(homedir(), ".config", "opencode"),
|
|
90
90
|
observerAvailable: true,
|
|
@@ -92,7 +92,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
92
92
|
{
|
|
93
93
|
id: "aider",
|
|
94
94
|
name: "Aider",
|
|
95
|
-
category: "
|
|
95
|
+
category: "ai-agent",
|
|
96
96
|
binary: "aider",
|
|
97
97
|
configDir: join(homedir(), ".aider"),
|
|
98
98
|
pipPackage: "aider-chat",
|
|
@@ -101,7 +101,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
101
101
|
{
|
|
102
102
|
id: "continue",
|
|
103
103
|
name: "Continue",
|
|
104
|
-
category: "
|
|
104
|
+
category: "ai-agent",
|
|
105
105
|
vscodeExtension: "continue.continue",
|
|
106
106
|
cursorExtension: "continue.continue",
|
|
107
107
|
observerAvailable: false,
|
|
@@ -109,7 +109,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
109
109
|
{
|
|
110
110
|
id: "cline",
|
|
111
111
|
name: "Cline",
|
|
112
|
-
category: "
|
|
112
|
+
category: "ai-agent",
|
|
113
113
|
vscodeExtension: "saoudrizwan.claude-dev",
|
|
114
114
|
cursorExtension: "saoudrizwan.claude-dev",
|
|
115
115
|
observerAvailable: false,
|
|
@@ -117,7 +117,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
117
117
|
{
|
|
118
118
|
id: "windsurf",
|
|
119
119
|
name: "Windsurf",
|
|
120
|
-
category: "
|
|
120
|
+
category: "ai-agent",
|
|
121
121
|
binary: "windsurf",
|
|
122
122
|
macApp: "/Applications/Windsurf.app",
|
|
123
123
|
observerAvailable: false,
|
|
@@ -125,7 +125,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
125
125
|
{
|
|
126
126
|
id: "letta",
|
|
127
127
|
name: "Letta Code",
|
|
128
|
-
category: "
|
|
128
|
+
category: "ai-agent",
|
|
129
129
|
binary: "letta",
|
|
130
130
|
configDir: join(homedir(), ".letta"),
|
|
131
131
|
pipPackage: "letta",
|
|
@@ -134,7 +134,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
134
134
|
{
|
|
135
135
|
id: "gemini",
|
|
136
136
|
name: "Gemini CLI",
|
|
137
|
-
category: "
|
|
137
|
+
category: "ai-agent",
|
|
138
138
|
binary: "gemini",
|
|
139
139
|
configDir: join(homedir(), ".gemini"),
|
|
140
140
|
observerAvailable: true,
|
package/src/lib/observe.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JobArbiter Observer ā Hook installer for
|
|
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.
|
|
@@ -98,7 +98,7 @@ function ensureObserverDirs(): void {
|
|
|
98
98
|
// āā Core Observer Script āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* The universal observer script. Runs as a hook in any
|
|
101
|
+
* The universal observer script. Runs as a hook in any AI agent.
|
|
102
102
|
* Reads session transcript data from stdin (JSON), extracts proficiency
|
|
103
103
|
* signals, and appends them to the local observations file.
|
|
104
104
|
*
|
|
@@ -109,7 +109,7 @@ function getObserverScript(): string {
|
|
|
109
109
|
return `#!/usr/bin/env node
|
|
110
110
|
/**
|
|
111
111
|
* JobArbiter Observer Hook
|
|
112
|
-
* Extracts proficiency signals from
|
|
112
|
+
* Extracts proficiency signals from AI agent sessions.
|
|
113
113
|
*
|
|
114
114
|
* Reads JSON from stdin, writes observations to:
|
|
115
115
|
* ~/.config/jobarbiter/observer/observations.json
|
|
@@ -139,7 +139,7 @@ process.stdin.on("end", () => {
|
|
|
139
139
|
const observation = extractSignals(data);
|
|
140
140
|
if (observation) appendObservation(observation);
|
|
141
141
|
} catch (err) {
|
|
142
|
-
// Silent failure ā never block the
|
|
142
|
+
// Silent failure ā never block the AI agent
|
|
143
143
|
fs.appendFileSync(
|
|
144
144
|
path.join(os.homedir(), ".config", "jobarbiter", "observer", "errors.log"),
|
|
145
145
|
\`[\${new Date().toISOString()}] \${err.message}\\n\`
|
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
|
|
|
@@ -150,7 +156,7 @@ export async function runOnboardWizard(opts: { force?: boolean; baseUrl?: string
|
|
|
150
156
|
}
|
|
151
157
|
// Onboarding incomplete ā resume
|
|
152
158
|
const resumeStep = (existingConfig.onboardingStep ?? 1) + 1;
|
|
153
|
-
console.log(`\n${sym.rocket} ${c.bold("Resuming onboarding")} from step ${resumeStep}/
|
|
159
|
+
console.log(`\n${sym.rocket} ${c.bold("Resuming onboarding")} from step ${resumeStep}/7\n`);
|
|
154
160
|
console.log(c.dim(` Account: ${existingConfig.userType} | API key configured`));
|
|
155
161
|
console.log(c.dim(` Run ${c.highlight("jobarbiter onboard --force")} to start over.\n`));
|
|
156
162
|
|
|
@@ -257,9 +263,9 @@ async function handleEmailVerification(
|
|
|
257
263
|
baseUrl: string,
|
|
258
264
|
userType: "worker" | "employer"
|
|
259
265
|
): Promise<{ email: string; apiKey: string; userId: string }> {
|
|
260
|
-
// Workers: 1) Account, 2) Tool Detection, 3)
|
|
261
|
-
// Employers: 1) Account, 2) (skip verification), 3) Company, 4) Domain, 5) What You Need, 6) Done
|
|
262
|
-
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;
|
|
263
269
|
|
|
264
270
|
console.log(`\n${sym.email} ${c.bold(`Step 1/${totalSteps} ā Create Your Account`)}\n`);
|
|
265
271
|
|
|
@@ -357,9 +363,15 @@ async function runWorkerFlow(prompt: Prompt, state: OnboardState, startStep = 2)
|
|
|
357
363
|
saveProgress(2);
|
|
358
364
|
}
|
|
359
365
|
|
|
360
|
-
// Step 3:
|
|
366
|
+
// Step 3: Connect AI Accounts (optional)
|
|
361
367
|
if (startStep <= 3) {
|
|
362
|
-
|
|
368
|
+
await runConnectAIAccountsStep(prompt);
|
|
369
|
+
saveProgress(3);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Step 4: Domains
|
|
373
|
+
if (startStep <= 4) {
|
|
374
|
+
console.log(`${sym.target} ${c.bold("Step 4/7 ā Your Domains")}\n`);
|
|
363
375
|
console.log(`What domains do you work in? ${c.dim("(comma-separated)")}`);
|
|
364
376
|
console.log(c.dim("Examples: full-stack dev, data engineering, trading, content creation\n"));
|
|
365
377
|
const domainsInput = await prompt.question(`${sym.arrow} `);
|
|
@@ -380,12 +392,12 @@ async function runWorkerFlow(prompt: Prompt, state: OnboardState, startStep = 2)
|
|
|
380
392
|
} catch (err) {
|
|
381
393
|
console.log(`${sym.warning} ${c.warning("Could not save profile details ā you can update later with 'jobarbiter profile create'")}\n`);
|
|
382
394
|
}
|
|
383
|
-
saveProgress(
|
|
395
|
+
saveProgress(4);
|
|
384
396
|
}
|
|
385
397
|
|
|
386
|
-
// Step
|
|
387
|
-
if (startStep <=
|
|
388
|
-
console.log(`${sym.link} ${c.bold("Step
|
|
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`);
|
|
389
401
|
console.log(`Connecting your GitHub lets us analyze your AI-assisted work patterns.`);
|
|
390
402
|
console.log(`This significantly boosts your proficiency score.\n`);
|
|
391
403
|
|
|
@@ -406,12 +418,12 @@ async function runWorkerFlow(prompt: Prompt, state: OnboardState, startStep = 2)
|
|
|
406
418
|
} else {
|
|
407
419
|
console.log(`${c.dim("Skipped ā you can connect later with 'jobarbiter git connect'")}\n`);
|
|
408
420
|
}
|
|
409
|
-
saveProgress(
|
|
421
|
+
saveProgress(5);
|
|
410
422
|
}
|
|
411
423
|
|
|
412
|
-
// Step
|
|
413
|
-
if (startStep <=
|
|
414
|
-
console.log(`${sym.link} ${c.bold("Step
|
|
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`);
|
|
415
427
|
console.log(`Your LinkedIn profile strengthens identity verification.`);
|
|
416
428
|
console.log(c.dim("We never post on your behalf or access your connections.\n"));
|
|
417
429
|
|
|
@@ -430,11 +442,11 @@ async function runWorkerFlow(prompt: Prompt, state: OnboardState, startStep = 2)
|
|
|
430
442
|
} else {
|
|
431
443
|
console.log(`${c.dim("Skipped ā you can connect later with 'jobarbiter identity linkedin <url>'")}\n`);
|
|
432
444
|
}
|
|
433
|
-
saveProgress(
|
|
445
|
+
saveProgress(6);
|
|
434
446
|
}
|
|
435
447
|
|
|
436
|
-
// Step
|
|
437
|
-
saveConfig({ ...config, onboardingComplete: true, onboardingStep:
|
|
448
|
+
// Step 7: Done!
|
|
449
|
+
saveConfig({ ...config, onboardingComplete: true, onboardingStep: 7 });
|
|
438
450
|
showWorkerCompletion(state);
|
|
439
451
|
}
|
|
440
452
|
|
|
@@ -444,15 +456,15 @@ async function runToolDetectionStep(
|
|
|
444
456
|
prompt: Prompt,
|
|
445
457
|
config: Config,
|
|
446
458
|
): Promise<{ tools: string[] }> {
|
|
447
|
-
console.log(`š ${c.bold("Step 2/
|
|
459
|
+
console.log(`š ${c.bold("Step 2/7 ā Detecting AI Tools")}\n`);
|
|
448
460
|
console.log(c.dim(" Scanning your machine...\n"));
|
|
449
461
|
|
|
450
462
|
const allTools = detectAllTools();
|
|
451
463
|
const installed = allTools.filter((t) => t.installed);
|
|
452
|
-
const notInstalled = allTools.filter((t) => !t.installed && t.category === "
|
|
464
|
+
const notInstalled = allTools.filter((t) => !t.installed && t.category === "ai-agent");
|
|
453
465
|
|
|
454
466
|
// Group by category
|
|
455
|
-
const codingAgents = installed.filter((t) => t.category === "
|
|
467
|
+
const codingAgents = installed.filter((t) => t.category === "ai-agent");
|
|
456
468
|
const chatTools = installed.filter((t) => t.category === "chat");
|
|
457
469
|
const orchestration = installed.filter((t) => t.category === "orchestration");
|
|
458
470
|
const apiProviders = installed.filter((t) => t.category === "api-provider");
|
|
@@ -466,7 +478,7 @@ async function runToolDetectionStep(
|
|
|
466
478
|
|
|
467
479
|
console.log(` ${c.bold("Found:")}`);
|
|
468
480
|
|
|
469
|
-
// Show
|
|
481
|
+
// Show AI agents with observer status
|
|
470
482
|
for (const tool of codingAgents) {
|
|
471
483
|
const display = formatToolDisplay(tool);
|
|
472
484
|
if (tool.observerAvailable) {
|
|
@@ -491,7 +503,7 @@ async function runToolDetectionStep(
|
|
|
491
503
|
console.log(` ${sym.check} ${tool.name} ${c.dim("configured")}`);
|
|
492
504
|
}
|
|
493
505
|
|
|
494
|
-
// Show not-detected
|
|
506
|
+
// Show not-detected AI agents
|
|
495
507
|
if (notInstalled.length > 0) {
|
|
496
508
|
console.log(`\n ${c.dim("Not detected (install to track):")}`);
|
|
497
509
|
for (const tool of notInstalled.slice(0, 5)) {
|
|
@@ -505,12 +517,12 @@ async function runToolDetectionStep(
|
|
|
505
517
|
// Collect tool names for profile
|
|
506
518
|
const toolNames = installed.map((t) => t.name);
|
|
507
519
|
|
|
508
|
-
// Observer installation for
|
|
520
|
+
// Observer installation for AI agents
|
|
509
521
|
const needsObserver = codingAgents.filter((t) => t.observerAvailable && !t.observerActive);
|
|
510
522
|
|
|
511
523
|
if (needsObserver.length > 0) {
|
|
512
524
|
console.log(`\n ${c.bold("Observers")}`);
|
|
513
|
-
console.log(` JobArbiter observes your
|
|
525
|
+
console.log(` JobArbiter observes your AI sessions to build your`);
|
|
514
526
|
console.log(` proficiency profile. ${c.bold("No code or prompts leave your machine")} ā`);
|
|
515
527
|
console.log(` only aggregate scores (tool usage, session counts, token volume).\n`);
|
|
516
528
|
console.log(c.dim(` Data stored locally: ~/.config/jobarbiter/observer/observations.json`));
|
|
@@ -567,8 +579,99 @@ async function runToolDetectionStep(
|
|
|
567
579
|
return { tools: toolNames };
|
|
568
580
|
}
|
|
569
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
|
+
|
|
570
673
|
function showWorkerCompletion(state: OnboardState): void {
|
|
571
|
-
console.log(`${sym.done} ${c.bold("Step
|
|
674
|
+
console.log(`${sym.done} ${c.bold("Step 7/7 ā You're In!")}\n`);
|
|
572
675
|
console.log(`Your profile is live. Here's what happens next:\n`);
|
|
573
676
|
|
|
574
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
|
+
}
|