jobarbiter 0.3.12 → 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/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
  // ============================================================
@@ -1161,6 +1503,28 @@ function handleError(e: unknown): void {
1161
1503
  process.exit(1);
1162
1504
  }
1163
1505
 
1506
+ /**
1507
+ * Parse duration strings like '2h', '30m', '1d' into seconds.
1508
+ */
1509
+ function parseDuration(input: string): number {
1510
+ const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d|w)$/i);
1511
+ if (!match) {
1512
+ // Try bare number as seconds
1513
+ const n = parseFloat(input);
1514
+ return isNaN(n) ? 0 : n;
1515
+ }
1516
+ const value = parseFloat(match[1]);
1517
+ const unit = match[2].toLowerCase();
1518
+ switch (unit) {
1519
+ case "s": return value;
1520
+ case "m": return value * 60;
1521
+ case "h": return value * 3600;
1522
+ case "d": return value * 86400;
1523
+ case "w": return value * 604800;
1524
+ default: return 0;
1525
+ }
1526
+ }
1527
+
1164
1528
  function formatCompensation(comp: Record<string, unknown> | undefined): string {
1165
1529
  if (!comp) return "Not listed";
1166
1530
  const min = comp.min as number;
@@ -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