@liebig-technology/clockodo-cli 0.1.0 → 0.2.1

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 (3) hide show
  1. package/README.md +10 -3
  2. package/dist/index.js +201 -34
  3. package/package.json +11 -13
package/README.md CHANGED
@@ -17,6 +17,8 @@ Commands:
17
17
  status Show running clock and today's summary
18
18
  start Start time tracking
19
19
  stop Stop time tracking
20
+ edit Edit the running clock entry
21
+ extend Extend the running clock by N minutes
20
22
  entries Manage time entries
21
23
  customers Manage customers
22
24
  projects Manage projects
@@ -54,18 +56,23 @@ Credentials are stored in `~/.config/clockodo-cli/config.json`. Environment vari
54
56
  ```bash
55
57
  # Time tracking
56
58
  clockodo start --customer 123 --service 456 --text "Working on feature"
59
+ clockodo start --customer 123 --service 456 --billable # explicit billability
57
60
  clockodo stop
58
61
  clockodo status
62
+ clockodo edit --text "New description" --billable # edit running entry
63
+ clockodo extend 30 # extend running clock by 30 minutes
59
64
 
60
65
  # Entries
61
66
  clockodo entries # list today's entries
62
67
  clockodo entries --since 2026-01-01 --until 2026-01-31 # date range
68
+ clockodo entries --billable # only billable entries
63
69
  clockodo entries create --from "09:00" --to "12:30" --customer 123 --service 456
70
+ clockodo entries update 42 --text "New description" --customer 789 --service 456
64
71
 
65
72
  # Reports
66
- clockodo report # today
67
- clockodo report week # this week
68
- clockodo report month # this month
73
+ clockodo report # today, grouped by project
74
+ clockodo report week --group customer # this week by customer
75
+ clockodo report month --group text # this month by description
69
76
 
70
77
  # Absences, work times, user reports
71
78
  clockodo absences list --year 2026
package/dist/index.js CHANGED
@@ -458,7 +458,7 @@ function registerAbsencesCommands(program2) {
458
458
  }
459
459
 
460
460
  // src/commands/clock.ts
461
- import { Billability } from "clockodo";
461
+ import { Billability, getEntryDurationUntilNow } from "clockodo";
462
462
 
463
463
  // src/lib/prompts.ts
464
464
  import * as p2 from "@clack/prompts";
@@ -595,7 +595,7 @@ function parseDateTime(input) {
595
595
 
596
596
  // src/commands/clock.ts
597
597
  function registerClockCommands(program2) {
598
- program2.command("start").description("Start time tracking").option("-c, --customer <id>", "Customer ID", parseIntStrict).option("-p, --project <id>", "Project ID", parseIntStrict).option("-s, --service <id>", "Service ID", parseIntStrict).option("-t, --text <description>", "Entry description").option("-b, --billable", "Mark as billable").action(async (cmdOpts) => {
598
+ program2.command("start").description("Start time tracking").option("-c, --customer <id>", "Customer ID", parseIntStrict).option("-p, --project <id>", "Project ID", parseIntStrict).option("-s, --service <id>", "Service ID", parseIntStrict).option("-t, --text <description>", "Entry description").option("-b, --billable", "Mark as billable").option("--no-billable", "Mark as not billable").action(async (cmdOpts) => {
599
599
  const opts = program2.opts();
600
600
  const mode = resolveOutputMode(opts);
601
601
  const client = getClient();
@@ -619,9 +619,11 @@ function registerClockCommands(program2) {
619
619
  const result = await client.startClock({
620
620
  customersId,
621
621
  servicesId,
622
+ ...cmdOpts.billable !== void 0 && {
623
+ billable: cmdOpts.billable ? Billability.Billable : Billability.NotBillable
624
+ },
622
625
  ...projectsId && { projectsId },
623
- ...cmdOpts.text && { text: cmdOpts.text },
624
- ...cmdOpts.billable && { billable: Billability.Billable }
626
+ ...cmdOpts.text && { text: cmdOpts.text }
625
627
  });
626
628
  if (mode !== "human") {
627
629
  printResult({ data: result.running }, opts);
@@ -652,6 +654,57 @@ function registerClockCommands(program2) {
652
654
  console.log(` Description: ${result.stopped.text}`);
653
655
  }
654
656
  });
657
+ program2.command("edit").description("Edit the running clock entry").option("-c, --customer <id>", "Customer ID", parseIntStrict).option("-p, --project <id>", "Project ID", parseIntStrict).option("-s, --service <id>", "Service ID", parseIntStrict).option("-t, --text <description>", "Entry description").option("-b, --billable", "Mark as billable").option("--no-billable", "Mark as not billable").action(async (cmdOpts) => {
658
+ const opts = program2.opts();
659
+ const client = getClient();
660
+ const clock = await client.getClock();
661
+ if (!clock.running) {
662
+ throw new CliError("No clock is currently running.", ExitCode.EMPTY_RESULTS);
663
+ }
664
+ const updates = { id: clock.running.id };
665
+ if (cmdOpts.customer !== void 0) updates.customersId = cmdOpts.customer;
666
+ if (cmdOpts.project !== void 0) updates.projectsId = cmdOpts.project;
667
+ if (cmdOpts.service !== void 0) updates.servicesId = cmdOpts.service;
668
+ if (cmdOpts.text !== void 0) updates.text = cmdOpts.text;
669
+ if (cmdOpts.billable !== void 0)
670
+ updates.billable = cmdOpts.billable ? Billability.Billable : Billability.NotBillable;
671
+ if (Object.keys(updates).length === 1) {
672
+ throw new CliError(
673
+ "No update flags provided.",
674
+ ExitCode.INVALID_ARGS,
675
+ "Use --text, --customer, --project, --service, --billable, or --no-billable"
676
+ );
677
+ }
678
+ const result = await client.editEntry(updates);
679
+ const mode = resolveOutputMode(opts);
680
+ if (mode !== "human") {
681
+ printResult({ data: result.entry }, opts);
682
+ return;
683
+ }
684
+ printSuccess("Running entry updated");
685
+ });
686
+ program2.command("extend").description("Extend the running clock by N minutes").argument("<minutes>", "Minutes to extend by", parseIntStrict).action(async (minutes) => {
687
+ const opts = program2.opts();
688
+ const client = getClient();
689
+ const clock = await client.getClock();
690
+ if (!clock.running) {
691
+ throw new CliError("No clock is currently running.", ExitCode.EMPTY_RESULTS);
692
+ }
693
+ const durationBefore = getEntryDurationUntilNow(clock.running);
694
+ const duration = durationBefore + minutes * 60;
695
+ const result = await client.changeClockDuration({
696
+ entriesId: clock.running.id,
697
+ durationBefore,
698
+ duration
699
+ });
700
+ const mode = resolveOutputMode(opts);
701
+ if (mode !== "human") {
702
+ printResult({ data: result.updated }, opts);
703
+ return;
704
+ }
705
+ printSuccess(`Clock extended by ${minutes} minutes`);
706
+ console.log(` New duration: ${formatDuration(duration)}`);
707
+ });
655
708
  }
656
709
 
657
710
  // src/commands/config.ts
@@ -764,10 +817,10 @@ function registerCustomersCommands(program2) {
764
817
  // src/commands/entries.ts
765
818
  import { styleText as styleText4 } from "util";
766
819
  import * as p4 from "@clack/prompts";
767
- import { getEntryDurationUntilNow, isTimeEntry } from "clockodo";
820
+ import { Billability as Billability2, getEntryDurationUntilNow as getEntryDurationUntilNow2, isTimeEntry } from "clockodo";
768
821
  function registerEntriesCommands(program2) {
769
822
  const entries = program2.command("entries").description("Manage time entries");
770
- entries.command("list", { isDefault: true }).description("List time entries").option("--since <date>", "Start date (default: today)", "today").option("--until <date>", "End date (default: today)").option("--customer <id>", "Filter by customer ID", parseIntStrict).option("--project <id>", "Filter by project ID", parseIntStrict).option("--service <id>", "Filter by service ID", parseIntStrict).option("--text <search>", "Filter by description text").option(
823
+ entries.command("list", { isDefault: true }).description("List time entries").option("--since <date>", "Start date (default: today)", "today").option("--until <date>", "End date (default: today)").option("--customer <id>", "Filter by customer ID", parseIntStrict).option("--project <id>", "Filter by project ID", parseIntStrict).option("--service <id>", "Filter by service ID", parseIntStrict).option("--text <search>", "Filter by description text").option("-b, --billable", "Show only billable entries").option("--no-billable", "Show only non-billable entries").option(
771
824
  "-g, --group <field>",
772
825
  "Group by: customer, project, service, text (shows summary table instead)"
773
826
  ).action(async (cmdOpts) => {
@@ -780,6 +833,8 @@ function registerEntriesCommands(program2) {
780
833
  if (cmdOpts.project) filter.projectsId = cmdOpts.project;
781
834
  if (cmdOpts.service) filter.servicesId = cmdOpts.service;
782
835
  if (cmdOpts.text) filter.text = cmdOpts.text;
836
+ if (cmdOpts.billable !== void 0)
837
+ filter.billable = cmdOpts.billable ? Billability2.Billable : Billability2.NotBillable;
783
838
  const result = await client.getEntries({
784
839
  timeSince: since,
785
840
  timeUntil: until,
@@ -795,7 +850,7 @@ function registerEntriesCommands(program2) {
795
850
  }
796
851
  return;
797
852
  }
798
- const totalSeconds = entryList.reduce((sum, e) => sum + getEntryDurationUntilNow(e), 0);
853
+ const totalSeconds = entryList.reduce((sum, e) => sum + getEntryDurationUntilNow2(e), 0);
799
854
  if (cmdOpts.group) {
800
855
  const groupKey = resolveGroupKey(cmdOpts.group);
801
856
  const groups = groupEntries(entryList, groupKey);
@@ -841,7 +896,7 @@ function registerEntriesCommands(program2) {
841
896
  formatDate(new Date(e.timeSince)),
842
897
  formatTime(e.timeSince),
843
898
  isTimeEntry(e) && !e.timeUntil ? styleText4("green", "running") : formatTime(e.timeUntil ?? e.timeSince),
844
- formatDuration(getEntryDurationUntilNow(e)),
899
+ formatDuration(getEntryDurationUntilNow2(e)),
845
900
  e.text || styleText4("dim", "\u2014")
846
901
  ]);
847
902
  printTable(["ID", "Date", "Start", "End", "Duration", "Description"], rows, opts);
@@ -862,7 +917,7 @@ function registerEntriesCommands(program2) {
862
917
  return;
863
918
  }
864
919
  const timeUntilDisplay = isTimeEntry(e) && !e.timeUntil ? "running" : formatTime(e.timeUntil ?? e.timeSince);
865
- const duration = getEntryDurationUntilNow(e);
920
+ const duration = getEntryDurationUntilNow2(e);
866
921
  printDetail(
867
922
  [
868
923
  ["ID", e.id],
@@ -874,12 +929,12 @@ function registerEntriesCommands(program2) {
874
929
  ["Customer ID", e.customersId],
875
930
  ["Project ID", e.projectsId ?? null],
876
931
  ["Service ID", isTimeEntry(e) ? e.servicesId : null],
877
- ["Billable", e.billable === 1 ? "Yes" : e.billable === 2 ? "Billed" : "No"]
932
+ ["Billable", formatBillable(e.billable)]
878
933
  ],
879
934
  opts
880
935
  );
881
936
  });
882
- entries.command("create").description("Create a time entry").requiredOption("--from <datetime>", "Start time (e.g., '2024-01-15 09:00' or '09:00')").requiredOption("--to <datetime>", "End time (e.g., '2024-01-15 17:00' or '17:00')").option("-c, --customer <id>", "Customer ID", parseIntStrict).option("-p, --project <id>", "Project ID", parseIntStrict).option("-s, --service <id>", "Service ID", parseIntStrict).option("-t, --text <description>", "Entry description").option("-b, --billable", "Mark as billable").action(async (cmdOpts) => {
937
+ entries.command("create").description("Create a time entry").requiredOption("--from <datetime>", "Start time (e.g., '2024-01-15 09:00' or '09:00')").requiredOption("--to <datetime>", "End time (e.g., '2024-01-15 17:00' or '17:00')").option("-c, --customer <id>", "Customer ID", parseIntStrict).option("-p, --project <id>", "Project ID", parseIntStrict).option("-s, --service <id>", "Service ID", parseIntStrict).option("-t, --text <description>", "Entry description").option("-b, --billable", "Mark as billable").option("--no-billable", "Mark as not billable").action(async (cmdOpts) => {
883
938
  const opts = program2.opts();
884
939
  const mode = resolveOutputMode(opts);
885
940
  const client = getClient();
@@ -900,10 +955,20 @@ function registerEntriesCommands(program2) {
900
955
  "Use --customer and --service flags, or set defaults via: clockodo config set"
901
956
  );
902
957
  }
958
+ let billable;
959
+ if (cmdOpts.billable !== void 0) {
960
+ billable = cmdOpts.billable ? Billability2.Billable : Billability2.NotBillable;
961
+ } else if (projectsId) {
962
+ const { data: project } = await client.getProject({ id: projectsId });
963
+ billable = project.billableDefault ? Billability2.Billable : Billability2.NotBillable;
964
+ } else {
965
+ const { data: customer } = await client.getCustomer({ id: customersId });
966
+ billable = customer.billableDefault ? Billability2.Billable : Billability2.NotBillable;
967
+ }
903
968
  const result = await client.addEntry({
904
969
  customersId,
905
970
  servicesId,
906
- billable: cmdOpts.billable ? 1 : 0,
971
+ billable,
907
972
  timeSince: parseDateTime(cmdOpts.from),
908
973
  timeUntil: parseDateTime(cmdOpts.to),
909
974
  ...projectsId && { projectsId },
@@ -914,20 +979,24 @@ function registerEntriesCommands(program2) {
914
979
  return;
915
980
  }
916
981
  const entry = result.entry;
917
- const duration = getEntryDurationUntilNow(entry);
982
+ const duration = getEntryDurationUntilNow2(entry);
918
983
  printSuccess(`Entry created (ID: ${entry.id})`);
919
984
  console.log(
920
985
  ` ${formatTime(entry.timeSince)} \u2014 ${entry.timeUntil ? formatTime(entry.timeUntil) : "?"} (${formatDuration(duration)})`
921
986
  );
922
987
  });
923
- entries.command("update <id>").description("Update a time entry").option("--from <datetime>", "New start time").option("--to <datetime>", "New end time").option("-t, --text <description>", "New description").option("-b, --billable", "Mark as billable").option("--no-billable", "Mark as not billable").action(async (id, cmdOpts) => {
988
+ entries.command("update <id>").description("Update a time entry").option("--from <datetime>", "New start time").option("--to <datetime>", "New end time").option("-t, --text <description>", "New description").option("-c, --customer <id>", "New customer ID", parseIntStrict).option("--project <id>", "New project ID", parseIntStrict).option("-s, --service <id>", "New service ID", parseIntStrict).option("-b, --billable", "Mark as billable").option("--no-billable", "Mark as not billable").action(async (id, cmdOpts) => {
924
989
  const opts = program2.opts();
925
990
  const client = getClient();
926
991
  const updates = { id: parseId(id) };
927
992
  if (cmdOpts.from) updates.timeSince = parseDateTime(cmdOpts.from);
928
993
  if (cmdOpts.to) updates.timeUntil = parseDateTime(cmdOpts.to);
929
994
  if (cmdOpts.text !== void 0) updates.text = cmdOpts.text;
930
- if (cmdOpts.billable !== void 0) updates.billable = cmdOpts.billable ? 1 : 0;
995
+ if (cmdOpts.customer !== void 0) updates.customersId = cmdOpts.customer;
996
+ if (cmdOpts.project !== void 0) updates.projectsId = cmdOpts.project;
997
+ if (cmdOpts.service !== void 0) updates.servicesId = cmdOpts.service;
998
+ if (cmdOpts.billable !== void 0)
999
+ updates.billable = cmdOpts.billable ? Billability2.Billable : Billability2.NotBillable;
931
1000
  const result = await client.editEntry(updates);
932
1001
  const mode = resolveOutputMode(opts);
933
1002
  if (mode !== "human") {
@@ -989,7 +1058,7 @@ function groupEntries(entries, key) {
989
1058
  groupValue = val != null ? String(val) : "(none)";
990
1059
  }
991
1060
  const existing = map.get(groupValue);
992
- const duration = getEntryDurationUntilNow(e);
1061
+ const duration = getEntryDurationUntilNow2(e);
993
1062
  if (existing) {
994
1063
  existing.count++;
995
1064
  existing.seconds += duration;
@@ -999,6 +1068,16 @@ function groupEntries(entries, key) {
999
1068
  }
1000
1069
  return [...map.entries()].map(([k, v]) => ({ key: k, ...v })).sort((a, b) => b.seconds - a.seconds);
1001
1070
  }
1071
+ function formatBillable(value) {
1072
+ switch (value) {
1073
+ case Billability2.Billable:
1074
+ return "Yes";
1075
+ case Billability2.Billed:
1076
+ return "Billed";
1077
+ default:
1078
+ return "No";
1079
+ }
1080
+ }
1002
1081
 
1003
1082
  // src/commands/projects.ts
1004
1083
  function registerProjectsCommands(program2) {
@@ -1058,14 +1137,112 @@ function registerProjectsCommands(program2) {
1058
1137
 
1059
1138
  // src/commands/report.ts
1060
1139
  import { styleText as styleText5 } from "util";
1140
+ import { getEntryDurationUntilNow as getEntryDurationUntilNow3 } from "clockodo";
1141
+ var GROUP_ALIASES = {
1142
+ customer: "customers_id",
1143
+ customers: "customers_id",
1144
+ customers_id: "customers_id",
1145
+ project: "projects_id",
1146
+ projects: "projects_id",
1147
+ projects_id: "projects_id",
1148
+ service: "services_id",
1149
+ services: "services_id",
1150
+ services_id: "services_id",
1151
+ text: "text",
1152
+ description: "text"
1153
+ };
1154
+ function resolveReportGroupKey(input) {
1155
+ const resolved = GROUP_ALIASES[input.toLowerCase()];
1156
+ if (!resolved) {
1157
+ throw new CliError(
1158
+ `Unknown group field: "${input}". Valid options: customer, project, service, text`,
1159
+ ExitCode.INVALID_ARGS
1160
+ );
1161
+ }
1162
+ return resolved;
1163
+ }
1164
+ function groupEntriesByText(entries) {
1165
+ const map = /* @__PURE__ */ new Map();
1166
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1167
+ for (const e of entries) {
1168
+ const groupKey = e.text || "(no description)";
1169
+ const duration = getEntryDurationUntilNow3(e);
1170
+ const existing = map.get(groupKey);
1171
+ const timeRange = { since: e.timeSince, until: e.timeUntil ?? now };
1172
+ if (existing) {
1173
+ existing.count++;
1174
+ existing.seconds += duration;
1175
+ existing.timeRanges.push(timeRange);
1176
+ } else {
1177
+ map.set(groupKey, {
1178
+ key: groupKey,
1179
+ count: 1,
1180
+ seconds: duration,
1181
+ timeRanges: [timeRange]
1182
+ });
1183
+ }
1184
+ }
1185
+ return [...map.values()].sort((a, b) => b.seconds - a.seconds);
1186
+ }
1187
+ async function runTextReport(program2, since, until) {
1188
+ const opts = program2.opts();
1189
+ const client = getClient();
1190
+ const result = await client.getEntries({
1191
+ timeSince: toClockodoDateTime(since),
1192
+ timeUntil: toClockodoDateTime(until)
1193
+ });
1194
+ const entryList = result.entries ?? [];
1195
+ const groups = groupEntriesByText(entryList);
1196
+ const totalSeconds = groups.reduce((sum, g) => sum + g.seconds, 0);
1197
+ const mode = resolveOutputMode(opts);
1198
+ if (mode !== "human") {
1199
+ printResult(
1200
+ {
1201
+ data: {
1202
+ period: { since: formatDate(since), until: formatDate(until) },
1203
+ groups,
1204
+ total: { seconds: totalSeconds, formatted: formatDuration(totalSeconds) }
1205
+ }
1206
+ },
1207
+ opts
1208
+ );
1209
+ return;
1210
+ }
1211
+ printReportHeader(since, until);
1212
+ if (groups.length === 0) {
1213
+ console.log(styleText5("dim", " No entries found for this period."));
1214
+ return;
1215
+ }
1216
+ const rows = groups.map((g) => {
1217
+ const ranges = g.timeRanges.map((r) => `${formatTime(r.since)}\u2013${formatTime(r.until)}`).join(", ");
1218
+ return [g.key, formatDuration(g.seconds), formatDecimalHours(g.seconds), ranges];
1219
+ });
1220
+ printTable(["Description", "Duration", "Hours", "Time Ranges"], rows, opts);
1221
+ printReportTotal(totalSeconds);
1222
+ }
1223
+ function printReportHeader(since, until) {
1224
+ console.log();
1225
+ console.log(` ${styleText5("bold", "Report")}: ${formatDate(since)} \u2014 ${formatDate(until)}`);
1226
+ console.log();
1227
+ }
1228
+ function printReportTotal(totalSeconds) {
1229
+ console.log();
1230
+ console.log(
1231
+ ` ${styleText5("bold", "Total")}: ${formatDuration(totalSeconds)} (${formatDecimalHours(totalSeconds)})`
1232
+ );
1233
+ console.log();
1234
+ }
1061
1235
  async function runReport(program2, since, until, cmdOpts) {
1236
+ const groupField = resolveReportGroupKey(cmdOpts.group ?? "projects_id");
1237
+ if (groupField === "text") {
1238
+ return runTextReport(program2, since, until);
1239
+ }
1062
1240
  const opts = program2.opts();
1063
1241
  const client = getClient();
1064
- const grouping = [cmdOpts.group ?? "projects_id"];
1065
1242
  const result = await client.getEntryGroups({
1066
1243
  timeSince: toClockodoDateTime(since),
1067
1244
  timeUntil: toClockodoDateTime(until),
1068
- grouping
1245
+ grouping: [groupField]
1069
1246
  });
1070
1247
  const groups = result.groups ?? [];
1071
1248
  const totalSeconds = groups.reduce((sum, g) => sum + (g.duration ?? 0), 0);
@@ -1083,9 +1260,7 @@ async function runReport(program2, since, until, cmdOpts) {
1083
1260
  );
1084
1261
  return;
1085
1262
  }
1086
- console.log();
1087
- console.log(` ${styleText5("bold", "Report")}: ${formatDate(since)} \u2014 ${formatDate(until)}`);
1088
- console.log();
1263
+ printReportHeader(since, until);
1089
1264
  if (groups.length === 0) {
1090
1265
  console.log(styleText5("dim", " No entries found for this period."));
1091
1266
  return;
@@ -1096,31 +1271,23 @@ async function runReport(program2, since, until, cmdOpts) {
1096
1271
  formatDecimalHours(g.duration ?? 0)
1097
1272
  ]);
1098
1273
  printTable(["Name", "Duration", "Hours"], rows, opts);
1099
- console.log();
1100
- console.log(
1101
- ` ${styleText5("bold", "Total")}: ${formatDuration(totalSeconds)} (${formatDecimalHours(totalSeconds)})`
1102
- );
1103
- console.log();
1274
+ printReportTotal(totalSeconds);
1104
1275
  }
1105
1276
  function registerReportCommands(program2) {
1106
1277
  const report = program2.command("report").description("Aggregated time reports");
1107
- report.command("today", { isDefault: true }).description("Today's summary").option(
1108
- "-g, --group <field>",
1109
- "Group by: projects_id, customers_id, services_id",
1110
- "projects_id"
1111
- ).action(async (cmdOpts) => {
1278
+ report.command("today", { isDefault: true }).description("Today's summary").option("-g, --group <field>", "Group by: customer, project, service, text", "projects_id").action(async (cmdOpts) => {
1112
1279
  const now = /* @__PURE__ */ new Date();
1113
1280
  await runReport(program2, startOfDay(now), endOfDay(now), cmdOpts);
1114
1281
  });
1115
- report.command("week").description("This week's summary (Mon-Sun)").option("-g, --group <field>", "Group by field", "projects_id").action(async (cmdOpts) => {
1282
+ report.command("week").description("This week's summary (Mon-Sun)").option("-g, --group <field>", "Group by: customer, project, service, text", "projects_id").action(async (cmdOpts) => {
1116
1283
  const now = /* @__PURE__ */ new Date();
1117
1284
  await runReport(program2, startOfWeek(now), endOfWeek(now), cmdOpts);
1118
1285
  });
1119
- report.command("month").description("This month's summary").option("-g, --group <field>", "Group by field", "projects_id").action(async (cmdOpts) => {
1286
+ report.command("month").description("This month's summary").option("-g, --group <field>", "Group by: customer, project, service, text", "projects_id").action(async (cmdOpts) => {
1120
1287
  const now = /* @__PURE__ */ new Date();
1121
1288
  await runReport(program2, startOfMonth(now), endOfMonth(now), cmdOpts);
1122
1289
  });
1123
- report.command("custom").description("Custom date range report").requiredOption("--since <date>", "Start date").requiredOption("--until <date>", "End date").option("-g, --group <field>", "Group by field", "projects_id").action(async (cmdOpts) => {
1290
+ report.command("custom").description("Custom date range report").requiredOption("--since <date>", "Start date").requiredOption("--until <date>", "End date").option("-g, --group <field>", "Group by: customer, project, service, text", "projects_id").action(async (cmdOpts) => {
1124
1291
  const since = new Date(parseDateTime(cmdOpts.since));
1125
1292
  const until = new Date(parseDateTime(cmdOpts.until));
1126
1293
  await runReport(program2, since, until, cmdOpts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liebig-technology/clockodo-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "AI-friendly CLI for the Clockodo time tracking API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,17 +13,6 @@
13
13
  "engines": {
14
14
  "node": ">=22.0.0"
15
15
  },
16
- "scripts": {
17
- "build": "tsup",
18
- "dev": "tsx src/index.ts",
19
- "typecheck": "tsc --noEmit",
20
- "lint": "biome check .",
21
- "lint:fix": "biome check --write .",
22
- "test": "vitest run",
23
- "test:watch": "vitest",
24
- "prepublishOnly": "pnpm build",
25
- "prepare": "husky"
26
- },
27
16
  "keywords": [
28
17
  "clockodo",
29
18
  "time-tracking",
@@ -62,5 +51,14 @@
62
51
  "*": [
63
52
  "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
64
53
  ]
54
+ },
55
+ "scripts": {
56
+ "build": "tsup",
57
+ "dev": "tsx src/index.ts",
58
+ "typecheck": "tsc --noEmit",
59
+ "lint": "biome check .",
60
+ "lint:fix": "biome check --write .",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest"
65
63
  }
66
- }
64
+ }