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/src/index.ts CHANGED
@@ -6,8 +6,13 @@ import { api, apiUnauthenticated, ApiError } from "./lib/api.js";
6
6
  import { output, outputList, success, error, setJsonMode } from "./lib/output.js";
7
7
  import { runOnboardWizard } from "./lib/onboard.js";
8
8
  import { detectAgents, installObservers, removeObservers, getObservationStatus } from "./lib/observe.js";
9
- import { getObservableTools, formatToolDisplay } from "./lib/detect-tools.js";
9
+ import { getObservableTools, formatToolDisplay, detectAllTools } from "./lib/detect-tools.js";
10
10
  import { runAutoPipeline, analyzeFile, analyzeRecent } from "./lib/analysis-pipeline.js";
11
+ import { pollAllSources } from "./lib/transcript-poller.js";
12
+ import { pauseObservation, resumeObservation, isPaused, getPauseStatus } from "./lib/privacy-pause.js";
13
+ import { loadPollState, savePollState, setDisabledSource, removeDisabledSource } from "./lib/poll-state.js";
14
+ import { installDaemon, uninstallDaemon, getDaemonStatus, reloadDaemon } from "./lib/launchd.js";
15
+ import { uninstallAll } from "./lib/uninstall.js";
11
16
 
12
17
  import { readFileSync } from "node:fs";
13
18
  import { join, dirname } from "node:path";
@@ -801,16 +806,42 @@ const observe = program.command("observe").description("Manage AI tool proficien
801
806
 
802
807
  observe
803
808
  .command("status")
804
- .description("Show observer status and accumulated data")
809
+ .description("Show observer status, pause state, daemon, and accumulated data")
805
810
  .action(async () => {
806
811
  try {
807
812
  const agents = detectAgents();
808
813
  const status = getObservationStatus();
809
-
810
- const detected = agents.filter((a) => a.installed);
814
+ const pauseStatus = getPauseStatus();
815
+ const daemonStatus = getDaemonStatus();
816
+ const pollState = loadPollState();
811
817
 
812
818
  console.log("\n🔍 AI Agent Observers\n");
813
- console.log(" Agents:");
819
+
820
+ // Pause state
821
+ if (pauseStatus.paused) {
822
+ console.log(` ⏸️ Status: ${pauseStatus.expiresAt
823
+ ? `PAUSED (auto-resumes ${new Date(pauseStatus.expiresAt).toLocaleString()})`
824
+ : "PAUSED (indefinite)"}`);
825
+ console.log(` Since: ${pauseStatus.pausedAt ? new Date(pauseStatus.pausedAt).toLocaleString() : "unknown"}`);
826
+ } else {
827
+ console.log(" ▶️ Status: ACTIVE");
828
+ }
829
+
830
+ // Daemon state
831
+ console.log(`\n Poller Daemon:`);
832
+ if (daemonStatus.installed) {
833
+ console.log(` ${daemonStatus.loaded ? "✅" : "⚠️"} ${daemonStatus.loaded ? "Running" : "Installed but not loaded"}`);
834
+ if (daemonStatus.interval) {
835
+ const hours = Math.floor(daemonStatus.interval / 3600);
836
+ const mins = Math.floor((daemonStatus.interval % 3600) / 60);
837
+ console.log(` Interval: ${hours > 0 ? `${hours}h` : ""}${mins > 0 ? `${mins}m` : ""}`);
838
+ }
839
+ } else {
840
+ console.log(" ⬚ Not installed (run: jobarbiter observe daemon install)");
841
+ }
842
+
843
+ // Agents
844
+ console.log("\n Agents:");
814
845
  for (const agent of agents) {
815
846
  if (!agent.installed) {
816
847
  console.log(` ⬚ ${agent.name} (not installed)`);
@@ -821,6 +852,20 @@ observe
821
852
  }
822
853
  }
823
854
 
855
+ // Per-source poll stats
856
+ const pollEntries = Object.entries(pollState.lastPoll);
857
+ if (pollEntries.length > 0) {
858
+ console.log("\n Last Poll Per Source:");
859
+ for (const [source, time] of pollEntries) {
860
+ console.log(` ${source}: ${new Date(time).toLocaleString()}`);
861
+ }
862
+ }
863
+
864
+ // Disabled sources
865
+ if (pollState.disabledSources.length > 0) {
866
+ console.log(`\n Disabled Sources: ${pollState.disabledSources.join(", ")}`);
867
+ }
868
+
824
869
  console.log("\n Accumulated Data:");
825
870
  console.log(` Sessions observed: ${status.totalSessions}`);
826
871
  console.log(` Total tokens: ${status.totalTokens.toLocaleString()}`);
@@ -834,9 +879,25 @@ observe
834
879
  }
835
880
  }
836
881
 
882
+ // Pause history (last 5)
883
+ if (pauseStatus.pauseWindows.length > 0) {
884
+ console.log("\n Pause History (last 5):");
885
+ const recent = pauseStatus.pauseWindows.slice(-5);
886
+ for (const w of recent) {
887
+ const start = new Date(w.start).toLocaleString();
888
+ const end = new Date(w.end).toLocaleString();
889
+ console.log(` ${start} → ${end}`);
890
+ }
891
+ }
892
+
837
893
  console.log(`\n Data file: ~/.config/jobarbiter/observer/observations.json\n`);
838
894
 
839
895
  output({
896
+ paused: pauseStatus.paused,
897
+ pausedAt: pauseStatus.pausedAt,
898
+ expiresAt: pauseStatus.expiresAt,
899
+ daemon: daemonStatus,
900
+ disabledSources: pollState.disabledSources,
840
901
  detectedAgents: agents.map((a) => ({
841
902
  id: a.id,
842
903
  name: a.name,
@@ -844,6 +905,7 @@ observe
844
905
  observerActive: a.hookInstalled,
845
906
  })),
846
907
  ...status,
908
+ pauseWindows: pauseStatus.pauseWindows,
847
909
  });
848
910
  } catch (e) {
849
911
  handleError(e);
@@ -974,6 +1036,286 @@ observe
974
1036
  }
975
1037
  });
976
1038
 
1039
+ // ── observe poll ────────────────────────────────────────────────────────
1040
+
1041
+ observe
1042
+ .command("poll")
1043
+ .description("One-shot poll for new transcripts from all sources")
1044
+ .option("--source <id>", "Poll a specific source only")
1045
+ .option("--dry-run", "Show what would be processed without writing")
1046
+ .option("--since <date>", "Only process transcripts since this date (ISO 8601)")
1047
+ .option("--verbose", "Show detailed output")
1048
+ .action(async (opts) => {
1049
+ try {
1050
+ const since = opts.since ? new Date(opts.since) : undefined;
1051
+ const result = await pollAllSources({
1052
+ source: opts.source,
1053
+ dryRun: opts.dryRun,
1054
+ since,
1055
+ verbose: opts.verbose,
1056
+ });
1057
+
1058
+ if (!opts.verbose) {
1059
+ if (result.newSessions > 0) {
1060
+ success(`Found ${result.newSessions} new session(s) from ${result.sourcesPolled.length} source(s)`);
1061
+ } else {
1062
+ console.log(`No new sessions found (polled ${result.sourcesPolled.length} sources)`);
1063
+ }
1064
+ if (result.skippedDuplicate > 0) {
1065
+ console.log(` Skipped ${result.skippedDuplicate} duplicate(s)`);
1066
+ }
1067
+ if (result.skippedPaused > 0) {
1068
+ console.log(` Skipped ${result.skippedPaused} from pause window(s)`);
1069
+ }
1070
+ }
1071
+
1072
+ if (result.errors.length > 0) {
1073
+ for (const e of result.errors) {
1074
+ console.log(` ⚠️ ${e}`);
1075
+ }
1076
+ }
1077
+
1078
+ output(result as unknown as Record<string, unknown>);
1079
+ } catch (e) {
1080
+ handleError(e);
1081
+ }
1082
+ });
1083
+
1084
+ // ── observe pause/resume ───────────────────────────────────────────────
1085
+
1086
+ observe
1087
+ .command("pause")
1088
+ .description("Pause all observation (hooks + poller)")
1089
+ .option("--for <duration>", "Auto-resume after duration (e.g. 2h, 30m, 1d)")
1090
+ .action(async (opts) => {
1091
+ try {
1092
+ let expiresAt: Date | undefined;
1093
+ if (opts.for) {
1094
+ const seconds = parseDuration(opts.for);
1095
+ if (seconds <= 0) {
1096
+ error("Invalid duration. Use format like '2h', '30m', '1d'");
1097
+ process.exit(1);
1098
+ }
1099
+ expiresAt = new Date(Date.now() + seconds * 1000);
1100
+ }
1101
+
1102
+ pauseObservation(expiresAt);
1103
+
1104
+ if (expiresAt) {
1105
+ success(`Observation paused until ${expiresAt.toLocaleString()}`);
1106
+ } else {
1107
+ success("Observation paused indefinitely. Run 'jobarbiter observe resume' to resume.");
1108
+ }
1109
+
1110
+ output({ paused: true, expiresAt: expiresAt?.toISOString() || null });
1111
+ } catch (e) {
1112
+ handleError(e);
1113
+ }
1114
+ });
1115
+
1116
+ observe
1117
+ .command("resume")
1118
+ .description("Resume observation after a pause")
1119
+ .action(async () => {
1120
+ try {
1121
+ if (!isPaused()) {
1122
+ console.log("Observation is not paused.");
1123
+ output({ resumed: false, reason: "not_paused" });
1124
+ process.exit(0);
1125
+ }
1126
+
1127
+ resumeObservation();
1128
+ success("Observation resumed. Pause window recorded.");
1129
+ output({ resumed: true });
1130
+ } catch (e) {
1131
+ handleError(e);
1132
+ }
1133
+ });
1134
+
1135
+ // ── observe interval ───────────────────────────────────────────────────
1136
+
1137
+ observe
1138
+ .command("interval <duration>")
1139
+ .description("Change poll frequency (e.g. 2h, 30m, 1d)")
1140
+ .action(async (duration) => {
1141
+ try {
1142
+ const seconds = parseDuration(duration);
1143
+ if (seconds <= 0) {
1144
+ error("Invalid duration. Use format like '2h', '30m', '1d'");
1145
+ process.exit(1);
1146
+ }
1147
+
1148
+ const state = loadPollState();
1149
+ state.interval = seconds;
1150
+ savePollState(state);
1151
+
1152
+ // Reload daemon if installed
1153
+ const daemonStatus = getDaemonStatus();
1154
+ if (daemonStatus.installed) {
1155
+ installDaemon(seconds);
1156
+ success(`Poll interval set to ${duration} and daemon reloaded`);
1157
+ } else {
1158
+ success(`Poll interval set to ${duration}`);
1159
+ }
1160
+
1161
+ output({ interval: seconds });
1162
+ } catch (e) {
1163
+ handleError(e);
1164
+ }
1165
+ });
1166
+
1167
+ // ── observe enable/disable ─────────────────────────────────────────────
1168
+
1169
+ observe
1170
+ .command("enable <source>")
1171
+ .description("Re-enable a disabled observation source")
1172
+ .action(async (source) => {
1173
+ try {
1174
+ removeDisabledSource(source);
1175
+ success(`Source '${source}' enabled`);
1176
+ output({ source, enabled: true });
1177
+ } catch (e) {
1178
+ handleError(e);
1179
+ }
1180
+ });
1181
+
1182
+ observe
1183
+ .command("disable <source>")
1184
+ .description("Disable observation for a specific source")
1185
+ .action(async (source) => {
1186
+ try {
1187
+ setDisabledSource(source);
1188
+ success(`Source '${source}' disabled`);
1189
+ output({ source, disabled: true });
1190
+ } catch (e) {
1191
+ handleError(e);
1192
+ }
1193
+ });
1194
+
1195
+ // ── observe daemon ─────────────────────────────────────────────────────
1196
+
1197
+ const daemon = observe.command("daemon").description("Manage the background polling daemon (macOS LaunchAgent)");
1198
+
1199
+ daemon
1200
+ .command("install")
1201
+ .description("Install the background polling daemon")
1202
+ .option("--interval <duration>", "Poll interval (e.g. 2h, 30m)", "2h")
1203
+ .action(async (opts) => {
1204
+ try {
1205
+ const seconds = parseDuration(opts.interval);
1206
+ if (seconds <= 0) {
1207
+ error("Invalid interval. Use format like '2h', '30m', '1d'");
1208
+ process.exit(1);
1209
+ }
1210
+
1211
+ installDaemon(seconds);
1212
+ success(`Polling daemon installed (interval: ${opts.interval})`);
1213
+ console.log(" The daemon will poll for new transcripts in the background.");
1214
+ console.log(" Log: ~/.config/jobarbiter/observer/poll.log");
1215
+ output({ installed: true, interval: seconds });
1216
+ } catch (e) {
1217
+ handleError(e);
1218
+ }
1219
+ });
1220
+
1221
+ daemon
1222
+ .command("uninstall")
1223
+ .description("Remove the background polling daemon")
1224
+ .action(async () => {
1225
+ try {
1226
+ uninstallDaemon();
1227
+ success("Polling daemon uninstalled");
1228
+ output({ uninstalled: true });
1229
+ } catch (e) {
1230
+ handleError(e);
1231
+ }
1232
+ });
1233
+
1234
+ daemon
1235
+ .command("status")
1236
+ .description("Show daemon status")
1237
+ .action(async () => {
1238
+ try {
1239
+ const status = getDaemonStatus();
1240
+
1241
+ if (!status.installed) {
1242
+ console.log("\nDaemon not installed. Run: jobarbiter observe daemon install\n");
1243
+ } else {
1244
+ console.log(`\nDaemon: ${status.loaded ? "✅ Running" : "⚠️ Installed but not loaded"}`);
1245
+ if (status.interval) {
1246
+ const hours = Math.floor(status.interval / 3600);
1247
+ const mins = Math.floor((status.interval % 3600) / 60);
1248
+ console.log(`Interval: ${hours > 0 ? `${hours}h` : ""}${mins > 0 ? `${mins}m` : ""}`);
1249
+ }
1250
+ console.log(`Plist: ${status.plistPath}\n`);
1251
+ }
1252
+
1253
+ output(status as unknown as Record<string, unknown>);
1254
+ } catch (e) {
1255
+ handleError(e);
1256
+ }
1257
+ });
1258
+
1259
+ // ============================================================
1260
+ // uninstall (clean removal)
1261
+ // ============================================================
1262
+
1263
+ program
1264
+ .command("uninstall")
1265
+ .description("Remove all JobArbiter components from this system")
1266
+ .option("--keep-data", "Keep observation data")
1267
+ .option("--keep-config", "Keep configuration files")
1268
+ .option("--force", "Skip confirmation prompts")
1269
+ .action(async (opts) => {
1270
+ try {
1271
+ if (!opts.force) {
1272
+ console.log("\n⚠️ This will remove:");
1273
+ console.log(" - Observer hooks from all AI tools");
1274
+ console.log(" - Background polling daemon");
1275
+ console.log(" - Hook scripts");
1276
+ if (!opts.keepData) console.log(" - Observation data");
1277
+ if (!opts.keepConfig) console.log(" - Configuration and API keys");
1278
+ console.log();
1279
+
1280
+ const readline = await import("node:readline");
1281
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1282
+ const answer = await new Promise<string>((resolve) => {
1283
+ rl.question("Continue? [y/N] ", (ans) => { rl.close(); resolve(ans); });
1284
+ });
1285
+ if (!answer.toLowerCase().startsWith("y")) {
1286
+ console.log("Cancelled.");
1287
+ process.exit(0);
1288
+ }
1289
+ }
1290
+
1291
+ const result = uninstallAll({
1292
+ keepData: opts.keepData,
1293
+ keepConfig: opts.keepConfig,
1294
+ force: opts.force,
1295
+ });
1296
+
1297
+ if (result.observersRemoved.length > 0) {
1298
+ success(`Removed observers: ${result.observersRemoved.join(", ")}`);
1299
+ }
1300
+ if (result.daemonUninstalled) success("Daemon uninstalled");
1301
+ if (result.hooksDeleted) success("Hook scripts deleted");
1302
+ if (result.dataDeleted) success("Observation data deleted");
1303
+ if (result.configDeleted) success("Configuration deleted");
1304
+
1305
+ for (const e of result.errors) {
1306
+ error(e);
1307
+ }
1308
+
1309
+ if (result.errors.length === 0) {
1310
+ console.log("\n✅ JobArbiter fully uninstalled.\n");
1311
+ }
1312
+
1313
+ output(result as unknown as Record<string, unknown>);
1314
+ } catch (e) {
1315
+ handleError(e);
1316
+ }
1317
+ });
1318
+
977
1319
  // ============================================================
978
1320
  // analyze (session analysis and report submission)
979
1321
  // ============================================================
@@ -1078,6 +1420,7 @@ program
1078
1420
  .command("update")
1079
1421
  .description("Check for and install CLI updates")
1080
1422
  .option("--check", "Just check for updates, don't install")
1423
+ .option("--no-reinstall-observers", "Skip automatic observer reinstall on major/minor bump")
1081
1424
  .action(async (opts) => {
1082
1425
  try {
1083
1426
  console.log(`\nCurrent version: v${CLI_VERSION}`);
@@ -1129,9 +1472,31 @@ program
1129
1472
  pa[0] !== pb[0] || pa[1] !== pb[1];
1130
1473
 
1131
1474
  if (majorMinorChanged) {
1132
- console.log("\n⚠️ Major/minor version changed. You may want to reinstall observers:");
1133
- console.log(" jobarbiter observe remove --all");
1134
- console.log(" jobarbiter observe install --all\n");
1475
+ if (opts.reinstallObservers) {
1476
+ console.log("\nReinstalling observers for new version...");
1477
+ try {
1478
+ const agents = await detectAgents();
1479
+ const installedIds = agents.filter(a => a.installed).map(a => a.id);
1480
+ if (installedIds.length === 0) {
1481
+ console.log(" No observers installed — nothing to reinstall.\n");
1482
+ } else {
1483
+ await removeObservers(installedIds);
1484
+ console.log(` ✔ Removed observers for: ${installedIds.join(", ")}`);
1485
+ await installObservers(installedIds);
1486
+ success(`Reinstalled observers for: ${installedIds.join(", ")}`);
1487
+ }
1488
+ } catch (reinstallErr) {
1489
+ error(`Failed to reinstall observers: ${reinstallErr instanceof Error ? reinstallErr.message : String(reinstallErr)}`);
1490
+ console.log(" You can manually reinstall with:");
1491
+ console.log(" jobarbiter observe remove --all");
1492
+ console.log(" jobarbiter observe install --all\n");
1493
+ }
1494
+ } else {
1495
+ console.log("\n⚠️ Major/minor version changed. Skipped observer reinstall (--no-reinstall-observers).");
1496
+ console.log(" To reinstall manually:");
1497
+ console.log(" jobarbiter observe remove --all");
1498
+ console.log(" jobarbiter observe install --all\n");
1499
+ }
1135
1500
  }
1136
1501
  } catch (e) {
1137
1502
  handleError(e);
@@ -1161,6 +1526,28 @@ function handleError(e: unknown): void {
1161
1526
  process.exit(1);
1162
1527
  }
1163
1528
 
1529
+ /**
1530
+ * Parse duration strings like '2h', '30m', '1d' into seconds.
1531
+ */
1532
+ function parseDuration(input: string): number {
1533
+ const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d|w)$/i);
1534
+ if (!match) {
1535
+ // Try bare number as seconds
1536
+ const n = parseFloat(input);
1537
+ return isNaN(n) ? 0 : n;
1538
+ }
1539
+ const value = parseFloat(match[1]);
1540
+ const unit = match[2].toLowerCase();
1541
+ switch (unit) {
1542
+ case "s": return value;
1543
+ case "m": return value * 60;
1544
+ case "h": return value * 3600;
1545
+ case "d": return value * 86400;
1546
+ case "w": return value * 604800;
1547
+ default: return 0;
1548
+ }
1549
+ }
1550
+
1164
1551
  function formatCompensation(comp: Record<string, unknown> | undefined): string {
1165
1552
  if (!comp) return "Not listed";
1166
1553
  const min = comp.min as number;