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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobarbiter",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "CLI for JobArbiter — the first AI Proficiency Marketplace",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -781,10 +781,10 @@ program
781
781
  });
782
782
 
783
783
  // ============================================================
784
- // observe (manage coding agent observers)
784
+ // observe (manage AI agent observers)
785
785
  // ============================================================
786
786
 
787
- const observe = program.command("observe").description("Manage coding agent proficiency observers");
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🔍 Coding Agent Observers\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 coding agents")
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 coding agents detected on this system.");
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 coding agents")
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 coding agents normally — data accumulates automatically.\n");
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");
@@ -15,7 +15,7 @@ import { execSync } from "node:child_process";
15
15
 
16
16
  // ── Types ──────────────────────────────────────────────────────────────
17
17
 
18
- export type ToolCategory = "coding-agent" | "chat" | "orchestration" | "api-provider";
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 Coding Agents
49
+ // AI AI Agents
50
50
  {
51
51
  id: "claude-code",
52
52
  name: "Claude Code",
53
- category: "coding-agent",
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: "coding-agent",
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: "coding-agent",
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: "coding-agent",
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: "coding-agent",
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: "coding-agent",
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: "coding-agent",
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: "coding-agent",
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: "coding-agent",
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: "coding-agent",
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: "coding-agent",
137
+ category: "ai-agent",
138
138
  binary: "gemini",
139
139
  configDir: join(homedir(), ".gemini"),
140
140
  observerAvailable: true,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JobArbiter Observer — Hook installer for coding agent CLIs
2
+ * JobArbiter Observer — Hook installer for AI agent CLIs
3
3
  *
4
4
  * Installs observation hooks that extract proficiency signals from
5
5
  * session transcripts. Uses detect-tools.ts for agent detection.
@@ -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 coding agent.
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 coding agent sessions.
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 coding agent
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\`