jobarbiter 0.3.13 → 0.4.1
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 +376 -6
- 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 +18 -0
- 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 +395 -8
- 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 +21 -0
- 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
package/src/lib/detect-tools.ts
CHANGED
|
@@ -17,6 +17,8 @@ import { execSync } from "node:child_process";
|
|
|
17
17
|
|
|
18
18
|
export type ToolCategory = "ai-agent" | "chat" | "orchestration" | "api-provider";
|
|
19
19
|
|
|
20
|
+
export type ObservationMethod = "hook" | "extension" | "poller" | "both" | "none";
|
|
21
|
+
|
|
20
22
|
export interface DetectedTool {
|
|
21
23
|
id: string;
|
|
22
24
|
name: string;
|
|
@@ -26,6 +28,7 @@ export interface DetectedTool {
|
|
|
26
28
|
configDir?: string;
|
|
27
29
|
observerAvailable: boolean;
|
|
28
30
|
observerActive: boolean;
|
|
31
|
+
observationMethod: ObservationMethod;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
interface ToolDefinition {
|
|
@@ -41,6 +44,7 @@ interface ToolDefinition {
|
|
|
41
44
|
npmPackage?: string;
|
|
42
45
|
envVars?: string[];
|
|
43
46
|
observerAvailable: boolean;
|
|
47
|
+
observationMethod: ObservationMethod;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
// ── Tool Definitions ───────────────────────────────────────────────────
|
|
@@ -54,6 +58,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
54
58
|
binary: "claude",
|
|
55
59
|
configDir: join(homedir(), ".claude"),
|
|
56
60
|
observerAvailable: true,
|
|
61
|
+
observationMethod: "hook",
|
|
57
62
|
},
|
|
58
63
|
{
|
|
59
64
|
id: "cursor",
|
|
@@ -63,6 +68,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
63
68
|
configDir: join(homedir(), ".cursor"),
|
|
64
69
|
macApp: "/Applications/Cursor.app",
|
|
65
70
|
observerAvailable: true,
|
|
71
|
+
observationMethod: "hook",
|
|
66
72
|
},
|
|
67
73
|
{
|
|
68
74
|
id: "github-copilot",
|
|
@@ -72,6 +78,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
72
78
|
vscodeExtension: "github.copilot",
|
|
73
79
|
cursorExtension: "github.copilot",
|
|
74
80
|
observerAvailable: false,
|
|
81
|
+
observationMethod: "extension",
|
|
75
82
|
},
|
|
76
83
|
{
|
|
77
84
|
id: "codex",
|
|
@@ -80,6 +87,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
80
87
|
binary: "codex",
|
|
81
88
|
configDir: join(homedir(), ".codex"),
|
|
82
89
|
observerAvailable: true,
|
|
90
|
+
observationMethod: "hook",
|
|
83
91
|
},
|
|
84
92
|
{
|
|
85
93
|
id: "opencode",
|
|
@@ -88,6 +96,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
88
96
|
binary: "opencode",
|
|
89
97
|
configDir: join(homedir(), ".config", "opencode"),
|
|
90
98
|
observerAvailable: true,
|
|
99
|
+
observationMethod: "hook",
|
|
91
100
|
},
|
|
92
101
|
{
|
|
93
102
|
id: "aider",
|
|
@@ -97,6 +106,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
97
106
|
configDir: join(homedir(), ".aider"),
|
|
98
107
|
pipPackage: "aider-chat",
|
|
99
108
|
observerAvailable: false,
|
|
109
|
+
observationMethod: "poller",
|
|
100
110
|
},
|
|
101
111
|
{
|
|
102
112
|
id: "continue",
|
|
@@ -105,6 +115,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
105
115
|
vscodeExtension: "continue.continue",
|
|
106
116
|
cursorExtension: "continue.continue",
|
|
107
117
|
observerAvailable: false,
|
|
118
|
+
observationMethod: "extension",
|
|
108
119
|
},
|
|
109
120
|
{
|
|
110
121
|
id: "cline",
|
|
@@ -113,6 +124,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
113
124
|
vscodeExtension: "saoudrizwan.claude-dev",
|
|
114
125
|
cursorExtension: "saoudrizwan.claude-dev",
|
|
115
126
|
observerAvailable: false,
|
|
127
|
+
observationMethod: "extension",
|
|
116
128
|
},
|
|
117
129
|
{
|
|
118
130
|
id: "windsurf",
|
|
@@ -121,6 +133,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
121
133
|
binary: "windsurf",
|
|
122
134
|
macApp: "/Applications/Windsurf.app",
|
|
123
135
|
observerAvailable: false,
|
|
136
|
+
observationMethod: "extension",
|
|
124
137
|
},
|
|
125
138
|
{
|
|
126
139
|
id: "copilot-chat",
|
|
@@ -129,6 +142,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
129
142
|
vscodeExtension: "github.copilot-chat",
|
|
130
143
|
cursorExtension: "github.copilot-chat",
|
|
131
144
|
observerAvailable: false,
|
|
145
|
+
observationMethod: "extension",
|
|
132
146
|
},
|
|
133
147
|
{
|
|
134
148
|
id: "zed-ai",
|
|
@@ -137,6 +151,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
137
151
|
macApp: "/Applications/Zed.app",
|
|
138
152
|
configDir: join(homedir(), platform() === "darwin" ? "Library/Application Support/Zed" : ".config/zed"),
|
|
139
153
|
observerAvailable: false,
|
|
154
|
+
observationMethod: "poller",
|
|
140
155
|
},
|
|
141
156
|
{
|
|
142
157
|
id: "amazon-q",
|
|
@@ -145,6 +160,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
145
160
|
binary: "q",
|
|
146
161
|
configDir: join(homedir(), ".aws", "amazonq"),
|
|
147
162
|
observerAvailable: false,
|
|
163
|
+
observationMethod: "poller",
|
|
148
164
|
},
|
|
149
165
|
{
|
|
150
166
|
id: "warp-ai",
|
|
@@ -153,6 +169,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
153
169
|
macApp: "/Applications/Warp.app",
|
|
154
170
|
configDir: join(homedir(), "Library", "Application Support", "dev.warp.Warp-Stable"),
|
|
155
171
|
observerAvailable: false,
|
|
172
|
+
observationMethod: "none",
|
|
156
173
|
},
|
|
157
174
|
{
|
|
158
175
|
id: "letta",
|
|
@@ -162,6 +179,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
162
179
|
configDir: join(homedir(), ".letta"),
|
|
163
180
|
pipPackage: "letta",
|
|
164
181
|
observerAvailable: false,
|
|
182
|
+
observationMethod: "poller",
|
|
165
183
|
},
|
|
166
184
|
{
|
|
167
185
|
id: "goose",
|
|
@@ -170,6 +188,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
170
188
|
binary: "goose",
|
|
171
189
|
configDir: join(homedir(), ".config", "goose"),
|
|
172
190
|
observerAvailable: false,
|
|
191
|
+
observationMethod: "poller",
|
|
173
192
|
},
|
|
174
193
|
{
|
|
175
194
|
id: "idx",
|
|
@@ -178,6 +197,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
178
197
|
// IDX is cloud-based; no local binary. Detect via config dir if any local cache exists.
|
|
179
198
|
configDir: join(homedir(), ".idx"),
|
|
180
199
|
observerAvailable: false,
|
|
200
|
+
observationMethod: "none",
|
|
181
201
|
},
|
|
182
202
|
{
|
|
183
203
|
id: "gemini",
|
|
@@ -186,6 +206,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
186
206
|
binary: "gemini",
|
|
187
207
|
configDir: join(homedir(), ".gemini"),
|
|
188
208
|
observerAvailable: true,
|
|
209
|
+
observationMethod: "hook",
|
|
189
210
|
},
|
|
190
211
|
|
|
191
212
|
// AI Chat/Desktop
|
|
@@ -195,6 +216,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
195
216
|
category: "chat",
|
|
196
217
|
macApp: "/Applications/ChatGPT.app",
|
|
197
218
|
observerAvailable: false,
|
|
219
|
+
observationMethod: "none",
|
|
198
220
|
},
|
|
199
221
|
{
|
|
200
222
|
id: "claude-desktop",
|
|
@@ -202,6 +224,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
202
224
|
category: "chat",
|
|
203
225
|
macApp: "/Applications/Claude.app",
|
|
204
226
|
observerAvailable: false,
|
|
227
|
+
observationMethod: "none",
|
|
205
228
|
},
|
|
206
229
|
{
|
|
207
230
|
id: "ollama",
|
|
@@ -210,6 +233,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
210
233
|
binary: "ollama",
|
|
211
234
|
configDir: join(homedir(), ".ollama"),
|
|
212
235
|
observerAvailable: false,
|
|
236
|
+
observationMethod: "none",
|
|
213
237
|
},
|
|
214
238
|
|
|
215
239
|
// AI Orchestration
|
|
@@ -220,6 +244,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
220
244
|
binary: "openclaw",
|
|
221
245
|
configDir: join(homedir(), ".openclaw"),
|
|
222
246
|
observerAvailable: false,
|
|
247
|
+
observationMethod: "none",
|
|
223
248
|
},
|
|
224
249
|
{
|
|
225
250
|
id: "langchain",
|
|
@@ -227,6 +252,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
227
252
|
category: "orchestration",
|
|
228
253
|
pipPackage: "langchain",
|
|
229
254
|
observerAvailable: false,
|
|
255
|
+
observationMethod: "none",
|
|
230
256
|
},
|
|
231
257
|
{
|
|
232
258
|
id: "crewai",
|
|
@@ -234,6 +260,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
234
260
|
category: "orchestration",
|
|
235
261
|
pipPackage: "crewai",
|
|
236
262
|
observerAvailable: false,
|
|
263
|
+
observationMethod: "none",
|
|
237
264
|
},
|
|
238
265
|
|
|
239
266
|
// API Providers (detected via env vars)
|
|
@@ -243,6 +270,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
243
270
|
category: "api-provider",
|
|
244
271
|
envVars: ["ANTHROPIC_API_KEY"],
|
|
245
272
|
observerAvailable: false,
|
|
273
|
+
observationMethod: "none",
|
|
246
274
|
},
|
|
247
275
|
{
|
|
248
276
|
id: "openai-api",
|
|
@@ -250,6 +278,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
250
278
|
category: "api-provider",
|
|
251
279
|
envVars: ["OPENAI_API_KEY"],
|
|
252
280
|
observerAvailable: false,
|
|
281
|
+
observationMethod: "none",
|
|
253
282
|
},
|
|
254
283
|
{
|
|
255
284
|
id: "google-api",
|
|
@@ -257,6 +286,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
257
286
|
category: "api-provider",
|
|
258
287
|
envVars: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
|
|
259
288
|
observerAvailable: false,
|
|
289
|
+
observationMethod: "none",
|
|
260
290
|
},
|
|
261
291
|
{
|
|
262
292
|
id: "groq-api",
|
|
@@ -264,6 +294,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
264
294
|
category: "api-provider",
|
|
265
295
|
envVars: ["GROQ_API_KEY"],
|
|
266
296
|
observerAvailable: false,
|
|
297
|
+
observationMethod: "none",
|
|
267
298
|
},
|
|
268
299
|
{
|
|
269
300
|
id: "mistral-api",
|
|
@@ -271,6 +302,7 @@ const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
271
302
|
category: "api-provider",
|
|
272
303
|
envVars: ["MISTRAL_API_KEY"],
|
|
273
304
|
observerAvailable: false,
|
|
305
|
+
observationMethod: "none",
|
|
274
306
|
},
|
|
275
307
|
];
|
|
276
308
|
|
|
@@ -526,6 +558,7 @@ export function detectAllTools(): DetectedTool[] {
|
|
|
526
558
|
configDir: installed ? configDir : undefined,
|
|
527
559
|
observerAvailable: def.observerAvailable,
|
|
528
560
|
observerActive,
|
|
561
|
+
observationMethod: def.observationMethod,
|
|
529
562
|
});
|
|
530
563
|
}
|
|
531
564
|
|
|
@@ -0,0 +1,193 @@
|
|
|
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
|
+
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
|
|
13
|
+
// ── Constants ──────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const LABEL = "ai.jobarbiter.observer";
|
|
16
|
+
const LAUNCH_AGENTS_DIR = join(homedir(), "Library", "LaunchAgents");
|
|
17
|
+
const PLIST_PATH = join(LAUNCH_AGENTS_DIR, `${LABEL}.plist`);
|
|
18
|
+
const OBSERVER_DIR = join(homedir(), ".config", "jobarbiter", "observer");
|
|
19
|
+
const LOG_PATH = join(OBSERVER_DIR, "poll.log");
|
|
20
|
+
|
|
21
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export interface DaemonStatus {
|
|
24
|
+
installed: boolean;
|
|
25
|
+
loaded: boolean;
|
|
26
|
+
interval: number | null;
|
|
27
|
+
plistPath: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function resolveJobarbiterPath(): string {
|
|
33
|
+
// Try to find the jobarbiter binary
|
|
34
|
+
try {
|
|
35
|
+
const path = execSync("command -v jobarbiter", { encoding: "utf-8", timeout: 3000 }).trim();
|
|
36
|
+
if (path) return path;
|
|
37
|
+
} catch {
|
|
38
|
+
// Fall through
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Fallback: use process.execPath with the dist path
|
|
42
|
+
// This covers the case where jobarbiter is run via node directly
|
|
43
|
+
return process.execPath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function generatePlist(intervalSeconds: number): string {
|
|
47
|
+
const binaryPath = resolveJobarbiterPath();
|
|
48
|
+
|
|
49
|
+
// If the binary is node itself, we need to figure out the script path
|
|
50
|
+
const isNodeBinary = binaryPath.includes("node") || binaryPath.includes("bun");
|
|
51
|
+
let programArgs: string;
|
|
52
|
+
|
|
53
|
+
if (isNodeBinary) {
|
|
54
|
+
// Find the actual CLI entry point
|
|
55
|
+
const cliPath = join(__dirname, "..", "index.js");
|
|
56
|
+
programArgs = ` <string>${binaryPath}</string>
|
|
57
|
+
<string>${cliPath}</string>
|
|
58
|
+
<string>observe</string>
|
|
59
|
+
<string>poll</string>`;
|
|
60
|
+
} else {
|
|
61
|
+
programArgs = ` <string>${binaryPath}</string>
|
|
62
|
+
<string>observe</string>
|
|
63
|
+
<string>poll</string>`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
67
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
68
|
+
<plist version="1.0">
|
|
69
|
+
<dict>
|
|
70
|
+
<key>Label</key>
|
|
71
|
+
<string>${LABEL}</string>
|
|
72
|
+
<key>ProgramArguments</key>
|
|
73
|
+
<array>
|
|
74
|
+
${programArgs}
|
|
75
|
+
</array>
|
|
76
|
+
<key>StartInterval</key>
|
|
77
|
+
<integer>${intervalSeconds}</integer>
|
|
78
|
+
<key>StandardOutPath</key>
|
|
79
|
+
<string>${LOG_PATH}</string>
|
|
80
|
+
<key>StandardErrorPath</key>
|
|
81
|
+
<string>${LOG_PATH}</string>
|
|
82
|
+
<key>EnvironmentVariables</key>
|
|
83
|
+
<dict>
|
|
84
|
+
<key>PATH</key>
|
|
85
|
+
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
86
|
+
</dict>
|
|
87
|
+
<key>RunAtLoad</key>
|
|
88
|
+
<true/>
|
|
89
|
+
<key>Nice</key>
|
|
90
|
+
<integer>10</integer>
|
|
91
|
+
</dict>
|
|
92
|
+
</plist>
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isLoaded(): boolean {
|
|
97
|
+
try {
|
|
98
|
+
const output = execSync(`launchctl list 2>/dev/null`, { encoding: "utf-8", timeout: 5000 });
|
|
99
|
+
return output.includes(LABEL);
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
export function installDaemon(intervalSeconds = 7200): void {
|
|
108
|
+
mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true });
|
|
109
|
+
mkdirSync(OBSERVER_DIR, { recursive: true });
|
|
110
|
+
|
|
111
|
+
// Unload existing if present
|
|
112
|
+
if (isLoaded()) {
|
|
113
|
+
try {
|
|
114
|
+
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { timeout: 5000 });
|
|
115
|
+
} catch {
|
|
116
|
+
// May not be loaded
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Write plist
|
|
121
|
+
const plist = generatePlist(intervalSeconds);
|
|
122
|
+
writeFileSync(PLIST_PATH, plist);
|
|
123
|
+
|
|
124
|
+
// Load
|
|
125
|
+
try {
|
|
126
|
+
execSync(`launchctl load "${PLIST_PATH}"`, { timeout: 5000 });
|
|
127
|
+
} catch (err) {
|
|
128
|
+
throw new Error(`Failed to load LaunchAgent: ${err instanceof Error ? err.message : String(err)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function uninstallDaemon(): void {
|
|
133
|
+
// Unload
|
|
134
|
+
if (isLoaded()) {
|
|
135
|
+
try {
|
|
136
|
+
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { timeout: 5000 });
|
|
137
|
+
} catch {
|
|
138
|
+
// Best effort
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Delete plist
|
|
143
|
+
if (existsSync(PLIST_PATH)) {
|
|
144
|
+
try {
|
|
145
|
+
unlinkSync(PLIST_PATH);
|
|
146
|
+
} catch {
|
|
147
|
+
// Best effort
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function getDaemonStatus(): DaemonStatus {
|
|
153
|
+
const installed = existsSync(PLIST_PATH);
|
|
154
|
+
const loaded = isLoaded();
|
|
155
|
+
|
|
156
|
+
let interval: number | null = null;
|
|
157
|
+
if (installed) {
|
|
158
|
+
try {
|
|
159
|
+
const content = readFileSync(PLIST_PATH, "utf-8");
|
|
160
|
+
const match = content.match(/<key>StartInterval<\/key>\s*<integer>(\d+)<\/integer>/);
|
|
161
|
+
if (match) {
|
|
162
|
+
interval = parseInt(match[1], 10);
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// Best effort
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
installed,
|
|
171
|
+
loaded,
|
|
172
|
+
interval,
|
|
173
|
+
plistPath: PLIST_PATH,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function reloadDaemon(): void {
|
|
178
|
+
if (!existsSync(PLIST_PATH)) {
|
|
179
|
+
throw new Error("Daemon not installed. Run 'jobarbiter observe daemon install' first.");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { timeout: 5000 });
|
|
184
|
+
} catch {
|
|
185
|
+
// May not be loaded
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
execSync(`launchctl load "${PLIST_PATH}"`, { timeout: 5000 });
|
|
190
|
+
} catch (err) {
|
|
191
|
+
throw new Error(`Failed to reload daemon: ${err instanceof Error ? err.message : String(err)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
package/src/lib/observe.ts
CHANGED
|
@@ -125,9 +125,29 @@ const fs = require("fs");
|
|
|
125
125
|
const path = require("path");
|
|
126
126
|
const os = require("os");
|
|
127
127
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
);
|
|
128
|
+
const OBSERVER_DIR = path.join(os.homedir(), ".config", "jobarbiter", "observer");
|
|
129
|
+
const OBSERVATIONS_FILE = path.join(OBSERVER_DIR, "observations.json");
|
|
130
|
+
const PAUSED_FILE = path.join(OBSERVER_DIR, "PAUSED");
|
|
131
|
+
|
|
132
|
+
// Check PAUSED sentinel FIRST — if paused, exit immediately
|
|
133
|
+
try {
|
|
134
|
+
if (fs.existsSync(PAUSED_FILE)) {
|
|
135
|
+
const raw = fs.readFileSync(PAUSED_FILE, "utf-8");
|
|
136
|
+
const pauseData = JSON.parse(raw);
|
|
137
|
+
if (pauseData.expiresAt) {
|
|
138
|
+
const expiresAt = new Date(pauseData.expiresAt).getTime();
|
|
139
|
+
if (Date.now() < expiresAt) {
|
|
140
|
+
process.exit(0); // Still paused
|
|
141
|
+
}
|
|
142
|
+
// Expired — clean up and continue
|
|
143
|
+
fs.unlinkSync(PAUSED_FILE);
|
|
144
|
+
} else {
|
|
145
|
+
process.exit(0); // Paused indefinitely
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// If PAUSED file is corrupted, continue
|
|
150
|
+
}
|
|
131
151
|
|
|
132
152
|
// Read stdin
|
|
133
153
|
let input = "";
|
|
@@ -283,8 +303,14 @@ function appendObservation(obs) {
|
|
|
283
303
|
// Fire-and-forget: trigger analysis pipeline via CLI
|
|
284
304
|
// Spawns detached process so the AI tool is never blocked
|
|
285
305
|
try {
|
|
286
|
-
const { spawn } = require("child_process");
|
|
287
|
-
//
|
|
306
|
+
const { spawn, execSync } = require("child_process");
|
|
307
|
+
// Self-healing: verify jobarbiter binary exists before spawning
|
|
308
|
+
try {
|
|
309
|
+
execSync("command -v jobarbiter", { stdio: "ignore", timeout: 3000 });
|
|
310
|
+
} catch {
|
|
311
|
+
// jobarbiter CLI not found — exit silently
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
288
314
|
const child = spawn("jobarbiter", ["analyze", "--auto"], {
|
|
289
315
|
detached: true,
|
|
290
316
|
stdio: "ignore",
|
package/src/lib/onboard.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type DetectedTool,
|
|
22
22
|
type ToolCategory,
|
|
23
23
|
} from "./detect-tools.js";
|
|
24
|
+
import { installDaemon } from "./launchd.js";
|
|
24
25
|
import {
|
|
25
26
|
loadProviderKeys,
|
|
26
27
|
saveProviderKey,
|
|
@@ -764,6 +765,24 @@ async function runToolDetectionStep(
|
|
|
764
765
|
|
|
765
766
|
if (result.installed.length > 0) {
|
|
766
767
|
console.log(`\n ${sym.check} ${c.success(`${result.installed.length} observer${result.installed.length > 1 ? "s" : ""} installed!`)}`);
|
|
768
|
+
|
|
769
|
+
// Install background polling daemon
|
|
770
|
+
try {
|
|
771
|
+
installDaemon(7200);
|
|
772
|
+
console.log(` ${sym.check} ${c.success("Background poller installed (every 2 hours)")}`);
|
|
773
|
+
} catch {
|
|
774
|
+
console.log(` ${c.dim("Background poller skipped (install later: jobarbiter observe daemon install)")}`);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Show poller-observable tools
|
|
778
|
+
const pollerTools = allTools.filter((t) => t.installed && t.observationMethod === "poller");
|
|
779
|
+
if (pollerTools.length > 0) {
|
|
780
|
+
console.log(`\n ${c.bold("Poller will also scan these tools:")}`);
|
|
781
|
+
for (const t of pollerTools) {
|
|
782
|
+
console.log(` ${sym.bullet} ${formatToolDisplay(t)}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
767
786
|
console.log(`\n ${c.bold("What happens now?")}`);
|
|
768
787
|
console.log(` Observers activate each time you use your AI tools.`);
|
|
769
788
|
console.log(` Just work normally — every session builds your profile.\n`);
|
|
@@ -772,6 +791,8 @@ async function runToolDetectionStep(
|
|
|
772
791
|
console.log(` a work report to JobArbiter. Our agents interpret these reports`);
|
|
773
792
|
console.log(` to build and evolve your narrative profile over time.`);
|
|
774
793
|
console.log(` Raw session data never leaves your machine.\n`);
|
|
794
|
+
|
|
795
|
+
console.log(` ${c.dim("Manage observers: jobarbiter observe --help")}`);
|
|
775
796
|
}
|
|
776
797
|
} else {
|
|
777
798
|
console.log(c.dim("\n Skipped — you can install observers later with 'jobarbiter observe install'.\n"));
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
|
|
13
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface PauseWindow {
|
|
16
|
+
start: string; // ISO timestamp
|
|
17
|
+
end: string; // ISO timestamp
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PollState {
|
|
21
|
+
version: number;
|
|
22
|
+
lastPoll: Record<string, string>; // source -> ISO timestamp
|
|
23
|
+
knownSessionIds: string[]; // rolling 1000
|
|
24
|
+
pauseWindows: PauseWindow[];
|
|
25
|
+
disabledSources: string[];
|
|
26
|
+
interval: number; // seconds
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Paths ──────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const OBSERVER_DIR = join(homedir(), ".config", "jobarbiter", "observer");
|
|
32
|
+
const POLL_STATE_FILE = join(OBSERVER_DIR, "poll-state.json");
|
|
33
|
+
|
|
34
|
+
function defaultPollState(): PollState {
|
|
35
|
+
return {
|
|
36
|
+
version: 1,
|
|
37
|
+
lastPoll: {},
|
|
38
|
+
knownSessionIds: [],
|
|
39
|
+
pauseWindows: [],
|
|
40
|
+
disabledSources: [],
|
|
41
|
+
interval: 7200,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Core Functions ─────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export function loadPollState(): PollState {
|
|
48
|
+
try {
|
|
49
|
+
if (!existsSync(POLL_STATE_FILE)) return defaultPollState();
|
|
50
|
+
const raw = readFileSync(POLL_STATE_FILE, "utf-8");
|
|
51
|
+
const data = JSON.parse(raw) as Partial<PollState>;
|
|
52
|
+
return {
|
|
53
|
+
...defaultPollState(),
|
|
54
|
+
...data,
|
|
55
|
+
};
|
|
56
|
+
} catch {
|
|
57
|
+
return defaultPollState();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function savePollState(state: PollState): void {
|
|
62
|
+
mkdirSync(OBSERVER_DIR, { recursive: true });
|
|
63
|
+
writeFileSync(POLL_STATE_FILE, JSON.stringify(state, null, 2) + "\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function addPauseWindow(start: string, end: string): void {
|
|
67
|
+
const state = loadPollState();
|
|
68
|
+
state.pauseWindows.push({ start, end });
|
|
69
|
+
// Keep last 50 windows
|
|
70
|
+
if (state.pauseWindows.length > 50) {
|
|
71
|
+
state.pauseWindows = state.pauseWindows.slice(-50);
|
|
72
|
+
}
|
|
73
|
+
savePollState(state);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isInPauseWindow(timestamp: string): boolean {
|
|
77
|
+
const state = loadPollState();
|
|
78
|
+
const ts = new Date(timestamp).getTime();
|
|
79
|
+
return state.pauseWindows.some((w) => {
|
|
80
|
+
const start = new Date(w.start).getTime();
|
|
81
|
+
const end = new Date(w.end).getTime();
|
|
82
|
+
return ts >= start && ts <= end;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function addKnownSession(sessionId: string): void {
|
|
87
|
+
const state = loadPollState();
|
|
88
|
+
if (!state.knownSessionIds.includes(sessionId)) {
|
|
89
|
+
state.knownSessionIds.push(sessionId);
|
|
90
|
+
// Rolling window of 1000
|
|
91
|
+
if (state.knownSessionIds.length > 1000) {
|
|
92
|
+
state.knownSessionIds = state.knownSessionIds.slice(-1000);
|
|
93
|
+
}
|
|
94
|
+
savePollState(state);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function isKnownSession(sessionId: string): boolean {
|
|
99
|
+
const state = loadPollState();
|
|
100
|
+
return state.knownSessionIds.includes(sessionId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getDisabledSources(): string[] {
|
|
104
|
+
return loadPollState().disabledSources;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function setDisabledSource(source: string): void {
|
|
108
|
+
const state = loadPollState();
|
|
109
|
+
if (!state.disabledSources.includes(source)) {
|
|
110
|
+
state.disabledSources.push(source);
|
|
111
|
+
savePollState(state);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function removeDisabledSource(source: string): void {
|
|
116
|
+
const state = loadPollState();
|
|
117
|
+
state.disabledSources = state.disabledSources.filter((s) => s !== source);
|
|
118
|
+
savePollState(state);
|
|
119
|
+
}
|