jobarbiter 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +7 -7
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/detect-tools.d.ts +1 -1
- package/dist/lib/detect-tools.js +12 -12
- package/dist/lib/observe.d.ts +1 -1
- package/dist/lib/observe.js +4 -4
- package/dist/lib/onboard.js +211 -76
- package/dist/lib/providers.d.ts +48 -0
- package/dist/lib/providers.js +157 -0
- package/package.json +1 -1
- package/src/index.ts +7 -7
- package/src/lib/config.ts +2 -0
- package/src/lib/detect-tools.ts +13 -13
- package/src/lib/observe.ts +4 -4
- package/src/lib/onboard.ts +228 -75
- package/src/lib/providers.ts +205 -0
|
@@ -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
|
@@ -6,6 +6,8 @@ export interface Config {
|
|
|
6
6
|
apiKey: string;
|
|
7
7
|
baseUrl: string;
|
|
8
8
|
userType: "worker" | "employer" | "seeker" | "poster";
|
|
9
|
+
onboardingComplete?: boolean;
|
|
10
|
+
onboardingStep?: number; // last completed step (1-7)
|
|
9
11
|
}
|
|
10
12
|
|
|
11
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\`
|