@togglhq/cli 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as CommanderError, i as shutdownCliTelemetry, n as captureCliException, r as initCliTelemetry, t as createProgram } from "./program-B6EuV2vO.js";
2
+ import { a as CommanderError, i as shutdownCliTelemetry, n as captureCliException, r as initCliTelemetry, t as createProgram } from "./program-B0eN4WNv.js";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
package/build/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as createProgram } from "./program-B6EuV2vO.js";
1
+ import { t as createProgram } from "./program-B0eN4WNv.js";
2
2
  export { createProgram };
@@ -10604,24 +10604,6 @@ async function createTimeEntryWithOrg(httpClient, organization_id, workspace_id,
10604
10604
  json: data
10605
10605
  }).json();
10606
10606
  }
10607
- async function getTimeEntriesRangeWithOrgClassic(httpClient, organization_id, workspace_id, { date_from, date_to, task_id, status_id, type, page, per_page, order_by, archived, time_block_id, include_taskless }, signal) {
10608
- return await httpClient.get(`organizations/${organization_id}/workspaces/${workspace_id}/time-entries`, {
10609
- signal,
10610
- searchParams: buildSearchParams$3({
10611
- date_from,
10612
- date_to,
10613
- task_id,
10614
- status_id,
10615
- type,
10616
- page,
10617
- per_page,
10618
- order_by,
10619
- archived,
10620
- time_block_id,
10621
- include_taskless
10622
- })
10623
- }).json();
10624
- }
10625
10607
  async function createTasklessTimeEntryWithOrg(httpClient, organization_id, workspace_id, data, signal) {
10626
10608
  return await httpClient.post(`organizations/${organization_id}/workspaces/${workspace_id}/time-entries`, {
10627
10609
  signal,
@@ -10972,7 +10954,7 @@ async function deleteMyWorkingHours(httpClient, organization_id, data, signal) {
10972
10954
  return response.json();
10973
10955
  }
10974
10956
  //#endregion
10975
- //#region ../cli-core/build/focus-client-B2l1BZIW.js
10957
+ //#region ../cli-core/build/focus-client-D9oJPY4f.js
10976
10958
  /** Request header the API reads to attribute traffic to MCP or CLI. */
10977
10959
  const TOGGL_CLIENT_HEADER = "X-Toggl-Client";
10978
10960
  /** Headers that attribute a raw `fetch` to MCP or CLI (FocusClient sets these itself). */
@@ -11000,6 +10982,20 @@ function normalizeDateFrom(value) {
11000
10982
  function normalizeDateTo(value) {
11001
10983
  return DATE_ONLY_RE.test(value) ? `${value}T23:59:59Z` : value;
11002
10984
  }
10985
+ function timeEntryProjectId(entry) {
10986
+ if (typeof entry.project_id === "number") return entry.project_id;
10987
+ if (typeof entry.project?.id === "number") return entry.project.id;
10988
+ if (typeof entry.task?.project_id === "number") return entry.task.project_id;
10989
+ }
10990
+ function filterTimeEntriesByProjectId(entries, projectId) {
10991
+ return entries.filter((entry) => timeEntryProjectId(entry) === projectId);
10992
+ }
10993
+ function parseOptionalBoolean(value) {
10994
+ if (value === void 0 || value === "") return void 0;
10995
+ if (typeof value === "boolean") return value;
10996
+ if (value === "true") return true;
10997
+ if (value === "false") return false;
10998
+ }
11003
10999
  const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
11004
11000
  const MIME_BY_EXTENSION = {
11005
11001
  ".png": "image/png",
@@ -11556,16 +11552,53 @@ var FocusClient = class {
11556
11552
  const date_from = normalizeDateFrom(rawFrom);
11557
11553
  const date_to = normalizeDateTo(rawTo);
11558
11554
  const type = params.type;
11559
- const response = await this.run("listTimeEntries", () => getTimeEntriesRangeWithOrgClassic(this.http, this.organizationIdNum, this.workspaceIdNum, {
11555
+ const include_taskless = parseOptionalBoolean(params.include_taskless) ?? true;
11556
+ const projectId = parseOptionalInt(params.project_id);
11557
+ const task_id = parseOptionalInt(params.task_id);
11558
+ const time_block_id = parseOptionalInt(params.time_block_id);
11559
+ const requestedPage = parseOptionalInt(params.page) ?? 1;
11560
+ const requestedPerPage = parseOptionalInt(params.per_page);
11561
+ const fetchPage = (page, per_page) => this.run("listTimeEntries", () => this.http.get(`organizations/${this.organizationIdNum}/workspaces/${this.workspaceIdNum}/time-entries`, { searchParams: buildSearchParams$2({
11560
11562
  date_from,
11561
11563
  date_to,
11562
- task_id: parseOptionalInt(params.task_id),
11563
- time_block_id: parseOptionalInt(params.time_block_id),
11564
+ task_id,
11565
+ time_block_id,
11564
11566
  type,
11565
- page: parseOptionalInt(params.page),
11566
- per_page: parseOptionalInt(params.per_page)
11567
- }));
11568
- return this.softValidateResponse("listTimeEntries", PaginatedTimeEntryWithTasksSchema, response);
11567
+ page,
11568
+ per_page,
11569
+ include_taskless,
11570
+ project_id: projectId
11571
+ }) }).json());
11572
+ if (projectId === void 0) {
11573
+ const response = await fetchPage(requestedPage, requestedPerPage);
11574
+ return this.softValidateResponse("listTimeEntries", PaginatedTimeEntryWithTasksSchema, response);
11575
+ }
11576
+ const perPage = requestedPerPage ?? 50;
11577
+ const needed = requestedPage * perPage;
11578
+ const matched = [];
11579
+ const apiPerPage = 100;
11580
+ const maxApiPages = 50;
11581
+ let truncated = false;
11582
+ for (let apiPage = 1; apiPage <= maxApiPages; apiPage += 1) {
11583
+ const response = await fetchPage(apiPage, apiPerPage);
11584
+ const validated = this.softValidateResponse("listTimeEntries", PaginatedTimeEntryWithTasksSchema, response);
11585
+ if (!validated.ok) return validated;
11586
+ const pageRows = validated.data.data;
11587
+ matched.push(...filterTimeEntriesByProjectId(pageRows, projectId));
11588
+ if (pageRows.length < apiPerPage) break;
11589
+ if (matched.length >= needed) break;
11590
+ if (apiPage === maxApiPages) truncated = true;
11591
+ }
11592
+ const start = (requestedPage - 1) * perPage;
11593
+ return {
11594
+ ok: true,
11595
+ data: {
11596
+ page: requestedPage,
11597
+ per_page: perPage,
11598
+ data: matched.slice(start, start + perPage),
11599
+ ...truncated ? { truncated: true } : {}
11600
+ }
11601
+ };
11569
11602
  }
11570
11603
  async startTracking(payload) {
11571
11604
  const input = validatePayload(TimeEntryStartTrackingPayloadSchema, payload, "startTracking payload");
@@ -12618,6 +12651,51 @@ function switchCliWorkspace(workspaceId, profile) {
12618
12651
  saveDeveloperToolsConfig(dev);
12619
12652
  return target;
12620
12653
  }
12654
+ const CLI_GENERIC_ERROR_CATEGORY = "cli.command_error";
12655
+ const MCP_GENERIC_ERROR_CATEGORY = "mcp.tool_error";
12656
+ const ERROR_CATEGORY_SET = new Set([
12657
+ "cli.validation_error",
12658
+ "cli.api_error",
12659
+ CLI_GENERIC_ERROR_CATEGORY,
12660
+ "mcp.validation_error",
12661
+ "mcp.api_error",
12662
+ "mcp.auth_error",
12663
+ MCP_GENERIC_ERROR_CATEGORY
12664
+ ]);
12665
+ function isDeclaredErrorCategory(value) {
12666
+ return typeof value === "string" && ERROR_CATEGORY_SET.has(value);
12667
+ }
12668
+ function firstToolResultText(source) {
12669
+ if (!source || typeof source !== "object") return null;
12670
+ const content = source.content;
12671
+ if (!Array.isArray(content)) return null;
12672
+ const first = content[0];
12673
+ if (first?.type === "text" && typeof first.text === "string") return first.text;
12674
+ return null;
12675
+ }
12676
+ function errorTextForClassification(source) {
12677
+ if (source instanceof Error) return source.message;
12678
+ if (typeof source === "string") return source;
12679
+ const toolText = firstToolResultText(source);
12680
+ if (toolText !== null) return toolText;
12681
+ return "";
12682
+ }
12683
+ function classifyCli(message) {
12684
+ if (message.startsWith("Invalid input:") || message.startsWith("Invalid input ")) return "cli.validation_error";
12685
+ if (/^\d{3}:\s/.test(message)) return "cli.api_error";
12686
+ return CLI_GENERIC_ERROR_CATEGORY;
12687
+ }
12688
+ function classifyMcp(message) {
12689
+ if (message.startsWith("Invalid input:") || message.startsWith("Invalid input ")) return "mcp.validation_error";
12690
+ if (message.startsWith("API Error (")) return "mcp.api_error";
12691
+ if (message.startsWith("Authentication failed:") || message.startsWith("Not authenticated.")) return "mcp.auth_error";
12692
+ return MCP_GENERIC_ERROR_CATEGORY;
12693
+ }
12694
+ function classifyErrorCategory(client, source) {
12695
+ const message = errorTextForClassification(source);
12696
+ if (isDeclaredErrorCategory(message) && message.startsWith(`${client}.`)) return message;
12697
+ return client === "cli" ? classifyCli(message) : classifyMcp(message);
12698
+ }
12621
12699
  /**
12622
12700
  * Exception messages are the one channel `sendDefaultPii: false` does not cover,
12623
12701
  * and this repo's errors put user data in them: validation failures embed the
@@ -12629,15 +12707,14 @@ function switchCliWorkspace(workspaceId, profile) {
12629
12707
  * Shared between the CLI and the public MCP build so the two cannot drift.
12630
12708
  */
12631
12709
  /**
12632
- * Fixed categories are `<surface>.<snake_case>` — anchored at both ends, because
12633
- * a prefix test would pass anything that merely *starts* that way. A real message
12634
- * like `cli.ts:12: unexpected token "secret"` begins with `cli.` and would
12635
- * otherwise sail through unredacted.
12710
+ * Fixed categories are the seven declared `<surface>.<snake_case>` values, not
12711
+ * a `cli|mcp` prefix regex. A prefix or regex test would pass a caught message
12712
+ * like `cli.customer_name`. A real filename message such as
12713
+ * `cli.ts:12: unexpected token "secret"` would also begin with `cli.`.
12636
12714
  */
12637
- const SAFE_CATEGORY_PATTERN = /^(?:cli|mcp)\.[a-z][a-z0-9_]*$/;
12638
12715
  const REDACTED_SENTRY_MESSAGE = "redacted";
12639
12716
  function isSafeSentryMessage(value) {
12640
- return typeof value === "string" && SAFE_CATEGORY_PATTERN.test(value);
12717
+ return isDeclaredErrorCategory(value);
12641
12718
  }
12642
12719
  /**
12643
12720
  * Replace every exception message that is not a known category, and any top-level
@@ -12719,7 +12796,7 @@ function normalizeAction(action) {
12719
12796
  /** Strict allowlist — never accept args/results. */
12720
12797
  function captureToolInvocation(event) {
12721
12798
  if (!enabled || !apiKey) return;
12722
- const task = sendCapture(TOOL_INVOCATION_EVENT, {
12799
+ const properties = {
12723
12800
  tool_name: event.toolName,
12724
12801
  action: normalizeAction(event.action),
12725
12802
  outcome: event.outcome,
@@ -12727,7 +12804,9 @@ function captureToolInvocation(event) {
12727
12804
  client,
12728
12805
  session_id: sessionId,
12729
12806
  package_version: packageVersion
12730
- }, event.distinctId).finally(() => {
12807
+ };
12808
+ if (event.outcome === "error") properties.error_category = classifyErrorCategory(client, event.error);
12809
+ const task = sendCapture(TOOL_INVOCATION_EVENT, properties, event.distinctId).finally(() => {
12731
12810
  pending.delete(task);
12732
12811
  });
12733
12812
  pending.add(task);
@@ -12910,7 +12989,7 @@ async function runSkillSetup(options) {
12910
12989
  });
12911
12990
  }
12912
12991
  //#endregion
12913
- //#region ../operations/build/validation-v3-RQf0k.js
12992
+ //#region ../operations/build/validation-CbrghKZi.js
12914
12993
  const optionalNumber = /* @__PURE__ */ optional$1(/* @__PURE__ */ number$2());
12915
12994
  const perPageField = (max) => /* @__PURE__ */ optional$1(/* @__PURE__ */ pipe$2(/* @__PURE__ */ number$2(), /* @__PURE__ */ integer(), /* @__PURE__ */ minValue$1(1), /* @__PURE__ */ maxValue$1(max), /* @__PURE__ */ description$1(`Results per page (1-${max})`)));
12916
12995
  const perPageSchema = perPageField(100);
@@ -12936,9 +13015,11 @@ const timeBlockListSchema = /* @__PURE__ */ object$2({
12936
13015
  const timeEntryListSchema = /* @__PURE__ */ object$2({
12937
13016
  date_from: dateField("Range start — date-only (e.g. '2025-01-01') or RFC3339 datetime. Date-only is expanded to T00:00:00Z."),
12938
13017
  date_to: dateField("Range end — date-only (e.g. '2025-01-07') or RFC3339 datetime. Date-only is expanded to T23:59:59Z."),
13018
+ project_id: /* @__PURE__ */ optional$1(/* @__PURE__ */ pipe$2(/* @__PURE__ */ number$2(), /* @__PURE__ */ description$1("Filter to entries on this project (task-linked or taskless)"))),
12939
13019
  task_id: /* @__PURE__ */ optional$1(/* @__PURE__ */ pipe$2(/* @__PURE__ */ number$2(), /* @__PURE__ */ description$1("Filter by task ID"))),
12940
13020
  time_block_id: /* @__PURE__ */ optional$1(/* @__PURE__ */ pipe$2(/* @__PURE__ */ number$2(), /* @__PURE__ */ description$1("Filter by time block ID"))),
12941
13021
  type: /* @__PURE__ */ optional$1(/* @__PURE__ */ pipe$2(/* @__PURE__ */ picklist$1(["activity", "break"]), /* @__PURE__ */ description$1("Filter by entry type"))),
13022
+ include_taskless: /* @__PURE__ */ optional$1(/* @__PURE__ */ pipe$2(/* @__PURE__ */ boolean$1(), /* @__PURE__ */ description$1("Include entries tracked on a project without a task. Defaults to true when omitted."))),
12942
13023
  page: /* @__PURE__ */ optional$1(/* @__PURE__ */ pipe$2(/* @__PURE__ */ number$2(), /* @__PURE__ */ description$1("Page number"))),
12943
13024
  per_page: perPageSchema
12944
13025
  });
@@ -13901,20 +13982,31 @@ const timeBlockOverrides = {
13901
13982
  };
13902
13983
  const timeEntryOverrides = {
13903
13984
  "focus.time-entries.list": {
13904
- agentGuidance: "Returns tracked time entries in a date range. Requires date_from and date_to. Accepts date-only strings (YYYY-MM-DD) or RFC3339 datetimes — date-only is automatically expanded: date_from to T00:00:00Z, date_to to T23:59:59Z. Includes task and project context.",
13905
- examples: [{
13906
- description: "List time entries for a week (date-only, expanded to full-day range)",
13907
- data: {
13908
- date_from: "2025-01-01",
13909
- date_to: "2025-01-07"
13910
- }
13911
- }, {
13912
- description: "List time entries with explicit RFC3339 timestamps",
13913
- data: {
13914
- date_from: "2025-01-01T00:00:00Z",
13915
- date_to: "2025-01-07T23:59:59Z"
13985
+ agentGuidance: "Returns tracked time entries in a date range, including taskless entries (tracked on a project with no task). Requires date_from and date_to. Accepts date-only strings (YYYY-MM-DD) or RFC3339 datetimes — date-only is automatically expanded: date_from to T00:00:00Z, date_to to T23:59:59Z. Optional project_id keeps only entries on that project (sent to the API; client also scans up to 50 list pages of 100, then slices page/per_page; not a full workspace scan). If truncated is true, more unfiltered rows exist than were scanned — narrow date_from/date_to. Pass include_taskless:false to hide taskless rows. Includes task and project context.",
13986
+ examples: [
13987
+ {
13988
+ description: "List time entries for a week (date-only, expanded to full-day range)",
13989
+ data: {
13990
+ date_from: "2025-01-01",
13991
+ date_to: "2025-01-07"
13992
+ }
13993
+ },
13994
+ {
13995
+ description: "List time entries with explicit RFC3339 timestamps",
13996
+ data: {
13997
+ date_from: "2025-01-01T00:00:00Z",
13998
+ date_to: "2025-01-07T23:59:59Z"
13999
+ }
14000
+ },
14001
+ {
14002
+ description: "List time entries for one project, including taskless rows",
14003
+ data: {
14004
+ date_from: "2025-01-01",
14005
+ date_to: "2025-01-07",
14006
+ project_id: 42
14007
+ }
13916
14008
  }
13917
- }]
14009
+ ]
13918
14010
  },
13919
14011
  "focus.time-entries.start": {
13920
14012
  agentGuidance: "type defaults to 'activity' if omitted; pass type:'break' to start a break. Optionally provide task_id, project_id, description, and time_block_id. Only one timer can be active at a time — stops the current one if running.",
@@ -14006,6 +14098,10 @@ const userOverrides = {
14006
14098
  }]
14007
14099
  }
14008
14100
  };
14101
+ const versionOverrides = { "focus.version.service": {
14102
+ description: "MCP package version plus the Focus API service revision.",
14103
+ agentGuidance: "mcp.package is this published MCP package version (e.g. 1.8.0). focus_api is the Focus API process revision (git SHA / build number) — it does not change when you update the npm MCP package."
14104
+ } };
14009
14105
  const dateRangeDay = /* @__PURE__ */ pipe$2(/* @__PURE__ */ string$1(), /* @__PURE__ */ regex(/^\d{4}-\d{2}-\d{2}$/, "must be YYYY-MM-DD"));
14010
14106
  const TimeOffMeUpdateInputSchema = /* @__PURE__ */ intersect([/* @__PURE__ */ object$2({ timeoff_id: /* @__PURE__ */ number$2() }), PublicMeEntryRequestSchema]);
14011
14107
  const emptySchema = /* @__PURE__ */ object$2({});
@@ -24035,6 +24131,7 @@ const focusOperationOverrides = {
24035
24131
  ...readOnlyPostOverrides,
24036
24132
  ...timeBlockOverrides,
24037
24133
  ...timeEntryOverrides,
24134
+ ...versionOverrides,
24038
24135
  ...statusOverrides,
24039
24136
  ...userOverrides,
24040
24137
  ...tagOverrides,
@@ -24981,6 +25078,7 @@ function envEnabled(raw) {
24981
25078
  }
24982
25079
  let sentryEnabled = false;
24983
25080
  let productAnalyticsEnabled = false;
25081
+ let lastCommandError;
24984
25082
  function initCliTelemetry(packageVersion) {
24985
25083
  productAnalyticsEnabled = initProductAnalytics({
24986
25084
  client: "cli",
@@ -25014,17 +25112,17 @@ function initCliTelemetry(packageVersion) {
25014
25112
  */
25015
25113
  function recordCliCommand(commandName, action, success) {
25016
25114
  if (!productAnalyticsEnabled) return;
25115
+ const error = success ? void 0 : lastCommandError;
25116
+ lastCommandError = void 0;
25017
25117
  captureToolInvocation({
25018
25118
  toolName: commandName,
25019
25119
  action,
25020
- outcome: success ? "success" : "error"
25120
+ outcome: success ? "success" : "error",
25121
+ error
25021
25122
  });
25022
25123
  }
25023
25124
  function classifyCliSentryError(error) {
25024
- const message = error instanceof Error ? error.message : String(error);
25025
- if (message.startsWith("Invalid input:") || message.startsWith("Invalid input ")) return "cli.validation_error";
25026
- if (/^\d{3}:\s/.test(message)) return "cli.api_error";
25027
- return "cli.command_error";
25125
+ return classifyErrorCategory("cli", error);
25028
25126
  }
25029
25127
  function toSentrySafeCliError(error) {
25030
25128
  const category = classifyCliSentryError(error);
@@ -25034,6 +25132,7 @@ function toSentrySafeCliError(error) {
25034
25132
  return safe;
25035
25133
  }
25036
25134
  function captureCliException(error) {
25135
+ lastCommandError = error;
25037
25136
  if (!sentryEnabled) return;
25038
25137
  const category = classifyCliSentryError(error);
25039
25138
  Sentry.withScope((scope) => {
@@ -25048,6 +25147,7 @@ async function shutdownCliTelemetry() {
25048
25147
  if (tasks.length > 0) await Promise.allSettled(tasks);
25049
25148
  sentryEnabled = false;
25050
25149
  productAnalyticsEnabled = false;
25150
+ lastCommandError = void 0;
25051
25151
  }
25052
25152
  //#endregion
25053
25153
  //#region src/output.ts
@@ -25624,4 +25724,4 @@ Common commands:
25624
25724
  //#endregion
25625
25725
  export { CommanderError as a, shutdownCliTelemetry as i, captureCliException as n, initCliTelemetry as r, createProgram as t };
25626
25726
 
25627
- //# sourceMappingURL=program-B6EuV2vO.js.map
25727
+ //# sourceMappingURL=program-B0eN4WNv.js.map
package/build/program.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as createProgram } from "./program-B6EuV2vO.js";
1
+ import { t as createProgram } from "./program-B0eN4WNv.js";
2
2
  export { createProgram };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@togglhq/cli",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "Toggl CLI for Toggl 2.0 API operations.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",