jobarbiter 0.3.13 β†’ 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 CHANGED
@@ -6,6 +6,11 @@ import { output, outputList, success, error, setJsonMode } from "./lib/output.js
6
6
  import { runOnboardWizard } from "./lib/onboard.js";
7
7
  import { detectAgents, installObservers, removeObservers, getObservationStatus } from "./lib/observe.js";
8
8
  import { runAutoPipeline, analyzeFile, analyzeRecent } from "./lib/analysis-pipeline.js";
9
+ import { pollAllSources } from "./lib/transcript-poller.js";
10
+ import { pauseObservation, resumeObservation, isPaused, getPauseStatus } from "./lib/privacy-pause.js";
11
+ import { loadPollState, savePollState, setDisabledSource, removeDisabledSource } from "./lib/poll-state.js";
12
+ import { installDaemon, uninstallDaemon, getDaemonStatus } from "./lib/launchd.js";
13
+ import { uninstallAll } from "./lib/uninstall.js";
9
14
  import { readFileSync } from "node:fs";
10
15
  import { join, dirname } from "node:path";
11
16
  import { fileURLToPath } from "node:url";
@@ -760,14 +765,40 @@ program
760
765
  const observe = program.command("observe").description("Manage AI tool proficiency observers");
761
766
  observe
762
767
  .command("status")
763
- .description("Show observer status and accumulated data")
768
+ .description("Show observer status, pause state, daemon, and accumulated data")
764
769
  .action(async () => {
765
770
  try {
766
771
  const agents = detectAgents();
767
772
  const status = getObservationStatus();
768
- const detected = agents.filter((a) => a.installed);
773
+ const pauseStatus = getPauseStatus();
774
+ const daemonStatus = getDaemonStatus();
775
+ const pollState = loadPollState();
769
776
  console.log("\nπŸ” AI Agent Observers\n");
770
- console.log(" Agents:");
777
+ // Pause state
778
+ if (pauseStatus.paused) {
779
+ console.log(` ⏸️ Status: ${pauseStatus.expiresAt
780
+ ? `PAUSED (auto-resumes ${new Date(pauseStatus.expiresAt).toLocaleString()})`
781
+ : "PAUSED (indefinite)"}`);
782
+ console.log(` Since: ${pauseStatus.pausedAt ? new Date(pauseStatus.pausedAt).toLocaleString() : "unknown"}`);
783
+ }
784
+ else {
785
+ console.log(" ▢️ Status: ACTIVE");
786
+ }
787
+ // Daemon state
788
+ console.log(`\n Poller Daemon:`);
789
+ if (daemonStatus.installed) {
790
+ console.log(` ${daemonStatus.loaded ? "βœ…" : "⚠️"} ${daemonStatus.loaded ? "Running" : "Installed but not loaded"}`);
791
+ if (daemonStatus.interval) {
792
+ const hours = Math.floor(daemonStatus.interval / 3600);
793
+ const mins = Math.floor((daemonStatus.interval % 3600) / 60);
794
+ console.log(` Interval: ${hours > 0 ? `${hours}h` : ""}${mins > 0 ? `${mins}m` : ""}`);
795
+ }
796
+ }
797
+ else {
798
+ console.log(" ⬚ Not installed (run: jobarbiter observe daemon install)");
799
+ }
800
+ // Agents
801
+ console.log("\n Agents:");
771
802
  for (const agent of agents) {
772
803
  if (!agent.installed) {
773
804
  console.log(` ⬚ ${agent.name} (not installed)`);
@@ -779,6 +810,18 @@ observe
779
810
  console.log(` ⚠️ ${agent.name} (detected, no observer)`);
780
811
  }
781
812
  }
813
+ // Per-source poll stats
814
+ const pollEntries = Object.entries(pollState.lastPoll);
815
+ if (pollEntries.length > 0) {
816
+ console.log("\n Last Poll Per Source:");
817
+ for (const [source, time] of pollEntries) {
818
+ console.log(` ${source}: ${new Date(time).toLocaleString()}`);
819
+ }
820
+ }
821
+ // Disabled sources
822
+ if (pollState.disabledSources.length > 0) {
823
+ console.log(`\n Disabled Sources: ${pollState.disabledSources.join(", ")}`);
824
+ }
782
825
  console.log("\n Accumulated Data:");
783
826
  console.log(` Sessions observed: ${status.totalSessions}`);
784
827
  console.log(` Total tokens: ${status.totalTokens.toLocaleString()}`);
@@ -790,8 +833,23 @@ observe
790
833
  console.log(` ${tool}: ${count} uses`);
791
834
  }
792
835
  }
836
+ // Pause history (last 5)
837
+ if (pauseStatus.pauseWindows.length > 0) {
838
+ console.log("\n Pause History (last 5):");
839
+ const recent = pauseStatus.pauseWindows.slice(-5);
840
+ for (const w of recent) {
841
+ const start = new Date(w.start).toLocaleString();
842
+ const end = new Date(w.end).toLocaleString();
843
+ console.log(` ${start} β†’ ${end}`);
844
+ }
845
+ }
793
846
  console.log(`\n Data file: ~/.config/jobarbiter/observer/observations.json\n`);
794
847
  output({
848
+ paused: pauseStatus.paused,
849
+ pausedAt: pauseStatus.pausedAt,
850
+ expiresAt: pauseStatus.expiresAt,
851
+ daemon: daemonStatus,
852
+ disabledSources: pollState.disabledSources,
795
853
  detectedAgents: agents.map((a) => ({
796
854
  id: a.id,
797
855
  name: a.name,
@@ -799,6 +857,7 @@ observe
799
857
  observerActive: a.hookInstalled,
800
858
  })),
801
859
  ...status,
860
+ pauseWindows: pauseStatus.pauseWindows,
802
861
  });
803
862
  }
804
863
  catch (e) {
@@ -914,6 +973,270 @@ observe
914
973
  handleError(e);
915
974
  }
916
975
  });
976
+ // ── observe poll ────────────────────────────────────────────────────────
977
+ observe
978
+ .command("poll")
979
+ .description("One-shot poll for new transcripts from all sources")
980
+ .option("--source <id>", "Poll a specific source only")
981
+ .option("--dry-run", "Show what would be processed without writing")
982
+ .option("--since <date>", "Only process transcripts since this date (ISO 8601)")
983
+ .option("--verbose", "Show detailed output")
984
+ .action(async (opts) => {
985
+ try {
986
+ const since = opts.since ? new Date(opts.since) : undefined;
987
+ const result = await pollAllSources({
988
+ source: opts.source,
989
+ dryRun: opts.dryRun,
990
+ since,
991
+ verbose: opts.verbose,
992
+ });
993
+ if (!opts.verbose) {
994
+ if (result.newSessions > 0) {
995
+ success(`Found ${result.newSessions} new session(s) from ${result.sourcesPolled.length} source(s)`);
996
+ }
997
+ else {
998
+ console.log(`No new sessions found (polled ${result.sourcesPolled.length} sources)`);
999
+ }
1000
+ if (result.skippedDuplicate > 0) {
1001
+ console.log(` Skipped ${result.skippedDuplicate} duplicate(s)`);
1002
+ }
1003
+ if (result.skippedPaused > 0) {
1004
+ console.log(` Skipped ${result.skippedPaused} from pause window(s)`);
1005
+ }
1006
+ }
1007
+ if (result.errors.length > 0) {
1008
+ for (const e of result.errors) {
1009
+ console.log(` ⚠️ ${e}`);
1010
+ }
1011
+ }
1012
+ output(result);
1013
+ }
1014
+ catch (e) {
1015
+ handleError(e);
1016
+ }
1017
+ });
1018
+ // ── observe pause/resume ───────────────────────────────────────────────
1019
+ observe
1020
+ .command("pause")
1021
+ .description("Pause all observation (hooks + poller)")
1022
+ .option("--for <duration>", "Auto-resume after duration (e.g. 2h, 30m, 1d)")
1023
+ .action(async (opts) => {
1024
+ try {
1025
+ let expiresAt;
1026
+ if (opts.for) {
1027
+ const seconds = parseDuration(opts.for);
1028
+ if (seconds <= 0) {
1029
+ error("Invalid duration. Use format like '2h', '30m', '1d'");
1030
+ process.exit(1);
1031
+ }
1032
+ expiresAt = new Date(Date.now() + seconds * 1000);
1033
+ }
1034
+ pauseObservation(expiresAt);
1035
+ if (expiresAt) {
1036
+ success(`Observation paused until ${expiresAt.toLocaleString()}`);
1037
+ }
1038
+ else {
1039
+ success("Observation paused indefinitely. Run 'jobarbiter observe resume' to resume.");
1040
+ }
1041
+ output({ paused: true, expiresAt: expiresAt?.toISOString() || null });
1042
+ }
1043
+ catch (e) {
1044
+ handleError(e);
1045
+ }
1046
+ });
1047
+ observe
1048
+ .command("resume")
1049
+ .description("Resume observation after a pause")
1050
+ .action(async () => {
1051
+ try {
1052
+ if (!isPaused()) {
1053
+ console.log("Observation is not paused.");
1054
+ output({ resumed: false, reason: "not_paused" });
1055
+ process.exit(0);
1056
+ }
1057
+ resumeObservation();
1058
+ success("Observation resumed. Pause window recorded.");
1059
+ output({ resumed: true });
1060
+ }
1061
+ catch (e) {
1062
+ handleError(e);
1063
+ }
1064
+ });
1065
+ // ── observe interval ───────────────────────────────────────────────────
1066
+ observe
1067
+ .command("interval <duration>")
1068
+ .description("Change poll frequency (e.g. 2h, 30m, 1d)")
1069
+ .action(async (duration) => {
1070
+ try {
1071
+ const seconds = parseDuration(duration);
1072
+ if (seconds <= 0) {
1073
+ error("Invalid duration. Use format like '2h', '30m', '1d'");
1074
+ process.exit(1);
1075
+ }
1076
+ const state = loadPollState();
1077
+ state.interval = seconds;
1078
+ savePollState(state);
1079
+ // Reload daemon if installed
1080
+ const daemonStatus = getDaemonStatus();
1081
+ if (daemonStatus.installed) {
1082
+ installDaemon(seconds);
1083
+ success(`Poll interval set to ${duration} and daemon reloaded`);
1084
+ }
1085
+ else {
1086
+ success(`Poll interval set to ${duration}`);
1087
+ }
1088
+ output({ interval: seconds });
1089
+ }
1090
+ catch (e) {
1091
+ handleError(e);
1092
+ }
1093
+ });
1094
+ // ── observe enable/disable ─────────────────────────────────────────────
1095
+ observe
1096
+ .command("enable <source>")
1097
+ .description("Re-enable a disabled observation source")
1098
+ .action(async (source) => {
1099
+ try {
1100
+ removeDisabledSource(source);
1101
+ success(`Source '${source}' enabled`);
1102
+ output({ source, enabled: true });
1103
+ }
1104
+ catch (e) {
1105
+ handleError(e);
1106
+ }
1107
+ });
1108
+ observe
1109
+ .command("disable <source>")
1110
+ .description("Disable observation for a specific source")
1111
+ .action(async (source) => {
1112
+ try {
1113
+ setDisabledSource(source);
1114
+ success(`Source '${source}' disabled`);
1115
+ output({ source, disabled: true });
1116
+ }
1117
+ catch (e) {
1118
+ handleError(e);
1119
+ }
1120
+ });
1121
+ // ── observe daemon ─────────────────────────────────────────────────────
1122
+ const daemon = observe.command("daemon").description("Manage the background polling daemon (macOS LaunchAgent)");
1123
+ daemon
1124
+ .command("install")
1125
+ .description("Install the background polling daemon")
1126
+ .option("--interval <duration>", "Poll interval (e.g. 2h, 30m)", "2h")
1127
+ .action(async (opts) => {
1128
+ try {
1129
+ const seconds = parseDuration(opts.interval);
1130
+ if (seconds <= 0) {
1131
+ error("Invalid interval. Use format like '2h', '30m', '1d'");
1132
+ process.exit(1);
1133
+ }
1134
+ installDaemon(seconds);
1135
+ success(`Polling daemon installed (interval: ${opts.interval})`);
1136
+ console.log(" The daemon will poll for new transcripts in the background.");
1137
+ console.log(" Log: ~/.config/jobarbiter/observer/poll.log");
1138
+ output({ installed: true, interval: seconds });
1139
+ }
1140
+ catch (e) {
1141
+ handleError(e);
1142
+ }
1143
+ });
1144
+ daemon
1145
+ .command("uninstall")
1146
+ .description("Remove the background polling daemon")
1147
+ .action(async () => {
1148
+ try {
1149
+ uninstallDaemon();
1150
+ success("Polling daemon uninstalled");
1151
+ output({ uninstalled: true });
1152
+ }
1153
+ catch (e) {
1154
+ handleError(e);
1155
+ }
1156
+ });
1157
+ daemon
1158
+ .command("status")
1159
+ .description("Show daemon status")
1160
+ .action(async () => {
1161
+ try {
1162
+ const status = getDaemonStatus();
1163
+ if (!status.installed) {
1164
+ console.log("\nDaemon not installed. Run: jobarbiter observe daemon install\n");
1165
+ }
1166
+ else {
1167
+ console.log(`\nDaemon: ${status.loaded ? "βœ… Running" : "⚠️ Installed but not loaded"}`);
1168
+ if (status.interval) {
1169
+ const hours = Math.floor(status.interval / 3600);
1170
+ const mins = Math.floor((status.interval % 3600) / 60);
1171
+ console.log(`Interval: ${hours > 0 ? `${hours}h` : ""}${mins > 0 ? `${mins}m` : ""}`);
1172
+ }
1173
+ console.log(`Plist: ${status.plistPath}\n`);
1174
+ }
1175
+ output(status);
1176
+ }
1177
+ catch (e) {
1178
+ handleError(e);
1179
+ }
1180
+ });
1181
+ // ============================================================
1182
+ // uninstall (clean removal)
1183
+ // ============================================================
1184
+ program
1185
+ .command("uninstall")
1186
+ .description("Remove all JobArbiter components from this system")
1187
+ .option("--keep-data", "Keep observation data")
1188
+ .option("--keep-config", "Keep configuration files")
1189
+ .option("--force", "Skip confirmation prompts")
1190
+ .action(async (opts) => {
1191
+ try {
1192
+ if (!opts.force) {
1193
+ console.log("\n⚠️ This will remove:");
1194
+ console.log(" - Observer hooks from all AI tools");
1195
+ console.log(" - Background polling daemon");
1196
+ console.log(" - Hook scripts");
1197
+ if (!opts.keepData)
1198
+ console.log(" - Observation data");
1199
+ if (!opts.keepConfig)
1200
+ console.log(" - Configuration and API keys");
1201
+ console.log();
1202
+ const readline = await import("node:readline");
1203
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1204
+ const answer = await new Promise((resolve) => {
1205
+ rl.question("Continue? [y/N] ", (ans) => { rl.close(); resolve(ans); });
1206
+ });
1207
+ if (!answer.toLowerCase().startsWith("y")) {
1208
+ console.log("Cancelled.");
1209
+ process.exit(0);
1210
+ }
1211
+ }
1212
+ const result = uninstallAll({
1213
+ keepData: opts.keepData,
1214
+ keepConfig: opts.keepConfig,
1215
+ force: opts.force,
1216
+ });
1217
+ if (result.observersRemoved.length > 0) {
1218
+ success(`Removed observers: ${result.observersRemoved.join(", ")}`);
1219
+ }
1220
+ if (result.daemonUninstalled)
1221
+ success("Daemon uninstalled");
1222
+ if (result.hooksDeleted)
1223
+ success("Hook scripts deleted");
1224
+ if (result.dataDeleted)
1225
+ success("Observation data deleted");
1226
+ if (result.configDeleted)
1227
+ success("Configuration deleted");
1228
+ for (const e of result.errors) {
1229
+ error(e);
1230
+ }
1231
+ if (result.errors.length === 0) {
1232
+ console.log("\nβœ… JobArbiter fully uninstalled.\n");
1233
+ }
1234
+ output(result);
1235
+ }
1236
+ catch (e) {
1237
+ handleError(e);
1238
+ }
1239
+ });
917
1240
  // ============================================================
918
1241
  // analyze (session analysis and report submission)
919
1242
  // ============================================================
@@ -1090,6 +1413,27 @@ function handleError(e) {
1090
1413
  }
1091
1414
  process.exit(1);
1092
1415
  }
1416
+ /**
1417
+ * Parse duration strings like '2h', '30m', '1d' into seconds.
1418
+ */
1419
+ function parseDuration(input) {
1420
+ const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d|w)$/i);
1421
+ if (!match) {
1422
+ // Try bare number as seconds
1423
+ const n = parseFloat(input);
1424
+ return isNaN(n) ? 0 : n;
1425
+ }
1426
+ const value = parseFloat(match[1]);
1427
+ const unit = match[2].toLowerCase();
1428
+ switch (unit) {
1429
+ case "s": return value;
1430
+ case "m": return value * 60;
1431
+ case "h": return value * 3600;
1432
+ case "d": return value * 86400;
1433
+ case "w": return value * 604800;
1434
+ default: return 0;
1435
+ }
1436
+ }
1093
1437
  function formatCompensation(comp) {
1094
1438
  if (!comp)
1095
1439
  return "Not listed";
@@ -8,6 +8,7 @@
8
8
  * Never logs or stores API key values β€” only checks for existence.
9
9
  */
10
10
  export type ToolCategory = "ai-agent" | "chat" | "orchestration" | "api-provider";
11
+ export type ObservationMethod = "hook" | "extension" | "poller" | "both" | "none";
11
12
  export interface DetectedTool {
12
13
  id: string;
13
14
  name: string;
@@ -17,6 +18,7 @@ export interface DetectedTool {
17
18
  configDir?: string;
18
19
  observerAvailable: boolean;
19
20
  observerActive: boolean;
21
+ observationMethod: ObservationMethod;
20
22
  }
21
23
  /**
22
24
  * Detect all AI tools on the system.
@@ -21,6 +21,7 @@ const TOOL_DEFINITIONS = [
21
21
  binary: "claude",
22
22
  configDir: join(homedir(), ".claude"),
23
23
  observerAvailable: true,
24
+ observationMethod: "hook",
24
25
  },
25
26
  {
26
27
  id: "cursor",
@@ -30,6 +31,7 @@ const TOOL_DEFINITIONS = [
30
31
  configDir: join(homedir(), ".cursor"),
31
32
  macApp: "/Applications/Cursor.app",
32
33
  observerAvailable: true,
34
+ observationMethod: "hook",
33
35
  },
34
36
  {
35
37
  id: "github-copilot",
@@ -39,6 +41,7 @@ const TOOL_DEFINITIONS = [
39
41
  vscodeExtension: "github.copilot",
40
42
  cursorExtension: "github.copilot",
41
43
  observerAvailable: false,
44
+ observationMethod: "extension",
42
45
  },
43
46
  {
44
47
  id: "codex",
@@ -47,6 +50,7 @@ const TOOL_DEFINITIONS = [
47
50
  binary: "codex",
48
51
  configDir: join(homedir(), ".codex"),
49
52
  observerAvailable: true,
53
+ observationMethod: "hook",
50
54
  },
51
55
  {
52
56
  id: "opencode",
@@ -55,6 +59,7 @@ const TOOL_DEFINITIONS = [
55
59
  binary: "opencode",
56
60
  configDir: join(homedir(), ".config", "opencode"),
57
61
  observerAvailable: true,
62
+ observationMethod: "hook",
58
63
  },
59
64
  {
60
65
  id: "aider",
@@ -64,6 +69,7 @@ const TOOL_DEFINITIONS = [
64
69
  configDir: join(homedir(), ".aider"),
65
70
  pipPackage: "aider-chat",
66
71
  observerAvailable: false,
72
+ observationMethod: "poller",
67
73
  },
68
74
  {
69
75
  id: "continue",
@@ -72,6 +78,7 @@ const TOOL_DEFINITIONS = [
72
78
  vscodeExtension: "continue.continue",
73
79
  cursorExtension: "continue.continue",
74
80
  observerAvailable: false,
81
+ observationMethod: "extension",
75
82
  },
76
83
  {
77
84
  id: "cline",
@@ -80,6 +87,7 @@ const TOOL_DEFINITIONS = [
80
87
  vscodeExtension: "saoudrizwan.claude-dev",
81
88
  cursorExtension: "saoudrizwan.claude-dev",
82
89
  observerAvailable: false,
90
+ observationMethod: "extension",
83
91
  },
84
92
  {
85
93
  id: "windsurf",
@@ -88,6 +96,7 @@ const TOOL_DEFINITIONS = [
88
96
  binary: "windsurf",
89
97
  macApp: "/Applications/Windsurf.app",
90
98
  observerAvailable: false,
99
+ observationMethod: "extension",
91
100
  },
92
101
  {
93
102
  id: "copilot-chat",
@@ -96,6 +105,7 @@ const TOOL_DEFINITIONS = [
96
105
  vscodeExtension: "github.copilot-chat",
97
106
  cursorExtension: "github.copilot-chat",
98
107
  observerAvailable: false,
108
+ observationMethod: "extension",
99
109
  },
100
110
  {
101
111
  id: "zed-ai",
@@ -104,6 +114,7 @@ const TOOL_DEFINITIONS = [
104
114
  macApp: "/Applications/Zed.app",
105
115
  configDir: join(homedir(), platform() === "darwin" ? "Library/Application Support/Zed" : ".config/zed"),
106
116
  observerAvailable: false,
117
+ observationMethod: "poller",
107
118
  },
108
119
  {
109
120
  id: "amazon-q",
@@ -112,6 +123,7 @@ const TOOL_DEFINITIONS = [
112
123
  binary: "q",
113
124
  configDir: join(homedir(), ".aws", "amazonq"),
114
125
  observerAvailable: false,
126
+ observationMethod: "poller",
115
127
  },
116
128
  {
117
129
  id: "warp-ai",
@@ -120,6 +132,7 @@ const TOOL_DEFINITIONS = [
120
132
  macApp: "/Applications/Warp.app",
121
133
  configDir: join(homedir(), "Library", "Application Support", "dev.warp.Warp-Stable"),
122
134
  observerAvailable: false,
135
+ observationMethod: "none",
123
136
  },
124
137
  {
125
138
  id: "letta",
@@ -129,6 +142,7 @@ const TOOL_DEFINITIONS = [
129
142
  configDir: join(homedir(), ".letta"),
130
143
  pipPackage: "letta",
131
144
  observerAvailable: false,
145
+ observationMethod: "poller",
132
146
  },
133
147
  {
134
148
  id: "goose",
@@ -137,6 +151,7 @@ const TOOL_DEFINITIONS = [
137
151
  binary: "goose",
138
152
  configDir: join(homedir(), ".config", "goose"),
139
153
  observerAvailable: false,
154
+ observationMethod: "poller",
140
155
  },
141
156
  {
142
157
  id: "idx",
@@ -145,6 +160,7 @@ const TOOL_DEFINITIONS = [
145
160
  // IDX is cloud-based; no local binary. Detect via config dir if any local cache exists.
146
161
  configDir: join(homedir(), ".idx"),
147
162
  observerAvailable: false,
163
+ observationMethod: "none",
148
164
  },
149
165
  {
150
166
  id: "gemini",
@@ -153,6 +169,7 @@ const TOOL_DEFINITIONS = [
153
169
  binary: "gemini",
154
170
  configDir: join(homedir(), ".gemini"),
155
171
  observerAvailable: true,
172
+ observationMethod: "hook",
156
173
  },
157
174
  // AI Chat/Desktop
158
175
  {
@@ -161,6 +178,7 @@ const TOOL_DEFINITIONS = [
161
178
  category: "chat",
162
179
  macApp: "/Applications/ChatGPT.app",
163
180
  observerAvailable: false,
181
+ observationMethod: "none",
164
182
  },
165
183
  {
166
184
  id: "claude-desktop",
@@ -168,6 +186,7 @@ const TOOL_DEFINITIONS = [
168
186
  category: "chat",
169
187
  macApp: "/Applications/Claude.app",
170
188
  observerAvailable: false,
189
+ observationMethod: "none",
171
190
  },
172
191
  {
173
192
  id: "ollama",
@@ -176,6 +195,7 @@ const TOOL_DEFINITIONS = [
176
195
  binary: "ollama",
177
196
  configDir: join(homedir(), ".ollama"),
178
197
  observerAvailable: false,
198
+ observationMethod: "none",
179
199
  },
180
200
  // AI Orchestration
181
201
  {
@@ -185,6 +205,7 @@ const TOOL_DEFINITIONS = [
185
205
  binary: "openclaw",
186
206
  configDir: join(homedir(), ".openclaw"),
187
207
  observerAvailable: false,
208
+ observationMethod: "none",
188
209
  },
189
210
  {
190
211
  id: "langchain",
@@ -192,6 +213,7 @@ const TOOL_DEFINITIONS = [
192
213
  category: "orchestration",
193
214
  pipPackage: "langchain",
194
215
  observerAvailable: false,
216
+ observationMethod: "none",
195
217
  },
196
218
  {
197
219
  id: "crewai",
@@ -199,6 +221,7 @@ const TOOL_DEFINITIONS = [
199
221
  category: "orchestration",
200
222
  pipPackage: "crewai",
201
223
  observerAvailable: false,
224
+ observationMethod: "none",
202
225
  },
203
226
  // API Providers (detected via env vars)
204
227
  {
@@ -207,6 +230,7 @@ const TOOL_DEFINITIONS = [
207
230
  category: "api-provider",
208
231
  envVars: ["ANTHROPIC_API_KEY"],
209
232
  observerAvailable: false,
233
+ observationMethod: "none",
210
234
  },
211
235
  {
212
236
  id: "openai-api",
@@ -214,6 +238,7 @@ const TOOL_DEFINITIONS = [
214
238
  category: "api-provider",
215
239
  envVars: ["OPENAI_API_KEY"],
216
240
  observerAvailable: false,
241
+ observationMethod: "none",
217
242
  },
218
243
  {
219
244
  id: "google-api",
@@ -221,6 +246,7 @@ const TOOL_DEFINITIONS = [
221
246
  category: "api-provider",
222
247
  envVars: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
223
248
  observerAvailable: false,
249
+ observationMethod: "none",
224
250
  },
225
251
  {
226
252
  id: "groq-api",
@@ -228,6 +254,7 @@ const TOOL_DEFINITIONS = [
228
254
  category: "api-provider",
229
255
  envVars: ["GROQ_API_KEY"],
230
256
  observerAvailable: false,
257
+ observationMethod: "none",
231
258
  },
232
259
  {
233
260
  id: "mistral-api",
@@ -235,6 +262,7 @@ const TOOL_DEFINITIONS = [
235
262
  category: "api-provider",
236
263
  envVars: ["MISTRAL_API_KEY"],
237
264
  observerAvailable: false,
265
+ observationMethod: "none",
238
266
  },
239
267
  ];
240
268
  // ── Detection Helpers ──────────────────────────────────────────────────
@@ -481,6 +509,7 @@ export function detectAllTools() {
481
509
  configDir: installed ? configDir : undefined,
482
510
  observerAvailable: def.observerAvailable,
483
511
  observerActive,
512
+ observationMethod: def.observationMethod,
484
513
  });
485
514
  }
486
515
  return results;
@@ -0,0 +1,16 @@
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
+ export interface DaemonStatus {
8
+ installed: boolean;
9
+ loaded: boolean;
10
+ interval: number | null;
11
+ plistPath: string;
12
+ }
13
+ export declare function installDaemon(intervalSeconds?: number): void;
14
+ export declare function uninstallDaemon(): void;
15
+ export declare function getDaemonStatus(): DaemonStatus;
16
+ export declare function reloadDaemon(): void;