@togglhq/cli 1.7.1 → 1.8.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.
package/README.md CHANGED
@@ -290,6 +290,18 @@ TOGGL_DEBUG=1 toggl tasks list
290
290
 
291
291
  Prints schema validation warnings from API responses to stderr.
292
292
 
293
+ ## Privacy & analytics
294
+
295
+ Public builds may send product analytics: command path, the leaf command as `action`, outcome (`success` or `error`), client, session id and package version. Command arguments and output are never collected. Signed-in sessions are identified by your Toggl account id so repeat use is not counted as a new person each time; unauthenticated processes stay anonymous.
296
+
297
+ Disable product analytics:
298
+
299
+ ```bash
300
+ TOGGL_PRODUCT_ANALYTICS=off
301
+ ```
302
+
303
+ Optional error reporting via Sentry is off unless you set `TOGGL_SENTRY=1` and `SENTRY_DSN`.
304
+
293
305
  ## License
294
306
 
295
307
  Proprietary Toggl software. See [LICENSE](LICENSE) and [Toggl legal terms](https://toggl.com/legal/).
package/build/cli.js CHANGED
@@ -1,11 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { n as CommanderError, t as createProgram } from "./program-BJrl7qqO.js";
2
+ import { a as CommanderError, i as shutdownCliTelemetry, n as captureCliException, r as initCliTelemetry, t as createProgram } from "./program-B6EuV2vO.js";
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  //#region src/cli.ts
7
+ const CLI_PACKAGE_VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../package.json"), "utf-8")).version;
8
+ initCliTelemetry(CLI_PACKAGE_VERSION);
4
9
  try {
5
10
  await createProgram({ enableUpdateNotification: true }).parseAsync(process.argv);
6
11
  } catch (error) {
12
+ captureCliException(error);
7
13
  if (error instanceof CommanderError) process.exit(error.exitCode);
8
14
  throw error;
15
+ } finally {
16
+ await shutdownCliTelemetry();
9
17
  }
10
18
  //#endregion
11
19
  export {};
package/build/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as createProgram } from "./program-BJrl7qqO.js";
1
+ import { t as createProgram } from "./program-B6EuV2vO.js";
2
2
  export { createProgram };
@@ -10,12 +10,13 @@ import { AsyncLocalStorage, AsyncResource } from "node:async_hooks";
10
10
  import { stripVTControlCharacters, styleText } from "node:util";
11
11
  import * as readline from "node:readline";
12
12
  import * as os from "node:os";
13
- import * as crypto from "node:crypto";
13
+ import * as crypto$1 from "node:crypto";
14
14
  import * as fs from "node:fs/promises";
15
15
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
16
16
  import { createInterface } from "node:readline/promises";
17
17
  import * as http from "node:http";
18
18
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
19
+ import * as Sentry from "@sentry/node";
19
20
  //#region \0rolldown/runtime.js
20
21
  var __create = Object.create;
21
22
  var __defProp = Object.defineProperty;
@@ -10811,7 +10812,7 @@ const PublicMeEntryRequestSchema = /* @__PURE__ */ object$2({
10811
10812
  ]))
10812
10813
  });
10813
10814
  //#endregion
10814
- //#region ../../node_modules/.pnpm/@toggl+queries@2.1.212_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_d4c0aa64421be7eab39751554cfd6b93/node_modules/@toggl/queries/dist/org/invitation/requests.mjs
10815
+ //#region ../../node_modules/.pnpm/@toggl+queries@2.1.216_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_8e2551e0eda340359e00ca954b6e5013/node_modules/@toggl/queries/dist/org/invitation/requests.mjs
10815
10816
  async function getInvitations(httpClient, signal) {
10816
10817
  return (await httpClient.get(`invitations`, { signal })).json();
10817
10818
  }
@@ -10828,7 +10829,7 @@ async function resendInvitation(httpClient, organization_id, organization_user_i
10828
10829
  return (await httpClient.post(`organizations/${organization_id}/invitations/${organization_user_id}/resend`, { signal })).json();
10829
10830
  }
10830
10831
  //#endregion
10831
- //#region ../../node_modules/.pnpm/@toggl+queries@2.1.212_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_d4c0aa64421be7eab39751554cfd6b93/node_modules/@toggl/queries/dist/org/utils/requests.mjs
10832
+ //#region ../../node_modules/.pnpm/@toggl+queries@2.1.216_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_8e2551e0eda340359e00ca954b6e5013/node_modules/@toggl/queries/dist/org/utils/requests.mjs
10832
10833
  function buildSearchParams$1(params, flattenArrays = false) {
10833
10834
  const searchParams = new URLSearchParams();
10834
10835
  Object.entries(params).forEach(([key, value]) => {
@@ -10840,7 +10841,7 @@ function buildSearchParams$1(params, flattenArrays = false) {
10840
10841
  return searchParams;
10841
10842
  }
10842
10843
  //#endregion
10843
- //#region ../../node_modules/.pnpm/@toggl+queries@2.1.212_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_d4c0aa64421be7eab39751554cfd6b93/node_modules/@toggl/queries/dist/org/organization-user/requests.mjs
10844
+ //#region ../../node_modules/.pnpm/@toggl+queries@2.1.216_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_8e2551e0eda340359e00ca954b6e5013/node_modules/@toggl/queries/dist/org/organization-user/requests.mjs
10844
10845
  async function getOrganizationUser(httpClient, organization_id, organization_user_id, signal) {
10845
10846
  return (await httpClient.get(`organizations/${organization_id}/users/${organization_user_id}`, { signal })).json();
10846
10847
  }
@@ -10854,7 +10855,7 @@ async function patchOrganizationUserActive(httpClient, organization_id, organiza
10854
10855
  return (await httpClient.patch(`organizations/${organization_id}/users/${organization_user_id}/active_status/${status}`, { signal })).json();
10855
10856
  }
10856
10857
  //#endregion
10857
- //#region ../../node_modules/.pnpm/@toggl+queries@2.1.212_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_d4c0aa64421be7eab39751554cfd6b93/node_modules/@toggl/queries/dist/org/workspace/requests.mjs
10858
+ //#region ../../node_modules/.pnpm/@toggl+queries@2.1.216_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_8e2551e0eda340359e00ca954b6e5013/node_modules/@toggl/queries/dist/org/workspace/requests.mjs
10858
10859
  async function getWorkspaces(httpClient, organization_id, signal) {
10859
10860
  return (await httpClient.get(`organizations/${organization_id}/workspaces`, { signal })).json();
10860
10861
  }
@@ -10874,7 +10875,7 @@ const WorkingHoursUpdateInputSchema = /* @__PURE__ */ object$2({
10874
10875
  });
10875
10876
  const StatusOKSchema = /* @__PURE__ */ object$2({ status: /* @__PURE__ */ string$1() });
10876
10877
  //#endregion
10877
- //#region ../../node_modules/.pnpm/@toggl+queries@2.1.212_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_d4c0aa64421be7eab39751554cfd6b93/node_modules/@toggl/queries/dist/shared-data/utils/requests.mjs
10878
+ //#region ../../node_modules/.pnpm/@toggl+queries@2.1.216_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_8e2551e0eda340359e00ca954b6e5013/node_modules/@toggl/queries/dist/shared-data/utils/requests.mjs
10878
10879
  const META_SYMBOL = Symbol.for("querygen:response:meta");
10879
10880
  function withResponseMeta(data, headers, status) {
10880
10881
  if (!data) return data;
@@ -10901,7 +10902,7 @@ function buildSearchParams(params, flattenArrays = false) {
10901
10902
  return searchParams;
10902
10903
  }
10903
10904
  //#endregion
10904
- //#region ../../node_modules/.pnpm/@toggl+queries@2.1.212_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_d4c0aa64421be7eab39751554cfd6b93/node_modules/@toggl/queries/dist/shared-data/public-holidays/requests.mjs
10905
+ //#region ../../node_modules/.pnpm/@toggl+queries@2.1.216_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_8e2551e0eda340359e00ca954b6e5013/node_modules/@toggl/queries/dist/shared-data/public-holidays/requests.mjs
10905
10906
  async function getPublicHolidays(httpClient, year, { country, organization_id } = {}, signal) {
10906
10907
  const response = await httpClient.get(`shared_data/api/publicholidays/${year}`, {
10907
10908
  signal,
@@ -10913,12 +10914,12 @@ async function getPublicHolidays(httpClient, year, { country, organization_id }
10913
10914
  return withResponseMeta(await response.json(), response.headers, response.status);
10914
10915
  }
10915
10916
  //#endregion
10916
- //#region ../../node_modules/.pnpm/@toggl+queries@2.1.212_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_d4c0aa64421be7eab39751554cfd6b93/node_modules/@toggl/queries/dist/shared-data/status/requests.mjs
10917
+ //#region ../../node_modules/.pnpm/@toggl+queries@2.1.216_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_8e2551e0eda340359e00ca954b6e5013/node_modules/@toggl/queries/dist/shared-data/status/requests.mjs
10917
10918
  async function getStatus(httpClient, signal) {
10918
10919
  return (await httpClient.get(`shared_data/api/status`, { signal })).json();
10919
10920
  }
10920
10921
  //#endregion
10921
- //#region ../../node_modules/.pnpm/@toggl+queries@2.1.212_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_d4c0aa64421be7eab39751554cfd6b93/node_modules/@toggl/queries/dist/org/timeoff/requests.mjs
10922
+ //#region ../../node_modules/.pnpm/@toggl+queries@2.1.216_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_8e2551e0eda340359e00ca954b6e5013/node_modules/@toggl/queries/dist/org/timeoff/requests.mjs
10922
10923
  async function timeoffListMyEntries(httpClient, organization_id, { from, to }, signal) {
10923
10924
  return (await httpClient.get(`timeoff/organizations/${organization_id}/me`, {
10924
10925
  signal,
@@ -10946,7 +10947,7 @@ async function timeoffDeleteMyEntry(httpClient, organization_id, id, signal) {
10946
10947
  return response.json();
10947
10948
  }
10948
10949
  //#endregion
10949
- //#region ../../node_modules/.pnpm/@toggl+queries@2.1.212_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_d4c0aa64421be7eab39751554cfd6b93/node_modules/@toggl/queries/dist/shared-data/working-hours/requests.mjs
10950
+ //#region ../../node_modules/.pnpm/@toggl+queries@2.1.216_@tanstack+react-query@5.99.0_react@19.2.5__@toggl+sync@0.2.4_rea_8e2551e0eda340359e00ca954b6e5013/node_modules/@toggl/queries/dist/shared-data/working-hours/requests.mjs
10950
10951
  async function getMyWorkingHours(httpClient, organization_id, signal) {
10951
10952
  return (await httpClient.get(`shared_data/api/workinghours/${organization_id}/me`, { signal })).json();
10952
10953
  }
@@ -10970,6 +10971,14 @@ async function deleteMyWorkingHours(httpClient, organization_id, data, signal) {
10970
10971
  if ([204, 205].includes(response.status)) return null;
10971
10972
  return response.json();
10972
10973
  }
10974
+ //#endregion
10975
+ //#region ../cli-core/build/focus-client-B2l1BZIW.js
10976
+ /** Request header the API reads to attribute traffic to MCP or CLI. */
10977
+ const TOGGL_CLIENT_HEADER = "X-Toggl-Client";
10978
+ /** Headers that attribute a raw `fetch` to MCP or CLI (FocusClient sets these itself). */
10979
+ function togglClientHeaders(clientSurface) {
10980
+ return { [TOGGL_CLIENT_HEADER]: clientSurface };
10981
+ }
10973
10982
  const customFieldValuesEntry = /* @__PURE__ */ optional$1(/* @__PURE__ */ nullable$1(/* @__PURE__ */ pipe$2(/* @__PURE__ */ record$1(/* @__PURE__ */ string$1(), /* @__PURE__ */ nullable$1(/* @__PURE__ */ union([
10974
10983
  /* @__PURE__ */ array$1(/* @__PURE__ */ number$2()),
10975
10984
  /* @__PURE__ */ string$1(),
@@ -11147,6 +11156,7 @@ var FocusClient = class {
11147
11156
  timeOffMeHttp;
11148
11157
  sharedDataHttp;
11149
11158
  tokenProvider;
11159
+ clientSurface;
11150
11160
  userId;
11151
11161
  organizationIdNum;
11152
11162
  workspaceId;
@@ -11155,6 +11165,7 @@ var FocusClient = class {
11155
11165
  let baseUrl = config.baseUrl.replace(/\/+$/, "");
11156
11166
  if (!baseUrl.endsWith("/api")) baseUrl += "/api";
11157
11167
  this.tokenProvider = config.tokenProvider;
11168
+ this.clientSurface = config.clientSurface;
11158
11169
  this.userId = config.userId;
11159
11170
  this.workspaceId = config.workspaceId;
11160
11171
  const orgParsed = Number.parseInt(config.organizationId, 10);
@@ -11170,6 +11181,7 @@ var FocusClient = class {
11170
11181
  request.headers.set("Authorization", `Bearer ${token}`);
11171
11182
  if (!request.headers.get("Content-Type")?.startsWith("multipart/form-data")) request.headers.set("Content-Type", "application/json");
11172
11183
  request.headers.set("X-Toggl-PostHog-Data", JSON.stringify({ platform_origin: "web" }));
11184
+ request.headers.set(TOGGL_CLIENT_HEADER, this.clientSurface);
11173
11185
  }
11174
11186
  });
11175
11187
  const orgBase = accountsOrgApiBaseUrl(config.accountsApiUrl);
@@ -11181,6 +11193,7 @@ var FocusClient = class {
11181
11193
  request.headers.set("Authorization", `Bearer ${token}`);
11182
11194
  request.headers.set("Content-Type", "application/json");
11183
11195
  request.headers.set("X-Toggl-PostHog-Data", JSON.stringify({ platform_origin: "web" }));
11196
+ request.headers.set(TOGGL_CLIENT_HEADER, this.clientSurface);
11184
11197
  }
11185
11198
  });
11186
11199
  this.timeOffMeHttp = createHttpClient({
@@ -11192,6 +11205,7 @@ var FocusClient = class {
11192
11205
  request.headers.set("Content-Type", "application/json");
11193
11206
  request.headers.set("X-Toggl-Product", "focus");
11194
11207
  request.headers.set("X-Toggl-PostHog-Data", JSON.stringify({ platform_origin: "web" }));
11208
+ request.headers.set(TOGGL_CLIENT_HEADER, this.clientSurface);
11195
11209
  }
11196
11210
  });
11197
11211
  this.sharedDataHttp = createHttpClient({
@@ -11202,6 +11216,7 @@ var FocusClient = class {
11202
11216
  request.headers.set("Authorization", `Bearer ${token}`);
11203
11217
  request.headers.set("Content-Type", "application/json");
11204
11218
  request.headers.set("X-Toggl-PostHog-Data", JSON.stringify({ platform_origin: "web" }));
11219
+ request.headers.set(TOGGL_CLIENT_HEADER, this.clientSurface);
11205
11220
  }
11206
11221
  });
11207
11222
  }
@@ -12254,10 +12269,10 @@ function renderOAuthCallbackPage(kind, detail) {
12254
12269
  </div>`);
12255
12270
  }
12256
12271
  function generateOAuthPKCE() {
12257
- const verifier = crypto.randomBytes(64).toString("base64url");
12272
+ const verifier = crypto$1.randomBytes(64).toString("base64url");
12258
12273
  return {
12259
12274
  verifier,
12260
- challenge: crypto.createHash("sha256").update(verifier, "ascii").digest("base64url")
12275
+ challenge: crypto$1.createHash("sha256").update(verifier, "ascii").digest("base64url")
12261
12276
  };
12262
12277
  }
12263
12278
  function waitForOAuthCallback(port = TOGGL_OAUTH_CALLBACK_PORT, timeoutMs = TOGGL_OAUTH_LOGIN_TIMEOUT_MS) {
@@ -12300,7 +12315,7 @@ function waitForOAuthCallback(port = TOGGL_OAUTH_CALLBACK_PORT, timeoutMs = TOGG
12300
12315
  });
12301
12316
  });
12302
12317
  }
12303
- async function exchangeAuthorizationCode(accountsApiUrl, code, verifier, redirectUri = togglOAuthRedirectUri(), clientId = TOGGL_OAUTH_CLIENT_ID) {
12318
+ async function exchangeAuthorizationCode(accountsApiUrl, code, verifier, clientSurface, redirectUri = togglOAuthRedirectUri(), clientId = TOGGL_OAUTH_CLIENT_ID) {
12304
12319
  const params = new URLSearchParams({
12305
12320
  grant_type: "authorization_code",
12306
12321
  code,
@@ -12308,7 +12323,10 @@ async function exchangeAuthorizationCode(accountsApiUrl, code, verifier, redirec
12308
12323
  client_id: clientId,
12309
12324
  redirect_uri: redirectUri
12310
12325
  });
12311
- const response = await fetch(`${accountsApiUrl}/api/oauth/token?${params}`, { method: "POST" });
12326
+ const response = await fetch(`${accountsApiUrl}/api/oauth/token?${params}`, {
12327
+ method: "POST",
12328
+ headers: togglClientHeaders(clientSurface)
12329
+ });
12312
12330
  if (!response.ok) {
12313
12331
  const text = await response.text();
12314
12332
  throw new Error(`Token exchange failed (${response.status}): ${text}`);
@@ -12408,18 +12426,20 @@ var WorkspaceSelectionRequiredError = class extends Error {
12408
12426
  this.workspaces = workspaces;
12409
12427
  }
12410
12428
  };
12411
- async function fetchAccountsApiMe(accountsApiUrl, token) {
12429
+ async function fetchAccountsApiMe(accountsApiUrl, token, clientSurface) {
12412
12430
  const response = await fetch(`${accountsApiUrl}/api/me`, { headers: {
12413
12431
  Authorization: `Bearer ${token}`,
12414
- "Content-Type": "application/json"
12432
+ "Content-Type": "application/json",
12433
+ ...togglClientHeaders(clientSurface)
12415
12434
  } });
12416
12435
  if (!response.ok) throw new Error(`Failed to fetch user info (${response.status})`);
12417
12436
  return response.json();
12418
12437
  }
12419
- async function fetchAccessibleWorkspaces(accountsApiUrl, token) {
12438
+ async function fetchAccessibleWorkspaces(accountsApiUrl, token, clientSurface) {
12420
12439
  const response = await fetch(`${accountsApiUrl}/org/api/organizations/me`, { headers: {
12421
12440
  Authorization: `Bearer ${token}`,
12422
- "Content-Type": "application/json"
12441
+ "Content-Type": "application/json",
12442
+ ...togglClientHeaders(clientSurface)
12423
12443
  } });
12424
12444
  if (!response.ok) throw new Error(`Failed to fetch organizations (${response.status})`);
12425
12445
  return (await response.json()).flatMap((org) => (org.workspaces ?? []).filter((ws) => (ws.toggl_products ?? []).includes("focus")).map((ws) => {
@@ -12470,11 +12490,11 @@ async function runCliAuthFlow(options = {}) {
12470
12490
  log
12471
12491
  });
12472
12492
  log("Exchanging authorization code...");
12473
- const tokens = await exchangeAuthorizationCode(accountsApiUrl, code, verifier, redirectUri, clientId);
12493
+ const tokens = await exchangeAuthorizationCode(accountsApiUrl, code, verifier, "cli", redirectUri, clientId);
12474
12494
  log("Fetching account info...");
12475
- const user = await fetchAccountsApiMe(accountsApiUrl, tokens.access_token);
12495
+ const user = await fetchAccountsApiMe(accountsApiUrl, tokens.access_token, "cli");
12476
12496
  if (!user.user_account_id) throw new Error("Could not determine user ID from API response.");
12477
- const workspaces = await fetchAccessibleWorkspaces(accountsApiUrl, tokens.access_token);
12497
+ const workspaces = await fetchAccessibleWorkspaces(accountsApiUrl, tokens.access_token, "cli");
12478
12498
  const selectedWorkspace = await resolveAuthenticatedWorkspace(workspaces, {
12479
12499
  explicitWorkspaceId: options.workspaceId,
12480
12500
  selectWorkspace: options.selectWorkspace,
@@ -12520,13 +12540,16 @@ const PUBLIC_CLI_PACKAGE_SPEC = `${PUBLIC_CLI_PACKAGE_NAME}@latest`;
12520
12540
  function npxMcpAuthCommand() {
12521
12541
  return `npx ${PUBLIC_MCP_PACKAGE_NAME} auth`;
12522
12542
  }
12523
- async function refreshTokens(accountsApiUrl, refreshToken, clientId = TOGGL_OAUTH_CLIENT_ID) {
12543
+ async function refreshTokens(accountsApiUrl, refreshToken, clientSurface, clientId = TOGGL_OAUTH_CLIENT_ID) {
12524
12544
  const params = new URLSearchParams({
12525
12545
  grant_type: "refresh_token",
12526
12546
  refresh_token: refreshToken,
12527
12547
  client_id: clientId
12528
12548
  });
12529
- const response = await fetch(`${accountsApiUrl}/api/oauth/token?${params}`, { method: "POST" });
12549
+ const response = await fetch(`${accountsApiUrl}/api/oauth/token?${params}`, {
12550
+ method: "POST",
12551
+ headers: togglClientHeaders(clientSurface)
12552
+ });
12530
12553
  if (!response.ok) {
12531
12554
  const text = await response.text();
12532
12555
  throw new Error(`Token refresh failed (${response.status}): ${text}`);
@@ -12538,7 +12561,7 @@ async function getValidToken(scope = "cli", profile) {
12538
12561
  if (!authConfig) throw new Error(scope === "cli" ? "Not authenticated. Run `toggl auth` to sign in." : `Not authenticated. Run \`${npxMcpAuthCommand()}\` to sign in.`);
12539
12562
  const now = Date.now() / 1e3;
12540
12563
  if (authConfig.expires_at > now + 60) return authConfig.access_token;
12541
- const tokens = await refreshTokens(authConfig.accounts_api_url, authConfig.refresh_token, authConfig.oauth_client_id ?? "000000");
12564
+ const tokens = await refreshTokens(authConfig.accounts_api_url, authConfig.refresh_token, scope, authConfig.oauth_client_id ?? "000000");
12542
12565
  authConfig.access_token = tokens.access_token;
12543
12566
  authConfig.refresh_token = tokens.refresh_token;
12544
12567
  authConfig.expires_at = now + tokens.expires_in;
@@ -12578,7 +12601,7 @@ async function refreshCliWorkspaces(profile) {
12578
12601
  const token = await getValidToken("cli", profile);
12579
12602
  const dev = loadDeveloperToolsConfig();
12580
12603
  const auth = dev.profiles[profileKey];
12581
- auth.workspaces = await fetchAccessibleWorkspaces(auth.accounts_api_url, token);
12604
+ auth.workspaces = await fetchAccessibleWorkspaces(auth.accounts_api_url, token, "cli");
12582
12605
  dev.profiles[profileKey] = auth;
12583
12606
  saveDeveloperToolsConfig(dev);
12584
12607
  return listCachedCliWorkspaces(profile);
@@ -12595,6 +12618,204 @@ function switchCliWorkspace(workspaceId, profile) {
12595
12618
  saveDeveloperToolsConfig(dev);
12596
12619
  return target;
12597
12620
  }
12621
+ /**
12622
+ * Exception messages are the one channel `sendDefaultPii: false` does not cover,
12623
+ * and this repo's errors put user data in them: validation failures embed the
12624
+ * rejected input (valibot renders `issue.received`) and API failures embed the
12625
+ * response body. Sentry also captures uncaught exceptions and `cause` chains
12626
+ * through its own global handlers, which bypass any call-site scrubbing, so the
12627
+ * last word has to be `beforeSend`.
12628
+ *
12629
+ * Shared between the CLI and the public MCP build so the two cannot drift.
12630
+ */
12631
+ /**
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.
12636
+ */
12637
+ const SAFE_CATEGORY_PATTERN = /^(?:cli|mcp)\.[a-z][a-z0-9_]*$/;
12638
+ const REDACTED_SENTRY_MESSAGE = "redacted";
12639
+ function isSafeSentryMessage(value) {
12640
+ return typeof value === "string" && SAFE_CATEGORY_PATTERN.test(value);
12641
+ }
12642
+ /**
12643
+ * Replace every exception message that is not a known category, and any top-level
12644
+ * message. Stack frames are left alone: they carry file, function and line, never
12645
+ * payloads.
12646
+ */
12647
+ function redactUnsafeSentryMessages(event) {
12648
+ if (event.message !== void 0 && !isSafeSentryMessage(event.message)) event.message = REDACTED_SENTRY_MESSAGE;
12649
+ for (const value of event.exception?.values ?? []) if (!isSafeSentryMessage(value.value)) value.value = REDACTED_SENTRY_MESSAGE;
12650
+ return event;
12651
+ }
12652
+ const DEFAULT_POSTHOG_HOST = "https://eu.i.posthog.com";
12653
+ /**
12654
+ * The shared Toggl **production** PostHog project — the same token the Track
12655
+ * webapp (`posthogApiKey.production`), the marketing site (`GATSBY_POSTHOG_ID`)
12656
+ * and the Focus mobile app (`EXPO_PUBLIC_POSTHOG_KEY`) send to. MCP/CLI events
12657
+ * therefore land beside product events for the same people, which is why the
12658
+ * `distinct_id` has to be the accounts nanoid and why `$lib` has to separate the
12659
+ * developer tools out again.
12660
+ */
12661
+ const SHARED_TOGGL_PRODUCTION_POSTHOG_TOKEN = "phc_PiFIHfmVYQVICbsScOwEv7D9wBwoNvrUFZ1Fq9WxBrp";
12662
+ /**
12663
+ * PostHog splits usage and billing by `$lib`, so it has to name the sending
12664
+ * client rather than this module: sharing the Toggl production project with the
12665
+ * webapp, marketing site and mobile app means a single library value would leave
12666
+ * MCP and CLI volume indistinguishable in that breakdown.
12667
+ */
12668
+ const POSTHOG_LIB_BY_CLIENT = {
12669
+ mcp: "MCP",
12670
+ cli: "CLI"
12671
+ };
12672
+ const TOOL_INVOCATION_EVENT = "toggl_tool_invocation";
12673
+ /** Bound collector I/O so analytics cannot hang a completed CLI/MCP exit path. */
12674
+ const CAPTURE_TIMEOUT_MS = 2e3;
12675
+ const FLUSH_TIMEOUT_MS = 2e3;
12676
+ /** Actions are catalog identifiers, so anything else is a caller bug, not data. */
12677
+ const ACTION_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
12678
+ let enabled = false;
12679
+ let apiKey = null;
12680
+ let host = DEFAULT_POSTHOG_HOST;
12681
+ let sessionId = "";
12682
+ let distinctId = "";
12683
+ let client = "mcp";
12684
+ let packageVersion = "0.0.0";
12685
+ const pending = /* @__PURE__ */ new Set();
12686
+ function envFlagDisabled(raw) {
12687
+ if (raw === void 0 || raw === "") return false;
12688
+ const normalized = raw.trim().toLowerCase();
12689
+ return normalized === "0" || normalized === "false" || normalized === "off" || normalized === "no";
12690
+ }
12691
+ /**
12692
+ * Initialize product analytics for a public MCP/CLI process.
12693
+ * No-ops when TOGGL_PRODUCT_ANALYTICS is 0/false/off, or when the API key is "off".
12694
+ */
12695
+ function initProductAnalytics(context) {
12696
+ if (envFlagDisabled(process.env.TOGGL_PRODUCT_ANALYTICS)) {
12697
+ enabled = false;
12698
+ return false;
12699
+ }
12700
+ const key = (process.env.TOGGL_POSTHOG_API_KEY ?? SHARED_TOGGL_PRODUCTION_POSTHOG_TOKEN).trim();
12701
+ if (!key || key.toLowerCase() === "off") {
12702
+ enabled = false;
12703
+ return false;
12704
+ }
12705
+ apiKey = key;
12706
+ host = (process.env.TOGGL_POSTHOG_HOST ?? DEFAULT_POSTHOG_HOST).replace(/\/$/, "");
12707
+ sessionId = context.sessionId ?? crypto.randomUUID();
12708
+ distinctId = context.distinctId ?? `anon:${sessionId}`;
12709
+ client = context.client;
12710
+ packageVersion = context.packageVersion;
12711
+ enabled = true;
12712
+ return true;
12713
+ }
12714
+ function normalizeAction(action) {
12715
+ if (typeof action !== "string") return null;
12716
+ const trimmed = action.trim();
12717
+ return ACTION_PATTERN.test(trimmed) ? trimmed : null;
12718
+ }
12719
+ /** Strict allowlist — never accept args/results. */
12720
+ function captureToolInvocation(event) {
12721
+ if (!enabled || !apiKey) return;
12722
+ const task = sendCapture(TOOL_INVOCATION_EVENT, {
12723
+ tool_name: event.toolName,
12724
+ action: normalizeAction(event.action),
12725
+ outcome: event.outcome,
12726
+ success: event.outcome === "success",
12727
+ client,
12728
+ session_id: sessionId,
12729
+ package_version: packageVersion
12730
+ }, event.distinctId).finally(() => {
12731
+ pending.delete(task);
12732
+ });
12733
+ pending.add(task);
12734
+ }
12735
+ async function sendCapture(event, properties, eventDistinctId) {
12736
+ if (!apiKey) return;
12737
+ const controller = new AbortController();
12738
+ const timer = setTimeout(() => controller.abort(), CAPTURE_TIMEOUT_MS);
12739
+ try {
12740
+ const response = await fetch(`${host}/i/v0/e/`, {
12741
+ method: "POST",
12742
+ headers: { "Content-Type": "application/json" },
12743
+ body: JSON.stringify({
12744
+ api_key: apiKey,
12745
+ event,
12746
+ distinct_id: eventDistinctId ?? distinctId,
12747
+ properties: {
12748
+ ...properties,
12749
+ $lib: POSTHOG_LIB_BY_CLIENT[client]
12750
+ }
12751
+ }),
12752
+ signal: controller.signal
12753
+ });
12754
+ if (!response.ok) await response.text().catch(() => void 0);
12755
+ } catch {} finally {
12756
+ clearTimeout(timer);
12757
+ }
12758
+ }
12759
+ async function shutdownProductAnalytics() {
12760
+ if (pending.size === 0) {
12761
+ enabled = false;
12762
+ return;
12763
+ }
12764
+ const inflight = [...pending];
12765
+ let flushTimer;
12766
+ try {
12767
+ await Promise.race([Promise.allSettled(inflight), new Promise((resolve) => {
12768
+ flushTimer = setTimeout(resolve, FLUSH_TIMEOUT_MS);
12769
+ })]);
12770
+ } finally {
12771
+ if (flushTimer !== void 0) clearTimeout(flushTimer);
12772
+ pending.clear();
12773
+ enabled = false;
12774
+ }
12775
+ }
12776
+ /**
12777
+ * Accounts mints the base62 nanoid as the JWT `sub`; it is the same value
12778
+ * accounts-be sends to PostHog as `distinct_id`, so it is short and URL-safe.
12779
+ */
12780
+ const NANOID_PATTERN = /^[0-9A-Za-z_-]{6,64}$/;
12781
+ /**
12782
+ * Read `sub` out of a Toggl accounts access token without verifying it.
12783
+ *
12784
+ * Verification would need the accounts JWKS; the value is only used as an
12785
+ * analytics key, and a forged token would only mislabel the forger's own events.
12786
+ */
12787
+ function decodeAccessTokenSubject(token) {
12788
+ if (!token) return null;
12789
+ const payloadSegment = token.split(".")[1];
12790
+ if (!payloadSegment) return null;
12791
+ try {
12792
+ const sub = JSON.parse(Buffer.from(payloadSegment, "base64url").toString("utf8")).sub;
12793
+ if (typeof sub !== "string" || !NANOID_PATTERN.test(sub)) return null;
12794
+ return sub;
12795
+ } catch {
12796
+ return null;
12797
+ }
12798
+ }
12799
+ /**
12800
+ * Stable PostHog identity for a signed-in MCP/CLI process.
12801
+ *
12802
+ * The nanoid comes from the persisted profile, so the same person keeps one
12803
+ * `distinct_id` across process restarts and across MCP and CLI — a per-process
12804
+ * UUID would make every invocation look like a new user and every funnel
12805
+ * one-step-deep.
12806
+ *
12807
+ * The profile's numeric `user_id` is `user_account_id`, which is deliberately
12808
+ * *not* used here: accounts-be, toggl_api and the Track webapp all identify a
12809
+ * person in PostHog by the nanoid, so a bigint key would orphan these events
12810
+ * from every other identity we already have for the same user.
12811
+ */
12812
+ function resolveProductAnalyticsDistinctId(scope) {
12813
+ try {
12814
+ return decodeAccessTokenSubject(loadProfile(scope)?.access_token);
12815
+ } catch {
12816
+ return null;
12817
+ }
12818
+ }
12598
12819
  //#endregion
12599
12820
  //#region src/cli-package-manifest.ts
12600
12821
  const pkgJsonPath = path$1.join(path$1.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
@@ -24752,6 +24973,83 @@ async function runValidatedOperation(operation, options, deps) {
24752
24973
  deps.output(response.data);
24753
24974
  }
24754
24975
  //#endregion
24976
+ //#region src/telemetry.ts
24977
+ function envEnabled(raw) {
24978
+ if (raw === void 0) return false;
24979
+ const normalized = raw.trim().toLowerCase();
24980
+ return normalized === "1" || normalized === "true" || normalized === "on" || normalized === "yes";
24981
+ }
24982
+ let sentryEnabled = false;
24983
+ let productAnalyticsEnabled = false;
24984
+ function initCliTelemetry(packageVersion) {
24985
+ productAnalyticsEnabled = initProductAnalytics({
24986
+ client: "cli",
24987
+ packageVersion,
24988
+ distinctId: resolveProductAnalyticsDistinctId("cli")
24989
+ });
24990
+ if (!envEnabled(process.env.TOGGL_SENTRY)) {
24991
+ sentryEnabled = false;
24992
+ return;
24993
+ }
24994
+ const dsn = process.env.SENTRY_DSN;
24995
+ if (!dsn || dsn === "off") {
24996
+ console.error("[Telemetry] TOGGL_SENTRY is set but SENTRY_DSN is missing; Sentry left disabled");
24997
+ sentryEnabled = false;
24998
+ return;
24999
+ }
25000
+ Sentry.init({
25001
+ dsn,
25002
+ environment: process.env.NODE_ENV ?? "production",
25003
+ tracesSampleRate: .1,
25004
+ sendDefaultPii: false,
25005
+ enableLogs: false,
25006
+ beforeSend: (event) => redactUnsafeSentryMessages(event)
25007
+ });
25008
+ Sentry.setTag("cli.session.id", crypto.randomUUID());
25009
+ sentryEnabled = true;
25010
+ }
25011
+ /**
25012
+ * `action` mirrors the MCP entity-tool action so one PostHog query can compare
25013
+ * `toggl reports list` with the `reports` tool's `list` action.
25014
+ */
25015
+ function recordCliCommand(commandName, action, success) {
25016
+ if (!productAnalyticsEnabled) return;
25017
+ captureToolInvocation({
25018
+ toolName: commandName,
25019
+ action,
25020
+ outcome: success ? "success" : "error"
25021
+ });
25022
+ }
25023
+ 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";
25028
+ }
25029
+ function toSentrySafeCliError(error) {
25030
+ const category = classifyCliSentryError(error);
25031
+ const safe = new Error(category);
25032
+ safe.name = error instanceof Error ? error.name : "Error";
25033
+ if (error instanceof Error && typeof error.stack === "string") safe.stack = error.message ? error.stack.replaceAll(error.message, category) : error.stack;
25034
+ return safe;
25035
+ }
25036
+ function captureCliException(error) {
25037
+ if (!sentryEnabled) return;
25038
+ const category = classifyCliSentryError(error);
25039
+ Sentry.withScope((scope) => {
25040
+ scope.setTag("cli.error_category", category);
25041
+ Sentry.captureException(toSentrySafeCliError(error));
25042
+ });
25043
+ }
25044
+ async function shutdownCliTelemetry() {
25045
+ const tasks = [];
25046
+ if (productAnalyticsEnabled) tasks.push(shutdownProductAnalytics());
25047
+ if (sentryEnabled) tasks.push(Sentry.close(2e3));
25048
+ if (tasks.length > 0) await Promise.allSettled(tasks);
25049
+ sentryEnabled = false;
25050
+ productAnalyticsEnabled = false;
25051
+ }
25052
+ //#endregion
24755
25053
  //#region src/output.ts
24756
25054
  function isRecord(value) {
24757
25055
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -25062,7 +25360,8 @@ function getClient(options) {
25062
25360
  tokenProvider: () => getValidToken("cli", options.profile),
25063
25361
  userId: config.user_id,
25064
25362
  organizationId: String(config.organization_id),
25065
- workspaceId: options.workspaceId ?? String(config.workspace_id)
25363
+ workspaceId: options.workspaceId ?? String(config.workspace_id),
25364
+ clientSurface: "cli"
25066
25365
  });
25067
25366
  }
25068
25367
  async function runOperation(operation, globalOptions, options, rawInput) {
@@ -25099,6 +25398,19 @@ function groupCommand(program, group) {
25099
25398
  for (const part of group) command = command.commands.find((candidate) => candidate.name() === part) ?? command.command(part).description(`${part} operations`);
25100
25399
  return command;
25101
25400
  }
25401
+ function cliCommandPath(command) {
25402
+ const parts = [];
25403
+ let current = command;
25404
+ while (current?.parent) {
25405
+ parts.unshift(current.name());
25406
+ current = current.parent;
25407
+ }
25408
+ return parts.filter(Boolean).join(" ") || command.name();
25409
+ }
25410
+ function recordActionAnalytics(actionCommand) {
25411
+ const failed = typeof process.exitCode === "number" && process.exitCode !== 0;
25412
+ recordCliCommand(cliCommandPath(actionCommand), actionCommand.name() || null, !failed);
25413
+ }
25102
25414
  function createProgram(programOptions = {}) {
25103
25415
  const promptAdapter = programOptions.promptAdapter ?? inquirerPromptAdapter;
25104
25416
  const isTty = programOptions.isTty ?? (() => Boolean(process.stdin.isTTY && process.stdout.isTTY));
@@ -25150,6 +25462,7 @@ function createProgram(programOptions = {}) {
25150
25462
  const globalOptions = program.opts();
25151
25463
  output(opts.refresh ? await refreshCliWorkspaces(globalOptions.profile) : listCachedCliWorkspaces(globalOptions.profile), globalOptions);
25152
25464
  } catch (error) {
25465
+ captureCliException(error);
25153
25466
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
25154
25467
  process.exitCode = 1;
25155
25468
  }
@@ -25169,6 +25482,7 @@ function createProgram(programOptions = {}) {
25169
25482
  organization_name: target.organization_name
25170
25483
  }, globalOptions);
25171
25484
  } catch (error) {
25485
+ captureCliException(error);
25172
25486
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
25173
25487
  process.exitCode = 1;
25174
25488
  }
@@ -25178,6 +25492,7 @@ function createProgram(programOptions = {}) {
25178
25492
  try {
25179
25493
  output(listCliProfiles(), program.opts());
25180
25494
  } catch (error) {
25495
+ captureCliException(error);
25181
25496
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
25182
25497
  process.exitCode = 1;
25183
25498
  }
@@ -25186,6 +25501,7 @@ function createProgram(programOptions = {}) {
25186
25501
  try {
25187
25502
  output(getCurrentCliProfile(), program.opts());
25188
25503
  } catch (error) {
25504
+ captureCliException(error);
25189
25505
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
25190
25506
  process.exitCode = 1;
25191
25507
  }
@@ -25199,6 +25515,7 @@ function createProgram(programOptions = {}) {
25199
25515
  exampleWhenNotInteractive: "Example: toggl profile switch production-12345"
25200
25516
  })), globalOptions);
25201
25517
  } catch (error) {
25518
+ captureCliException(error);
25202
25519
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
25203
25520
  process.exitCode = 1;
25204
25521
  }
@@ -25212,6 +25529,7 @@ function createProgram(programOptions = {}) {
25212
25529
  exampleWhenNotInteractive: "Example: toggl profile remove production-12345"
25213
25530
  })), globalOptions);
25214
25531
  } catch (error) {
25532
+ captureCliException(error);
25215
25533
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
25216
25534
  process.exitCode = 1;
25217
25535
  }
@@ -25224,6 +25542,7 @@ function createProgram(programOptions = {}) {
25224
25542
  json: globalOptions.json
25225
25543
  });
25226
25544
  } catch (error) {
25545
+ captureCliException(error);
25227
25546
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
25228
25547
  process.exitCode = 1;
25229
25548
  }
@@ -25236,6 +25555,7 @@ function createProgram(programOptions = {}) {
25236
25555
  try {
25237
25556
  await runSkillSetup(opts);
25238
25557
  } catch (error) {
25558
+ captureCliException(error);
25239
25559
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
25240
25560
  process.exitCode = 1;
25241
25561
  }
@@ -25275,6 +25595,7 @@ Common commands:
25275
25595
  const entityContext = willPrompt && !cmdOptions.dryRun ? { client: getClient(globalOptions) } : void 0;
25276
25596
  await runOperation(operation, globalOptions, cmdOptions, willPrompt ? await promptForOperationInput(operation, rawInput, promptAdapter, entityContext) : rawInput);
25277
25597
  } catch (error) {
25598
+ captureCliException(error);
25278
25599
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
25279
25600
  process.exitCode = 1;
25280
25601
  }
@@ -25286,6 +25607,9 @@ Common commands:
25286
25607
  command.addHelpText("after", `\nExamples:\n${exampleLines}`);
25287
25608
  }
25288
25609
  }
25610
+ program.hook("postAction", (_thisCommand, actionCommand) => {
25611
+ recordActionAnalytics(actionCommand);
25612
+ });
25289
25613
  if (programOptions.enableUpdateNotification) program.hook("postAction", (_thisCommand, actionCommand) => {
25290
25614
  maybeNotifyUpdateAvailable({
25291
25615
  exitCode: typeof process.exitCode === "number" ? process.exitCode : 0,
@@ -25298,6 +25622,6 @@ Common commands:
25298
25622
  return program;
25299
25623
  }
25300
25624
  //#endregion
25301
- export { CommanderError as n, createProgram as t };
25625
+ export { CommanderError as a, shutdownCliTelemetry as i, captureCliException as n, initCliTelemetry as r, createProgram as t };
25302
25626
 
25303
- //# sourceMappingURL=program-BJrl7qqO.js.map
25627
+ //# sourceMappingURL=program-B6EuV2vO.js.map
package/build/program.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as createProgram } from "./program-BJrl7qqO.js";
1
+ import { t as createProgram } from "./program-B6EuV2vO.js";
2
2
  export { createProgram };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@togglhq/cli",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "Toggl CLI for Toggl 2.0 API operations.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -50,12 +50,13 @@
50
50
  "node": ">=20"
51
51
  },
52
52
  "dependencies": {
53
+ "@sentry/node": "10.47.0",
53
54
  "skills": "1.5.2"
54
55
  },
55
56
  "devDependencies": {
56
57
  "@toggl/cli-core": "workspace:^",
57
58
  "@toggl/operations": "workspace:^",
58
- "@toggl/queries": "2.1.212",
59
+ "@toggl/queries": "2.1.216",
59
60
  "@types/node": "22.19.11",
60
61
  "tsdown": "0.21.8",
61
62
  "typescript": "5.9.3",