@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.
Files changed (2) hide show
  1. package/dist/timetrakkr.cjs +271 -37
  2. package/package.json +2 -2
@@ -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 import_node_fs7 = require("node:fs");
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 import_node_fs5 = require("node:fs");
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, import_node_fs5.readFileSync)(logPath(home, host, sessionId));
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, import_node_fs5.statSync)(logPath(home, host, sessionId)).size;
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, import_node_fs5.readdirSync)(hostDir(home, host)).filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -".json".length)).filter(isSafeSessionId);
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, import_node_fs5.rmSync)(metaPath(home, meta.host, meta.sessionId), { force: true });
1009
- (0, import_node_fs5.rmSync)(logPath(home, meta.host, meta.sessionId), { force: true });
1010
- (0, import_node_fs5.rmSync)(tokensPath(home, meta.host, meta.sessionId), { force: true });
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 import_node_fs6 = require("node:fs");
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, import_node_fs6.statSync)(path);
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, import_node_fs6.openSync)(path, "r");
1217
+ const fd = (0, import_node_fs7.openSync)(path, "r");
1032
1218
  try {
1033
- (0, import_node_fs6.readSync)(fd, buffer, 0, length, offset);
1219
+ (0, import_node_fs7.readSync)(fd, buffer, 0, length, offset);
1034
1220
  } finally {
1035
- (0, import_node_fs6.closeSync)(fd);
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, import_node_fs7.truncateSync)(logPath(home, host, sessionId), meta.syncedBytes);
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 import_node_fs8 = require("node:fs");
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, import_node_fs8.openSync)(path, "wx", 384);
1660
- (0, import_node_fs8.writeSync)(fd, JSON.stringify({ at: now }));
1661
- (0, import_node_fs8.closeSync)(fd);
1662
- return () => (0, import_node_fs8.rmSync)(path, { force: true });
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, import_node_fs8.rmSync)(path, { force: true });
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, import_node_fs8.rmSync)(loginLinkPath(home), { force: true });
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 import_node_fs9 = require("node:fs");
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, import_node_fs9.statSync)(path).size > ERROR_LOG_MAX_BYTES) {
2076
- (0, import_node_fs9.truncateSync)(path);
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 import_node_os2 = require("node:os");
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, import_node_os2.hostname)()}`.slice(0, MAX_CLIENT_NAME);
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 import_node_fs10 = require("node:fs");
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, import_node_fs10.writeFileSync)(path, owner, { flag: "wx", mode: 384 });
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, import_node_fs10.statSync)(path).mtimeMs > LOCK_STALE_MS) {
2663
- (0, import_node_fs10.rmSync)(path, { force: true });
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, import_node_fs10.readFileSync)(path, "utf8") === owner) {
2685
- (0, import_node_fs10.rmSync)(path, { force: true });
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.1" : "development";
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.1",
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": "./dist/timetrakkr.cjs"
16
+ "timetrakkr": "dist/timetrakkr.cjs"
17
17
  },
18
18
  "files": [
19
19
  "dist/timetrakkr.cjs"