@timetrakkr/cli 1.0.1 → 1.1.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/timetrakkr.cjs +271 -37
- package/package.json +2 -2
package/dist/timetrakkr.cjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
// packages/agent-cli/src/cli.ts
|
|
5
5
|
var import_node_child_process2 = require("node:child_process");
|
|
6
|
+
var import_promises = require("node:readline/promises");
|
|
6
7
|
|
|
7
8
|
// packages/agent-cli/src/browser.ts
|
|
8
9
|
var OPENABLE = /^https?:\/\/[A-Za-z0-9._~:/?&=%-]+$/;
|
|
@@ -714,6 +715,18 @@ var DEFAULT_MODEL_PRICES = [
|
|
|
714
715
|
var FORMAT = new RegExp("\\p{Cf}", "gu");
|
|
715
716
|
var CONTROL = new RegExp("\\p{Cc}", "gu");
|
|
716
717
|
|
|
718
|
+
// packages/core/src/agent-time/check-in-clock.ts
|
|
719
|
+
var CHECK_IN_BOUNDS = {
|
|
720
|
+
/** A longer silence between check-ins counts 0 and closes the turn. */
|
|
721
|
+
gapSeconds: 900,
|
|
722
|
+
/** No turn counts for longer than this. */
|
|
723
|
+
turnCapSeconds: 14400,
|
|
724
|
+
/** Check-in sessions one person may have open at once. */
|
|
725
|
+
maxOpenSessions: 20
|
|
726
|
+
};
|
|
727
|
+
var GAP_MS = CHECK_IN_BOUNDS.gapSeconds * 1e3;
|
|
728
|
+
var TURN_CAP_MS = CHECK_IN_BOUNDS.turnCapSeconds * 1e3;
|
|
729
|
+
|
|
717
730
|
// packages/agent-cli/src/git.ts
|
|
718
731
|
var import_node_child_process = require("node:child_process");
|
|
719
732
|
function git(root, env, args) {
|
|
@@ -908,6 +921,179 @@ function resolveContext(local, rules, credentials) {
|
|
|
908
921
|
};
|
|
909
922
|
}
|
|
910
923
|
|
|
924
|
+
// packages/agent-cli/src/cursor-setup.ts
|
|
925
|
+
var import_node_fs5 = require("node:fs");
|
|
926
|
+
var import_node_os2 = require("node:os");
|
|
927
|
+
var import_node_path4 = require("node:path");
|
|
928
|
+
var GATE_MARK = "timetrakkr-cursor-full-turns";
|
|
929
|
+
var CURSOR_GATE_COMMAND = `node -e 0 ${GATE_MARK}`;
|
|
930
|
+
var CURSOR_GATE_EVENTS = ["beforeSubmitPrompt", "stop"];
|
|
931
|
+
var BACKUP_SUFFIX = ".timetrakkr-backup";
|
|
932
|
+
function cursorDir(env) {
|
|
933
|
+
return (0, import_node_path4.join)(env.HOME || (0, import_node_os2.homedir)(), ".cursor");
|
|
934
|
+
}
|
|
935
|
+
var isGate = (entry) => isRecord(entry) && typeof entry.command === "string" && entry.command.trim().split(/\s+/).at(-1) === GATE_MARK;
|
|
936
|
+
function parse(text) {
|
|
937
|
+
let value;
|
|
938
|
+
try {
|
|
939
|
+
value = JSON.parse(text);
|
|
940
|
+
} catch {
|
|
941
|
+
return { why: "it is not valid JSON" };
|
|
942
|
+
}
|
|
943
|
+
if (!isRecord(value)) {
|
|
944
|
+
return { why: "it is not a JSON object" };
|
|
945
|
+
}
|
|
946
|
+
const hooks = value.hooks ?? {};
|
|
947
|
+
if (!isRecord(hooks)) {
|
|
948
|
+
return { why: '"hooks" is not an object' };
|
|
949
|
+
}
|
|
950
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
951
|
+
if (!Array.isArray(entries)) {
|
|
952
|
+
return { why: `"hooks.${event}" is not a list` };
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return { file: { ...value, hooks } };
|
|
956
|
+
}
|
|
957
|
+
function readCursorHooks(env) {
|
|
958
|
+
const dir = cursorDir(env);
|
|
959
|
+
if (!(0, import_node_fs5.existsSync)(dir)) {
|
|
960
|
+
return { kind: "no-cursor" };
|
|
961
|
+
}
|
|
962
|
+
const path = (0, import_node_path4.join)(dir, "hooks.json");
|
|
963
|
+
let text;
|
|
964
|
+
try {
|
|
965
|
+
text = (0, import_node_fs5.readFileSync)(path, "utf8");
|
|
966
|
+
} catch {
|
|
967
|
+
return { kind: "missing", path };
|
|
968
|
+
}
|
|
969
|
+
const parsed = parse(text);
|
|
970
|
+
if ("why" in parsed) {
|
|
971
|
+
return { kind: "invalid", path, why: parsed.why };
|
|
972
|
+
}
|
|
973
|
+
const missing = CURSOR_GATE_EVENTS.filter(
|
|
974
|
+
(event) => !(parsed.file.hooks[event] ?? []).some(isGate)
|
|
975
|
+
);
|
|
976
|
+
return { kind: "ok", path, file: parsed.file, missing };
|
|
977
|
+
}
|
|
978
|
+
function write(path, file) {
|
|
979
|
+
const temporary = `${path}.timetrakkr-tmp`;
|
|
980
|
+
(0, import_node_fs5.writeFileSync)(temporary, `${JSON.stringify(file, null, 2)}
|
|
981
|
+
`);
|
|
982
|
+
(0, import_node_fs5.renameSync)(temporary, path);
|
|
983
|
+
}
|
|
984
|
+
function added(file, events) {
|
|
985
|
+
const hooks = { ...file.hooks };
|
|
986
|
+
for (const event of events) {
|
|
987
|
+
hooks[event] = [...hooks[event] ?? [], { command: CURSOR_GATE_COMMAND }];
|
|
988
|
+
}
|
|
989
|
+
return { ...file, version: file.version ?? 1, hooks };
|
|
990
|
+
}
|
|
991
|
+
function removed(file) {
|
|
992
|
+
const hooks = {};
|
|
993
|
+
for (const [event, entries] of Object.entries(file.hooks)) {
|
|
994
|
+
const kept = entries.filter((entry) => !isGate(entry));
|
|
995
|
+
if (kept.length > 0 || entries.length === 0) {
|
|
996
|
+
hooks[event] = kept;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return { ...file, hooks };
|
|
1000
|
+
}
|
|
1001
|
+
function preview(events) {
|
|
1002
|
+
return events.map((event) => ` "${event}": [{ "command": "${CURSOR_GATE_COMMAND}" }]`);
|
|
1003
|
+
}
|
|
1004
|
+
function refuse(rt, path, why) {
|
|
1005
|
+
rt.err(
|
|
1006
|
+
`timetrakkr: ${path} cannot be read safely: ${why}. Fix or move it, then run this again. Nothing was changed.`
|
|
1007
|
+
);
|
|
1008
|
+
return 1;
|
|
1009
|
+
}
|
|
1010
|
+
async function confirmed(rt, args, path) {
|
|
1011
|
+
if (args.includes("--yes")) {
|
|
1012
|
+
return true;
|
|
1013
|
+
}
|
|
1014
|
+
const answer = await rt.ask(`Add them to ${path}? [y/N] `);
|
|
1015
|
+
if (answer === null) {
|
|
1016
|
+
rt.err('timetrakkr: nobody to ask. Run "timetrakkr cursor setup --yes" to add them.');
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1020
|
+
}
|
|
1021
|
+
async function setUp(rt, args, hooks) {
|
|
1022
|
+
if (hooks.kind === "invalid") {
|
|
1023
|
+
return refuse(rt, hooks.path, hooks.why);
|
|
1024
|
+
}
|
|
1025
|
+
if (hooks.kind === "no-cursor") {
|
|
1026
|
+
rt.err("timetrakkr: Cursor is not installed here (no ~/.cursor). Nothing was changed.");
|
|
1027
|
+
return 1;
|
|
1028
|
+
}
|
|
1029
|
+
const file = hooks.kind === "ok" ? hooks.file : { version: 1, hooks: {} };
|
|
1030
|
+
const missing = hooks.kind === "ok" ? hooks.missing : [...CURSOR_GATE_EVENTS];
|
|
1031
|
+
if (missing.length === 0) {
|
|
1032
|
+
rt.out(`Cursor full-turn hooks are already set up in ${hooks.path}.`);
|
|
1033
|
+
return 0;
|
|
1034
|
+
}
|
|
1035
|
+
rt.out(
|
|
1036
|
+
[
|
|
1037
|
+
"The Cursor CLI runs the Time Trakkr plugin\u2019s prompt and stop hooks only when your own",
|
|
1038
|
+
"hooks.json defines those events too. Without them, a turn counts from its first tool call",
|
|
1039
|
+
"to its last. These entries do nothing themselves; they let Cursor run the plugin\u2019s hooks:",
|
|
1040
|
+
"",
|
|
1041
|
+
...preview(missing),
|
|
1042
|
+
"",
|
|
1043
|
+
`They go in ${hooks.path}, next to anything already there.`
|
|
1044
|
+
].join("\n")
|
|
1045
|
+
);
|
|
1046
|
+
if (!await confirmed(rt, args, hooks.path)) {
|
|
1047
|
+
rt.out("Nothing was changed.");
|
|
1048
|
+
return 1;
|
|
1049
|
+
}
|
|
1050
|
+
if (hooks.kind === "ok") {
|
|
1051
|
+
(0, import_node_fs5.copyFileSync)(hooks.path, `${hooks.path}${BACKUP_SUFFIX}`);
|
|
1052
|
+
}
|
|
1053
|
+
write(hooks.path, added(file, missing));
|
|
1054
|
+
rt.out(
|
|
1055
|
+
hooks.kind === "ok" ? `Added. The file as it was is at ${hooks.path}${BACKUP_SUFFIX}. Undo with "timetrakkr cursor setup --undo".` : `Added. Undo with "timetrakkr cursor setup --undo".`
|
|
1056
|
+
);
|
|
1057
|
+
return 0;
|
|
1058
|
+
}
|
|
1059
|
+
function undo(rt, hooks) {
|
|
1060
|
+
if (hooks.kind === "invalid") {
|
|
1061
|
+
return refuse(rt, hooks.path, hooks.why);
|
|
1062
|
+
}
|
|
1063
|
+
if (hooks.kind !== "ok" || hooks.missing.length === CURSOR_GATE_EVENTS.length) {
|
|
1064
|
+
rt.out("Nothing to undo: there are no Time Trakkr entries in ~/.cursor/hooks.json.");
|
|
1065
|
+
return 0;
|
|
1066
|
+
}
|
|
1067
|
+
write(hooks.path, removed(hooks.file));
|
|
1068
|
+
rt.out(`Removed the Time Trakkr entries from ${hooks.path}.`);
|
|
1069
|
+
return 0;
|
|
1070
|
+
}
|
|
1071
|
+
async function cursorCommand(rt, args) {
|
|
1072
|
+
if (args[0] !== "setup") {
|
|
1073
|
+
rt.err("Usage: timetrakkr cursor setup [--yes] [--undo]");
|
|
1074
|
+
return 1;
|
|
1075
|
+
}
|
|
1076
|
+
const hooks = readCursorHooks(rt.env);
|
|
1077
|
+
return args.includes("--undo") ? undo(rt, hooks) : setUp(rt, args.slice(1), hooks);
|
|
1078
|
+
}
|
|
1079
|
+
function cursorStatusLine(env) {
|
|
1080
|
+
const hooks = readCursorHooks(env);
|
|
1081
|
+
switch (hooks.kind) {
|
|
1082
|
+
case "no-cursor":
|
|
1083
|
+
return null;
|
|
1084
|
+
case "invalid":
|
|
1085
|
+
return `Cursor: ${hooks.path} cannot be read (${hooks.why})`;
|
|
1086
|
+
case "ok":
|
|
1087
|
+
if (hooks.missing.length === 0) {
|
|
1088
|
+
return "Cursor: whole turns counted (timetrakkr cursor setup is done)";
|
|
1089
|
+
}
|
|
1090
|
+
break;
|
|
1091
|
+
default:
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
return 'Cursor: only the time from the first to the last tool call of each turn is counted in the CLI; run "timetrakkr cursor setup" to count whole turns';
|
|
1095
|
+
}
|
|
1096
|
+
|
|
911
1097
|
// packages/agent-cli/src/day.ts
|
|
912
1098
|
var pad = (n) => String(n).padStart(2, "0");
|
|
913
1099
|
function localDay(at) {
|
|
@@ -916,7 +1102,7 @@ function localDay(at) {
|
|
|
916
1102
|
}
|
|
917
1103
|
|
|
918
1104
|
// packages/agent-cli/src/drop.ts
|
|
919
|
-
var
|
|
1105
|
+
var import_node_fs8 = require("node:fs");
|
|
920
1106
|
|
|
921
1107
|
// packages/agent-cli/src/identity.ts
|
|
922
1108
|
var import_node_crypto4 = require("node:crypto");
|
|
@@ -943,7 +1129,7 @@ function ownedBy(session, identities) {
|
|
|
943
1129
|
}
|
|
944
1130
|
|
|
945
1131
|
// packages/agent-cli/src/store.ts
|
|
946
|
-
var
|
|
1132
|
+
var import_node_fs6 = require("node:fs");
|
|
947
1133
|
var EVENT_KINDS = ["turn_start", "activity", "wait", "turn_end"];
|
|
948
1134
|
function parseEvent(line) {
|
|
949
1135
|
try {
|
|
@@ -966,7 +1152,7 @@ function appendEvent(home, host, sessionId, event) {
|
|
|
966
1152
|
function readLog(home, host, sessionId) {
|
|
967
1153
|
let raw;
|
|
968
1154
|
try {
|
|
969
|
-
raw = (0,
|
|
1155
|
+
raw = (0, import_node_fs6.readFileSync)(logPath(home, host, sessionId));
|
|
970
1156
|
} catch {
|
|
971
1157
|
return { events: [], bytes: 0 };
|
|
972
1158
|
}
|
|
@@ -975,7 +1161,7 @@ function readLog(home, host, sessionId) {
|
|
|
975
1161
|
}
|
|
976
1162
|
function logSize(home, host, sessionId) {
|
|
977
1163
|
try {
|
|
978
|
-
return (0,
|
|
1164
|
+
return (0, import_node_fs6.statSync)(logPath(home, host, sessionId)).size;
|
|
979
1165
|
} catch {
|
|
980
1166
|
return 0;
|
|
981
1167
|
}
|
|
@@ -992,7 +1178,7 @@ function writeMeta(home, meta) {
|
|
|
992
1178
|
}
|
|
993
1179
|
function sessionIdsOf(home, host) {
|
|
994
1180
|
try {
|
|
995
|
-
return (0,
|
|
1181
|
+
return (0, import_node_fs6.readdirSync)(hostDir(home, host)).filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -".json".length)).filter(isSafeSessionId);
|
|
996
1182
|
} catch {
|
|
997
1183
|
return [];
|
|
998
1184
|
}
|
|
@@ -1005,15 +1191,15 @@ function listMetas(home) {
|
|
|
1005
1191
|
function pruneSessions(home, before) {
|
|
1006
1192
|
for (const meta of listMetas(home)) {
|
|
1007
1193
|
if (meta.updatedAt < before) {
|
|
1008
|
-
(0,
|
|
1009
|
-
(0,
|
|
1010
|
-
(0,
|
|
1194
|
+
(0, import_node_fs6.rmSync)(metaPath(home, meta.host, meta.sessionId), { force: true });
|
|
1195
|
+
(0, import_node_fs6.rmSync)(logPath(home, meta.host, meta.sessionId), { force: true });
|
|
1196
|
+
(0, import_node_fs6.rmSync)(tokensPath(home, meta.host, meta.sessionId), { force: true });
|
|
1011
1197
|
}
|
|
1012
1198
|
}
|
|
1013
1199
|
}
|
|
1014
1200
|
|
|
1015
1201
|
// packages/agent-cli/src/hosts/transcript.ts
|
|
1016
|
-
var
|
|
1202
|
+
var import_node_fs7 = require("node:fs");
|
|
1017
1203
|
var NEWLINE = 10;
|
|
1018
1204
|
function countOf(value, optional) {
|
|
1019
1205
|
if (value === void 0 && optional) {
|
|
@@ -1022,17 +1208,17 @@ function countOf(value, optional) {
|
|
|
1022
1208
|
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
|
|
1023
1209
|
}
|
|
1024
1210
|
function readWholeLines(path, offset) {
|
|
1025
|
-
const stat = (0,
|
|
1211
|
+
const stat = (0, import_node_fs7.statSync)(path);
|
|
1026
1212
|
if (!stat.isFile() || stat.size < offset) {
|
|
1027
1213
|
return null;
|
|
1028
1214
|
}
|
|
1029
1215
|
const length = Math.min(stat.size - offset, TRANSCRIPT_READ_BYTES);
|
|
1030
1216
|
const buffer = Buffer.alloc(length);
|
|
1031
|
-
const fd = (0,
|
|
1217
|
+
const fd = (0, import_node_fs7.openSync)(path, "r");
|
|
1032
1218
|
try {
|
|
1033
|
-
(0,
|
|
1219
|
+
(0, import_node_fs7.readSync)(fd, buffer, 0, length, offset);
|
|
1034
1220
|
} finally {
|
|
1035
|
-
(0,
|
|
1221
|
+
(0, import_node_fs7.closeSync)(fd);
|
|
1036
1222
|
}
|
|
1037
1223
|
const end = buffer.lastIndexOf(NEWLINE);
|
|
1038
1224
|
if (end === -1) {
|
|
@@ -1279,7 +1465,7 @@ function dropPending(home, meta) {
|
|
|
1279
1465
|
return;
|
|
1280
1466
|
}
|
|
1281
1467
|
if (size > meta.syncedBytes) {
|
|
1282
|
-
(0,
|
|
1468
|
+
(0, import_node_fs8.truncateSync)(logPath(home, host, sessionId), meta.syncedBytes);
|
|
1283
1469
|
markTokensUnreliable(home, host, sessionId);
|
|
1284
1470
|
}
|
|
1285
1471
|
const last = readLog(home, host, sessionId).events.at(-1);
|
|
@@ -1507,7 +1693,7 @@ async function flush(options) {
|
|
|
1507
1693
|
|
|
1508
1694
|
// packages/agent-cli/src/hosts/claude-code.ts
|
|
1509
1695
|
function mapClaudeCodeHook(event, payload) {
|
|
1510
|
-
if (!isRecord(payload)) {
|
|
1696
|
+
if (!isRecord(payload) || "cursor_version" in payload) {
|
|
1511
1697
|
return null;
|
|
1512
1698
|
}
|
|
1513
1699
|
const { session_id: sessionId, cwd } = payload;
|
|
@@ -1597,6 +1783,7 @@ function mapCodexHook(event, payload) {
|
|
|
1597
1783
|
}
|
|
1598
1784
|
|
|
1599
1785
|
// packages/agent-cli/src/hosts/cursor.ts
|
|
1786
|
+
var AUTO_MODEL = "default";
|
|
1600
1787
|
function directoryOf(payload) {
|
|
1601
1788
|
if (typeof payload.cwd === "string" && payload.cwd.length > 0) {
|
|
1602
1789
|
return payload.cwd;
|
|
@@ -1614,13 +1801,15 @@ function mapCursorHook(event, payload) {
|
|
|
1614
1801
|
if (!isSafeSessionId(sessionId) || cwd === null) {
|
|
1615
1802
|
return null;
|
|
1616
1803
|
}
|
|
1617
|
-
const model = modelOf(payload.model);
|
|
1804
|
+
const model = payload.model === AUTO_MODEL ? null : modelOf(payload.model);
|
|
1805
|
+
const turnId = isSafeSessionId(payload.generation_id) ? payload.generation_id : void 0;
|
|
1618
1806
|
const signal = (kind, durationMs) => ({
|
|
1619
1807
|
type: "event",
|
|
1620
1808
|
sessionId,
|
|
1621
1809
|
cwd,
|
|
1622
1810
|
event: durationMs === void 0 ? { kind } : { kind, durationMs },
|
|
1623
|
-
...model ? { model } : {}
|
|
1811
|
+
...model ? { model } : {},
|
|
1812
|
+
...turnId ? { turnId } : {}
|
|
1624
1813
|
});
|
|
1625
1814
|
switch (event) {
|
|
1626
1815
|
case "sessionStart":
|
|
@@ -1642,7 +1831,7 @@ function mapCursorHook(event, payload) {
|
|
|
1642
1831
|
}
|
|
1643
1832
|
|
|
1644
1833
|
// packages/agent-cli/src/login-background.ts
|
|
1645
|
-
var
|
|
1834
|
+
var import_node_fs9 = require("node:fs");
|
|
1646
1835
|
function lockAge(home, now) {
|
|
1647
1836
|
const value = readJson(loginLockPath(home));
|
|
1648
1837
|
return isRecord(value) && typeof value.at === "number" ? now - value.at : null;
|
|
@@ -1656,15 +1845,15 @@ function takeLoginLock(home, now) {
|
|
|
1656
1845
|
const path = loginLockPath(home);
|
|
1657
1846
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1658
1847
|
try {
|
|
1659
|
-
const fd = (0,
|
|
1660
|
-
(0,
|
|
1661
|
-
(0,
|
|
1662
|
-
return () => (0,
|
|
1848
|
+
const fd = (0, import_node_fs9.openSync)(path, "wx", 384);
|
|
1849
|
+
(0, import_node_fs9.writeSync)(fd, JSON.stringify({ at: now }));
|
|
1850
|
+
(0, import_node_fs9.closeSync)(fd);
|
|
1851
|
+
return () => (0, import_node_fs9.rmSync)(path, { force: true });
|
|
1663
1852
|
} catch {
|
|
1664
1853
|
if (loginInProgress(home, now)) {
|
|
1665
1854
|
return null;
|
|
1666
1855
|
}
|
|
1667
|
-
(0,
|
|
1856
|
+
(0, import_node_fs9.rmSync)(path, { force: true });
|
|
1668
1857
|
}
|
|
1669
1858
|
}
|
|
1670
1859
|
return null;
|
|
@@ -1680,7 +1869,7 @@ function writeLoginLink(home, link) {
|
|
|
1680
1869
|
writePrivateJson(loginLinkPath(home), link);
|
|
1681
1870
|
}
|
|
1682
1871
|
function clearLoginLink(home) {
|
|
1683
|
-
(0,
|
|
1872
|
+
(0, import_node_fs9.rmSync)(loginLinkPath(home), { force: true });
|
|
1684
1873
|
}
|
|
1685
1874
|
async function waitForLoginLink(home, now) {
|
|
1686
1875
|
const deadline = Date.now() + LOGIN_LINK_WAIT_MS;
|
|
@@ -1870,6 +2059,7 @@ async function refresh(run2, existing) {
|
|
|
1870
2059
|
identity: tracked && credentials.ok ? identitiesOf(home, credentials.credentials)[0] ?? null : base.identity ?? null,
|
|
1871
2060
|
credentialId: tracked && credentials.ok ? credentials.credentials.cacheId : base.credentialId ?? null,
|
|
1872
2061
|
model,
|
|
2062
|
+
turnId: (hookSignal.type === "event" ? hookSignal.turnId : void 0) ?? base.turnId ?? null,
|
|
1873
2063
|
updatedAt: at
|
|
1874
2064
|
};
|
|
1875
2065
|
if (tracked || existing) {
|
|
@@ -1905,12 +2095,32 @@ function readTokens(run2, meta) {
|
|
|
1905
2095
|
modelFrom(hookSignal) ?? meta.model
|
|
1906
2096
|
);
|
|
1907
2097
|
}
|
|
2098
|
+
function unopenedTurn(home, host, hookSignal) {
|
|
2099
|
+
if (hookSignal.type !== "event" || hookSignal.event.kind !== "activity" || hookSignal.turnId === void 0 || readMeta(home, host, hookSignal.sessionId)?.turnId === hookSignal.turnId) {
|
|
2100
|
+
return null;
|
|
2101
|
+
}
|
|
2102
|
+
return { ...hookSignal, event: { kind: "turn_start" } };
|
|
2103
|
+
}
|
|
2104
|
+
function isOpenAlready(home, host, hookSignal) {
|
|
2105
|
+
return hookSignal.type === "event" && hookSignal.event.kind === "turn_start" && hookSignal.turnId !== void 0 && readMeta(home, host, hookSignal.sessionId)?.turnId === hookSignal.turnId;
|
|
2106
|
+
}
|
|
1908
2107
|
async function handleHook(rt, host, event, signal = AbortSignal.timeout(NETWORK_BUDGET_MS)) {
|
|
1909
2108
|
const at = rt.now();
|
|
1910
2109
|
const hookSignal = ADAPTERS[host](event, parsePayload(await rt.stdin()));
|
|
1911
2110
|
if (!hookSignal) {
|
|
1912
2111
|
return;
|
|
1913
2112
|
}
|
|
2113
|
+
const home = stateHome(rt.env);
|
|
2114
|
+
if (isOpenAlready(home, host, hookSignal)) {
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
const opened = unopenedTurn(home, host, hookSignal);
|
|
2118
|
+
if (opened) {
|
|
2119
|
+
await handleSignal(rt, host, opened, at, signal);
|
|
2120
|
+
}
|
|
2121
|
+
await handleSignal(rt, host, hookSignal, at, signal);
|
|
2122
|
+
}
|
|
2123
|
+
async function handleSignal(rt, host, hookSignal, at, signal) {
|
|
1914
2124
|
const home = stateHome(rt.env);
|
|
1915
2125
|
const auth = authSession(rt, home, at, signal);
|
|
1916
2126
|
const credentials = reachesServer(hookSignal) ? await auth.fresh() : readCredentials(rt.env);
|
|
@@ -2059,6 +2269,10 @@ async function printStatus(rt) {
|
|
|
2059
2269
|
lines.push(`Last error: ${oneLine(session.meta.lastError)}`);
|
|
2060
2270
|
}
|
|
2061
2271
|
}
|
|
2272
|
+
const cursor = cursorStatusLine(rt.env);
|
|
2273
|
+
if (cursor) {
|
|
2274
|
+
lines.push(cursor);
|
|
2275
|
+
}
|
|
2062
2276
|
for (const line of lines) {
|
|
2063
2277
|
rt.out(line);
|
|
2064
2278
|
}
|
|
@@ -2066,14 +2280,14 @@ async function printStatus(rt) {
|
|
|
2066
2280
|
}
|
|
2067
2281
|
|
|
2068
2282
|
// packages/agent-cli/src/error-log.ts
|
|
2069
|
-
var
|
|
2283
|
+
var import_node_fs10 = require("node:fs");
|
|
2070
2284
|
function logHookError(home, now, where, error) {
|
|
2071
2285
|
try {
|
|
2072
2286
|
const path = errorLogPath(home);
|
|
2073
2287
|
const message = error instanceof Error ? error.message : "unexpected error";
|
|
2074
2288
|
try {
|
|
2075
|
-
if ((0,
|
|
2076
|
-
(0,
|
|
2289
|
+
if ((0, import_node_fs10.statSync)(path).size > ERROR_LOG_MAX_BYTES) {
|
|
2290
|
+
(0, import_node_fs10.truncateSync)(path);
|
|
2077
2291
|
}
|
|
2078
2292
|
} catch {
|
|
2079
2293
|
}
|
|
@@ -2172,7 +2386,7 @@ function recordEstimate(rt, args) {
|
|
|
2172
2386
|
|
|
2173
2387
|
// packages/agent-cli/src/login.ts
|
|
2174
2388
|
var import_node_crypto5 = require("node:crypto");
|
|
2175
|
-
var
|
|
2389
|
+
var import_node_os3 = require("node:os");
|
|
2176
2390
|
|
|
2177
2391
|
// packages/agent-cli/src/login-listener.ts
|
|
2178
2392
|
var import_node_http = require("node:http");
|
|
@@ -2493,7 +2707,7 @@ async function signIn(rt, options, onLink = () => void 0) {
|
|
|
2493
2707
|
const { endpoints } = discovered;
|
|
2494
2708
|
const listener = await startLoginListener();
|
|
2495
2709
|
try {
|
|
2496
|
-
const clientName = `Time Trakkr CLI on ${(0,
|
|
2710
|
+
const clientName = `Time Trakkr CLI on ${(0, import_node_os3.hostname)()}`.slice(0, MAX_CLIENT_NAME);
|
|
2497
2711
|
const client = await registerClient(endpoints.register, clientName, listener.redirectUri, setup);
|
|
2498
2712
|
if (!client.ok) {
|
|
2499
2713
|
rt.err(`Could not start sign-in with ${origin}: ${client.error}.`);
|
|
@@ -2647,11 +2861,11 @@ async function logout(rt) {
|
|
|
2647
2861
|
|
|
2648
2862
|
// packages/agent-cli/src/renew.ts
|
|
2649
2863
|
var import_node_crypto6 = require("node:crypto");
|
|
2650
|
-
var
|
|
2864
|
+
var import_node_fs11 = require("node:fs");
|
|
2651
2865
|
var sleep2 = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
2652
2866
|
function tryTakeLock(path, owner) {
|
|
2653
2867
|
try {
|
|
2654
|
-
(0,
|
|
2868
|
+
(0, import_node_fs11.writeFileSync)(path, owner, { flag: "wx", mode: 384 });
|
|
2655
2869
|
return true;
|
|
2656
2870
|
} catch {
|
|
2657
2871
|
return false;
|
|
@@ -2659,8 +2873,8 @@ function tryTakeLock(path, owner) {
|
|
|
2659
2873
|
}
|
|
2660
2874
|
function clearStaleLock(path) {
|
|
2661
2875
|
try {
|
|
2662
|
-
if (Date.now() - (0,
|
|
2663
|
-
(0,
|
|
2876
|
+
if (Date.now() - (0, import_node_fs11.statSync)(path).mtimeMs > LOCK_STALE_MS) {
|
|
2877
|
+
(0, import_node_fs11.rmSync)(path, { force: true });
|
|
2664
2878
|
}
|
|
2665
2879
|
} catch {
|
|
2666
2880
|
}
|
|
@@ -2681,8 +2895,8 @@ async function takeLock(home, signal) {
|
|
|
2681
2895
|
function releaseLock(home, owner) {
|
|
2682
2896
|
const path = authLockPath(home);
|
|
2683
2897
|
try {
|
|
2684
|
-
if ((0,
|
|
2685
|
-
(0,
|
|
2898
|
+
if ((0, import_node_fs11.readFileSync)(path, "utf8") === owner) {
|
|
2899
|
+
(0, import_node_fs11.rmSync)(path, { force: true });
|
|
2686
2900
|
}
|
|
2687
2901
|
} catch {
|
|
2688
2902
|
}
|
|
@@ -2724,7 +2938,7 @@ async function renewSignIn(home, now, rejected, signal) {
|
|
|
2724
2938
|
}
|
|
2725
2939
|
|
|
2726
2940
|
// packages/agent-cli/src/version.ts
|
|
2727
|
-
var CLI_VERSION = true ? "1.0
|
|
2941
|
+
var CLI_VERSION = true ? "1.1.0" : "development";
|
|
2728
2942
|
|
|
2729
2943
|
// packages/agent-cli/src/main.ts
|
|
2730
2944
|
var USAGE = [
|
|
@@ -2748,6 +2962,10 @@ var USAGE = [
|
|
|
2748
2962
|
" person or a team. Sent with the session for comparison; never",
|
|
2749
2963
|
" billed. --days and --weeks also work and can be combined:",
|
|
2750
2964
|
` a day is ${HOURS_PER_DAY} hours and a week is ${DAYS_PER_WEEK} days.`,
|
|
2965
|
+
" cursor setup [--yes] [--undo]",
|
|
2966
|
+
" Let the Cursor CLI report whole turns: adds two entries that do",
|
|
2967
|
+
" nothing themselves to ~/.cursor/hooks.json, after showing them",
|
|
2968
|
+
" and asking. --undo removes exactly those entries.",
|
|
2751
2969
|
" hook <host> <event> Called by an agent host; reads the hook payload on stdin",
|
|
2752
2970
|
" --version Print the version"
|
|
2753
2971
|
].join("\n");
|
|
@@ -2794,6 +3012,8 @@ async function run(argv, rt) {
|
|
|
2794
3012
|
return switchTracking(rt, false);
|
|
2795
3013
|
case "estimate":
|
|
2796
3014
|
return recordEstimate(rt, rest);
|
|
3015
|
+
case "cursor":
|
|
3016
|
+
return await cursorCommand(rt, rest);
|
|
2797
3017
|
case "version":
|
|
2798
3018
|
case "--version":
|
|
2799
3019
|
case "-v":
|
|
@@ -2845,6 +3065,19 @@ function openBrowser(url) {
|
|
|
2845
3065
|
detached(opener.command, opener.args);
|
|
2846
3066
|
}
|
|
2847
3067
|
}
|
|
3068
|
+
async function ask(question) {
|
|
3069
|
+
if (!process.stdin.isTTY) {
|
|
3070
|
+
return null;
|
|
3071
|
+
}
|
|
3072
|
+
const prompt = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
|
|
3073
|
+
try {
|
|
3074
|
+
return await prompt.question(question);
|
|
3075
|
+
} catch {
|
|
3076
|
+
return null;
|
|
3077
|
+
} finally {
|
|
3078
|
+
prompt.close();
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
2848
3081
|
var runtime = {
|
|
2849
3082
|
env: process.env,
|
|
2850
3083
|
cwd: process.cwd(),
|
|
@@ -2860,7 +3093,8 @@ var runtime = {
|
|
|
2860
3093
|
detached(process.execPath, [script, ...args]);
|
|
2861
3094
|
}
|
|
2862
3095
|
},
|
|
2863
|
-
openBrowser
|
|
3096
|
+
openBrowser,
|
|
3097
|
+
ask
|
|
2864
3098
|
};
|
|
2865
3099
|
var isHook = process.argv[2] === "hook";
|
|
2866
3100
|
function exit(code) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timetrakkr/cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Records how long a coding agent worked and reports it to Time Trakkr",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"time-tracking",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"homepage": "https://timetrakkr.com",
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"bin": {
|
|
16
|
-
"timetrakkr": "
|
|
16
|
+
"timetrakkr": "dist/timetrakkr.cjs"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist/timetrakkr.cjs"
|