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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript Poller
|
|
3
|
+
*
|
|
4
|
+
* Core polling logic that discovers and processes new transcripts
|
|
5
|
+
* from AI tools that don't support hooks (aider, goose, letta, etc.).
|
|
6
|
+
* Uses existing discoverTranscripts() and parseTranscriptFile() from
|
|
7
|
+
* transcript-reader.ts.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { discoverTranscripts, parseTranscriptFile } from "./transcript-reader.js";
|
|
13
|
+
import { detectAllTools } from "./detect-tools.js";
|
|
14
|
+
import { isPaused } from "./privacy-pause.js";
|
|
15
|
+
import { loadPollState, savePollState, addKnownSession, isKnownSession, isInPauseWindow, } from "./poll-state.js";
|
|
16
|
+
import { analyzeSession } from "./session-analyzer.js";
|
|
17
|
+
// ── Paths ──────────────────────────────────────────────────────────────
|
|
18
|
+
const OBSERVER_DIR = join(homedir(), ".config", "jobarbiter", "observer");
|
|
19
|
+
const OBSERVATIONS_FILE = join(OBSERVER_DIR, "observations.json");
|
|
20
|
+
// ── Source-to-tool mapping ─────────────────────────────────────────────
|
|
21
|
+
const POLLER_SOURCES = [
|
|
22
|
+
"aider", "goose", "letta", "zed", "amazon-q",
|
|
23
|
+
"claude-code", "cursor", "codex", "gemini", "opencode",
|
|
24
|
+
"continue", "cline", "copilot-chat", "windsurf", "openclaw", "warp", "idx",
|
|
25
|
+
];
|
|
26
|
+
// ── Main Entry Point ───────────────────────────────────────────────────
|
|
27
|
+
export async function pollAllSources(options) {
|
|
28
|
+
const result = {
|
|
29
|
+
sourcesPolled: [],
|
|
30
|
+
newSessions: 0,
|
|
31
|
+
skippedPaused: 0,
|
|
32
|
+
skippedDuplicate: 0,
|
|
33
|
+
errors: [],
|
|
34
|
+
};
|
|
35
|
+
// Check pause state first
|
|
36
|
+
if (isPaused()) {
|
|
37
|
+
if (options?.verbose) {
|
|
38
|
+
result.errors.push("Observation is paused — skipping poll");
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
const state = loadPollState();
|
|
43
|
+
// Determine which sources to poll
|
|
44
|
+
let sourcesToPoll;
|
|
45
|
+
if (options?.source) {
|
|
46
|
+
if (!POLLER_SOURCES.includes(options.source)) {
|
|
47
|
+
result.errors.push(`Unknown source: ${options.source}`);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
sourcesToPoll = [options.source];
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Filter to installed + not disabled
|
|
54
|
+
const allTools = detectAllTools();
|
|
55
|
+
const installedToolIds = new Set(allTools.filter((t) => t.installed).map((t) => t.id));
|
|
56
|
+
const disabledSet = new Set(state.disabledSources);
|
|
57
|
+
sourcesToPoll = POLLER_SOURCES.filter((source) => {
|
|
58
|
+
// Map source names to tool IDs (most are the same)
|
|
59
|
+
const toolId = sourceToToolId(source);
|
|
60
|
+
return installedToolIds.has(toolId) && !disabledSet.has(source);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (sourcesToPoll.length === 0) {
|
|
64
|
+
if (options?.verbose) {
|
|
65
|
+
result.errors.push("No sources to poll (none installed or all disabled)");
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
// Discover all transcripts, then filter to sources we want
|
|
70
|
+
const oldestSince = sourcesToPoll.reduce((oldest, source) => {
|
|
71
|
+
const sourceDate = options?.since || (state.lastPoll[source]
|
|
72
|
+
? new Date(state.lastPoll[source])
|
|
73
|
+
: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000));
|
|
74
|
+
return sourceDate < oldest ? sourceDate : oldest;
|
|
75
|
+
}, new Date());
|
|
76
|
+
const allDiscovered = discoverTranscripts({
|
|
77
|
+
since: oldestSince,
|
|
78
|
+
maxFiles: 50,
|
|
79
|
+
});
|
|
80
|
+
const sourcesToPollSet = new Set(sourcesToPoll);
|
|
81
|
+
for (const entry of allDiscovered) {
|
|
82
|
+
const source = entry.source;
|
|
83
|
+
if (!sourcesToPollSet.has(source))
|
|
84
|
+
continue;
|
|
85
|
+
result.sourcesPolled.push(source);
|
|
86
|
+
for (const file of entry.files) {
|
|
87
|
+
try {
|
|
88
|
+
const transcript = parseTranscriptFile(file, source);
|
|
89
|
+
if (!transcript || transcript.messages.length < 3)
|
|
90
|
+
continue;
|
|
91
|
+
const sessionId = transcript.sessionId;
|
|
92
|
+
// Dedup check
|
|
93
|
+
if (isKnownSession(sessionId)) {
|
|
94
|
+
result.skippedDuplicate++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
// Pause window check
|
|
98
|
+
if (transcript.startTime && isInPauseWindow(transcript.startTime)) {
|
|
99
|
+
result.skippedPaused++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (options?.dryRun) {
|
|
103
|
+
result.newSessions++;
|
|
104
|
+
if (options.verbose) {
|
|
105
|
+
console.log(` [dry-run] Would process: ${source}/${sessionId} (${transcript.messages.length} messages)`);
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Analyze and extract signals
|
|
110
|
+
const report = analyzeSession(transcript);
|
|
111
|
+
const observation = {
|
|
112
|
+
timestamp: new Date().toISOString(),
|
|
113
|
+
agent: source,
|
|
114
|
+
sessionId,
|
|
115
|
+
source: "poller",
|
|
116
|
+
signals: {
|
|
117
|
+
toolsUsed: report.qualitativeAssessment.toolFluency.toolsUsed,
|
|
118
|
+
fileExtensions: report.qualitativeAssessment.domainExpertise.domains,
|
|
119
|
+
messageCount: report.quantitativeMetrics.messageCount,
|
|
120
|
+
thinkingBlocks: report.quantitativeMetrics.thinkingBlocks,
|
|
121
|
+
tokenUsage: report.quantitativeMetrics.tokenCount > 0
|
|
122
|
+
? { total: report.quantitativeMetrics.tokenCount }
|
|
123
|
+
: null,
|
|
124
|
+
duration: report.quantitativeMetrics.sessionDurationMinutes > 0
|
|
125
|
+
? report.quantitativeMetrics.sessionDurationMinutes
|
|
126
|
+
: null,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
appendObservation(observation);
|
|
130
|
+
addKnownSession(sessionId);
|
|
131
|
+
result.newSessions++;
|
|
132
|
+
if (options?.verbose) {
|
|
133
|
+
console.log(` Processed: ${source}/${sessionId} (${transcript.messages.length} messages)`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
result.errors.push(`${source}/${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Update last poll time for this source
|
|
141
|
+
state.lastPoll[source] = new Date().toISOString();
|
|
142
|
+
}
|
|
143
|
+
// Save updated poll state
|
|
144
|
+
savePollState(state);
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
148
|
+
function sourceToToolId(source) {
|
|
149
|
+
const mapping = {
|
|
150
|
+
"zed": "zed-ai",
|
|
151
|
+
"amazon-q": "amazon-q",
|
|
152
|
+
"copilot-chat": "copilot-chat",
|
|
153
|
+
"warp": "warp-ai",
|
|
154
|
+
};
|
|
155
|
+
return mapping[source] || source;
|
|
156
|
+
}
|
|
157
|
+
function appendObservation(obs) {
|
|
158
|
+
mkdirSync(OBSERVER_DIR, { recursive: true });
|
|
159
|
+
try {
|
|
160
|
+
let data;
|
|
161
|
+
if (existsSync(OBSERVATIONS_FILE)) {
|
|
162
|
+
data = JSON.parse(readFileSync(OBSERVATIONS_FILE, "utf-8"));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
data = {
|
|
166
|
+
version: 1,
|
|
167
|
+
installedAt: new Date().toISOString(),
|
|
168
|
+
agents: {},
|
|
169
|
+
sessions: [],
|
|
170
|
+
accumulated: {
|
|
171
|
+
totalSessions: 0,
|
|
172
|
+
totalTokens: 0,
|
|
173
|
+
toolCounts: {},
|
|
174
|
+
domainSignals: [],
|
|
175
|
+
lastSubmitted: null,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const sessions = data.sessions;
|
|
180
|
+
sessions.push(obs);
|
|
181
|
+
const accumulated = data.accumulated;
|
|
182
|
+
accumulated.totalSessions = (accumulated.totalSessions || 0) + 1;
|
|
183
|
+
// Update tool counts
|
|
184
|
+
const signals = obs.signals;
|
|
185
|
+
const toolsUsed = (signals.toolsUsed || []);
|
|
186
|
+
const toolCounts = accumulated.toolCounts;
|
|
187
|
+
for (const tool of toolsUsed) {
|
|
188
|
+
toolCounts[tool] = (toolCounts[tool] || 0) + 1;
|
|
189
|
+
}
|
|
190
|
+
// Update token totals
|
|
191
|
+
const tokenUsage = signals.tokenUsage;
|
|
192
|
+
if (tokenUsage?.total) {
|
|
193
|
+
accumulated.totalTokens = (accumulated.totalTokens || 0) + tokenUsage.total;
|
|
194
|
+
}
|
|
195
|
+
// Rolling window of 500 sessions
|
|
196
|
+
if (sessions.length > 500) {
|
|
197
|
+
data.sessions = sessions.slice(-500);
|
|
198
|
+
}
|
|
199
|
+
writeFileSync(OBSERVATIONS_FILE, JSON.stringify(data, null, 2) + "\n");
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// If file is corrupted, start fresh
|
|
203
|
+
const freshData = {
|
|
204
|
+
version: 1,
|
|
205
|
+
installedAt: new Date().toISOString(),
|
|
206
|
+
agents: {},
|
|
207
|
+
sessions: [obs],
|
|
208
|
+
accumulated: {
|
|
209
|
+
totalSessions: 1,
|
|
210
|
+
totalTokens: 0,
|
|
211
|
+
toolCounts: {},
|
|
212
|
+
domainSignals: [],
|
|
213
|
+
lastSubmitted: null,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
writeFileSync(OBSERVATIONS_FILE, JSON.stringify(freshData, null, 2) + "\n");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uninstall Module
|
|
3
|
+
*
|
|
4
|
+
* Clean removal of all JobArbiter components:
|
|
5
|
+
* observers, daemon, hooks, and optionally data/config.
|
|
6
|
+
*/
|
|
7
|
+
export interface UninstallOptions {
|
|
8
|
+
keepData?: boolean;
|
|
9
|
+
keepConfig?: boolean;
|
|
10
|
+
force?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface UninstallResult {
|
|
13
|
+
observersRemoved: string[];
|
|
14
|
+
daemonUninstalled: boolean;
|
|
15
|
+
hooksDeleted: boolean;
|
|
16
|
+
dataDeleted: boolean;
|
|
17
|
+
configDeleted: boolean;
|
|
18
|
+
errors: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function uninstallAll(options?: UninstallOptions): UninstallResult;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uninstall Module
|
|
3
|
+
*
|
|
4
|
+
* Clean removal of all JobArbiter components:
|
|
5
|
+
* observers, daemon, hooks, and optionally data/config.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, rmSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { detectAgents, removeObservers } from "./observe.js";
|
|
11
|
+
import { uninstallDaemon } from "./launchd.js";
|
|
12
|
+
// ── Paths ──────────────────────────────────────────────────────────────
|
|
13
|
+
const CONFIG_DIR = join(homedir(), ".config", "jobarbiter");
|
|
14
|
+
const OBSERVER_DIR = join(CONFIG_DIR, "observer");
|
|
15
|
+
const HOOKS_DIR = join(OBSERVER_DIR, "hooks");
|
|
16
|
+
// ── Main Function ──────────────────────────────────────────────────────
|
|
17
|
+
export function uninstallAll(options = {}) {
|
|
18
|
+
const result = {
|
|
19
|
+
observersRemoved: [],
|
|
20
|
+
daemonUninstalled: false,
|
|
21
|
+
hooksDeleted: false,
|
|
22
|
+
dataDeleted: false,
|
|
23
|
+
configDeleted: false,
|
|
24
|
+
errors: [],
|
|
25
|
+
};
|
|
26
|
+
// 1. Remove observers from all agents
|
|
27
|
+
try {
|
|
28
|
+
const agents = detectAgents();
|
|
29
|
+
const withHooks = agents.filter((a) => a.hookInstalled);
|
|
30
|
+
if (withHooks.length > 0) {
|
|
31
|
+
const removeResult = removeObservers(withHooks.map((a) => a.id));
|
|
32
|
+
result.observersRemoved = removeResult.removed;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
result.errors.push(`Failed to remove observers: ${err instanceof Error ? err.message : String(err)}`);
|
|
37
|
+
}
|
|
38
|
+
// 2. Uninstall launchd daemon
|
|
39
|
+
try {
|
|
40
|
+
uninstallDaemon();
|
|
41
|
+
result.daemonUninstalled = true;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
result.errors.push(`Failed to uninstall daemon: ${err instanceof Error ? err.message : String(err)}`);
|
|
45
|
+
}
|
|
46
|
+
// 3. Delete observer hooks directory
|
|
47
|
+
try {
|
|
48
|
+
if (existsSync(HOOKS_DIR)) {
|
|
49
|
+
rmSync(HOOKS_DIR, { recursive: true, force: true });
|
|
50
|
+
result.hooksDeleted = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
result.errors.push(`Failed to delete hooks: ${err instanceof Error ? err.message : String(err)}`);
|
|
55
|
+
}
|
|
56
|
+
// 4. Optionally delete observation data
|
|
57
|
+
if (!options.keepData) {
|
|
58
|
+
try {
|
|
59
|
+
if (existsSync(OBSERVER_DIR)) {
|
|
60
|
+
rmSync(OBSERVER_DIR, { recursive: true, force: true });
|
|
61
|
+
result.dataDeleted = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
result.errors.push(`Failed to delete observer data: ${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// 5. Optionally delete config
|
|
69
|
+
if (!options.keepConfig) {
|
|
70
|
+
try {
|
|
71
|
+
if (existsSync(CONFIG_DIR)) {
|
|
72
|
+
rmSync(CONFIG_DIR, { recursive: true, force: true });
|
|
73
|
+
result.configDeleted = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
result.errors.push(`Failed to delete config: ${err instanceof Error ? err.message : String(err)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|