@liebig-technology/clockodo-cli 0.1.0 → 0.3.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 (3) hide show
  1. package/README.md +58 -23
  2. package/dist/index.js +496 -59
  3. package/package.json +11 -13
package/README.md CHANGED
@@ -6,28 +6,31 @@ CLI for the [Clockodo](https://www.clockodo.com) time tracking API. Human-friend
6
6
  Usage: clockodo [options] [command]
7
7
 
8
8
  Options:
9
- -j, --json Output as JSON
10
- -p, --plain Output as plain text (no colors)
11
- --no-color Disable colors
12
- --no-input Disable interactive prompts
13
- -v, --verbose Verbose output
9
+ -V, --version output the version number
10
+ -j, --json Output as JSON
11
+ -p, --plain Output as plain text (no colors)
12
+ --no-color Disable colors
13
+ --no-input Disable interactive prompts
14
+ -v, --verbose Verbose output
14
15
 
15
16
  Commands:
16
- config Manage CLI configuration
17
- status Show running clock and today's summary
18
- start Start time tracking
19
- stop Stop time tracking
20
- entries Manage time entries
21
- customers Manage customers
22
- projects Manage projects
23
- services Manage services
24
- users User management
25
- report Aggregated time reports
26
- absences Manage absences
27
- worktimes Show work time intervals
28
- userreport Show user report (overtime, holidays, absences)
29
- userreports Show user reports for all users
30
- schema Output machine-readable CLI structure (for AI agents)
17
+ config Manage CLI configuration
18
+ status [options] Show running clock and today's summary
19
+ start [options] Start time tracking
20
+ stop Stop time tracking
21
+ edit [options] Edit the running clock entry
22
+ extend <minutes> Extend the running clock by N minutes
23
+ entries Manage time entries
24
+ customers Manage customers
25
+ projects Manage projects
26
+ services Manage services
27
+ users User management
28
+ report Aggregated time reports
29
+ absences Manage absences
30
+ worktimes [options] Show work time intervals
31
+ userreport [options] Show user report (overtime, holidays, absences)
32
+ userreports [options] Show user reports for all users
33
+ schema Output machine-readable CLI structure (for AI agents)
31
34
  ```
32
35
 
33
36
  Every command supports `--help` for full usage details.
@@ -54,18 +57,31 @@ Credentials are stored in `~/.config/clockodo-cli/config.json`. Environment vari
54
57
  ```bash
55
58
  # Time tracking
56
59
  clockodo start --customer 123 --service 456 --text "Working on feature"
60
+ clockodo start --customer 123 --service 456 --billable # explicit billability
57
61
  clockodo stop
58
62
  clockodo status
63
+ clockodo edit --text "New description" --billable # edit running entry
64
+ clockodo extend 30 # extend running clock by 30 minutes
59
65
 
60
66
  # Entries
61
67
  clockodo entries # list today's entries
62
68
  clockodo entries --since 2026-01-01 --until 2026-01-31 # date range
69
+ clockodo entries --billable # only billable entries
63
70
  clockodo entries create --from "09:00" --to "12:30" --customer 123 --service 456
71
+ clockodo entries update 42 --text "New description" --customer 789 --service 456
72
+
73
+ # Customers, projects, services
74
+ clockodo customers # list all customers
75
+ clockodo customers create --name "Acme Corp" # create a customer
76
+ clockodo customers update 42 --name "Acme Inc" # rename customer 42
77
+ clockodo customers delete 42 --force # delete without confirmation
78
+ clockodo projects create --name "Website" --customer 42
79
+ clockodo services create --name "Development"
64
80
 
65
81
  # Reports
66
- clockodo report # today
67
- clockodo report week # this week
68
- clockodo report month # this month
82
+ clockodo report # today, grouped by project
83
+ clockodo report week --group customer # this week by customer
84
+ clockodo report month --group text # this month by description
69
85
 
70
86
  # Absences, work times, user reports
71
87
  clockodo absences list --year 2026
@@ -75,6 +91,25 @@ clockodo userreport --year 2026
75
91
 
76
92
  When `--customer` or `--service` is not provided, `start` and `entries create` launch an interactive picker (disable with `--no-input`). You can also set defaults via `clockodo config set`.
77
93
 
94
+ ## Shell Prompt Integration
95
+
96
+ `clockodo status --prompt` outputs a single plain-text line for embedding in your terminal prompt:
97
+
98
+ ```bash
99
+ clockodo status --prompt # "Working on feature 1h 23m" or empty if idle
100
+ clockodo status --prompt --json # {"data":{"running":true,"text":"...","elapsed":4980,"formatted":"1h 23m"}}
101
+ ```
102
+
103
+ Example for [Starship](https://starship.rs/):
104
+
105
+ ```toml
106
+ # ~/.config/starship.toml
107
+ [custom.clockodo]
108
+ command = "clockodo status --prompt"
109
+ when = true
110
+ symbol = "⏱ "
111
+ ```
112
+
78
113
  ## AI Agent Integration
79
114
 
80
115
  ```bash
package/dist/index.js CHANGED
@@ -290,6 +290,9 @@ function printDetail(entries, options) {
290
290
  function printSuccess(message) {
291
291
  console.error(styleText2("green", `\u2713 ${message}`));
292
292
  }
293
+ function printInfo(message) {
294
+ console.error(styleText2("cyan", `\u2139 ${message}`));
295
+ }
293
296
 
294
297
  // src/lib/validate.ts
295
298
  function parseId(input, label = "ID") {
@@ -441,10 +444,10 @@ function registerAbsencesCommands(program2) {
441
444
  const opts = program2.opts();
442
445
  const client = getClient();
443
446
  if (!cmdOpts.force && process.stdout.isTTY) {
444
- const confirm3 = await p.confirm({
447
+ const confirm6 = await p.confirm({
445
448
  message: `Delete absence ${id}?`
446
449
  });
447
- if (!confirm3 || p.isCancel(confirm3)) return;
450
+ if (!confirm6 || p.isCancel(confirm6)) return;
448
451
  }
449
452
  const absenceId = parseId(id);
450
453
  await client.deleteAbsence({ id: absenceId });
@@ -458,7 +461,7 @@ function registerAbsencesCommands(program2) {
458
461
  }
459
462
 
460
463
  // src/commands/clock.ts
461
- import { Billability } from "clockodo";
464
+ import { Billability, getEntryDurationUntilNow } from "clockodo";
462
465
 
463
466
  // src/lib/prompts.ts
464
467
  import * as p2 from "@clack/prompts";
@@ -562,6 +565,18 @@ function formatDecimalHours(seconds) {
562
565
  function toClockodoDateTime(date) {
563
566
  return date.toISOString().replace(/\.\d{3}Z$/, "Z");
564
567
  }
568
+ function parseDateTimeUntil(input) {
569
+ const lower = input.toLowerCase();
570
+ if (lower === "today" || lower === "yesterday" || lower === "tomorrow") {
571
+ const now = /* @__PURE__ */ new Date();
572
+ const offset = lower === "yesterday" ? -1 : lower === "tomorrow" ? 1 : 0;
573
+ return toClockodoDateTime(_endOfDay(addDays(now, offset)));
574
+ }
575
+ if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
576
+ return toClockodoDateTime(_endOfDay(/* @__PURE__ */ new Date(`${input}T00:00:00`)));
577
+ }
578
+ return parseDateTime(input);
579
+ }
565
580
  function parseDateTime(input) {
566
581
  const now = /* @__PURE__ */ new Date();
567
582
  switch (input.toLowerCase()) {
@@ -595,7 +610,7 @@ function parseDateTime(input) {
595
610
 
596
611
  // src/commands/clock.ts
597
612
  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) => {
613
+ 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
614
  const opts = program2.opts();
600
615
  const mode = resolveOutputMode(opts);
601
616
  const client = getClient();
@@ -619,9 +634,11 @@ function registerClockCommands(program2) {
619
634
  const result = await client.startClock({
620
635
  customersId,
621
636
  servicesId,
637
+ ...cmdOpts.billable !== void 0 && {
638
+ billable: cmdOpts.billable ? Billability.Billable : Billability.NotBillable
639
+ },
622
640
  ...projectsId && { projectsId },
623
- ...cmdOpts.text && { text: cmdOpts.text },
624
- ...cmdOpts.billable && { billable: Billability.Billable }
641
+ ...cmdOpts.text && { text: cmdOpts.text }
625
642
  });
626
643
  if (mode !== "human") {
627
644
  printResult({ data: result.running }, opts);
@@ -652,6 +669,57 @@ function registerClockCommands(program2) {
652
669
  console.log(` Description: ${result.stopped.text}`);
653
670
  }
654
671
  });
672
+ 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) => {
673
+ const opts = program2.opts();
674
+ const client = getClient();
675
+ const clock = await client.getClock();
676
+ if (!clock.running) {
677
+ throw new CliError("No clock is currently running.", ExitCode.EMPTY_RESULTS);
678
+ }
679
+ const updates = { id: clock.running.id };
680
+ if (cmdOpts.customer !== void 0) updates.customersId = cmdOpts.customer;
681
+ if (cmdOpts.project !== void 0) updates.projectsId = cmdOpts.project;
682
+ if (cmdOpts.service !== void 0) updates.servicesId = cmdOpts.service;
683
+ if (cmdOpts.text !== void 0) updates.text = cmdOpts.text;
684
+ if (cmdOpts.billable !== void 0)
685
+ updates.billable = cmdOpts.billable ? Billability.Billable : Billability.NotBillable;
686
+ if (Object.keys(updates).length === 1) {
687
+ throw new CliError(
688
+ "No update flags provided.",
689
+ ExitCode.INVALID_ARGS,
690
+ "Use --text, --customer, --project, --service, --billable, or --no-billable"
691
+ );
692
+ }
693
+ const result = await client.editEntry(updates);
694
+ const mode = resolveOutputMode(opts);
695
+ if (mode !== "human") {
696
+ printResult({ data: result.entry }, opts);
697
+ return;
698
+ }
699
+ printSuccess("Running entry updated");
700
+ });
701
+ program2.command("extend").description("Extend the running clock by N minutes").argument("<minutes>", "Minutes to extend by", parseIntStrict).action(async (minutes) => {
702
+ const opts = program2.opts();
703
+ const client = getClient();
704
+ const clock = await client.getClock();
705
+ if (!clock.running) {
706
+ throw new CliError("No clock is currently running.", ExitCode.EMPTY_RESULTS);
707
+ }
708
+ const durationBefore = getEntryDurationUntilNow(clock.running);
709
+ const duration = durationBefore + minutes * 60;
710
+ const result = await client.changeClockDuration({
711
+ entriesId: clock.running.id,
712
+ durationBefore,
713
+ duration
714
+ });
715
+ const mode = resolveOutputMode(opts);
716
+ if (mode !== "human") {
717
+ printResult({ data: result.updated }, opts);
718
+ return;
719
+ }
720
+ printSuccess(`Clock extended by ${minutes} minutes`);
721
+ console.log(` New duration: ${formatDuration(duration)}`);
722
+ });
655
723
  }
656
724
 
657
725
  // src/commands/config.ts
@@ -712,6 +780,8 @@ function registerConfigCommands(program2) {
712
780
  }
713
781
 
714
782
  // src/commands/customers.ts
783
+ import * as p4 from "@clack/prompts";
784
+ import { Billability as Billability2 } from "clockodo";
715
785
  function registerCustomersCommands(program2) {
716
786
  const customers = program2.command("customers").description("Manage customers");
717
787
  customers.command("list", { isDefault: true }).description("List customers").option("--active", "Show only active customers").option("--search <text>", "Search by name").action(async (cmdOpts) => {
@@ -725,6 +795,14 @@ function registerCustomersCommands(program2) {
725
795
  );
726
796
  const items = result.data ?? [];
727
797
  const mode = resolveOutputMode(opts);
798
+ if (items.length === 0) {
799
+ if (mode !== "human") {
800
+ printResult({ data: [], meta: { count: 0 } }, opts);
801
+ } else {
802
+ printInfo("No customers found.");
803
+ }
804
+ return;
805
+ }
728
806
  if (mode !== "human") {
729
807
  printResult({ data: items, meta: { count: items.length } }, opts);
730
808
  return;
@@ -759,27 +837,89 @@ function registerCustomersCommands(program2) {
759
837
  opts
760
838
  );
761
839
  });
840
+ customers.command("create").description("Create a customer").requiredOption("--name <name>", "Customer name").option("--number <text>", "Customer number").option("--note <text>", "Note").option("--active", "Set as active (default)").option("--no-active", "Set as inactive").option("--billable", "Set as billable by default").option("--no-billable", "Set as not billable by default").action(async (cmdOpts) => {
841
+ const opts = program2.opts();
842
+ const client = getClient();
843
+ const params = {
844
+ name: cmdOpts.name,
845
+ ...cmdOpts.number != null && { number: cmdOpts.number },
846
+ ...cmdOpts.note != null && { note: cmdOpts.note },
847
+ ...cmdOpts.active !== void 0 && { active: cmdOpts.active },
848
+ ...cmdOpts.billable !== void 0 && {
849
+ billableDefault: cmdOpts.billable ? Billability2.Billable : Billability2.NotBillable
850
+ }
851
+ };
852
+ const result = await client.addCustomer(params);
853
+ const mode = resolveOutputMode(opts);
854
+ if (mode !== "human") {
855
+ printResult({ data: result.data }, opts);
856
+ return;
857
+ }
858
+ printSuccess(`Customer created (ID: ${result.data.id})`);
859
+ console.log(` ${result.data.name}`);
860
+ });
861
+ customers.command("update <id>").description("Update a customer").option("--name <name>", "New name").option("--number <text>", "New number").option("--note <text>", "New note").option("--active", "Set as active").option("--no-active", "Set as inactive").option("--billable", "Set as billable by default").option("--no-billable", "Set as not billable by default").action(async (id, cmdOpts) => {
862
+ const opts = program2.opts();
863
+ const client = getClient();
864
+ const params = {
865
+ id: parseId(id),
866
+ ...cmdOpts.name != null && { name: cmdOpts.name },
867
+ ...cmdOpts.number != null && { number: cmdOpts.number },
868
+ ...cmdOpts.note != null && { note: cmdOpts.note },
869
+ ...cmdOpts.active !== void 0 && { active: cmdOpts.active },
870
+ ...cmdOpts.billable !== void 0 && {
871
+ billableDefault: cmdOpts.billable ? Billability2.Billable : Billability2.NotBillable
872
+ }
873
+ };
874
+ const result = await client.editCustomer(params);
875
+ const mode = resolveOutputMode(opts);
876
+ if (mode !== "human") {
877
+ printResult({ data: result.data }, opts);
878
+ return;
879
+ }
880
+ printSuccess(`Customer ${id} updated`);
881
+ });
882
+ customers.command("delete <id>").description("Delete a customer").option("-f, --force", "Skip confirmation").action(async (id, cmdOpts) => {
883
+ const opts = program2.opts();
884
+ const client = getClient();
885
+ if (!cmdOpts.force && process.stdout.isTTY) {
886
+ const confirm6 = await p4.confirm({
887
+ message: `Delete customer ${id}?`
888
+ });
889
+ if (!confirm6 || p4.isCancel(confirm6)) return;
890
+ }
891
+ const customerId = parseId(id);
892
+ await client.deleteCustomer({ id: customerId });
893
+ const mode = resolveOutputMode(opts);
894
+ if (mode !== "human") {
895
+ printResult({ data: { success: true, id: customerId } }, opts);
896
+ return;
897
+ }
898
+ printSuccess(`Customer ${id} deleted`);
899
+ });
762
900
  }
763
901
 
764
902
  // src/commands/entries.ts
765
903
  import { styleText as styleText4 } from "util";
766
- import * as p4 from "@clack/prompts";
767
- import { getEntryDurationUntilNow, isTimeEntry } from "clockodo";
904
+ import * as p5 from "@clack/prompts";
905
+ import { Billability as Billability3, getEntryDurationUntilNow as getEntryDurationUntilNow2, isTimeEntry } from "clockodo";
768
906
  function registerEntriesCommands(program2) {
769
907
  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(
908
+ 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
909
  "-g, --group <field>",
772
910
  "Group by: customer, project, service, text (shows summary table instead)"
773
911
  ).action(async (cmdOpts) => {
774
912
  const opts = program2.opts();
775
913
  const client = getClient();
776
914
  const since = parseDateTime(cmdOpts.since);
777
- const until = cmdOpts.until ? parseDateTime(cmdOpts.until) : toClockodoDateTime(endOfDay(/* @__PURE__ */ new Date()));
915
+ const until = cmdOpts.until ? parseDateTimeUntil(cmdOpts.until) : toClockodoDateTime(endOfDay(/* @__PURE__ */ new Date()));
778
916
  const filter = {};
779
917
  if (cmdOpts.customer) filter.customersId = cmdOpts.customer;
780
918
  if (cmdOpts.project) filter.projectsId = cmdOpts.project;
781
919
  if (cmdOpts.service) filter.servicesId = cmdOpts.service;
782
920
  if (cmdOpts.text) filter.text = cmdOpts.text;
921
+ if (cmdOpts.billable !== void 0)
922
+ filter.billable = cmdOpts.billable ? Billability3.Billable : Billability3.NotBillable;
783
923
  const result = await client.getEntries({
784
924
  timeSince: since,
785
925
  timeUntil: until,
@@ -795,7 +935,7 @@ function registerEntriesCommands(program2) {
795
935
  }
796
936
  return;
797
937
  }
798
- const totalSeconds = entryList.reduce((sum, e) => sum + getEntryDurationUntilNow(e), 0);
938
+ const totalSeconds = entryList.reduce((sum, e) => sum + getEntryDurationUntilNow2(e), 0);
799
939
  if (cmdOpts.group) {
800
940
  const groupKey = resolveGroupKey(cmdOpts.group);
801
941
  const groups = groupEntries(entryList, groupKey);
@@ -841,7 +981,7 @@ function registerEntriesCommands(program2) {
841
981
  formatDate(new Date(e.timeSince)),
842
982
  formatTime(e.timeSince),
843
983
  isTimeEntry(e) && !e.timeUntil ? styleText4("green", "running") : formatTime(e.timeUntil ?? e.timeSince),
844
- formatDuration(getEntryDurationUntilNow(e)),
984
+ formatDuration(getEntryDurationUntilNow2(e)),
845
985
  e.text || styleText4("dim", "\u2014")
846
986
  ]);
847
987
  printTable(["ID", "Date", "Start", "End", "Duration", "Description"], rows, opts);
@@ -862,7 +1002,7 @@ function registerEntriesCommands(program2) {
862
1002
  return;
863
1003
  }
864
1004
  const timeUntilDisplay = isTimeEntry(e) && !e.timeUntil ? "running" : formatTime(e.timeUntil ?? e.timeSince);
865
- const duration = getEntryDurationUntilNow(e);
1005
+ const duration = getEntryDurationUntilNow2(e);
866
1006
  printDetail(
867
1007
  [
868
1008
  ["ID", e.id],
@@ -874,12 +1014,12 @@ function registerEntriesCommands(program2) {
874
1014
  ["Customer ID", e.customersId],
875
1015
  ["Project ID", e.projectsId ?? null],
876
1016
  ["Service ID", isTimeEntry(e) ? e.servicesId : null],
877
- ["Billable", e.billable === 1 ? "Yes" : e.billable === 2 ? "Billed" : "No"]
1017
+ ["Billable", formatBillable(e.billable)]
878
1018
  ],
879
1019
  opts
880
1020
  );
881
1021
  });
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) => {
1022
+ 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
1023
  const opts = program2.opts();
884
1024
  const mode = resolveOutputMode(opts);
885
1025
  const client = getClient();
@@ -900,10 +1040,20 @@ function registerEntriesCommands(program2) {
900
1040
  "Use --customer and --service flags, or set defaults via: clockodo config set"
901
1041
  );
902
1042
  }
1043
+ let billable;
1044
+ if (cmdOpts.billable !== void 0) {
1045
+ billable = cmdOpts.billable ? Billability3.Billable : Billability3.NotBillable;
1046
+ } else if (projectsId) {
1047
+ const { data: project } = await client.getProject({ id: projectsId });
1048
+ billable = project.billableDefault ? Billability3.Billable : Billability3.NotBillable;
1049
+ } else {
1050
+ const { data: customer } = await client.getCustomer({ id: customersId });
1051
+ billable = customer.billableDefault ? Billability3.Billable : Billability3.NotBillable;
1052
+ }
903
1053
  const result = await client.addEntry({
904
1054
  customersId,
905
1055
  servicesId,
906
- billable: cmdOpts.billable ? 1 : 0,
1056
+ billable,
907
1057
  timeSince: parseDateTime(cmdOpts.from),
908
1058
  timeUntil: parseDateTime(cmdOpts.to),
909
1059
  ...projectsId && { projectsId },
@@ -914,20 +1064,24 @@ function registerEntriesCommands(program2) {
914
1064
  return;
915
1065
  }
916
1066
  const entry = result.entry;
917
- const duration = getEntryDurationUntilNow(entry);
1067
+ const duration = getEntryDurationUntilNow2(entry);
918
1068
  printSuccess(`Entry created (ID: ${entry.id})`);
919
1069
  console.log(
920
1070
  ` ${formatTime(entry.timeSince)} \u2014 ${entry.timeUntil ? formatTime(entry.timeUntil) : "?"} (${formatDuration(duration)})`
921
1071
  );
922
1072
  });
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) => {
1073
+ 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
1074
  const opts = program2.opts();
925
1075
  const client = getClient();
926
1076
  const updates = { id: parseId(id) };
927
1077
  if (cmdOpts.from) updates.timeSince = parseDateTime(cmdOpts.from);
928
1078
  if (cmdOpts.to) updates.timeUntil = parseDateTime(cmdOpts.to);
929
1079
  if (cmdOpts.text !== void 0) updates.text = cmdOpts.text;
930
- if (cmdOpts.billable !== void 0) updates.billable = cmdOpts.billable ? 1 : 0;
1080
+ if (cmdOpts.customer !== void 0) updates.customersId = cmdOpts.customer;
1081
+ if (cmdOpts.project !== void 0) updates.projectsId = cmdOpts.project;
1082
+ if (cmdOpts.service !== void 0) updates.servicesId = cmdOpts.service;
1083
+ if (cmdOpts.billable !== void 0)
1084
+ updates.billable = cmdOpts.billable ? Billability3.Billable : Billability3.NotBillable;
931
1085
  const result = await client.editEntry(updates);
932
1086
  const mode = resolveOutputMode(opts);
933
1087
  if (mode !== "human") {
@@ -940,10 +1094,10 @@ function registerEntriesCommands(program2) {
940
1094
  const opts = program2.opts();
941
1095
  const client = getClient();
942
1096
  if (!cmdOpts.force && process.stdout.isTTY) {
943
- const confirm3 = await p4.confirm({
1097
+ const confirm6 = await p5.confirm({
944
1098
  message: `Delete entry ${id}?`
945
1099
  });
946
- if (!confirm3 || p4.isCancel(confirm3)) return;
1100
+ if (!confirm6 || p5.isCancel(confirm6)) return;
947
1101
  }
948
1102
  const entryId = parseId(id);
949
1103
  await client.deleteEntry({ id: entryId });
@@ -989,7 +1143,7 @@ function groupEntries(entries, key) {
989
1143
  groupValue = val != null ? String(val) : "(none)";
990
1144
  }
991
1145
  const existing = map.get(groupValue);
992
- const duration = getEntryDurationUntilNow(e);
1146
+ const duration = getEntryDurationUntilNow2(e);
993
1147
  if (existing) {
994
1148
  existing.count++;
995
1149
  existing.seconds += duration;
@@ -999,8 +1153,19 @@ function groupEntries(entries, key) {
999
1153
  }
1000
1154
  return [...map.entries()].map(([k, v]) => ({ key: k, ...v })).sort((a, b) => b.seconds - a.seconds);
1001
1155
  }
1156
+ function formatBillable(value) {
1157
+ switch (value) {
1158
+ case Billability3.Billable:
1159
+ return "Yes";
1160
+ case Billability3.Billed:
1161
+ return "Billed";
1162
+ default:
1163
+ return "No";
1164
+ }
1165
+ }
1002
1166
 
1003
1167
  // src/commands/projects.ts
1168
+ import * as p6 from "@clack/prompts";
1004
1169
  function registerProjectsCommands(program2) {
1005
1170
  const projects = program2.command("projects").description("Manage projects");
1006
1171
  projects.command("list", { isDefault: true }).description("List projects").option("--customer <id>", "Filter by customer ID", parseIntStrict).option("--active", "Show only active projects").option("--search <text>", "Search by name").action(async (cmdOpts) => {
@@ -1015,17 +1180,25 @@ function registerProjectsCommands(program2) {
1015
1180
  );
1016
1181
  const items = result.data ?? [];
1017
1182
  const mode = resolveOutputMode(opts);
1183
+ if (items.length === 0) {
1184
+ if (mode !== "human") {
1185
+ printResult({ data: [], meta: { count: 0 } }, opts);
1186
+ } else {
1187
+ printInfo("No projects found.");
1188
+ }
1189
+ return;
1190
+ }
1018
1191
  if (mode !== "human") {
1019
1192
  printResult({ data: items, meta: { count: items.length } }, opts);
1020
1193
  return;
1021
1194
  }
1022
- const rows = items.map((p5) => [
1023
- String(p5.id),
1024
- p5.name,
1025
- p5.number ?? "\u2014",
1026
- String(p5.customersId),
1027
- p5.active ? "Yes" : "No",
1028
- p5.completed ? "Yes" : "No"
1195
+ const rows = items.map((proj) => [
1196
+ String(proj.id),
1197
+ proj.name,
1198
+ proj.number ?? "\u2014",
1199
+ String(proj.customersId),
1200
+ proj.active ? "Yes" : "No",
1201
+ proj.completed ? "Yes" : "No"
1029
1202
  ]);
1030
1203
  printTable(["ID", "Name", "Number", "Customer", "Active", "Completed"], rows, opts);
1031
1204
  });
@@ -1033,39 +1206,212 @@ function registerProjectsCommands(program2) {
1033
1206
  const opts = program2.opts();
1034
1207
  const client = getClient();
1035
1208
  const result = await client.getProject({ id: parseId(id) });
1036
- const p5 = result.data;
1209
+ const proj = result.data;
1037
1210
  const mode = resolveOutputMode(opts);
1038
1211
  if (mode !== "human") {
1039
- printResult({ data: p5 }, opts);
1212
+ printResult({ data: proj }, opts);
1040
1213
  return;
1041
1214
  }
1042
1215
  printDetail(
1043
1216
  [
1044
- ["ID", p5.id],
1045
- ["Name", p5.name],
1046
- ["Number", p5.number ?? null],
1047
- ["Customer ID", p5.customersId],
1048
- ["Active", p5.active],
1049
- ["Completed", p5.completed],
1050
- ["Budget", p5.budget?.amount ?? null],
1051
- ["Budget Type", p5.budget?.monetary ? "Money" : "Hours"],
1052
- ["Note", p5.note ?? null]
1217
+ ["ID", proj.id],
1218
+ ["Name", proj.name],
1219
+ ["Number", proj.number ?? null],
1220
+ ["Customer ID", proj.customersId],
1221
+ ["Active", proj.active],
1222
+ ["Completed", proj.completed],
1223
+ ["Budget", proj.budget?.amount ?? null],
1224
+ ["Budget Type", proj.budget?.monetary ? "Money" : "Hours"],
1225
+ ["Note", proj.note ?? null]
1053
1226
  ],
1054
1227
  opts
1055
1228
  );
1056
1229
  });
1230
+ projects.command("create").description("Create a project").requiredOption("--name <name>", "Project name").requiredOption("--customer <id>", "Customer ID", parseIntStrict).option("--number <text>", "Project number").option("--note <text>", "Note").option("--active", "Set as active (default)").option("--no-active", "Set as inactive").option("--billable", "Set as billable by default").option("--no-billable", "Set as not billable by default").action(async (cmdOpts) => {
1231
+ const opts = program2.opts();
1232
+ const client = getClient();
1233
+ const params = {
1234
+ name: cmdOpts.name,
1235
+ customersId: cmdOpts.customer,
1236
+ ...cmdOpts.number != null && { number: cmdOpts.number },
1237
+ ...cmdOpts.note != null && { note: cmdOpts.note },
1238
+ ...cmdOpts.active !== void 0 && { active: cmdOpts.active },
1239
+ ...cmdOpts.billable !== void 0 && {
1240
+ billableDefault: cmdOpts.billable
1241
+ }
1242
+ };
1243
+ const result = await client.addProject(params);
1244
+ const mode = resolveOutputMode(opts);
1245
+ if (mode !== "human") {
1246
+ printResult({ data: result.data }, opts);
1247
+ return;
1248
+ }
1249
+ printSuccess(`Project created (ID: ${result.data.id})`);
1250
+ console.log(` ${result.data.name}`);
1251
+ });
1252
+ projects.command("update <id>").description("Update a project").option("--name <name>", "New name").option("--customer <id>", "New customer ID", parseIntStrict).option("--number <text>", "New number").option("--note <text>", "New note").option("--active", "Set as active").option("--no-active", "Set as inactive").option("--billable", "Set as billable by default").option("--no-billable", "Set as not billable by default").action(async (id, cmdOpts) => {
1253
+ const opts = program2.opts();
1254
+ const client = getClient();
1255
+ const params = {
1256
+ id: parseId(id),
1257
+ ...cmdOpts.name != null && { name: cmdOpts.name },
1258
+ ...cmdOpts.customer != null && { customersId: cmdOpts.customer },
1259
+ ...cmdOpts.number != null && { number: cmdOpts.number },
1260
+ ...cmdOpts.note != null && { note: cmdOpts.note },
1261
+ ...cmdOpts.active !== void 0 && { active: cmdOpts.active },
1262
+ ...cmdOpts.billable !== void 0 && {
1263
+ billableDefault: cmdOpts.billable
1264
+ }
1265
+ };
1266
+ const result = await client.editProject(params);
1267
+ const mode = resolveOutputMode(opts);
1268
+ if (mode !== "human") {
1269
+ printResult({ data: result.data }, opts);
1270
+ return;
1271
+ }
1272
+ printSuccess(`Project ${id} updated`);
1273
+ });
1274
+ projects.command("delete <id>").description("Delete a project").option("-f, --force", "Skip confirmation").action(async (id, cmdOpts) => {
1275
+ const opts = program2.opts();
1276
+ const client = getClient();
1277
+ if (!cmdOpts.force && process.stdout.isTTY) {
1278
+ const confirm6 = await p6.confirm({
1279
+ message: `Delete project ${id}?`
1280
+ });
1281
+ if (!confirm6 || p6.isCancel(confirm6)) return;
1282
+ }
1283
+ const projectId = parseId(id);
1284
+ await client.deleteProject({ id: projectId });
1285
+ const mode = resolveOutputMode(opts);
1286
+ if (mode !== "human") {
1287
+ printResult({ data: { success: true, id: projectId } }, opts);
1288
+ return;
1289
+ }
1290
+ printSuccess(`Project ${id} deleted`);
1291
+ });
1057
1292
  }
1058
1293
 
1059
1294
  // src/commands/report.ts
1060
1295
  import { styleText as styleText5 } from "util";
1296
+ import { getEntryDurationUntilNow as getEntryDurationUntilNow3 } from "clockodo";
1297
+ var GROUP_ALIASES = {
1298
+ customer: "customers_id",
1299
+ customers: "customers_id",
1300
+ customers_id: "customers_id",
1301
+ project: "projects_id",
1302
+ projects: "projects_id",
1303
+ projects_id: "projects_id",
1304
+ service: "services_id",
1305
+ services: "services_id",
1306
+ services_id: "services_id",
1307
+ text: "text",
1308
+ description: "text"
1309
+ };
1310
+ function resolveReportGroupKey(input) {
1311
+ const resolved = GROUP_ALIASES[input.toLowerCase()];
1312
+ if (!resolved) {
1313
+ throw new CliError(
1314
+ `Unknown group field: "${input}". Valid options: customer, project, service, text`,
1315
+ ExitCode.INVALID_ARGS
1316
+ );
1317
+ }
1318
+ return resolved;
1319
+ }
1320
+ function groupEntriesByText(entries) {
1321
+ const map = /* @__PURE__ */ new Map();
1322
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1323
+ for (const e of entries) {
1324
+ const groupKey = e.text || "(no description)";
1325
+ const duration = getEntryDurationUntilNow3(e);
1326
+ const existing = map.get(groupKey);
1327
+ const timeRange = { since: e.timeSince, until: e.timeUntil ?? now };
1328
+ if (existing) {
1329
+ existing.count++;
1330
+ existing.seconds += duration;
1331
+ existing.timeRanges.push(timeRange);
1332
+ } else {
1333
+ map.set(groupKey, {
1334
+ key: groupKey,
1335
+ count: 1,
1336
+ seconds: duration,
1337
+ timeRanges: [timeRange]
1338
+ });
1339
+ }
1340
+ }
1341
+ return [...map.values()].sort((a, b) => b.seconds - a.seconds);
1342
+ }
1343
+ function buildFilter(cmdOpts) {
1344
+ const filter = {};
1345
+ if (cmdOpts.customer !== void 0) filter.customersId = cmdOpts.customer;
1346
+ if (cmdOpts.project !== void 0) filter.projectsId = cmdOpts.project;
1347
+ if (cmdOpts.service !== void 0) filter.servicesId = cmdOpts.service;
1348
+ if (cmdOpts.text !== void 0) filter.text = cmdOpts.text;
1349
+ if (cmdOpts.user !== void 0) filter.usersId = cmdOpts.user;
1350
+ return Object.keys(filter).length > 0 ? filter : void 0;
1351
+ }
1352
+ async function runTextReport(program2, since, until, cmdOpts) {
1353
+ const opts = program2.opts();
1354
+ const client = getClient();
1355
+ const filter = buildFilter(cmdOpts);
1356
+ const result = await client.getEntries({
1357
+ timeSince: toClockodoDateTime(since),
1358
+ timeUntil: toClockodoDateTime(until),
1359
+ ...filter && { filter }
1360
+ });
1361
+ const entryList = result.entries ?? [];
1362
+ const groups = groupEntriesByText(entryList);
1363
+ const totalSeconds = groups.reduce((sum, g) => sum + g.seconds, 0);
1364
+ const mode = resolveOutputMode(opts);
1365
+ if (mode !== "human") {
1366
+ printResult(
1367
+ {
1368
+ data: {
1369
+ period: { since: formatDate(since), until: formatDate(until) },
1370
+ groups,
1371
+ total: { seconds: totalSeconds, formatted: formatDuration(totalSeconds) }
1372
+ }
1373
+ },
1374
+ opts
1375
+ );
1376
+ return;
1377
+ }
1378
+ printReportHeader(since, until);
1379
+ if (groups.length === 0) {
1380
+ console.log(styleText5("dim", " No entries found for this period."));
1381
+ return;
1382
+ }
1383
+ const rows = groups.map((g) => {
1384
+ const ranges = g.timeRanges.map((r) => `${formatTime(r.since)}\u2013${formatTime(r.until)}`).join(", ");
1385
+ return [g.key, formatDuration(g.seconds), formatDecimalHours(g.seconds), ranges];
1386
+ });
1387
+ printTable(["Description", "Duration", "Hours", "Time Ranges"], rows, opts);
1388
+ printReportTotal(totalSeconds);
1389
+ }
1390
+ function printReportHeader(since, until) {
1391
+ console.log();
1392
+ console.log(` ${styleText5("bold", "Report")}: ${formatDate(since)} \u2014 ${formatDate(until)}`);
1393
+ console.log();
1394
+ }
1395
+ function printReportTotal(totalSeconds) {
1396
+ console.log();
1397
+ console.log(
1398
+ ` ${styleText5("bold", "Total")}: ${formatDuration(totalSeconds)} (${formatDecimalHours(totalSeconds)})`
1399
+ );
1400
+ console.log();
1401
+ }
1061
1402
  async function runReport(program2, since, until, cmdOpts) {
1403
+ const groupField = resolveReportGroupKey(cmdOpts.group ?? "project");
1404
+ if (groupField === "text") {
1405
+ return runTextReport(program2, since, until, cmdOpts);
1406
+ }
1062
1407
  const opts = program2.opts();
1063
1408
  const client = getClient();
1064
- const grouping = [cmdOpts.group ?? "projects_id"];
1409
+ const filter = buildFilter(cmdOpts);
1065
1410
  const result = await client.getEntryGroups({
1066
1411
  timeSince: toClockodoDateTime(since),
1067
1412
  timeUntil: toClockodoDateTime(until),
1068
- grouping
1413
+ grouping: [groupField],
1414
+ ...filter && { filter }
1069
1415
  });
1070
1416
  const groups = result.groups ?? [];
1071
1417
  const totalSeconds = groups.reduce((sum, g) => sum + (g.duration ?? 0), 0);
@@ -1083,9 +1429,7 @@ async function runReport(program2, since, until, cmdOpts) {
1083
1429
  );
1084
1430
  return;
1085
1431
  }
1086
- console.log();
1087
- console.log(` ${styleText5("bold", "Report")}: ${formatDate(since)} \u2014 ${formatDate(until)}`);
1088
- console.log();
1432
+ printReportHeader(since, until);
1089
1433
  if (groups.length === 0) {
1090
1434
  console.log(styleText5("dim", " No entries found for this period."));
1091
1435
  return;
@@ -1096,33 +1440,39 @@ async function runReport(program2, since, until, cmdOpts) {
1096
1440
  formatDecimalHours(g.duration ?? 0)
1097
1441
  ]);
1098
1442
  printTable(["Name", "Duration", "Hours"], rows, opts);
1099
- console.log();
1100
- console.log(
1101
- ` ${styleText5("bold", "Total")}: ${formatDuration(totalSeconds)} (${formatDecimalHours(totalSeconds)})`
1102
- );
1103
- console.log();
1443
+ printReportTotal(totalSeconds);
1444
+ }
1445
+ function addFilterOptions(cmd) {
1446
+ return cmd.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 <text>", "Filter by description text").option("--user <id>", "Filter by user ID", parseIntStrict);
1104
1447
  }
1105
1448
  function registerReportCommands(program2) {
1106
1449
  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"
1450
+ addFilterOptions(
1451
+ report.command("today", { isDefault: true }).description("Today's summary").option("-g, --group <field>", "Group by: customer, project, service, text", "project")
1111
1452
  ).action(async (cmdOpts) => {
1112
1453
  const now = /* @__PURE__ */ new Date();
1113
1454
  await runReport(program2, startOfDay(now), endOfDay(now), cmdOpts);
1114
1455
  });
1115
- report.command("week").description("This week's summary (Mon-Sun)").option("-g, --group <field>", "Group by field", "projects_id").action(async (cmdOpts) => {
1456
+ addFilterOptions(
1457
+ report.command("week").description("This week's summary (Mon-Sun)").option("-g, --group <field>", "Group by: customer, project, service, text", "project")
1458
+ ).action(async (cmdOpts) => {
1116
1459
  const now = /* @__PURE__ */ new Date();
1117
1460
  await runReport(program2, startOfWeek(now), endOfWeek(now), cmdOpts);
1118
1461
  });
1119
- report.command("month").description("This month's summary").option("-g, --group <field>", "Group by field", "projects_id").action(async (cmdOpts) => {
1462
+ addFilterOptions(
1463
+ report.command("month").description("This month's summary").option("-g, --group <field>", "Group by: customer, project, service, text", "project")
1464
+ ).action(async (cmdOpts) => {
1120
1465
  const now = /* @__PURE__ */ new Date();
1121
1466
  await runReport(program2, startOfMonth(now), endOfMonth(now), cmdOpts);
1122
1467
  });
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) => {
1468
+ addFilterOptions(
1469
+ 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", "project")
1470
+ ).action(async (cmdOpts) => {
1124
1471
  const since = new Date(parseDateTime(cmdOpts.since));
1125
- const until = new Date(parseDateTime(cmdOpts.until));
1472
+ const until = new Date(parseDateTimeUntil(cmdOpts.until));
1473
+ if (since >= until) {
1474
+ throw new CliError("--since must be before --until", ExitCode.INVALID_ARGS);
1475
+ }
1126
1476
  await runReport(program2, since, until, cmdOpts);
1127
1477
  });
1128
1478
  }
@@ -1159,6 +1509,7 @@ function registerSchemaCommand(program2) {
1159
1509
  }
1160
1510
 
1161
1511
  // src/commands/services.ts
1512
+ import * as p7 from "@clack/prompts";
1162
1513
  function registerServicesCommands(program2) {
1163
1514
  const services = program2.command("services").description("Manage services");
1164
1515
  services.command("list", { isDefault: true }).description("List services").option("--active", "Show only active services").option("--search <text>", "Search by name").action(async (cmdOpts) => {
@@ -1172,6 +1523,14 @@ function registerServicesCommands(program2) {
1172
1523
  );
1173
1524
  const items = result.data ?? [];
1174
1525
  const mode = resolveOutputMode(opts);
1526
+ if (items.length === 0) {
1527
+ if (mode !== "human") {
1528
+ printResult({ data: [], meta: { count: 0 } }, opts);
1529
+ } else {
1530
+ printInfo("No services found.");
1531
+ }
1532
+ return;
1533
+ }
1175
1534
  if (mode !== "human") {
1176
1535
  printResult({ data: items, meta: { count: items.length } }, opts);
1177
1536
  return;
@@ -1205,16 +1564,94 @@ function registerServicesCommands(program2) {
1205
1564
  opts
1206
1565
  );
1207
1566
  });
1567
+ services.command("create").description("Create a service").requiredOption("--name <name>", "Service name").option("--number <text>", "Service number").option("--note <text>", "Note").option("--active", "Set as active (default)").option("--no-active", "Set as inactive").action(async (cmdOpts) => {
1568
+ const opts = program2.opts();
1569
+ const client = getClient();
1570
+ const params = {
1571
+ name: cmdOpts.name,
1572
+ ...cmdOpts.number != null && { number: cmdOpts.number },
1573
+ ...cmdOpts.note != null && { note: cmdOpts.note },
1574
+ ...cmdOpts.active !== void 0 && { active: cmdOpts.active }
1575
+ };
1576
+ const result = await client.addService(params);
1577
+ const mode = resolveOutputMode(opts);
1578
+ if (mode !== "human") {
1579
+ printResult({ data: result.data }, opts);
1580
+ return;
1581
+ }
1582
+ printSuccess(`Service created (ID: ${result.data.id})`);
1583
+ console.log(` ${result.data.name}`);
1584
+ });
1585
+ services.command("update <id>").description("Update a service").option("--name <name>", "New name").option("--number <text>", "New number").option("--note <text>", "New note").option("--active", "Set as active").option("--no-active", "Set as inactive").action(async (id, cmdOpts) => {
1586
+ const opts = program2.opts();
1587
+ const client = getClient();
1588
+ const params = {
1589
+ id: parseId(id),
1590
+ ...cmdOpts.name != null && { name: cmdOpts.name },
1591
+ ...cmdOpts.number != null && { number: cmdOpts.number },
1592
+ ...cmdOpts.note != null && { note: cmdOpts.note },
1593
+ ...cmdOpts.active !== void 0 && { active: cmdOpts.active }
1594
+ };
1595
+ const result = await client.editService(params);
1596
+ const mode = resolveOutputMode(opts);
1597
+ if (mode !== "human") {
1598
+ printResult({ data: result.data }, opts);
1599
+ return;
1600
+ }
1601
+ printSuccess(`Service ${id} updated`);
1602
+ });
1603
+ services.command("delete <id>").description("Delete a service").option("-f, --force", "Skip confirmation").action(async (id, cmdOpts) => {
1604
+ const opts = program2.opts();
1605
+ const client = getClient();
1606
+ if (!cmdOpts.force && process.stdout.isTTY) {
1607
+ const confirm6 = await p7.confirm({
1608
+ message: `Delete service ${id}?`
1609
+ });
1610
+ if (!confirm6 || p7.isCancel(confirm6)) return;
1611
+ }
1612
+ const serviceId = parseId(id);
1613
+ await client.deleteService({ id: serviceId });
1614
+ const mode = resolveOutputMode(opts);
1615
+ if (mode !== "human") {
1616
+ printResult({ data: { success: true, id: serviceId } }, opts);
1617
+ return;
1618
+ }
1619
+ printSuccess(`Service ${id} deleted`);
1620
+ });
1208
1621
  }
1209
1622
 
1210
1623
  // src/commands/status.ts
1211
1624
  import { styleText as styleText6 } from "util";
1212
1625
  import { isTimeEntry as isTimeEntry2 } from "clockodo";
1213
1626
  function registerStatusCommand(program2) {
1214
- program2.command("status").description("Show running clock and today's summary").action(async () => {
1627
+ program2.command("status").description("Show running clock and today's summary").option("--prompt", "Single-line output for shell prompts (p10k, starship)").action(async (cmdOpts) => {
1215
1628
  const opts = program2.opts();
1216
1629
  const client = getClient();
1217
1630
  const mode = resolveOutputMode(opts);
1631
+ if (cmdOpts.prompt) {
1632
+ const clock = await client.getClock();
1633
+ const running2 = clock.running;
1634
+ const elapsed = running2 ? elapsedSince(running2.timeSince) : 0;
1635
+ if (mode !== "human") {
1636
+ printResult(
1637
+ {
1638
+ data: {
1639
+ running: !!running2,
1640
+ text: running2?.text ?? null,
1641
+ elapsed,
1642
+ formatted: formatDuration(elapsed)
1643
+ }
1644
+ },
1645
+ opts
1646
+ );
1647
+ return;
1648
+ }
1649
+ if (running2) {
1650
+ const parts = [running2.text, formatDuration(elapsed)].filter(Boolean);
1651
+ console.log(parts.join(" "));
1652
+ }
1653
+ return;
1654
+ }
1218
1655
  const now = /* @__PURE__ */ new Date();
1219
1656
  const [clockResult, entriesResult] = await Promise.all([
1220
1657
  client.getClock(),
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.3.0",
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
+ }