@liebig-technology/clockodo-cli 0.2.1 → 0.3.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 +50 -22
  2. package/dist/index.js +313 -43
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,30 +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
- edit Edit the running clock entry
21
- extend Extend the running clock by N minutes
22
- entries Manage time entries
23
- customers Manage customers
24
- projects Manage projects
25
- services Manage services
26
- users User management
27
- report Aggregated time reports
28
- absences Manage absences
29
- worktimes Show work time intervals
30
- userreport Show user report (overtime, holidays, absences)
31
- userreports Show user reports for all users
32
- 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)
33
34
  ```
34
35
 
35
36
  Every command supports `--help` for full usage details.
@@ -69,6 +70,14 @@ clockodo entries --billable # only billable entries
69
70
  clockodo entries create --from "09:00" --to "12:30" --customer 123 --service 456
70
71
  clockodo entries update 42 --text "New description" --customer 789 --service 456
71
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"
80
+
72
81
  # Reports
73
82
  clockodo report # today, grouped by project
74
83
  clockodo report week --group customer # this week by customer
@@ -82,6 +91,25 @@ clockodo userreport --year 2026
82
91
 
83
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`.
84
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
+
85
113
  ## AI Agent Integration
86
114
 
87
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 });
@@ -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()) {
@@ -765,6 +780,8 @@ function registerConfigCommands(program2) {
765
780
  }
766
781
 
767
782
  // src/commands/customers.ts
783
+ import * as p4 from "@clack/prompts";
784
+ import { Billability as Billability2 } from "clockodo";
768
785
  function registerCustomersCommands(program2) {
769
786
  const customers = program2.command("customers").description("Manage customers");
770
787
  customers.command("list", { isDefault: true }).description("List customers").option("--active", "Show only active customers").option("--search <text>", "Search by name").action(async (cmdOpts) => {
@@ -778,6 +795,14 @@ function registerCustomersCommands(program2) {
778
795
  );
779
796
  const items = result.data ?? [];
780
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
+ }
781
806
  if (mode !== "human") {
782
807
  printResult({ data: items, meta: { count: items.length } }, opts);
783
808
  return;
@@ -812,12 +837,72 @@ function registerCustomersCommands(program2) {
812
837
  opts
813
838
  );
814
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
+ });
815
900
  }
816
901
 
817
902
  // src/commands/entries.ts
818
903
  import { styleText as styleText4 } from "util";
819
- import * as p4 from "@clack/prompts";
820
- import { Billability as Billability2, getEntryDurationUntilNow as getEntryDurationUntilNow2, isTimeEntry } from "clockodo";
904
+ import * as p5 from "@clack/prompts";
905
+ import { Billability as Billability3, getEntryDurationUntilNow as getEntryDurationUntilNow2, isTimeEntry } from "clockodo";
821
906
  function registerEntriesCommands(program2) {
822
907
  const entries = program2.command("entries").description("Manage time entries");
823
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(
@@ -827,14 +912,14 @@ function registerEntriesCommands(program2) {
827
912
  const opts = program2.opts();
828
913
  const client = getClient();
829
914
  const since = parseDateTime(cmdOpts.since);
830
- 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()));
831
916
  const filter = {};
832
917
  if (cmdOpts.customer) filter.customersId = cmdOpts.customer;
833
918
  if (cmdOpts.project) filter.projectsId = cmdOpts.project;
834
919
  if (cmdOpts.service) filter.servicesId = cmdOpts.service;
835
920
  if (cmdOpts.text) filter.text = cmdOpts.text;
836
921
  if (cmdOpts.billable !== void 0)
837
- filter.billable = cmdOpts.billable ? Billability2.Billable : Billability2.NotBillable;
922
+ filter.billable = cmdOpts.billable ? Billability3.Billable : Billability3.NotBillable;
838
923
  const result = await client.getEntries({
839
924
  timeSince: since,
840
925
  timeUntil: until,
@@ -957,13 +1042,13 @@ function registerEntriesCommands(program2) {
957
1042
  }
958
1043
  let billable;
959
1044
  if (cmdOpts.billable !== void 0) {
960
- billable = cmdOpts.billable ? Billability2.Billable : Billability2.NotBillable;
1045
+ billable = cmdOpts.billable ? Billability3.Billable : Billability3.NotBillable;
961
1046
  } else if (projectsId) {
962
1047
  const { data: project } = await client.getProject({ id: projectsId });
963
- billable = project.billableDefault ? Billability2.Billable : Billability2.NotBillable;
1048
+ billable = project.billableDefault ? Billability3.Billable : Billability3.NotBillable;
964
1049
  } else {
965
1050
  const { data: customer } = await client.getCustomer({ id: customersId });
966
- billable = customer.billableDefault ? Billability2.Billable : Billability2.NotBillable;
1051
+ billable = customer.billableDefault ? Billability3.Billable : Billability3.NotBillable;
967
1052
  }
968
1053
  const result = await client.addEntry({
969
1054
  customersId,
@@ -996,7 +1081,7 @@ function registerEntriesCommands(program2) {
996
1081
  if (cmdOpts.project !== void 0) updates.projectsId = cmdOpts.project;
997
1082
  if (cmdOpts.service !== void 0) updates.servicesId = cmdOpts.service;
998
1083
  if (cmdOpts.billable !== void 0)
999
- updates.billable = cmdOpts.billable ? Billability2.Billable : Billability2.NotBillable;
1084
+ updates.billable = cmdOpts.billable ? Billability3.Billable : Billability3.NotBillable;
1000
1085
  const result = await client.editEntry(updates);
1001
1086
  const mode = resolveOutputMode(opts);
1002
1087
  if (mode !== "human") {
@@ -1009,10 +1094,10 @@ function registerEntriesCommands(program2) {
1009
1094
  const opts = program2.opts();
1010
1095
  const client = getClient();
1011
1096
  if (!cmdOpts.force && process.stdout.isTTY) {
1012
- const confirm3 = await p4.confirm({
1097
+ const confirm6 = await p5.confirm({
1013
1098
  message: `Delete entry ${id}?`
1014
1099
  });
1015
- if (!confirm3 || p4.isCancel(confirm3)) return;
1100
+ if (!confirm6 || p5.isCancel(confirm6)) return;
1016
1101
  }
1017
1102
  const entryId = parseId(id);
1018
1103
  await client.deleteEntry({ id: entryId });
@@ -1070,9 +1155,9 @@ function groupEntries(entries, key) {
1070
1155
  }
1071
1156
  function formatBillable(value) {
1072
1157
  switch (value) {
1073
- case Billability2.Billable:
1158
+ case Billability3.Billable:
1074
1159
  return "Yes";
1075
- case Billability2.Billed:
1160
+ case Billability3.Billed:
1076
1161
  return "Billed";
1077
1162
  default:
1078
1163
  return "No";
@@ -1080,6 +1165,7 @@ function formatBillable(value) {
1080
1165
  }
1081
1166
 
1082
1167
  // src/commands/projects.ts
1168
+ import * as p6 from "@clack/prompts";
1083
1169
  function registerProjectsCommands(program2) {
1084
1170
  const projects = program2.command("projects").description("Manage projects");
1085
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) => {
@@ -1094,17 +1180,25 @@ function registerProjectsCommands(program2) {
1094
1180
  );
1095
1181
  const items = result.data ?? [];
1096
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
+ }
1097
1191
  if (mode !== "human") {
1098
1192
  printResult({ data: items, meta: { count: items.length } }, opts);
1099
1193
  return;
1100
1194
  }
1101
- const rows = items.map((p5) => [
1102
- String(p5.id),
1103
- p5.name,
1104
- p5.number ?? "\u2014",
1105
- String(p5.customersId),
1106
- p5.active ? "Yes" : "No",
1107
- 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"
1108
1202
  ]);
1109
1203
  printTable(["ID", "Name", "Number", "Customer", "Active", "Completed"], rows, opts);
1110
1204
  });
@@ -1112,27 +1206,89 @@ function registerProjectsCommands(program2) {
1112
1206
  const opts = program2.opts();
1113
1207
  const client = getClient();
1114
1208
  const result = await client.getProject({ id: parseId(id) });
1115
- const p5 = result.data;
1209
+ const proj = result.data;
1116
1210
  const mode = resolveOutputMode(opts);
1117
1211
  if (mode !== "human") {
1118
- printResult({ data: p5 }, opts);
1212
+ printResult({ data: proj }, opts);
1119
1213
  return;
1120
1214
  }
1121
1215
  printDetail(
1122
1216
  [
1123
- ["ID", p5.id],
1124
- ["Name", p5.name],
1125
- ["Number", p5.number ?? null],
1126
- ["Customer ID", p5.customersId],
1127
- ["Active", p5.active],
1128
- ["Completed", p5.completed],
1129
- ["Budget", p5.budget?.amount ?? null],
1130
- ["Budget Type", p5.budget?.monetary ? "Money" : "Hours"],
1131
- ["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]
1132
1226
  ],
1133
1227
  opts
1134
1228
  );
1135
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
+ });
1136
1292
  }
1137
1293
 
1138
1294
  // src/commands/report.ts
@@ -1184,12 +1340,23 @@ function groupEntriesByText(entries) {
1184
1340
  }
1185
1341
  return [...map.values()].sort((a, b) => b.seconds - a.seconds);
1186
1342
  }
1187
- async function runTextReport(program2, since, until) {
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) {
1188
1353
  const opts = program2.opts();
1189
1354
  const client = getClient();
1355
+ const filter = buildFilter(cmdOpts);
1190
1356
  const result = await client.getEntries({
1191
1357
  timeSince: toClockodoDateTime(since),
1192
- timeUntil: toClockodoDateTime(until)
1358
+ timeUntil: toClockodoDateTime(until),
1359
+ ...filter && { filter }
1193
1360
  });
1194
1361
  const entryList = result.entries ?? [];
1195
1362
  const groups = groupEntriesByText(entryList);
@@ -1233,16 +1400,18 @@ function printReportTotal(totalSeconds) {
1233
1400
  console.log();
1234
1401
  }
1235
1402
  async function runReport(program2, since, until, cmdOpts) {
1236
- const groupField = resolveReportGroupKey(cmdOpts.group ?? "projects_id");
1403
+ const groupField = resolveReportGroupKey(cmdOpts.group ?? "project");
1237
1404
  if (groupField === "text") {
1238
- return runTextReport(program2, since, until);
1405
+ return runTextReport(program2, since, until, cmdOpts);
1239
1406
  }
1240
1407
  const opts = program2.opts();
1241
1408
  const client = getClient();
1409
+ const filter = buildFilter(cmdOpts);
1242
1410
  const result = await client.getEntryGroups({
1243
1411
  timeSince: toClockodoDateTime(since),
1244
1412
  timeUntil: toClockodoDateTime(until),
1245
- grouping: [groupField]
1413
+ grouping: [groupField],
1414
+ ...filter && { filter }
1246
1415
  });
1247
1416
  const groups = result.groups ?? [];
1248
1417
  const totalSeconds = groups.reduce((sum, g) => sum + (g.duration ?? 0), 0);
@@ -1273,23 +1442,37 @@ async function runReport(program2, since, until, cmdOpts) {
1273
1442
  printTable(["Name", "Duration", "Hours"], rows, opts);
1274
1443
  printReportTotal(totalSeconds);
1275
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);
1447
+ }
1276
1448
  function registerReportCommands(program2) {
1277
1449
  const report = program2.command("report").description("Aggregated time reports");
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) => {
1450
+ addFilterOptions(
1451
+ report.command("today", { isDefault: true }).description("Today's summary").option("-g, --group <field>", "Group by: customer, project, service, text", "project")
1452
+ ).action(async (cmdOpts) => {
1279
1453
  const now = /* @__PURE__ */ new Date();
1280
1454
  await runReport(program2, startOfDay(now), endOfDay(now), cmdOpts);
1281
1455
  });
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) => {
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) => {
1283
1459
  const now = /* @__PURE__ */ new Date();
1284
1460
  await runReport(program2, startOfWeek(now), endOfWeek(now), cmdOpts);
1285
1461
  });
1286
- report.command("month").description("This month's summary").option("-g, --group <field>", "Group by: customer, project, service, text", "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) => {
1287
1465
  const now = /* @__PURE__ */ new Date();
1288
1466
  await runReport(program2, startOfMonth(now), endOfMonth(now), cmdOpts);
1289
1467
  });
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) => {
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) => {
1291
1471
  const since = new Date(parseDateTime(cmdOpts.since));
1292
- 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
+ }
1293
1476
  await runReport(program2, since, until, cmdOpts);
1294
1477
  });
1295
1478
  }
@@ -1326,6 +1509,7 @@ function registerSchemaCommand(program2) {
1326
1509
  }
1327
1510
 
1328
1511
  // src/commands/services.ts
1512
+ import * as p7 from "@clack/prompts";
1329
1513
  function registerServicesCommands(program2) {
1330
1514
  const services = program2.command("services").description("Manage services");
1331
1515
  services.command("list", { isDefault: true }).description("List services").option("--active", "Show only active services").option("--search <text>", "Search by name").action(async (cmdOpts) => {
@@ -1339,6 +1523,14 @@ function registerServicesCommands(program2) {
1339
1523
  );
1340
1524
  const items = result.data ?? [];
1341
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
+ }
1342
1534
  if (mode !== "human") {
1343
1535
  printResult({ data: items, meta: { count: items.length } }, opts);
1344
1536
  return;
@@ -1372,16 +1564,94 @@ function registerServicesCommands(program2) {
1372
1564
  opts
1373
1565
  );
1374
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
+ });
1375
1621
  }
1376
1622
 
1377
1623
  // src/commands/status.ts
1378
1624
  import { styleText as styleText6 } from "util";
1379
1625
  import { isTimeEntry as isTimeEntry2 } from "clockodo";
1380
1626
  function registerStatusCommand(program2) {
1381
- 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) => {
1382
1628
  const opts = program2.opts();
1383
1629
  const client = getClient();
1384
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
+ }
1385
1655
  const now = /* @__PURE__ */ new Date();
1386
1656
  const [clockResult, entriesResult] = await Promise.all([
1387
1657
  client.getClock(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liebig-technology/clockodo-cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "AI-friendly CLI for the Clockodo time tracking API",
5
5
  "type": "module",
6
6
  "bin": {