@togglhq/cli 1.7.2 → 1.8.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.
- package/README.md +12 -0
- package/build/cli.js +9 -1
- package/build/index.js +1 -1
- package/build/{program-COtKnkok.js → program-CH5epPVt.js} +392 -19
- package/build/program.js +1 -1
- package/package.json +2 -1
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 {
|
|
2
|
+
import { a as CommanderError, i as shutdownCliTelemetry, n as captureCliException, r as initCliTelemetry, t as createProgram } from "./program-CH5epPVt.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-
|
|
1
|
+
import { t as createProgram } from "./program-CH5epPVt.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;
|
|
@@ -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}`, {
|
|
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}`, {
|
|
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,250 @@ function switchCliWorkspace(workspaceId, profile) {
|
|
|
12595
12618
|
saveDeveloperToolsConfig(dev);
|
|
12596
12619
|
return target;
|
|
12597
12620
|
}
|
|
12621
|
+
const CLI_GENERIC_ERROR_CATEGORY = "cli.command_error";
|
|
12622
|
+
const MCP_GENERIC_ERROR_CATEGORY = "mcp.tool_error";
|
|
12623
|
+
const ERROR_CATEGORY_SET = new Set([
|
|
12624
|
+
"cli.validation_error",
|
|
12625
|
+
"cli.api_error",
|
|
12626
|
+
CLI_GENERIC_ERROR_CATEGORY,
|
|
12627
|
+
"mcp.validation_error",
|
|
12628
|
+
"mcp.api_error",
|
|
12629
|
+
"mcp.auth_error",
|
|
12630
|
+
MCP_GENERIC_ERROR_CATEGORY
|
|
12631
|
+
]);
|
|
12632
|
+
function isDeclaredErrorCategory(value) {
|
|
12633
|
+
return typeof value === "string" && ERROR_CATEGORY_SET.has(value);
|
|
12634
|
+
}
|
|
12635
|
+
function firstToolResultText(source) {
|
|
12636
|
+
if (!source || typeof source !== "object") return null;
|
|
12637
|
+
const content = source.content;
|
|
12638
|
+
if (!Array.isArray(content)) return null;
|
|
12639
|
+
const first = content[0];
|
|
12640
|
+
if (first?.type === "text" && typeof first.text === "string") return first.text;
|
|
12641
|
+
return null;
|
|
12642
|
+
}
|
|
12643
|
+
function errorTextForClassification(source) {
|
|
12644
|
+
if (source instanceof Error) return source.message;
|
|
12645
|
+
if (typeof source === "string") return source;
|
|
12646
|
+
const toolText = firstToolResultText(source);
|
|
12647
|
+
if (toolText !== null) return toolText;
|
|
12648
|
+
return "";
|
|
12649
|
+
}
|
|
12650
|
+
function classifyCli(message) {
|
|
12651
|
+
if (message.startsWith("Invalid input:") || message.startsWith("Invalid input ")) return "cli.validation_error";
|
|
12652
|
+
if (/^\d{3}:\s/.test(message)) return "cli.api_error";
|
|
12653
|
+
return CLI_GENERIC_ERROR_CATEGORY;
|
|
12654
|
+
}
|
|
12655
|
+
function classifyMcp(message) {
|
|
12656
|
+
if (message.startsWith("Invalid input:") || message.startsWith("Invalid input ")) return "mcp.validation_error";
|
|
12657
|
+
if (message.startsWith("API Error (")) return "mcp.api_error";
|
|
12658
|
+
if (message.startsWith("Authentication failed:") || message.startsWith("Not authenticated.")) return "mcp.auth_error";
|
|
12659
|
+
return MCP_GENERIC_ERROR_CATEGORY;
|
|
12660
|
+
}
|
|
12661
|
+
function classifyErrorCategory(client, source) {
|
|
12662
|
+
const message = errorTextForClassification(source);
|
|
12663
|
+
if (isDeclaredErrorCategory(message) && message.startsWith(`${client}.`)) return message;
|
|
12664
|
+
return client === "cli" ? classifyCli(message) : classifyMcp(message);
|
|
12665
|
+
}
|
|
12666
|
+
/**
|
|
12667
|
+
* Exception messages are the one channel `sendDefaultPii: false` does not cover,
|
|
12668
|
+
* and this repo's errors put user data in them: validation failures embed the
|
|
12669
|
+
* rejected input (valibot renders `issue.received`) and API failures embed the
|
|
12670
|
+
* response body. Sentry also captures uncaught exceptions and `cause` chains
|
|
12671
|
+
* through its own global handlers, which bypass any call-site scrubbing, so the
|
|
12672
|
+
* last word has to be `beforeSend`.
|
|
12673
|
+
*
|
|
12674
|
+
* Shared between the CLI and the public MCP build so the two cannot drift.
|
|
12675
|
+
*/
|
|
12676
|
+
/**
|
|
12677
|
+
* Fixed categories are the seven declared `<surface>.<snake_case>` values, not
|
|
12678
|
+
* a `cli|mcp` prefix regex. A prefix or regex test would pass a caught message
|
|
12679
|
+
* like `cli.customer_name`. A real filename message such as
|
|
12680
|
+
* `cli.ts:12: unexpected token "secret"` would also begin with `cli.`.
|
|
12681
|
+
*/
|
|
12682
|
+
const REDACTED_SENTRY_MESSAGE = "redacted";
|
|
12683
|
+
function isSafeSentryMessage(value) {
|
|
12684
|
+
return isDeclaredErrorCategory(value);
|
|
12685
|
+
}
|
|
12686
|
+
/**
|
|
12687
|
+
* Replace every exception message that is not a known category, and any top-level
|
|
12688
|
+
* message. Stack frames are left alone: they carry file, function and line, never
|
|
12689
|
+
* payloads.
|
|
12690
|
+
*/
|
|
12691
|
+
function redactUnsafeSentryMessages(event) {
|
|
12692
|
+
if (event.message !== void 0 && !isSafeSentryMessage(event.message)) event.message = REDACTED_SENTRY_MESSAGE;
|
|
12693
|
+
for (const value of event.exception?.values ?? []) if (!isSafeSentryMessage(value.value)) value.value = REDACTED_SENTRY_MESSAGE;
|
|
12694
|
+
return event;
|
|
12695
|
+
}
|
|
12696
|
+
const DEFAULT_POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
12697
|
+
/**
|
|
12698
|
+
* The shared Toggl **production** PostHog project — the same token the Track
|
|
12699
|
+
* webapp (`posthogApiKey.production`), the marketing site (`GATSBY_POSTHOG_ID`)
|
|
12700
|
+
* and the Focus mobile app (`EXPO_PUBLIC_POSTHOG_KEY`) send to. MCP/CLI events
|
|
12701
|
+
* therefore land beside product events for the same people, which is why the
|
|
12702
|
+
* `distinct_id` has to be the accounts nanoid and why `$lib` has to separate the
|
|
12703
|
+
* developer tools out again.
|
|
12704
|
+
*/
|
|
12705
|
+
const SHARED_TOGGL_PRODUCTION_POSTHOG_TOKEN = "phc_PiFIHfmVYQVICbsScOwEv7D9wBwoNvrUFZ1Fq9WxBrp";
|
|
12706
|
+
/**
|
|
12707
|
+
* PostHog splits usage and billing by `$lib`, so it has to name the sending
|
|
12708
|
+
* client rather than this module: sharing the Toggl production project with the
|
|
12709
|
+
* webapp, marketing site and mobile app means a single library value would leave
|
|
12710
|
+
* MCP and CLI volume indistinguishable in that breakdown.
|
|
12711
|
+
*/
|
|
12712
|
+
const POSTHOG_LIB_BY_CLIENT = {
|
|
12713
|
+
mcp: "MCP",
|
|
12714
|
+
cli: "CLI"
|
|
12715
|
+
};
|
|
12716
|
+
const TOOL_INVOCATION_EVENT = "toggl_tool_invocation";
|
|
12717
|
+
/** Bound collector I/O so analytics cannot hang a completed CLI/MCP exit path. */
|
|
12718
|
+
const CAPTURE_TIMEOUT_MS = 2e3;
|
|
12719
|
+
const FLUSH_TIMEOUT_MS = 2e3;
|
|
12720
|
+
/** Actions are catalog identifiers, so anything else is a caller bug, not data. */
|
|
12721
|
+
const ACTION_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
12722
|
+
let enabled = false;
|
|
12723
|
+
let apiKey = null;
|
|
12724
|
+
let host = DEFAULT_POSTHOG_HOST;
|
|
12725
|
+
let sessionId = "";
|
|
12726
|
+
let distinctId = "";
|
|
12727
|
+
let client = "mcp";
|
|
12728
|
+
let packageVersion = "0.0.0";
|
|
12729
|
+
const pending = /* @__PURE__ */ new Set();
|
|
12730
|
+
function envFlagDisabled(raw) {
|
|
12731
|
+
if (raw === void 0 || raw === "") return false;
|
|
12732
|
+
const normalized = raw.trim().toLowerCase();
|
|
12733
|
+
return normalized === "0" || normalized === "false" || normalized === "off" || normalized === "no";
|
|
12734
|
+
}
|
|
12735
|
+
/**
|
|
12736
|
+
* Initialize product analytics for a public MCP/CLI process.
|
|
12737
|
+
* No-ops when TOGGL_PRODUCT_ANALYTICS is 0/false/off, or when the API key is "off".
|
|
12738
|
+
*/
|
|
12739
|
+
function initProductAnalytics(context) {
|
|
12740
|
+
if (envFlagDisabled(process.env.TOGGL_PRODUCT_ANALYTICS)) {
|
|
12741
|
+
enabled = false;
|
|
12742
|
+
return false;
|
|
12743
|
+
}
|
|
12744
|
+
const key = (process.env.TOGGL_POSTHOG_API_KEY ?? SHARED_TOGGL_PRODUCTION_POSTHOG_TOKEN).trim();
|
|
12745
|
+
if (!key || key.toLowerCase() === "off") {
|
|
12746
|
+
enabled = false;
|
|
12747
|
+
return false;
|
|
12748
|
+
}
|
|
12749
|
+
apiKey = key;
|
|
12750
|
+
host = (process.env.TOGGL_POSTHOG_HOST ?? DEFAULT_POSTHOG_HOST).replace(/\/$/, "");
|
|
12751
|
+
sessionId = context.sessionId ?? crypto.randomUUID();
|
|
12752
|
+
distinctId = context.distinctId ?? `anon:${sessionId}`;
|
|
12753
|
+
client = context.client;
|
|
12754
|
+
packageVersion = context.packageVersion;
|
|
12755
|
+
enabled = true;
|
|
12756
|
+
return true;
|
|
12757
|
+
}
|
|
12758
|
+
function normalizeAction(action) {
|
|
12759
|
+
if (typeof action !== "string") return null;
|
|
12760
|
+
const trimmed = action.trim();
|
|
12761
|
+
return ACTION_PATTERN.test(trimmed) ? trimmed : null;
|
|
12762
|
+
}
|
|
12763
|
+
/** Strict allowlist — never accept args/results. */
|
|
12764
|
+
function captureToolInvocation(event) {
|
|
12765
|
+
if (!enabled || !apiKey) return;
|
|
12766
|
+
const properties = {
|
|
12767
|
+
tool_name: event.toolName,
|
|
12768
|
+
action: normalizeAction(event.action),
|
|
12769
|
+
outcome: event.outcome,
|
|
12770
|
+
success: event.outcome === "success",
|
|
12771
|
+
client,
|
|
12772
|
+
session_id: sessionId,
|
|
12773
|
+
package_version: packageVersion
|
|
12774
|
+
};
|
|
12775
|
+
if (event.outcome === "error") properties.error_category = classifyErrorCategory(client, event.error);
|
|
12776
|
+
const task = sendCapture(TOOL_INVOCATION_EVENT, properties, event.distinctId).finally(() => {
|
|
12777
|
+
pending.delete(task);
|
|
12778
|
+
});
|
|
12779
|
+
pending.add(task);
|
|
12780
|
+
}
|
|
12781
|
+
async function sendCapture(event, properties, eventDistinctId) {
|
|
12782
|
+
if (!apiKey) return;
|
|
12783
|
+
const controller = new AbortController();
|
|
12784
|
+
const timer = setTimeout(() => controller.abort(), CAPTURE_TIMEOUT_MS);
|
|
12785
|
+
try {
|
|
12786
|
+
const response = await fetch(`${host}/i/v0/e/`, {
|
|
12787
|
+
method: "POST",
|
|
12788
|
+
headers: { "Content-Type": "application/json" },
|
|
12789
|
+
body: JSON.stringify({
|
|
12790
|
+
api_key: apiKey,
|
|
12791
|
+
event,
|
|
12792
|
+
distinct_id: eventDistinctId ?? distinctId,
|
|
12793
|
+
properties: {
|
|
12794
|
+
...properties,
|
|
12795
|
+
$lib: POSTHOG_LIB_BY_CLIENT[client]
|
|
12796
|
+
}
|
|
12797
|
+
}),
|
|
12798
|
+
signal: controller.signal
|
|
12799
|
+
});
|
|
12800
|
+
if (!response.ok) await response.text().catch(() => void 0);
|
|
12801
|
+
} catch {} finally {
|
|
12802
|
+
clearTimeout(timer);
|
|
12803
|
+
}
|
|
12804
|
+
}
|
|
12805
|
+
async function shutdownProductAnalytics() {
|
|
12806
|
+
if (pending.size === 0) {
|
|
12807
|
+
enabled = false;
|
|
12808
|
+
return;
|
|
12809
|
+
}
|
|
12810
|
+
const inflight = [...pending];
|
|
12811
|
+
let flushTimer;
|
|
12812
|
+
try {
|
|
12813
|
+
await Promise.race([Promise.allSettled(inflight), new Promise((resolve) => {
|
|
12814
|
+
flushTimer = setTimeout(resolve, FLUSH_TIMEOUT_MS);
|
|
12815
|
+
})]);
|
|
12816
|
+
} finally {
|
|
12817
|
+
if (flushTimer !== void 0) clearTimeout(flushTimer);
|
|
12818
|
+
pending.clear();
|
|
12819
|
+
enabled = false;
|
|
12820
|
+
}
|
|
12821
|
+
}
|
|
12822
|
+
/**
|
|
12823
|
+
* Accounts mints the base62 nanoid as the JWT `sub`; it is the same value
|
|
12824
|
+
* accounts-be sends to PostHog as `distinct_id`, so it is short and URL-safe.
|
|
12825
|
+
*/
|
|
12826
|
+
const NANOID_PATTERN = /^[0-9A-Za-z_-]{6,64}$/;
|
|
12827
|
+
/**
|
|
12828
|
+
* Read `sub` out of a Toggl accounts access token without verifying it.
|
|
12829
|
+
*
|
|
12830
|
+
* Verification would need the accounts JWKS; the value is only used as an
|
|
12831
|
+
* analytics key, and a forged token would only mislabel the forger's own events.
|
|
12832
|
+
*/
|
|
12833
|
+
function decodeAccessTokenSubject(token) {
|
|
12834
|
+
if (!token) return null;
|
|
12835
|
+
const payloadSegment = token.split(".")[1];
|
|
12836
|
+
if (!payloadSegment) return null;
|
|
12837
|
+
try {
|
|
12838
|
+
const sub = JSON.parse(Buffer.from(payloadSegment, "base64url").toString("utf8")).sub;
|
|
12839
|
+
if (typeof sub !== "string" || !NANOID_PATTERN.test(sub)) return null;
|
|
12840
|
+
return sub;
|
|
12841
|
+
} catch {
|
|
12842
|
+
return null;
|
|
12843
|
+
}
|
|
12844
|
+
}
|
|
12845
|
+
/**
|
|
12846
|
+
* Stable PostHog identity for a signed-in MCP/CLI process.
|
|
12847
|
+
*
|
|
12848
|
+
* The nanoid comes from the persisted profile, so the same person keeps one
|
|
12849
|
+
* `distinct_id` across process restarts and across MCP and CLI — a per-process
|
|
12850
|
+
* UUID would make every invocation look like a new user and every funnel
|
|
12851
|
+
* one-step-deep.
|
|
12852
|
+
*
|
|
12853
|
+
* The profile's numeric `user_id` is `user_account_id`, which is deliberately
|
|
12854
|
+
* *not* used here: accounts-be, toggl_api and the Track webapp all identify a
|
|
12855
|
+
* person in PostHog by the nanoid, so a bigint key would orphan these events
|
|
12856
|
+
* from every other identity we already have for the same user.
|
|
12857
|
+
*/
|
|
12858
|
+
function resolveProductAnalyticsDistinctId(scope) {
|
|
12859
|
+
try {
|
|
12860
|
+
return decodeAccessTokenSubject(loadProfile(scope)?.access_token);
|
|
12861
|
+
} catch {
|
|
12862
|
+
return null;
|
|
12863
|
+
}
|
|
12864
|
+
}
|
|
12598
12865
|
//#endregion
|
|
12599
12866
|
//#region src/cli-package-manifest.ts
|
|
12600
12867
|
const pkgJsonPath = path$1.join(path$1.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
@@ -24752,6 +25019,86 @@ async function runValidatedOperation(operation, options, deps) {
|
|
|
24752
25019
|
deps.output(response.data);
|
|
24753
25020
|
}
|
|
24754
25021
|
//#endregion
|
|
25022
|
+
//#region src/telemetry.ts
|
|
25023
|
+
function envEnabled(raw) {
|
|
25024
|
+
if (raw === void 0) return false;
|
|
25025
|
+
const normalized = raw.trim().toLowerCase();
|
|
25026
|
+
return normalized === "1" || normalized === "true" || normalized === "on" || normalized === "yes";
|
|
25027
|
+
}
|
|
25028
|
+
let sentryEnabled = false;
|
|
25029
|
+
let productAnalyticsEnabled = false;
|
|
25030
|
+
let lastCommandError;
|
|
25031
|
+
function initCliTelemetry(packageVersion) {
|
|
25032
|
+
productAnalyticsEnabled = initProductAnalytics({
|
|
25033
|
+
client: "cli",
|
|
25034
|
+
packageVersion,
|
|
25035
|
+
distinctId: resolveProductAnalyticsDistinctId("cli")
|
|
25036
|
+
});
|
|
25037
|
+
if (!envEnabled(process.env.TOGGL_SENTRY)) {
|
|
25038
|
+
sentryEnabled = false;
|
|
25039
|
+
return;
|
|
25040
|
+
}
|
|
25041
|
+
const dsn = process.env.SENTRY_DSN;
|
|
25042
|
+
if (!dsn || dsn === "off") {
|
|
25043
|
+
console.error("[Telemetry] TOGGL_SENTRY is set but SENTRY_DSN is missing; Sentry left disabled");
|
|
25044
|
+
sentryEnabled = false;
|
|
25045
|
+
return;
|
|
25046
|
+
}
|
|
25047
|
+
Sentry.init({
|
|
25048
|
+
dsn,
|
|
25049
|
+
environment: process.env.NODE_ENV ?? "production",
|
|
25050
|
+
tracesSampleRate: .1,
|
|
25051
|
+
sendDefaultPii: false,
|
|
25052
|
+
enableLogs: false,
|
|
25053
|
+
beforeSend: (event) => redactUnsafeSentryMessages(event)
|
|
25054
|
+
});
|
|
25055
|
+
Sentry.setTag("cli.session.id", crypto.randomUUID());
|
|
25056
|
+
sentryEnabled = true;
|
|
25057
|
+
}
|
|
25058
|
+
/**
|
|
25059
|
+
* `action` mirrors the MCP entity-tool action so one PostHog query can compare
|
|
25060
|
+
* `toggl reports list` with the `reports` tool's `list` action.
|
|
25061
|
+
*/
|
|
25062
|
+
function recordCliCommand(commandName, action, success) {
|
|
25063
|
+
if (!productAnalyticsEnabled) return;
|
|
25064
|
+
const error = success ? void 0 : lastCommandError;
|
|
25065
|
+
lastCommandError = void 0;
|
|
25066
|
+
captureToolInvocation({
|
|
25067
|
+
toolName: commandName,
|
|
25068
|
+
action,
|
|
25069
|
+
outcome: success ? "success" : "error",
|
|
25070
|
+
error
|
|
25071
|
+
});
|
|
25072
|
+
}
|
|
25073
|
+
function classifyCliSentryError(error) {
|
|
25074
|
+
return classifyErrorCategory("cli", error);
|
|
25075
|
+
}
|
|
25076
|
+
function toSentrySafeCliError(error) {
|
|
25077
|
+
const category = classifyCliSentryError(error);
|
|
25078
|
+
const safe = new Error(category);
|
|
25079
|
+
safe.name = error instanceof Error ? error.name : "Error";
|
|
25080
|
+
if (error instanceof Error && typeof error.stack === "string") safe.stack = error.message ? error.stack.replaceAll(error.message, category) : error.stack;
|
|
25081
|
+
return safe;
|
|
25082
|
+
}
|
|
25083
|
+
function captureCliException(error) {
|
|
25084
|
+
lastCommandError = error;
|
|
25085
|
+
if (!sentryEnabled) return;
|
|
25086
|
+
const category = classifyCliSentryError(error);
|
|
25087
|
+
Sentry.withScope((scope) => {
|
|
25088
|
+
scope.setTag("cli.error_category", category);
|
|
25089
|
+
Sentry.captureException(toSentrySafeCliError(error));
|
|
25090
|
+
});
|
|
25091
|
+
}
|
|
25092
|
+
async function shutdownCliTelemetry() {
|
|
25093
|
+
const tasks = [];
|
|
25094
|
+
if (productAnalyticsEnabled) tasks.push(shutdownProductAnalytics());
|
|
25095
|
+
if (sentryEnabled) tasks.push(Sentry.close(2e3));
|
|
25096
|
+
if (tasks.length > 0) await Promise.allSettled(tasks);
|
|
25097
|
+
sentryEnabled = false;
|
|
25098
|
+
productAnalyticsEnabled = false;
|
|
25099
|
+
lastCommandError = void 0;
|
|
25100
|
+
}
|
|
25101
|
+
//#endregion
|
|
24755
25102
|
//#region src/output.ts
|
|
24756
25103
|
function isRecord(value) {
|
|
24757
25104
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -25062,7 +25409,8 @@ function getClient(options) {
|
|
|
25062
25409
|
tokenProvider: () => getValidToken("cli", options.profile),
|
|
25063
25410
|
userId: config.user_id,
|
|
25064
25411
|
organizationId: String(config.organization_id),
|
|
25065
|
-
workspaceId: options.workspaceId ?? String(config.workspace_id)
|
|
25412
|
+
workspaceId: options.workspaceId ?? String(config.workspace_id),
|
|
25413
|
+
clientSurface: "cli"
|
|
25066
25414
|
});
|
|
25067
25415
|
}
|
|
25068
25416
|
async function runOperation(operation, globalOptions, options, rawInput) {
|
|
@@ -25099,6 +25447,19 @@ function groupCommand(program, group) {
|
|
|
25099
25447
|
for (const part of group) command = command.commands.find((candidate) => candidate.name() === part) ?? command.command(part).description(`${part} operations`);
|
|
25100
25448
|
return command;
|
|
25101
25449
|
}
|
|
25450
|
+
function cliCommandPath(command) {
|
|
25451
|
+
const parts = [];
|
|
25452
|
+
let current = command;
|
|
25453
|
+
while (current?.parent) {
|
|
25454
|
+
parts.unshift(current.name());
|
|
25455
|
+
current = current.parent;
|
|
25456
|
+
}
|
|
25457
|
+
return parts.filter(Boolean).join(" ") || command.name();
|
|
25458
|
+
}
|
|
25459
|
+
function recordActionAnalytics(actionCommand) {
|
|
25460
|
+
const failed = typeof process.exitCode === "number" && process.exitCode !== 0;
|
|
25461
|
+
recordCliCommand(cliCommandPath(actionCommand), actionCommand.name() || null, !failed);
|
|
25462
|
+
}
|
|
25102
25463
|
function createProgram(programOptions = {}) {
|
|
25103
25464
|
const promptAdapter = programOptions.promptAdapter ?? inquirerPromptAdapter;
|
|
25104
25465
|
const isTty = programOptions.isTty ?? (() => Boolean(process.stdin.isTTY && process.stdout.isTTY));
|
|
@@ -25150,6 +25511,7 @@ function createProgram(programOptions = {}) {
|
|
|
25150
25511
|
const globalOptions = program.opts();
|
|
25151
25512
|
output(opts.refresh ? await refreshCliWorkspaces(globalOptions.profile) : listCachedCliWorkspaces(globalOptions.profile), globalOptions);
|
|
25152
25513
|
} catch (error) {
|
|
25514
|
+
captureCliException(error);
|
|
25153
25515
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
25154
25516
|
process.exitCode = 1;
|
|
25155
25517
|
}
|
|
@@ -25169,6 +25531,7 @@ function createProgram(programOptions = {}) {
|
|
|
25169
25531
|
organization_name: target.organization_name
|
|
25170
25532
|
}, globalOptions);
|
|
25171
25533
|
} catch (error) {
|
|
25534
|
+
captureCliException(error);
|
|
25172
25535
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
25173
25536
|
process.exitCode = 1;
|
|
25174
25537
|
}
|
|
@@ -25178,6 +25541,7 @@ function createProgram(programOptions = {}) {
|
|
|
25178
25541
|
try {
|
|
25179
25542
|
output(listCliProfiles(), program.opts());
|
|
25180
25543
|
} catch (error) {
|
|
25544
|
+
captureCliException(error);
|
|
25181
25545
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
25182
25546
|
process.exitCode = 1;
|
|
25183
25547
|
}
|
|
@@ -25186,6 +25550,7 @@ function createProgram(programOptions = {}) {
|
|
|
25186
25550
|
try {
|
|
25187
25551
|
output(getCurrentCliProfile(), program.opts());
|
|
25188
25552
|
} catch (error) {
|
|
25553
|
+
captureCliException(error);
|
|
25189
25554
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
25190
25555
|
process.exitCode = 1;
|
|
25191
25556
|
}
|
|
@@ -25199,6 +25564,7 @@ function createProgram(programOptions = {}) {
|
|
|
25199
25564
|
exampleWhenNotInteractive: "Example: toggl profile switch production-12345"
|
|
25200
25565
|
})), globalOptions);
|
|
25201
25566
|
} catch (error) {
|
|
25567
|
+
captureCliException(error);
|
|
25202
25568
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
25203
25569
|
process.exitCode = 1;
|
|
25204
25570
|
}
|
|
@@ -25212,6 +25578,7 @@ function createProgram(programOptions = {}) {
|
|
|
25212
25578
|
exampleWhenNotInteractive: "Example: toggl profile remove production-12345"
|
|
25213
25579
|
})), globalOptions);
|
|
25214
25580
|
} catch (error) {
|
|
25581
|
+
captureCliException(error);
|
|
25215
25582
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
25216
25583
|
process.exitCode = 1;
|
|
25217
25584
|
}
|
|
@@ -25224,6 +25591,7 @@ function createProgram(programOptions = {}) {
|
|
|
25224
25591
|
json: globalOptions.json
|
|
25225
25592
|
});
|
|
25226
25593
|
} catch (error) {
|
|
25594
|
+
captureCliException(error);
|
|
25227
25595
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
25228
25596
|
process.exitCode = 1;
|
|
25229
25597
|
}
|
|
@@ -25236,6 +25604,7 @@ function createProgram(programOptions = {}) {
|
|
|
25236
25604
|
try {
|
|
25237
25605
|
await runSkillSetup(opts);
|
|
25238
25606
|
} catch (error) {
|
|
25607
|
+
captureCliException(error);
|
|
25239
25608
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
25240
25609
|
process.exitCode = 1;
|
|
25241
25610
|
}
|
|
@@ -25275,6 +25644,7 @@ Common commands:
|
|
|
25275
25644
|
const entityContext = willPrompt && !cmdOptions.dryRun ? { client: getClient(globalOptions) } : void 0;
|
|
25276
25645
|
await runOperation(operation, globalOptions, cmdOptions, willPrompt ? await promptForOperationInput(operation, rawInput, promptAdapter, entityContext) : rawInput);
|
|
25277
25646
|
} catch (error) {
|
|
25647
|
+
captureCliException(error);
|
|
25278
25648
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
25279
25649
|
process.exitCode = 1;
|
|
25280
25650
|
}
|
|
@@ -25286,6 +25656,9 @@ Common commands:
|
|
|
25286
25656
|
command.addHelpText("after", `\nExamples:\n${exampleLines}`);
|
|
25287
25657
|
}
|
|
25288
25658
|
}
|
|
25659
|
+
program.hook("postAction", (_thisCommand, actionCommand) => {
|
|
25660
|
+
recordActionAnalytics(actionCommand);
|
|
25661
|
+
});
|
|
25289
25662
|
if (programOptions.enableUpdateNotification) program.hook("postAction", (_thisCommand, actionCommand) => {
|
|
25290
25663
|
maybeNotifyUpdateAvailable({
|
|
25291
25664
|
exitCode: typeof process.exitCode === "number" ? process.exitCode : 0,
|
|
@@ -25298,6 +25671,6 @@ Common commands:
|
|
|
25298
25671
|
return program;
|
|
25299
25672
|
}
|
|
25300
25673
|
//#endregion
|
|
25301
|
-
export { CommanderError as n, createProgram as t };
|
|
25674
|
+
export { CommanderError as a, shutdownCliTelemetry as i, captureCliException as n, initCliTelemetry as r, createProgram as t };
|
|
25302
25675
|
|
|
25303
|
-
//# sourceMappingURL=program-
|
|
25676
|
+
//# sourceMappingURL=program-CH5epPVt.js.map
|
package/build/program.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createProgram } from "./program-
|
|
1
|
+
import { t as createProgram } from "./program-CH5epPVt.js";
|
|
2
2
|
export { createProgram };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@togglhq/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Toggl CLI for Toggl 2.0 API operations.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -50,6 +50,7 @@
|
|
|
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": {
|