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.
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobarbiter",
3
- "version": "0.3.13",
3
+ "version": "0.4.1",
4
4
  "description": "CLI for JobArbiter — the first AI Proficiency Marketplace",
5
5
  "type": "module",
6
6
  "bin": {