jobarbiter 0.3.12 → 0.4.0
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 +347 -3
- package/dist/lib/detect-tools.d.ts +2 -0
- package/dist/lib/detect-tools.js +29 -0
- package/dist/lib/launchd.d.ts +16 -0
- package/dist/lib/launchd.js +171 -0
- package/dist/lib/observe.js +31 -5
- package/dist/lib/onboard.js +198 -14
- package/dist/lib/poll-state.d.ts +28 -0
- package/dist/lib/poll-state.js +91 -0
- package/dist/lib/privacy-pause.d.ts +17 -0
- package/dist/lib/privacy-pause.js +137 -0
- package/dist/lib/transcript-poller.d.ts +23 -0
- package/dist/lib/transcript-poller.js +218 -0
- package/dist/lib/uninstall.d.ts +20 -0
- package/dist/lib/uninstall.js +81 -0
- package/package.json +1 -1
- package/src/index.ts +369 -5
- package/src/lib/detect-tools.ts +33 -0
- package/src/lib/launchd.ts +193 -0
- package/src/lib/observe.ts +31 -5
- package/src/lib/onboard.ts +221 -16
- package/src/lib/poll-state.ts +119 -0
- package/src/lib/privacy-pause.ts +167 -0
- package/src/lib/transcript-poller.ts +274 -0
- package/src/lib/uninstall.ts +104 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LaunchAgent Management for macOS
|
|
3
|
+
*
|
|
4
|
+
* Manages the ai.jobarbiter.observer LaunchAgent for periodic transcript polling.
|
|
5
|
+
* Generates plist, writes to ~/Library/LaunchAgents/, manages via launchctl.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
// ── Constants ──────────────────────────────────────────────────────────
|
|
12
|
+
const LABEL = "ai.jobarbiter.observer";
|
|
13
|
+
const LAUNCH_AGENTS_DIR = join(homedir(), "Library", "LaunchAgents");
|
|
14
|
+
const PLIST_PATH = join(LAUNCH_AGENTS_DIR, `${LABEL}.plist`);
|
|
15
|
+
const OBSERVER_DIR = join(homedir(), ".config", "jobarbiter", "observer");
|
|
16
|
+
const LOG_PATH = join(OBSERVER_DIR, "poll.log");
|
|
17
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
18
|
+
function resolveJobarbiterPath() {
|
|
19
|
+
// Try to find the jobarbiter binary
|
|
20
|
+
try {
|
|
21
|
+
const path = execSync("command -v jobarbiter", { encoding: "utf-8", timeout: 3000 }).trim();
|
|
22
|
+
if (path)
|
|
23
|
+
return path;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Fall through
|
|
27
|
+
}
|
|
28
|
+
// Fallback: use process.execPath with the dist path
|
|
29
|
+
// This covers the case where jobarbiter is run via node directly
|
|
30
|
+
return process.execPath;
|
|
31
|
+
}
|
|
32
|
+
function generatePlist(intervalSeconds) {
|
|
33
|
+
const binaryPath = resolveJobarbiterPath();
|
|
34
|
+
// If the binary is node itself, we need to figure out the script path
|
|
35
|
+
const isNodeBinary = binaryPath.includes("node") || binaryPath.includes("bun");
|
|
36
|
+
let programArgs;
|
|
37
|
+
if (isNodeBinary) {
|
|
38
|
+
// Find the actual CLI entry point
|
|
39
|
+
const cliPath = join(__dirname, "..", "index.js");
|
|
40
|
+
programArgs = ` <string>${binaryPath}</string>
|
|
41
|
+
<string>${cliPath}</string>
|
|
42
|
+
<string>observe</string>
|
|
43
|
+
<string>poll</string>`;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
programArgs = ` <string>${binaryPath}</string>
|
|
47
|
+
<string>observe</string>
|
|
48
|
+
<string>poll</string>`;
|
|
49
|
+
}
|
|
50
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
51
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
52
|
+
<plist version="1.0">
|
|
53
|
+
<dict>
|
|
54
|
+
<key>Label</key>
|
|
55
|
+
<string>${LABEL}</string>
|
|
56
|
+
<key>ProgramArguments</key>
|
|
57
|
+
<array>
|
|
58
|
+
${programArgs}
|
|
59
|
+
</array>
|
|
60
|
+
<key>StartInterval</key>
|
|
61
|
+
<integer>${intervalSeconds}</integer>
|
|
62
|
+
<key>StandardOutPath</key>
|
|
63
|
+
<string>${LOG_PATH}</string>
|
|
64
|
+
<key>StandardErrorPath</key>
|
|
65
|
+
<string>${LOG_PATH}</string>
|
|
66
|
+
<key>EnvironmentVariables</key>
|
|
67
|
+
<dict>
|
|
68
|
+
<key>PATH</key>
|
|
69
|
+
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
70
|
+
</dict>
|
|
71
|
+
<key>RunAtLoad</key>
|
|
72
|
+
<true/>
|
|
73
|
+
<key>Nice</key>
|
|
74
|
+
<integer>10</integer>
|
|
75
|
+
</dict>
|
|
76
|
+
</plist>
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
function isLoaded() {
|
|
80
|
+
try {
|
|
81
|
+
const output = execSync(`launchctl list 2>/dev/null`, { encoding: "utf-8", timeout: 5000 });
|
|
82
|
+
return output.includes(LABEL);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
89
|
+
export function installDaemon(intervalSeconds = 7200) {
|
|
90
|
+
mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true });
|
|
91
|
+
mkdirSync(OBSERVER_DIR, { recursive: true });
|
|
92
|
+
// Unload existing if present
|
|
93
|
+
if (isLoaded()) {
|
|
94
|
+
try {
|
|
95
|
+
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { timeout: 5000 });
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// May not be loaded
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Write plist
|
|
102
|
+
const plist = generatePlist(intervalSeconds);
|
|
103
|
+
writeFileSync(PLIST_PATH, plist);
|
|
104
|
+
// Load
|
|
105
|
+
try {
|
|
106
|
+
execSync(`launchctl load "${PLIST_PATH}"`, { timeout: 5000 });
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
throw new Error(`Failed to load LaunchAgent: ${err instanceof Error ? err.message : String(err)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function uninstallDaemon() {
|
|
113
|
+
// Unload
|
|
114
|
+
if (isLoaded()) {
|
|
115
|
+
try {
|
|
116
|
+
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { timeout: 5000 });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Best effort
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Delete plist
|
|
123
|
+
if (existsSync(PLIST_PATH)) {
|
|
124
|
+
try {
|
|
125
|
+
unlinkSync(PLIST_PATH);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Best effort
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function getDaemonStatus() {
|
|
133
|
+
const installed = existsSync(PLIST_PATH);
|
|
134
|
+
const loaded = isLoaded();
|
|
135
|
+
let interval = null;
|
|
136
|
+
if (installed) {
|
|
137
|
+
try {
|
|
138
|
+
const content = readFileSync(PLIST_PATH, "utf-8");
|
|
139
|
+
const match = content.match(/<key>StartInterval<\/key>\s*<integer>(\d+)<\/integer>/);
|
|
140
|
+
if (match) {
|
|
141
|
+
interval = parseInt(match[1], 10);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Best effort
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
installed,
|
|
150
|
+
loaded,
|
|
151
|
+
interval,
|
|
152
|
+
plistPath: PLIST_PATH,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export function reloadDaemon() {
|
|
156
|
+
if (!existsSync(PLIST_PATH)) {
|
|
157
|
+
throw new Error("Daemon not installed. Run 'jobarbiter observe daemon install' first.");
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { timeout: 5000 });
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// May not be loaded
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
execSync(`launchctl load "${PLIST_PATH}"`, { timeout: 5000 });
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
throw new Error(`Failed to reload daemon: ${err instanceof Error ? err.message : String(err)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
package/dist/lib/observe.js
CHANGED
|
@@ -91,9 +91,29 @@ const fs = require("fs");
|
|
|
91
91
|
const path = require("path");
|
|
92
92
|
const os = require("os");
|
|
93
93
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
);
|
|
94
|
+
const OBSERVER_DIR = path.join(os.homedir(), ".config", "jobarbiter", "observer");
|
|
95
|
+
const OBSERVATIONS_FILE = path.join(OBSERVER_DIR, "observations.json");
|
|
96
|
+
const PAUSED_FILE = path.join(OBSERVER_DIR, "PAUSED");
|
|
97
|
+
|
|
98
|
+
// Check PAUSED sentinel FIRST — if paused, exit immediately
|
|
99
|
+
try {
|
|
100
|
+
if (fs.existsSync(PAUSED_FILE)) {
|
|
101
|
+
const raw = fs.readFileSync(PAUSED_FILE, "utf-8");
|
|
102
|
+
const pauseData = JSON.parse(raw);
|
|
103
|
+
if (pauseData.expiresAt) {
|
|
104
|
+
const expiresAt = new Date(pauseData.expiresAt).getTime();
|
|
105
|
+
if (Date.now() < expiresAt) {
|
|
106
|
+
process.exit(0); // Still paused
|
|
107
|
+
}
|
|
108
|
+
// Expired — clean up and continue
|
|
109
|
+
fs.unlinkSync(PAUSED_FILE);
|
|
110
|
+
} else {
|
|
111
|
+
process.exit(0); // Paused indefinitely
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// If PAUSED file is corrupted, continue
|
|
116
|
+
}
|
|
97
117
|
|
|
98
118
|
// Read stdin
|
|
99
119
|
let input = "";
|
|
@@ -249,8 +269,14 @@ function appendObservation(obs) {
|
|
|
249
269
|
// Fire-and-forget: trigger analysis pipeline via CLI
|
|
250
270
|
// Spawns detached process so the AI tool is never blocked
|
|
251
271
|
try {
|
|
252
|
-
const { spawn } = require("child_process");
|
|
253
|
-
//
|
|
272
|
+
const { spawn, execSync } = require("child_process");
|
|
273
|
+
// Self-healing: verify jobarbiter binary exists before spawning
|
|
274
|
+
try {
|
|
275
|
+
execSync("command -v jobarbiter", { stdio: "ignore", timeout: 3000 });
|
|
276
|
+
} catch {
|
|
277
|
+
// jobarbiter CLI not found — exit silently
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
254
280
|
const child = spawn("jobarbiter", ["analyze", "--auto"], {
|
|
255
281
|
detached: true,
|
|
256
282
|
stdio: "ignore",
|
package/dist/lib/onboard.js
CHANGED
|
@@ -13,6 +13,7 @@ import { loadConfig, saveConfig, getConfigPath } from "./config.js";
|
|
|
13
13
|
import { apiUnauthenticated, api, ApiError } from "./api.js";
|
|
14
14
|
import { installObservers } from "./observe.js";
|
|
15
15
|
import { detectAllTools, formatToolDisplay, } from "./detect-tools.js";
|
|
16
|
+
import { installDaemon } from "./launchd.js";
|
|
16
17
|
import { loadProviderKeys, saveProviderKey, validateProviderKey, } from "./providers.js";
|
|
17
18
|
// ── ANSI Colors ────────────────────────────────────────────────────────
|
|
18
19
|
const colors = {
|
|
@@ -160,19 +161,68 @@ export async function runOnboardWizard(opts) {
|
|
|
160
161
|
const userType = await selectUserType(prompt);
|
|
161
162
|
state.userType = userType;
|
|
162
163
|
// Step 2: Email & Verification
|
|
163
|
-
const
|
|
164
|
-
state.email = email;
|
|
165
|
-
state.apiKey = apiKey;
|
|
166
|
-
state.userId = userId;
|
|
164
|
+
const verificationResult = await handleEmailVerification(prompt, baseUrl, userType);
|
|
165
|
+
state.email = verificationResult.email;
|
|
166
|
+
state.apiKey = verificationResult.apiKey;
|
|
167
|
+
state.userId = verificationResult.userId;
|
|
168
|
+
// If returning user, override userType from backend
|
|
169
|
+
const effectiveUserType = verificationResult.isReturningUser && verificationResult.userType
|
|
170
|
+
? verificationResult.userType
|
|
171
|
+
: userType;
|
|
172
|
+
state.userType = effectiveUserType;
|
|
167
173
|
// Save config immediately after verification (with step progress)
|
|
168
174
|
saveConfig({
|
|
169
|
-
apiKey,
|
|
175
|
+
apiKey: verificationResult.apiKey,
|
|
170
176
|
baseUrl,
|
|
171
|
-
userType,
|
|
177
|
+
userType: effectiveUserType,
|
|
172
178
|
onboardingStep: 1,
|
|
173
179
|
onboardingComplete: false,
|
|
174
180
|
});
|
|
175
|
-
|
|
181
|
+
// Returning user: show progress and resume from first incomplete step
|
|
182
|
+
if (verificationResult.isReturningUser) {
|
|
183
|
+
const config = {
|
|
184
|
+
apiKey: verificationResult.apiKey,
|
|
185
|
+
baseUrl,
|
|
186
|
+
userType: effectiveUserType,
|
|
187
|
+
};
|
|
188
|
+
const progress = await fetchOnboardingProgress(config);
|
|
189
|
+
const { firstIncomplete } = showReturningUserProgress(progress);
|
|
190
|
+
if (firstIncomplete > (effectiveUserType === "worker" ? 6 : 5)) {
|
|
191
|
+
// Everything done except completion step
|
|
192
|
+
const continueAnyway = await prompt.confirm(`All steps look complete. Re-run onboarding anyway?`, false);
|
|
193
|
+
if (!continueAnyway) {
|
|
194
|
+
saveConfig({ ...config, onboardingComplete: true, onboardingStep: effectiveUserType === "worker" ? 7 : 6 });
|
|
195
|
+
console.log(`\n${sym.check} ${c.success("You're all set!")} Run ${c.highlight("jobarbiter status")} to check your account.\n`);
|
|
196
|
+
prompt.close();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
const continueFromStep = await prompt.confirm(`Continue from step ${firstIncomplete}?`);
|
|
202
|
+
if (!continueFromStep) {
|
|
203
|
+
// Let them start from step 2
|
|
204
|
+
console.log(c.dim("Starting from the beginning...\n"));
|
|
205
|
+
if (effectiveUserType === "worker") {
|
|
206
|
+
await runWorkerFlow(prompt, state, 2);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
await runEmployerFlow(prompt, state);
|
|
210
|
+
}
|
|
211
|
+
prompt.close();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Resume from firstIncomplete
|
|
216
|
+
if (effectiveUserType === "worker") {
|
|
217
|
+
await runWorkerFlow(prompt, state, firstIncomplete);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
await runEmployerFlow(prompt, state);
|
|
221
|
+
}
|
|
222
|
+
prompt.close();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (effectiveUserType === "worker") {
|
|
176
226
|
await runWorkerFlow(prompt, state);
|
|
177
227
|
}
|
|
178
228
|
else {
|
|
@@ -233,6 +283,7 @@ async function handleEmailVerification(prompt, baseUrl, userType) {
|
|
|
233
283
|
}
|
|
234
284
|
// Call register API
|
|
235
285
|
console.log(c.dim("\nSending verification code..."));
|
|
286
|
+
let isReturningUser = false;
|
|
236
287
|
try {
|
|
237
288
|
await apiUnauthenticated(baseUrl, "POST", "/v1/auth/register", {
|
|
238
289
|
email,
|
|
@@ -241,18 +292,34 @@ async function handleEmailVerification(prompt, baseUrl, userType) {
|
|
|
241
292
|
}
|
|
242
293
|
catch (err) {
|
|
243
294
|
if (err instanceof ApiError && err.status === 409) {
|
|
244
|
-
// Email already registered
|
|
245
|
-
|
|
295
|
+
// Email already registered — switch to login flow
|
|
296
|
+
isReturningUser = true;
|
|
297
|
+
console.log(`\n${sym.rocket} ${c.bold("Welcome back!")} Sending verification code...`);
|
|
298
|
+
try {
|
|
299
|
+
await apiUnauthenticated(baseUrl, "POST", "/v1/auth/login", { email });
|
|
300
|
+
}
|
|
301
|
+
catch (loginErr) {
|
|
302
|
+
throw new Error("Could not send login verification code. Please try again later.");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
throw err;
|
|
246
307
|
}
|
|
247
|
-
throw err;
|
|
248
308
|
}
|
|
249
|
-
|
|
309
|
+
if (!isReturningUser) {
|
|
310
|
+
console.log(`\n${sym.check} Verification code sent to ${c.highlight(email)}`);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
console.log(`${sym.check} Verification code sent to ${c.highlight(email)}`);
|
|
314
|
+
}
|
|
250
315
|
console.log(c.dim(" (Check your inbox and spam folder. Code expires in 15 minutes.)\n"));
|
|
251
316
|
// Get verification code
|
|
252
317
|
let apiKey;
|
|
253
318
|
let userId;
|
|
319
|
+
let returnedUserType;
|
|
254
320
|
let attempts = 0;
|
|
255
321
|
const maxAttempts = 5;
|
|
322
|
+
const verifyEndpoint = isReturningUser ? "/v1/auth/verify-login" : "/v1/auth/verify";
|
|
256
323
|
while (attempts < maxAttempts) {
|
|
257
324
|
const code = await prompt.question(`Enter 6-digit code: `);
|
|
258
325
|
if (!code || code.length !== 6) {
|
|
@@ -260,12 +327,15 @@ async function handleEmailVerification(prompt, baseUrl, userType) {
|
|
|
260
327
|
continue;
|
|
261
328
|
}
|
|
262
329
|
try {
|
|
263
|
-
const result = await apiUnauthenticated(baseUrl, "POST",
|
|
330
|
+
const result = await apiUnauthenticated(baseUrl, "POST", verifyEndpoint, {
|
|
264
331
|
email,
|
|
265
332
|
code: code.trim(),
|
|
266
333
|
});
|
|
267
334
|
apiKey = result.apiKey;
|
|
268
335
|
userId = result.id;
|
|
336
|
+
if (isReturningUser) {
|
|
337
|
+
returnedUserType = result.userType;
|
|
338
|
+
}
|
|
269
339
|
break;
|
|
270
340
|
}
|
|
271
341
|
catch (err) {
|
|
@@ -287,8 +357,105 @@ async function handleEmailVerification(prompt, baseUrl, userType) {
|
|
|
287
357
|
if (!apiKey || !userId) {
|
|
288
358
|
throw new Error("Verification failed. Please try again.");
|
|
289
359
|
}
|
|
290
|
-
|
|
291
|
-
|
|
360
|
+
if (isReturningUser) {
|
|
361
|
+
console.log(`\n${sym.check} ${c.success("Welcome back! Logged in successfully.")}\n`);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
console.log(`\n${sym.check} ${c.success("Email verified! Account created.")}\n`);
|
|
365
|
+
}
|
|
366
|
+
return { email, apiKey, userId, isReturningUser, userType: returnedUserType };
|
|
367
|
+
}
|
|
368
|
+
async function fetchOnboardingProgress(config) {
|
|
369
|
+
const progress = {
|
|
370
|
+
accountCreated: true,
|
|
371
|
+
userType: config.userType,
|
|
372
|
+
toolsDetected: [],
|
|
373
|
+
aiAccountsConnected: false,
|
|
374
|
+
domainsSet: [],
|
|
375
|
+
githubConnected: false,
|
|
376
|
+
linkedinConnected: false,
|
|
377
|
+
};
|
|
378
|
+
// Fetch profile
|
|
379
|
+
try {
|
|
380
|
+
const profile = await api(config, "GET", "/v1/profile");
|
|
381
|
+
if (profile.domains && Array.isArray(profile.domains) && profile.domains.length > 0) {
|
|
382
|
+
progress.domainsSet = profile.domains;
|
|
383
|
+
}
|
|
384
|
+
if (profile.tools && typeof profile.tools === "object") {
|
|
385
|
+
const tools = profile.tools;
|
|
386
|
+
if (tools.primary && Array.isArray(tools.primary) && tools.primary.length > 0) {
|
|
387
|
+
progress.toolsDetected = tools.primary;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (profile.bio) {
|
|
391
|
+
progress.bio = profile.bio;
|
|
392
|
+
}
|
|
393
|
+
if (profile.githubUsername) {
|
|
394
|
+
progress.githubConnected = true;
|
|
395
|
+
progress.githubUsername = profile.githubUsername;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
// Profile doesn't exist yet — that's fine
|
|
400
|
+
}
|
|
401
|
+
// Check AI accounts
|
|
402
|
+
const existingProviders = loadProviderKeys();
|
|
403
|
+
progress.aiAccountsConnected = existingProviders.length > 0;
|
|
404
|
+
// Check verification status (LinkedIn etc.)
|
|
405
|
+
try {
|
|
406
|
+
const verificationStatus = await api(config, "GET", "/v1/verification/status");
|
|
407
|
+
if (verificationStatus.linkedin) {
|
|
408
|
+
progress.linkedinConnected = true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// Endpoint may not exist — gracefully ignore
|
|
413
|
+
}
|
|
414
|
+
return progress;
|
|
415
|
+
}
|
|
416
|
+
function showReturningUserProgress(progress) {
|
|
417
|
+
const isWorker = progress.userType === "worker";
|
|
418
|
+
const totalSteps = isWorker ? 7 : 6;
|
|
419
|
+
console.log(`${c.bold("Here's your onboarding progress:")}\n`);
|
|
420
|
+
if (isWorker) {
|
|
421
|
+
const steps = [
|
|
422
|
+
{ done: true, label: `Account created (${progress.userType})` },
|
|
423
|
+
{ done: progress.toolsDetected.length > 0, label: progress.toolsDetected.length > 0
|
|
424
|
+
? `AI Tools detected (${progress.toolsDetected.join(", ")})`
|
|
425
|
+
: "AI Tools not detected" },
|
|
426
|
+
{ done: progress.aiAccountsConnected, label: progress.aiAccountsConnected
|
|
427
|
+
? "AI Accounts connected"
|
|
428
|
+
: "AI Accounts not connected" },
|
|
429
|
+
{ done: progress.domainsSet.length > 0, label: progress.domainsSet.length > 0
|
|
430
|
+
? `Domains set (${progress.domainsSet.join(", ")})`
|
|
431
|
+
: "Domains not set" },
|
|
432
|
+
{ done: progress.githubConnected, label: progress.githubConnected
|
|
433
|
+
? `GitHub connected (${progress.githubUsername})`
|
|
434
|
+
: "GitHub not connected" },
|
|
435
|
+
{ done: progress.linkedinConnected, label: progress.linkedinConnected
|
|
436
|
+
? "LinkedIn connected"
|
|
437
|
+
: "LinkedIn not connected" },
|
|
438
|
+
{ done: false, label: "Completion" },
|
|
439
|
+
];
|
|
440
|
+
let firstIncomplete = totalSteps; // default to last
|
|
441
|
+
for (let i = 0; i < steps.length; i++) {
|
|
442
|
+
const step = steps[i];
|
|
443
|
+
const icon = step.done ? sym.check : sym.cross;
|
|
444
|
+
console.log(` ${icon} Step ${i + 1}/${totalSteps} — ${step.label}`);
|
|
445
|
+
if (!step.done && firstIncomplete === totalSteps) {
|
|
446
|
+
firstIncomplete = i + 1;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
console.log();
|
|
450
|
+
return { firstIncomplete };
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
// Employer — simpler progress
|
|
454
|
+
console.log(` ${sym.check} Step 1/6 — Account created (employer)`);
|
|
455
|
+
console.log(` ${sym.cross} Step 2/6 — Remaining setup`);
|
|
456
|
+
console.log();
|
|
457
|
+
return { firstIncomplete: 2 };
|
|
458
|
+
}
|
|
292
459
|
}
|
|
293
460
|
// ── Worker Flow ────────────────────────────────────────────────────────
|
|
294
461
|
async function runWorkerFlow(prompt, state, startStep = 2) {
|
|
@@ -479,6 +646,22 @@ async function runToolDetectionStep(prompt, config) {
|
|
|
479
646
|
}
|
|
480
647
|
if (result.installed.length > 0) {
|
|
481
648
|
console.log(`\n ${sym.check} ${c.success(`${result.installed.length} observer${result.installed.length > 1 ? "s" : ""} installed!`)}`);
|
|
649
|
+
// Install background polling daemon
|
|
650
|
+
try {
|
|
651
|
+
installDaemon(7200);
|
|
652
|
+
console.log(` ${sym.check} ${c.success("Background poller installed (every 2 hours)")}`);
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
console.log(` ${c.dim("Background poller skipped (install later: jobarbiter observe daemon install)")}`);
|
|
656
|
+
}
|
|
657
|
+
// Show poller-observable tools
|
|
658
|
+
const pollerTools = allTools.filter((t) => t.installed && t.observationMethod === "poller");
|
|
659
|
+
if (pollerTools.length > 0) {
|
|
660
|
+
console.log(`\n ${c.bold("Poller will also scan these tools:")}`);
|
|
661
|
+
for (const t of pollerTools) {
|
|
662
|
+
console.log(` ${sym.bullet} ${formatToolDisplay(t)}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
482
665
|
console.log(`\n ${c.bold("What happens now?")}`);
|
|
483
666
|
console.log(` Observers activate each time you use your AI tools.`);
|
|
484
667
|
console.log(` Just work normally — every session builds your profile.\n`);
|
|
@@ -487,6 +670,7 @@ async function runToolDetectionStep(prompt, config) {
|
|
|
487
670
|
console.log(` a work report to JobArbiter. Our agents interpret these reports`);
|
|
488
671
|
console.log(` to build and evolve your narrative profile over time.`);
|
|
489
672
|
console.log(` Raw session data never leaves your machine.\n`);
|
|
673
|
+
console.log(` ${c.dim("Manage observers: jobarbiter observe --help")}`);
|
|
490
674
|
}
|
|
491
675
|
}
|
|
492
676
|
else {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll State Management
|
|
3
|
+
*
|
|
4
|
+
* Read/write ~/.config/jobarbiter/observer/poll-state.json
|
|
5
|
+
* Tracks per-source poll times, known sessions, pause windows,
|
|
6
|
+
* disabled sources, and interval configuration.
|
|
7
|
+
*/
|
|
8
|
+
export interface PauseWindow {
|
|
9
|
+
start: string;
|
|
10
|
+
end: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PollState {
|
|
13
|
+
version: number;
|
|
14
|
+
lastPoll: Record<string, string>;
|
|
15
|
+
knownSessionIds: string[];
|
|
16
|
+
pauseWindows: PauseWindow[];
|
|
17
|
+
disabledSources: string[];
|
|
18
|
+
interval: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function loadPollState(): PollState;
|
|
21
|
+
export declare function savePollState(state: PollState): void;
|
|
22
|
+
export declare function addPauseWindow(start: string, end: string): void;
|
|
23
|
+
export declare function isInPauseWindow(timestamp: string): boolean;
|
|
24
|
+
export declare function addKnownSession(sessionId: string): void;
|
|
25
|
+
export declare function isKnownSession(sessionId: string): boolean;
|
|
26
|
+
export declare function getDisabledSources(): string[];
|
|
27
|
+
export declare function setDisabledSource(source: string): void;
|
|
28
|
+
export declare function removeDisabledSource(source: string): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll State Management
|
|
3
|
+
*
|
|
4
|
+
* Read/write ~/.config/jobarbiter/observer/poll-state.json
|
|
5
|
+
* Tracks per-source poll times, known sessions, pause windows,
|
|
6
|
+
* disabled sources, and interval configuration.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
// ── Paths ──────────────────────────────────────────────────────────────
|
|
12
|
+
const OBSERVER_DIR = join(homedir(), ".config", "jobarbiter", "observer");
|
|
13
|
+
const POLL_STATE_FILE = join(OBSERVER_DIR, "poll-state.json");
|
|
14
|
+
function defaultPollState() {
|
|
15
|
+
return {
|
|
16
|
+
version: 1,
|
|
17
|
+
lastPoll: {},
|
|
18
|
+
knownSessionIds: [],
|
|
19
|
+
pauseWindows: [],
|
|
20
|
+
disabledSources: [],
|
|
21
|
+
interval: 7200,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// ── Core Functions ─────────────────────────────────────────────────────
|
|
25
|
+
export function loadPollState() {
|
|
26
|
+
try {
|
|
27
|
+
if (!existsSync(POLL_STATE_FILE))
|
|
28
|
+
return defaultPollState();
|
|
29
|
+
const raw = readFileSync(POLL_STATE_FILE, "utf-8");
|
|
30
|
+
const data = JSON.parse(raw);
|
|
31
|
+
return {
|
|
32
|
+
...defaultPollState(),
|
|
33
|
+
...data,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return defaultPollState();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function savePollState(state) {
|
|
41
|
+
mkdirSync(OBSERVER_DIR, { recursive: true });
|
|
42
|
+
writeFileSync(POLL_STATE_FILE, JSON.stringify(state, null, 2) + "\n");
|
|
43
|
+
}
|
|
44
|
+
export function addPauseWindow(start, end) {
|
|
45
|
+
const state = loadPollState();
|
|
46
|
+
state.pauseWindows.push({ start, end });
|
|
47
|
+
// Keep last 50 windows
|
|
48
|
+
if (state.pauseWindows.length > 50) {
|
|
49
|
+
state.pauseWindows = state.pauseWindows.slice(-50);
|
|
50
|
+
}
|
|
51
|
+
savePollState(state);
|
|
52
|
+
}
|
|
53
|
+
export function isInPauseWindow(timestamp) {
|
|
54
|
+
const state = loadPollState();
|
|
55
|
+
const ts = new Date(timestamp).getTime();
|
|
56
|
+
return state.pauseWindows.some((w) => {
|
|
57
|
+
const start = new Date(w.start).getTime();
|
|
58
|
+
const end = new Date(w.end).getTime();
|
|
59
|
+
return ts >= start && ts <= end;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export function addKnownSession(sessionId) {
|
|
63
|
+
const state = loadPollState();
|
|
64
|
+
if (!state.knownSessionIds.includes(sessionId)) {
|
|
65
|
+
state.knownSessionIds.push(sessionId);
|
|
66
|
+
// Rolling window of 1000
|
|
67
|
+
if (state.knownSessionIds.length > 1000) {
|
|
68
|
+
state.knownSessionIds = state.knownSessionIds.slice(-1000);
|
|
69
|
+
}
|
|
70
|
+
savePollState(state);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function isKnownSession(sessionId) {
|
|
74
|
+
const state = loadPollState();
|
|
75
|
+
return state.knownSessionIds.includes(sessionId);
|
|
76
|
+
}
|
|
77
|
+
export function getDisabledSources() {
|
|
78
|
+
return loadPollState().disabledSources;
|
|
79
|
+
}
|
|
80
|
+
export function setDisabledSource(source) {
|
|
81
|
+
const state = loadPollState();
|
|
82
|
+
if (!state.disabledSources.includes(source)) {
|
|
83
|
+
state.disabledSources.push(source);
|
|
84
|
+
savePollState(state);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function removeDisabledSource(source) {
|
|
88
|
+
const state = loadPollState();
|
|
89
|
+
state.disabledSources = state.disabledSources.filter((s) => s !== source);
|
|
90
|
+
savePollState(state);
|
|
91
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy Pause Management
|
|
3
|
+
*
|
|
4
|
+
* Manages the PAUSED sentinel file at ~/.config/jobarbiter/observer/PAUSED
|
|
5
|
+
* When paused, all observation hooks and pollers exit immediately.
|
|
6
|
+
*/
|
|
7
|
+
import { type PauseWindow } from "./poll-state.js";
|
|
8
|
+
export interface PauseStatus {
|
|
9
|
+
paused: boolean;
|
|
10
|
+
pausedAt: string | null;
|
|
11
|
+
expiresAt: string | null;
|
|
12
|
+
pauseWindows: PauseWindow[];
|
|
13
|
+
}
|
|
14
|
+
export declare function pauseObservation(expiresAt?: Date): void;
|
|
15
|
+
export declare function resumeObservation(): void;
|
|
16
|
+
export declare function isPaused(): boolean;
|
|
17
|
+
export declare function getPauseStatus(): PauseStatus;
|