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/dist/index.js +347 -3
- 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 +198 -14
- 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 +369 -5
- 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 +221 -16
- 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
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
|
|
814
|
+
const pauseStatus = getPauseStatus();
|
|
815
|
+
const daemonStatus = getDaemonStatus();
|
|
816
|
+
const pollState = loadPollState();
|
|
811
817
|
|
|
812
818
|
console.log("\nš AI Agent Observers\n");
|
|
813
|
-
|
|
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;
|
package/src/lib/detect-tools.ts
CHANGED
|
@@ -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
|
|