jobarbiter 0.3.0 → 0.3.2
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 +172 -43
- package/dist/lib/config.js +1 -1
- package/dist/lib/detect-tools.d.ts +46 -0
- package/dist/lib/detect-tools.js +473 -0
- package/dist/lib/observe.d.ts +52 -0
- package/dist/lib/observe.js +672 -0
- package/dist/lib/onboard.d.ts +13 -0
- package/dist/lib/onboard.js +580 -0
- package/package.json +15 -5
- package/src/index.ts +194 -48
- package/src/lib/config.ts +1 -1
- package/src/lib/detect-tools.ts +526 -0
- package/src/lib/observe.ts +753 -0
- package/src/lib/onboard.ts +694 -0
- package/test/smoke.test.ts +205 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Onboard Wizard
|
|
3
|
+
*
|
|
4
|
+
* A polished, step-by-step wizard that guides users through:
|
|
5
|
+
* - Account creation (email + verification)
|
|
6
|
+
* - User type selection (worker vs employer)
|
|
7
|
+
* - Profile setup (track, tools, domains)
|
|
8
|
+
* - Optional integrations (GitHub)
|
|
9
|
+
*/
|
|
10
|
+
import * as readline from "node:readline";
|
|
11
|
+
import { loadConfig, saveConfig, getConfigPath } from "./config.js";
|
|
12
|
+
import { apiUnauthenticated, api, ApiError } from "./api.js";
|
|
13
|
+
import { installObservers } from "./observe.js";
|
|
14
|
+
import { detectAllTools, formatToolDisplay, } from "./detect-tools.js";
|
|
15
|
+
// ── ANSI Colors ────────────────────────────────────────────────────────
|
|
16
|
+
const colors = {
|
|
17
|
+
reset: "\x1b[0m",
|
|
18
|
+
bold: "\x1b[1m",
|
|
19
|
+
dim: "\x1b[2m",
|
|
20
|
+
// Text colors
|
|
21
|
+
red: "\x1b[31m",
|
|
22
|
+
green: "\x1b[32m",
|
|
23
|
+
yellow: "\x1b[33m",
|
|
24
|
+
blue: "\x1b[34m",
|
|
25
|
+
magenta: "\x1b[35m",
|
|
26
|
+
cyan: "\x1b[36m",
|
|
27
|
+
white: "\x1b[37m",
|
|
28
|
+
gray: "\x1b[90m",
|
|
29
|
+
};
|
|
30
|
+
const c = {
|
|
31
|
+
success: (s) => `${colors.green}${s}${colors.reset}`,
|
|
32
|
+
error: (s) => `${colors.red}${s}${colors.reset}`,
|
|
33
|
+
warning: (s) => `${colors.yellow}${s}${colors.reset}`,
|
|
34
|
+
info: (s) => `${colors.cyan}${s}${colors.reset}`,
|
|
35
|
+
bold: (s) => `${colors.bold}${s}${colors.reset}`,
|
|
36
|
+
dim: (s) => `${colors.dim}${s}${colors.reset}`,
|
|
37
|
+
highlight: (s) => `${colors.bold}${colors.cyan}${s}${colors.reset}`,
|
|
38
|
+
};
|
|
39
|
+
// ── Symbols ────────────────────────────────────────────────────────────
|
|
40
|
+
const sym = {
|
|
41
|
+
check: c.success("✓"),
|
|
42
|
+
cross: c.error("✗"),
|
|
43
|
+
arrow: c.info("❯"),
|
|
44
|
+
bullet: c.dim("•"),
|
|
45
|
+
rocket: "🚀",
|
|
46
|
+
email: "📧",
|
|
47
|
+
target: "🎯",
|
|
48
|
+
tools: "🛠️",
|
|
49
|
+
link: "🔗",
|
|
50
|
+
done: "✅",
|
|
51
|
+
company: "🏢",
|
|
52
|
+
lock: "🔒",
|
|
53
|
+
money: "💰",
|
|
54
|
+
warning: "⚠️",
|
|
55
|
+
};
|
|
56
|
+
// ── Readline Helper ────────────────────────────────────────────────────
|
|
57
|
+
class Prompt {
|
|
58
|
+
rl;
|
|
59
|
+
closed = false;
|
|
60
|
+
constructor() {
|
|
61
|
+
this.rl = readline.createInterface({
|
|
62
|
+
input: process.stdin,
|
|
63
|
+
output: process.stdout,
|
|
64
|
+
});
|
|
65
|
+
// Handle Ctrl+C gracefully
|
|
66
|
+
this.rl.on("close", () => {
|
|
67
|
+
if (!this.closed) {
|
|
68
|
+
console.log("\n\n" + c.dim("Onboarding cancelled. Run 'jobarbiter onboard' anytime to continue."));
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async question(prompt) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
this.rl.question(prompt, (answer) => {
|
|
76
|
+
resolve(answer.trim());
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async confirm(prompt, defaultYes = true) {
|
|
81
|
+
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
82
|
+
const answer = await this.question(`${prompt} ${c.dim(hint)} `);
|
|
83
|
+
if (answer === "")
|
|
84
|
+
return defaultYes;
|
|
85
|
+
return answer.toLowerCase().startsWith("y");
|
|
86
|
+
}
|
|
87
|
+
close() {
|
|
88
|
+
this.closed = true;
|
|
89
|
+
this.rl.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// ── Main Wizard ────────────────────────────────────────────────────────
|
|
93
|
+
export async function runOnboardWizard(opts) {
|
|
94
|
+
const baseUrl = opts.baseUrl || "https://jobarbiter-api-production.up.railway.app";
|
|
95
|
+
// Check for existing config
|
|
96
|
+
const existingConfig = loadConfig();
|
|
97
|
+
if (existingConfig && !opts.force) {
|
|
98
|
+
console.log(`\n${sym.warning} ${c.warning("You already have a JobArbiter account configured.")}`);
|
|
99
|
+
console.log(`\n Run ${c.highlight("jobarbiter status")} to check your account.`);
|
|
100
|
+
console.log(` Run ${c.highlight("jobarbiter onboard --force")} to start fresh.\n`);
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
const prompt = new Prompt();
|
|
104
|
+
const state = { baseUrl };
|
|
105
|
+
try {
|
|
106
|
+
// Step 1: Welcome
|
|
107
|
+
await showWelcome();
|
|
108
|
+
const userType = await selectUserType(prompt);
|
|
109
|
+
state.userType = userType;
|
|
110
|
+
// Step 2: Email & Verification
|
|
111
|
+
const { email, apiKey, userId } = await handleEmailVerification(prompt, baseUrl, userType);
|
|
112
|
+
state.email = email;
|
|
113
|
+
state.apiKey = apiKey;
|
|
114
|
+
state.userId = userId;
|
|
115
|
+
// Save config immediately after verification
|
|
116
|
+
saveConfig({
|
|
117
|
+
apiKey,
|
|
118
|
+
baseUrl,
|
|
119
|
+
userType,
|
|
120
|
+
});
|
|
121
|
+
if (userType === "worker") {
|
|
122
|
+
await runWorkerFlow(prompt, state);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
await runEmployerFlow(prompt, state);
|
|
126
|
+
}
|
|
127
|
+
prompt.close();
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
prompt.close();
|
|
131
|
+
if (err instanceof ApiError) {
|
|
132
|
+
console.log(`\n${sym.cross} ${c.error(err.message)}`);
|
|
133
|
+
if (err.body.details) {
|
|
134
|
+
console.log(c.dim(JSON.stringify(err.body.details, null, 2)));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (err instanceof Error) {
|
|
138
|
+
console.log(`\n${sym.cross} ${c.error(err.message)}`);
|
|
139
|
+
}
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ── Welcome Screen ─────────────────────────────────────────────────────
|
|
144
|
+
async function showWelcome() {
|
|
145
|
+
console.clear();
|
|
146
|
+
console.log(`
|
|
147
|
+
${sym.rocket} ${c.bold("Welcome to JobArbiter")} — The AI Proficiency Marketplace
|
|
148
|
+
|
|
149
|
+
${c.dim("Your AI skills have value. Let's prove it.")}
|
|
150
|
+
`);
|
|
151
|
+
}
|
|
152
|
+
// ── User Type Selection ────────────────────────────────────────────────
|
|
153
|
+
async function selectUserType(prompt) {
|
|
154
|
+
console.log(`${c.bold("What brings you here?")}\n`);
|
|
155
|
+
console.log(` ${c.highlight("1.")} I build with AI ${c.dim("(Worker)")}`);
|
|
156
|
+
console.log(` ${c.highlight("2.")} I'm hiring AI talent ${c.dim("(Employer)")}\n`);
|
|
157
|
+
while (true) {
|
|
158
|
+
const answer = await prompt.question(`Your choice ${c.dim("[1/2]")}: `);
|
|
159
|
+
if (answer === "1" || answer.toLowerCase() === "worker")
|
|
160
|
+
return "worker";
|
|
161
|
+
if (answer === "2" || answer.toLowerCase() === "employer")
|
|
162
|
+
return "employer";
|
|
163
|
+
console.log(c.error("Please enter 1 or 2"));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ── Email & Verification ───────────────────────────────────────────────
|
|
167
|
+
async function handleEmailVerification(prompt, baseUrl, userType) {
|
|
168
|
+
// Workers: 1) Account, 2) Tool Detection, 3) Domains, 4) GitHub, 5) LinkedIn, 6) Done
|
|
169
|
+
// Employers: 1) Account, 2) (skip verification), 3) Company, 4) Domain, 5) What You Need, 6) Done
|
|
170
|
+
const totalSteps = 6;
|
|
171
|
+
console.log(`\n${sym.email} ${c.bold(`Step 1/${totalSteps} — Create Your Account`)}\n`);
|
|
172
|
+
// Get email
|
|
173
|
+
let email;
|
|
174
|
+
while (true) {
|
|
175
|
+
email = await prompt.question(`Email: `);
|
|
176
|
+
if (email && email.includes("@") && email.includes("."))
|
|
177
|
+
break;
|
|
178
|
+
console.log(c.error("Please enter a valid email address"));
|
|
179
|
+
}
|
|
180
|
+
// Call register API
|
|
181
|
+
console.log(c.dim("\nSending verification code..."));
|
|
182
|
+
try {
|
|
183
|
+
await apiUnauthenticated(baseUrl, "POST", "/v1/auth/register", {
|
|
184
|
+
email,
|
|
185
|
+
userType,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
if (err instanceof ApiError && err.status === 409) {
|
|
190
|
+
// Email already registered and verified
|
|
191
|
+
throw new Error(`This email is already registered. Run 'jobarbiter verify-email --email ${email}' if you need to re-verify, or use a different email.`);
|
|
192
|
+
}
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
console.log(`\n${sym.check} Verification code sent to ${c.highlight(email)}`);
|
|
196
|
+
console.log(c.dim(" (Check your inbox and spam folder. Code expires in 15 minutes.)\n"));
|
|
197
|
+
// Get verification code
|
|
198
|
+
let apiKey;
|
|
199
|
+
let userId;
|
|
200
|
+
let attempts = 0;
|
|
201
|
+
const maxAttempts = 5;
|
|
202
|
+
while (attempts < maxAttempts) {
|
|
203
|
+
const code = await prompt.question(`Enter 6-digit code: `);
|
|
204
|
+
if (!code || code.length !== 6) {
|
|
205
|
+
console.log(c.error("Code must be 6 digits"));
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const result = await apiUnauthenticated(baseUrl, "POST", "/v1/auth/verify", {
|
|
210
|
+
email,
|
|
211
|
+
code: code.trim(),
|
|
212
|
+
});
|
|
213
|
+
apiKey = result.apiKey;
|
|
214
|
+
userId = result.id;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
attempts++;
|
|
219
|
+
if (err instanceof ApiError) {
|
|
220
|
+
const remaining = err.body.attemptsRemaining ?? (maxAttempts - attempts);
|
|
221
|
+
if (remaining > 0) {
|
|
222
|
+
console.log(c.error(`Invalid code. ${remaining} attempts remaining.`));
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
throw new Error("Too many failed attempts. Run 'jobarbiter resend-code --email " + email + "' to get a new code.");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!apiKey || !userId) {
|
|
234
|
+
throw new Error("Verification failed. Please try again.");
|
|
235
|
+
}
|
|
236
|
+
console.log(`\n${sym.check} ${c.success("Email verified! Account created.")}\n`);
|
|
237
|
+
return { email, apiKey, userId };
|
|
238
|
+
}
|
|
239
|
+
// ── Worker Flow ────────────────────────────────────────────────────────
|
|
240
|
+
async function runWorkerFlow(prompt, state) {
|
|
241
|
+
const config = {
|
|
242
|
+
apiKey: state.apiKey,
|
|
243
|
+
baseUrl: state.baseUrl,
|
|
244
|
+
userType: "worker",
|
|
245
|
+
};
|
|
246
|
+
// Step 2: Auto-detect AI Tools
|
|
247
|
+
const detectedToolsResult = await runToolDetectionStep(prompt, config);
|
|
248
|
+
state.tools = detectedToolsResult.tools;
|
|
249
|
+
// Step 3: Domains
|
|
250
|
+
console.log(`${sym.target} ${c.bold("Step 3/6 — Your Domains")}\n`);
|
|
251
|
+
console.log(`What domains do you work in? ${c.dim("(comma-separated)")}`);
|
|
252
|
+
console.log(c.dim("Examples: full-stack dev, data engineering, trading, content creation\n"));
|
|
253
|
+
const domainsInput = await prompt.question(`${sym.arrow} `);
|
|
254
|
+
const domains = domainsInput.split(",").map(s => s.trim()).filter(Boolean);
|
|
255
|
+
state.domains = domains;
|
|
256
|
+
// Create/update profile
|
|
257
|
+
console.log(c.dim("\nSaving profile..."));
|
|
258
|
+
try {
|
|
259
|
+
await api(config, "POST", "/v1/profile", {
|
|
260
|
+
domains,
|
|
261
|
+
tools: {
|
|
262
|
+
primary: state.tools,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
console.log(`${sym.check} Profile saved\n`);
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
console.log(`${sym.warning} ${c.warning("Could not save profile details — you can update later with 'jobarbiter profile create'")}\n`);
|
|
269
|
+
}
|
|
270
|
+
// Step 4: Connect GitHub (optional)
|
|
271
|
+
console.log(`${sym.link} ${c.bold("Step 4/6 — Connect GitHub")} ${c.dim("(optional)")}\n`);
|
|
272
|
+
console.log(`Connecting your GitHub lets us analyze your AI-assisted work patterns.`);
|
|
273
|
+
console.log(`This significantly boosts your proficiency score.\n`);
|
|
274
|
+
const githubUsername = await prompt.question(`GitHub username ${c.dim("(press Enter to skip)")}: `);
|
|
275
|
+
if (githubUsername) {
|
|
276
|
+
console.log(c.dim("\nConnecting GitHub..."));
|
|
277
|
+
try {
|
|
278
|
+
await api(config, "POST", "/v1/attestations/git/connect", {
|
|
279
|
+
provider: "github",
|
|
280
|
+
username: githubUsername,
|
|
281
|
+
});
|
|
282
|
+
console.log(`${sym.check} GitHub connected: ${c.highlight(githubUsername)}\n`);
|
|
283
|
+
state.githubUsername = githubUsername;
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
console.log(`${sym.warning} ${c.warning("Could not connect GitHub — you can try later with 'jobarbiter git connect'")}\n`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter git connect'")}\n`);
|
|
291
|
+
}
|
|
292
|
+
// Step 5: Connect LinkedIn (optional)
|
|
293
|
+
console.log(`${sym.link} ${c.bold("Step 5/6 — Connect LinkedIn")} ${c.dim("(optional)")}\n`);
|
|
294
|
+
console.log(`Your LinkedIn profile strengthens identity verification.`);
|
|
295
|
+
console.log(c.dim("We never post on your behalf or access your connections.\n"));
|
|
296
|
+
const linkedinUrl = await prompt.question(`LinkedIn URL ${c.dim("(press Enter to skip)")}: `);
|
|
297
|
+
if (linkedinUrl) {
|
|
298
|
+
console.log(c.dim("\nSubmitting for verification..."));
|
|
299
|
+
try {
|
|
300
|
+
await api(config, "POST", "/v1/verification/linkedin", {
|
|
301
|
+
linkedinUrl: linkedinUrl.trim(),
|
|
302
|
+
});
|
|
303
|
+
console.log(`${sym.check} LinkedIn submitted for verification\n`);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
console.log(`${sym.warning} ${c.warning("Could not submit LinkedIn — you can try later with 'jobarbiter identity linkedin <url>'")}\n`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
console.log(`${c.dim("Skipped — you can connect later with 'jobarbiter identity linkedin <url>'")}\n`);
|
|
311
|
+
}
|
|
312
|
+
// Step 6: Done!
|
|
313
|
+
showWorkerCompletion(state);
|
|
314
|
+
}
|
|
315
|
+
// ── Tool Detection Step ────────────────────────────────────────────────
|
|
316
|
+
async function runToolDetectionStep(prompt, config) {
|
|
317
|
+
console.log(`🔍 ${c.bold("Step 2/6 — Detecting AI Tools")}\n`);
|
|
318
|
+
console.log(c.dim(" Scanning your machine...\n"));
|
|
319
|
+
const allTools = detectAllTools();
|
|
320
|
+
const installed = allTools.filter((t) => t.installed);
|
|
321
|
+
const notInstalled = allTools.filter((t) => !t.installed && t.category === "coding-agent");
|
|
322
|
+
// Group by category
|
|
323
|
+
const codingAgents = installed.filter((t) => t.category === "coding-agent");
|
|
324
|
+
const chatTools = installed.filter((t) => t.category === "chat");
|
|
325
|
+
const orchestration = installed.filter((t) => t.category === "orchestration");
|
|
326
|
+
const apiProviders = installed.filter((t) => t.category === "api-provider");
|
|
327
|
+
// Display found tools
|
|
328
|
+
if (installed.length === 0) {
|
|
329
|
+
console.log(` ${c.dim("No AI tools detected on this system.")}\n`);
|
|
330
|
+
console.log(c.dim(" You can add tools later with 'jobarbiter observe install'.\n"));
|
|
331
|
+
return { tools: [] };
|
|
332
|
+
}
|
|
333
|
+
console.log(` ${c.bold("Found:")}`);
|
|
334
|
+
// Show coding agents with observer status
|
|
335
|
+
for (const tool of codingAgents) {
|
|
336
|
+
const display = formatToolDisplay(tool);
|
|
337
|
+
if (tool.observerAvailable) {
|
|
338
|
+
if (tool.observerActive) {
|
|
339
|
+
console.log(` ${sym.check} ${display} ${c.dim("(observer active)")}`);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
console.log(` ${sym.check} ${display} ${c.success("(observer available)")}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
console.log(` ${sym.check} ${display} ${c.dim("(detected)")}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Show other tools
|
|
350
|
+
for (const tool of chatTools) {
|
|
351
|
+
console.log(` ${sym.check} ${formatToolDisplay(tool)} ${c.dim("(detected)")}`);
|
|
352
|
+
}
|
|
353
|
+
for (const tool of orchestration) {
|
|
354
|
+
console.log(` ${sym.check} ${formatToolDisplay(tool)} ${c.dim("(detected)")}`);
|
|
355
|
+
}
|
|
356
|
+
for (const tool of apiProviders) {
|
|
357
|
+
console.log(` ${sym.check} ${tool.name} ${c.dim("configured")}`);
|
|
358
|
+
}
|
|
359
|
+
// Show not-detected coding agents
|
|
360
|
+
if (notInstalled.length > 0) {
|
|
361
|
+
console.log(`\n ${c.dim("Not detected (install to track):")}`);
|
|
362
|
+
for (const tool of notInstalled.slice(0, 5)) {
|
|
363
|
+
console.log(` ${c.dim("⬚")} ${tool.name}`);
|
|
364
|
+
}
|
|
365
|
+
if (notInstalled.length > 5) {
|
|
366
|
+
console.log(` ${c.dim(`... and ${notInstalled.length - 5} more`)}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Collect tool names for profile
|
|
370
|
+
const toolNames = installed.map((t) => t.name);
|
|
371
|
+
// Observer installation for coding agents
|
|
372
|
+
const needsObserver = codingAgents.filter((t) => t.observerAvailable && !t.observerActive);
|
|
373
|
+
if (needsObserver.length > 0) {
|
|
374
|
+
console.log(`\n ${c.bold("Observers")}`);
|
|
375
|
+
console.log(` JobArbiter observes your coding sessions to build your`);
|
|
376
|
+
console.log(` proficiency profile. ${c.bold("No code or prompts leave your machine")} —`);
|
|
377
|
+
console.log(` only aggregate scores (tool usage, session counts, token volume).\n`);
|
|
378
|
+
console.log(c.dim(` Data stored locally: ~/.config/jobarbiter/observer/observations.json`));
|
|
379
|
+
console.log(c.dim(` Review anytime: jobarbiter observe status\n`));
|
|
380
|
+
const observerNames = needsObserver.map((t) => t.name).join(", ");
|
|
381
|
+
const installAll = await prompt.confirm(` Install observers for detected tools? (${observerNames})`);
|
|
382
|
+
if (installAll) {
|
|
383
|
+
const toInstall = needsObserver.map((t) => t.id);
|
|
384
|
+
console.log(c.dim("\n Installing observers..."));
|
|
385
|
+
const result = installObservers(toInstall);
|
|
386
|
+
for (const name of result.installed) {
|
|
387
|
+
console.log(` ${sym.check} ${name}`);
|
|
388
|
+
}
|
|
389
|
+
for (const name of result.skipped) {
|
|
390
|
+
console.log(` ${c.dim("—")} ${name} ${c.dim("(already installed)")}`);
|
|
391
|
+
}
|
|
392
|
+
for (const { agent, error: errMsg } of result.errors) {
|
|
393
|
+
console.log(` ${sym.cross} ${agent}: ${c.error(errMsg)}`);
|
|
394
|
+
}
|
|
395
|
+
if (result.installed.length > 0) {
|
|
396
|
+
console.log(`\n ${sym.check} ${c.success(`${result.installed.length} observer${result.installed.length > 1 ? "s" : ""} installed!`)}`);
|
|
397
|
+
console.log(c.dim(` Your proficiency profile will start building automatically.\n`));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
console.log(c.dim("\n Skipped — you can install observers later with 'jobarbiter observe install'.\n"));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else if (codingAgents.length > 0) {
|
|
405
|
+
const hasActiveObservers = codingAgents.some((t) => t.observerActive);
|
|
406
|
+
if (hasActiveObservers) {
|
|
407
|
+
console.log(`\n ${c.dim("All detected agents already have observers installed.")}\n`);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
console.log();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// "Did we miss anything?" prompt
|
|
414
|
+
console.log(` ${c.dim("Did we miss anything?")}`);
|
|
415
|
+
const additionalTools = await prompt.question(` Other AI tools you use ${c.dim("(comma-separated, or press Enter)")}: `);
|
|
416
|
+
if (additionalTools.trim()) {
|
|
417
|
+
const additional = additionalTools.split(",").map((s) => s.trim()).filter(Boolean);
|
|
418
|
+
toolNames.push(...additional);
|
|
419
|
+
console.log(` ${sym.check} Added: ${additional.join(", ")}\n`);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
console.log();
|
|
423
|
+
}
|
|
424
|
+
return { tools: toolNames };
|
|
425
|
+
}
|
|
426
|
+
function showWorkerCompletion(state) {
|
|
427
|
+
console.log(`${sym.done} ${c.bold("Step 6/6 — You're In!")}\n`);
|
|
428
|
+
console.log(`Your profile is live. Here's what happens next:\n`);
|
|
429
|
+
console.log(` 📊 Your proficiency score builds automatically from:`);
|
|
430
|
+
console.log(` ${sym.bullet} Coding agent observation ${c.dim("(biggest factor — 35%)")}`);
|
|
431
|
+
console.log(` ${sym.bullet} GitHub contribution analysis ${c.dim("(20%)")}`);
|
|
432
|
+
console.log(` ${sym.bullet} Token consumption patterns ${c.dim("(15%)")}`);
|
|
433
|
+
console.log(` ${sym.bullet} Tool diversity & fluency ${c.dim("(15%)")}`);
|
|
434
|
+
console.log(` ${sym.bullet} Outcome verification ${c.dim("(15%)")}\n`);
|
|
435
|
+
console.log(` 🎯 Your proficiency ${c.bold("track")} (Orchestrator, Systems Builder, or`);
|
|
436
|
+
console.log(` Domain Translator) is determined automatically as we observe`);
|
|
437
|
+
console.log(` how you work. No need to self-assess.\n`);
|
|
438
|
+
console.log(` 🤖 For deeper attestation, install the ${c.highlight("jobarbiter-proficiency")}`);
|
|
439
|
+
console.log(` skill in your AI agent (OpenClaw, Claude Code, etc.)\n`);
|
|
440
|
+
console.log(` ${c.bold("Useful commands:")}`);
|
|
441
|
+
console.log(` ${c.highlight("jobarbiter profile score")} — Check your proficiency score`);
|
|
442
|
+
console.log(` ${c.highlight("jobarbiter observe status")} — See collected observation data`);
|
|
443
|
+
console.log(` ${c.highlight("jobarbiter observe install")} — Add observers to new agents`);
|
|
444
|
+
console.log(` ${c.highlight("jobarbiter status")} — Account overview\n`);
|
|
445
|
+
console.log(`${c.bold("Welcome to the future of work.")} ${sym.rocket}\n`);
|
|
446
|
+
console.log(c.dim(`Config saved to: ${getConfigPath()}`));
|
|
447
|
+
console.log(c.dim(`API Key: ${state.apiKey.slice(0, 20)}... (shown only once)\n`));
|
|
448
|
+
}
|
|
449
|
+
// ── Employer Flow ──────────────────────────────────────────────────────
|
|
450
|
+
async function runEmployerFlow(prompt, state) {
|
|
451
|
+
const config = {
|
|
452
|
+
apiKey: state.apiKey,
|
|
453
|
+
baseUrl: state.baseUrl,
|
|
454
|
+
userType: "employer",
|
|
455
|
+
};
|
|
456
|
+
// Step 3: Company Setup
|
|
457
|
+
console.log(`${sym.company} ${c.bold("Step 3/6 — Your Company")}\n`);
|
|
458
|
+
const companyName = await prompt.question(`Company name: `);
|
|
459
|
+
state.companyName = companyName;
|
|
460
|
+
const companyWebsite = await prompt.question(`Company website: `);
|
|
461
|
+
state.companyWebsite = companyWebsite;
|
|
462
|
+
const companyIndustry = await prompt.question(`Industry: `);
|
|
463
|
+
state.companyIndustry = companyIndustry;
|
|
464
|
+
console.log(`\nCompany size:`);
|
|
465
|
+
console.log(` ${c.highlight("1.")} 1-10`);
|
|
466
|
+
console.log(` ${c.highlight("2.")} 11-50`);
|
|
467
|
+
console.log(` ${c.highlight("3.")} 51-200`);
|
|
468
|
+
console.log(` ${c.highlight("4.")} 201-1000`);
|
|
469
|
+
console.log(` ${c.highlight("5.")} 1000+\n`);
|
|
470
|
+
let companySize;
|
|
471
|
+
while (true) {
|
|
472
|
+
const answer = await prompt.question(`Company size ${c.dim("[1-5]")}: `);
|
|
473
|
+
const sizes = ["1-10", "11-50", "51-200", "201-1000", "1000+"];
|
|
474
|
+
const idx = parseInt(answer) - 1;
|
|
475
|
+
if (idx >= 0 && idx < 5) {
|
|
476
|
+
companySize = sizes[idx];
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
console.log(c.error("Please enter 1-5"));
|
|
480
|
+
}
|
|
481
|
+
state.companySize = companySize;
|
|
482
|
+
// Create company
|
|
483
|
+
console.log(c.dim("\nCreating company profile..."));
|
|
484
|
+
try {
|
|
485
|
+
await api(config, "POST", "/v1/company", {
|
|
486
|
+
companyData: {
|
|
487
|
+
name: companyName,
|
|
488
|
+
website: companyWebsite,
|
|
489
|
+
industry: companyIndustry,
|
|
490
|
+
size: companySize,
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
console.log(`${sym.check} Company profile created\n`);
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
// Company creation might not exist yet, best-effort
|
|
497
|
+
console.log(`${sym.warning} ${c.warning("Company profile saved locally — backend integration pending")}\n`);
|
|
498
|
+
}
|
|
499
|
+
// Step 4: Domain Verification
|
|
500
|
+
console.log(`${sym.lock} ${c.bold("Step 4/6 — Verify Your Domain")} ${c.dim("(optional)")}\n`);
|
|
501
|
+
// Extract domain from website
|
|
502
|
+
let domain = "";
|
|
503
|
+
try {
|
|
504
|
+
const url = new URL(companyWebsite.startsWith("http") ? companyWebsite : `https://${companyWebsite}`);
|
|
505
|
+
domain = url.hostname.replace(/^www\./, "");
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
domain = companyWebsite.replace(/^(https?:\/\/)?(www\.)?/, "").split("/")[0];
|
|
509
|
+
}
|
|
510
|
+
console.log(`To prove you represent ${c.highlight(companyName)}, we'll verify your domain.\n`);
|
|
511
|
+
console.log(`Domain to verify: ${c.highlight(domain)}\n`);
|
|
512
|
+
// Generate a fake verification token for display
|
|
513
|
+
const verifyToken = `ja-verify-${Math.random().toString(36).slice(2, 14)}`;
|
|
514
|
+
console.log(`Add this TXT record to your DNS:`);
|
|
515
|
+
console.log(` ${c.bold("Name:")} _jobarbiter-verify`);
|
|
516
|
+
console.log(` ${c.bold("Value:")} ${verifyToken}\n`);
|
|
517
|
+
const checkNow = await prompt.confirm(`Press Enter when ready to verify, or 's' to skip`);
|
|
518
|
+
if (checkNow) {
|
|
519
|
+
console.log(c.dim("\nChecking DNS... (this is a preview — verification not yet implemented)"));
|
|
520
|
+
console.log(`${sym.warning} ${c.warning("Domain verification will be available soon. Continuing...")}\n`);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
console.log(`${c.dim("Skipped — you can verify your domain later")}\n`);
|
|
524
|
+
}
|
|
525
|
+
// Step 5: What Are You Looking For?
|
|
526
|
+
console.log(`${sym.target} ${c.bold("Step 5/6 — What Do You Need?")}\n`);
|
|
527
|
+
console.log(`What track are you hiring for?`);
|
|
528
|
+
console.log(` ${c.highlight("1.")} Orchestrator`);
|
|
529
|
+
console.log(` ${c.highlight("2.")} Systems Builder`);
|
|
530
|
+
console.log(` ${c.highlight("3.")} Domain Translator`);
|
|
531
|
+
console.log(` ${c.highlight("4.")} Not sure yet\n`);
|
|
532
|
+
let hiringTrack;
|
|
533
|
+
while (true) {
|
|
534
|
+
const answer = await prompt.question(`Track ${c.dim("[1/2/3/4]")}: `);
|
|
535
|
+
if (answer === "1") {
|
|
536
|
+
hiringTrack = "orchestrator";
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
if (answer === "2") {
|
|
540
|
+
hiringTrack = "systemsBuilder";
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
if (answer === "3") {
|
|
544
|
+
hiringTrack = "domainTranslator";
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
if (answer === "4") {
|
|
548
|
+
hiringTrack = undefined;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
console.log(c.error("Please enter 1-4"));
|
|
552
|
+
}
|
|
553
|
+
state.hiringTrack = hiringTrack;
|
|
554
|
+
const minScoreInput = await prompt.question(`\nMinimum proficiency score ${c.dim("(1-100, recommended: 70)")}: `);
|
|
555
|
+
const minScore = parseInt(minScoreInput) || 70;
|
|
556
|
+
state.minScore = minScore;
|
|
557
|
+
console.log(`\nKey tools they should know ${c.dim("(comma-separated, optional)")}:`);
|
|
558
|
+
const hiringToolsInput = await prompt.question(`${sym.arrow} `);
|
|
559
|
+
const hiringTools = hiringToolsInput.split(",").map(s => s.trim()).filter(Boolean);
|
|
560
|
+
state.hiringTools = hiringTools;
|
|
561
|
+
// Step 6: Pricing Overview & Done
|
|
562
|
+
showEmployerCompletion(state);
|
|
563
|
+
}
|
|
564
|
+
function showEmployerCompletion(state) {
|
|
565
|
+
console.log(`\n${sym.money} ${c.bold("Step 6/6 — How It Works")}\n`);
|
|
566
|
+
console.log(`JobArbiter uses value-based pricing:\n`);
|
|
567
|
+
console.log(` ${sym.bullet} Search profiles: ${c.highlight("$50")} ${c.dim("(browse matching candidates)")}`);
|
|
568
|
+
console.log(` ${sym.bullet} Unlock full profile: ${c.highlight("$250")} ${c.dim("(see detailed proficiency data)")}`);
|
|
569
|
+
console.log(` ${sym.bullet} Introduction: ${c.highlight("$2,500")} ${c.dim("(we connect you directly)")}`);
|
|
570
|
+
console.log(` ${sym.bullet} Success fee: ${c.highlight("5%")} ${c.dim("(only if you hire)")}\n`);
|
|
571
|
+
console.log(`That's ${c.bold("75% cheaper")} than traditional recruiters,`);
|
|
572
|
+
console.log(`with ${c.bold("10x better signal")}.\n`);
|
|
573
|
+
console.log(`${sym.done} ${c.bold("You're all set!")}\n`);
|
|
574
|
+
console.log(` Run ${c.highlight("jobarbiter search")} to find AI-proficient candidates.`);
|
|
575
|
+
console.log(` Run ${c.highlight("jobarbiter opportunities create")} to post a role.\n`);
|
|
576
|
+
console.log(` Install the ${c.highlight("jobarbiter-hire")} skill in your AI agent`);
|
|
577
|
+
console.log(` for automated candidate discovery.\n`);
|
|
578
|
+
console.log(c.dim(`Config saved to: ${getConfigPath()}`));
|
|
579
|
+
console.log(c.dim(`API Key: ${state.apiKey.slice(0, 20)}... (shown only once)\n`));
|
|
580
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jobarbiter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "CLI for JobArbiter — the first AI Proficiency Marketplace",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,17 +8,27 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
|
-
"dev": "tsx src/index.ts"
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest"
|
|
12
14
|
},
|
|
13
|
-
"keywords": [
|
|
15
|
+
"keywords": [
|
|
16
|
+
"jobarbiter",
|
|
17
|
+
"ai",
|
|
18
|
+
"agents",
|
|
19
|
+
"jobs",
|
|
20
|
+
"matching",
|
|
21
|
+
"x402"
|
|
22
|
+
],
|
|
14
23
|
"author": "RetroDigio",
|
|
15
24
|
"license": "MIT",
|
|
16
25
|
"dependencies": {
|
|
17
26
|
"commander": "^13.0.0"
|
|
18
27
|
},
|
|
19
28
|
"devDependencies": {
|
|
20
|
-
"
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
21
30
|
"tsx": "^4.0.0",
|
|
22
|
-
"
|
|
31
|
+
"typescript": "^5.7.0",
|
|
32
|
+
"vitest": "^4.0.0"
|
|
23
33
|
}
|
|
24
34
|
}
|