@takuhon/activity 0.15.0 → 0.15.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/dist/index.js CHANGED
@@ -5,6 +5,9 @@ import {
5
5
  formatCodingTime
6
6
  } from "@takuhon/core";
7
7
 
8
+ // src/fetch-impl.ts
9
+ var defaultFetch = (input, init) => fetch(input, init);
10
+
8
11
  // src/github.ts
9
12
  var GITHUB_API = "https://api.github.com";
10
13
  var USER_AGENT = "takuhon-activity";
@@ -16,7 +19,7 @@ var GitHubFetchError = class extends Error {
16
19
  }
17
20
  };
18
21
  var GitHubClient = class {
19
- constructor(fetchImpl = fetch) {
22
+ constructor(fetchImpl = defaultFetch) {
20
23
  this.fetchImpl = fetchImpl;
21
24
  }
22
25
  fetchImpl;
@@ -96,7 +99,7 @@ var WakaTimeFetchError = class extends Error {
96
99
  }
97
100
  };
98
101
  var WakaTimeClient = class {
99
- constructor(fetchImpl = fetch) {
102
+ constructor(fetchImpl = defaultFetch) {
100
103
  this.fetchImpl = fetchImpl;
101
104
  }
102
105
  fetchImpl;
@@ -121,7 +124,7 @@ var WakaTimeClient = class {
121
124
 
122
125
  // src/fetch-snapshot.ts
123
126
  async function fetchActivitySnapshot(config, secrets = {}, deps = {}) {
124
- const fetchImpl = deps.fetch ?? fetch;
127
+ const fetchImpl = deps.fetch ?? defaultFetch;
125
128
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
126
129
  const onError = deps.onError ?? (() => void 0);
127
130
  const github = new GitHubClient(fetchImpl);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fetch-snapshot.ts","../src/github.ts","../src/wakatime.ts"],"sourcesContent":["/**\n * Build an {@link ActivitySnapshot} from the owner's configured sources.\n *\n * Each source is fetched independently and degrades gracefully: a source that\n * is disabled, unconfigured, missing its secret, or failing is simply omitted\n * (its error is reported via {@link ActivityFetchDeps.onError}) rather than\n * aborting the whole snapshot. The caller (a CLI command or scheduled job)\n * decides what to do with a snapshot that gathered nothing — typically keeping\n * the last-known one rather than overwriting it with an empty result.\n *\n * This module performs network I/O, so it lives outside `@takuhon/core` (which\n * is platform-independent); it reuses core's pure transforms to shape the data.\n */\n\nimport {\n computeLanguagePercentages,\n deriveRankTier,\n formatCodingTime,\n type ActivitySettings,\n type ActivitySnapshot,\n} from '@takuhon/core';\n\nimport { GitHubClient } from './github.js';\nimport { WakaTimeClient } from './wakatime.js';\n\n/** Secrets for the activity sources. Never persisted; supplied per sync run. */\nexport interface ActivitySecrets {\n /** GitHub token. Optional for languages; REQUIRED for contributions. */\n githubToken?: string;\n /** WakaTime API key. REQUIRED to read coding time. */\n wakatimeKey?: string;\n}\n\nexport interface ActivityFetchDeps {\n /** HTTP client. Defaults to the runtime global `fetch` (Node 22+ / Workers). */\n fetch?: typeof fetch;\n /** Clock for `lastSyncedAt`; injectable for deterministic tests. */\n now?: () => Date;\n /** Reports a per-source failure so the caller can audit-log it. */\n onError?: (\n source: 'github-languages' | 'github-contributions' | 'wakatime',\n err: unknown,\n ) => void;\n}\n\n/**\n * Fetch and assemble the activity snapshot for the given config. Always\n * resolves (never rejects): failures surface through `onError` and leave the\n * corresponding field absent.\n */\nexport async function fetchActivitySnapshot(\n config: ActivitySettings,\n secrets: ActivitySecrets = {},\n deps: ActivityFetchDeps = {},\n): Promise<ActivitySnapshot> {\n const fetchImpl = deps.fetch ?? fetch;\n const now = deps.now ?? ((): Date => new Date());\n const onError = deps.onError ?? ((): void => undefined);\n const github = new GitHubClient(fetchImpl);\n const wakatime = new WakaTimeClient(fetchImpl);\n\n const snapshot: ActivitySnapshot = { lastSyncedAt: now().toISOString() };\n\n let contributionsTotal: number | undefined;\n let codingSeconds: number | undefined;\n\n const githubUser = config.github?.username;\n if (githubUser !== undefined && githubUser !== '') {\n if (config.github?.showLanguages !== false) {\n try {\n const languages = computeLanguagePercentages(\n await github.fetchLanguages(githubUser, secrets.githubToken),\n );\n if (languages.length > 0) snapshot.languages = languages;\n } catch (err) {\n onError('github-languages', err);\n }\n }\n // Contributions require a token (GraphQL-only); skip silently without one.\n if (config.github?.showContributions !== false && secrets.githubToken) {\n try {\n const calendar = await github.fetchContributions(githubUser, secrets.githubToken);\n snapshot.contributions = calendar;\n contributionsTotal = calendar.total;\n } catch (err) {\n onError('github-contributions', err);\n }\n }\n }\n\n const wakatimeUser = config.wakatime?.username;\n if (\n wakatimeUser !== undefined &&\n wakatimeUser !== '' &&\n config.wakatime?.showCodingTime !== false &&\n secrets.wakatimeKey\n ) {\n try {\n codingSeconds = await wakatime.fetchCodingSeconds(wakatimeUser, secrets.wakatimeKey);\n snapshot.codingTime = formatCodingTime(codingSeconds);\n } catch (err) {\n onError('wakatime', err);\n }\n }\n\n // Only rank when at least one signal was gathered, so an unconfigured /\n // failed sync does not stamp a misleading \"D\".\n if (\n config.showRank !== false &&\n (contributionsTotal !== undefined || codingSeconds !== undefined)\n ) {\n snapshot.rank = deriveRankTier({ contributions: contributionsTotal, codingSeconds });\n }\n\n return snapshot;\n}\n\n/** True when the snapshot carries no metric data (only `lastSyncedAt`). */\nexport function isEmptySnapshot(snapshot: ActivitySnapshot): boolean {\n return (\n snapshot.languages === undefined &&\n snapshot.contributions === undefined &&\n snapshot.codingTime === undefined &&\n snapshot.rank === undefined\n );\n}\n","/**\n * GitHub fetchers for the activity snapshot.\n *\n * Two signals are read:\n * - **Language bytes** via the REST `repos/{owner}/{repo}/languages` endpoint,\n * aggregated across the user's owned (non-fork) repositories. REST works\n * unauthenticated (at a low rate limit); a token raises the limit.\n * - **Contribution calendar** via the GraphQL `contributionsCollection`. The\n * calendar is only exposed over GraphQL (the REST events API does not provide\n * it), so this REQUIRES a token.\n *\n * The `fetch` implementation is injected so tests can supply fakes; in\n * production it defaults to the runtime global (Node 22+ / Workers).\n */\n\nimport type { ContributionCalendar } from '@takuhon/core';\n\nconst GITHUB_API = 'https://api.github.com';\n\n/** GitHub requires a User-Agent on every request. */\nconst USER_AGENT = 'takuhon-activity';\n\n/** Default cap on repositories scanned for languages, to bound fan-out. */\nexport const DEFAULT_MAX_REPOS = 50;\n\ninterface RepoSummary {\n name: string;\n fork: boolean;\n}\n\ninterface GraphQLContributionResponse {\n data?: {\n user?: {\n contributionsCollection?: {\n contributionCalendar?: {\n totalContributions: number;\n weeks: { contributionDays: { date: string; contributionCount: number }[] }[];\n };\n };\n };\n };\n errors?: { message: string }[];\n}\n\n/** Thrown when a GitHub request fails; carries no token or host-path detail. */\nexport class GitHubFetchError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'GitHubFetchError';\n }\n}\n\nexport class GitHubClient {\n constructor(private readonly fetchImpl: typeof fetch = fetch) {}\n\n /**\n * Aggregate language byte counts across the user's owned, non-fork repos\n * (capped at `maxRepos`, most-recently-pushed first). Returns raw bytes per\n * language; the caller converts to percentages via `@takuhon/core`.\n */\n async fetchLanguages(\n username: string,\n token?: string,\n maxRepos = DEFAULT_MAX_REPOS,\n ): Promise<Record<string, number>> {\n const repos = await this.getJson<RepoSummary[]>(\n `${GITHUB_API}/users/${encodeURIComponent(username)}/repos?per_page=100&type=owner&sort=pushed`,\n token,\n );\n const owned = repos.filter((repo) => !repo.fork).slice(0, maxRepos);\n\n const totals: Record<string, number> = {};\n for (const repo of owned) {\n const langs = await this.getJson<Record<string, number>>(\n `${GITHUB_API}/repos/${encodeURIComponent(username)}/${encodeURIComponent(repo.name)}/languages`,\n token,\n );\n for (const [name, bytes] of Object.entries(langs)) {\n totals[name] = (totals[name] ?? 0) + bytes;\n }\n }\n return totals;\n }\n\n /**\n * Read the trailing-year contribution calendar. Requires a token (the\n * calendar is GraphQL-only).\n */\n async fetchContributions(username: string, token: string): Promise<ContributionCalendar> {\n const query =\n 'query($login:String!){user(login:$login){contributionsCollection{contributionCalendar{totalContributions weeks{contributionDays{date contributionCount}}}}}}';\n const res = await this.fetchImpl(`${GITHUB_API}/graphql`, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n 'user-agent': USER_AGENT,\n },\n body: JSON.stringify({ query, variables: { login: username } }),\n });\n if (!res.ok) {\n throw new GitHubFetchError(`GitHub GraphQL responded ${String(res.status)}.`);\n }\n const json = (await res.json()) as GraphQLContributionResponse;\n if (json.errors && json.errors.length > 0) {\n throw new GitHubFetchError(`GitHub GraphQL error: ${json.errors[0]?.message ?? 'unknown'}.`);\n }\n const calendar = json.data?.user?.contributionsCollection?.contributionCalendar;\n if (!calendar) {\n throw new GitHubFetchError('GitHub GraphQL response missing the contribution calendar.');\n }\n const days = calendar.weeks\n .flatMap((week) => week.contributionDays)\n .map((day) => ({ date: day.date, count: day.contributionCount }));\n return { total: calendar.totalContributions, days };\n }\n\n private async getJson<T>(url: string, token?: string): Promise<T> {\n const headers: Record<string, string> = {\n accept: 'application/vnd.github+json',\n 'user-agent': USER_AGENT,\n };\n if (token !== undefined && token !== '') headers.authorization = `Bearer ${token}`;\n const res = await this.fetchImpl(url, { headers });\n if (!res.ok) {\n throw new GitHubFetchError(`GitHub API responded ${String(res.status)}.`);\n }\n return (await res.json()) as T;\n }\n}\n","/**\n * WakaTime fetcher for the activity snapshot.\n *\n * Reads total coding seconds over a range from the `users/{user}/stats/{range}`\n * endpoint. WakaTime has no unauthenticated mode, so an API key is REQUIRED; it\n * is sent as HTTP Basic auth (the key base64-encoded) and only ever flows\n * through this sync step — never into any stored document.\n *\n * The `fetch` implementation is injected for tests; production defaults to the\n * runtime global.\n */\n\nconst WAKATIME_API = 'https://wakatime.com/api/v1';\n\n/** Default stats range — recent activity rather than all-time. */\nexport const DEFAULT_WAKATIME_RANGE = 'last_year';\n\ninterface WakaTimeStatsResponse {\n data?: { total_seconds?: number };\n}\n\n/** Thrown when a WakaTime request fails; carries no key or host-path detail. */\nexport class WakaTimeFetchError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'WakaTimeFetchError';\n }\n}\n\nexport class WakaTimeClient {\n constructor(private readonly fetchImpl: typeof fetch = fetch) {}\n\n /** Total coding seconds for `username` over `range` (default last year). */\n async fetchCodingSeconds(\n username: string,\n apiKey: string,\n range = DEFAULT_WAKATIME_RANGE,\n ): Promise<number> {\n const res = await this.fetchImpl(\n `${WAKATIME_API}/users/${encodeURIComponent(username)}/stats/${encodeURIComponent(range)}`,\n // WakaTime Basic auth: the API key is the username, base64-encoded.\n { headers: { authorization: `Basic ${btoa(apiKey)}` } },\n );\n if (!res.ok) {\n throw new WakaTimeFetchError(`WakaTime API responded ${String(res.status)}.`);\n }\n const json = (await res.json()) as WakaTimeStatsResponse;\n const seconds = json.data?.total_seconds;\n if (typeof seconds !== 'number') {\n throw new WakaTimeFetchError('WakaTime response missing data.total_seconds.');\n }\n return seconds;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/fetch-snapshot.ts","../src/fetch-impl.ts","../src/github.ts","../src/wakatime.ts"],"sourcesContent":["/**\n * Build an {@link ActivitySnapshot} from the owner's configured sources.\n *\n * Each source is fetched independently and degrades gracefully: a source that\n * is disabled, unconfigured, missing its secret, or failing is simply omitted\n * (its error is reported via {@link ActivityFetchDeps.onError}) rather than\n * aborting the whole snapshot. The caller (a CLI command or scheduled job)\n * decides what to do with a snapshot that gathered nothing — typically keeping\n * the last-known one rather than overwriting it with an empty result.\n *\n * This module performs network I/O, so it lives outside `@takuhon/core` (which\n * is platform-independent); it reuses core's pure transforms to shape the data.\n */\n\nimport {\n computeLanguagePercentages,\n deriveRankTier,\n formatCodingTime,\n type ActivitySettings,\n type ActivitySnapshot,\n} from '@takuhon/core';\n\nimport { defaultFetch } from './fetch-impl.js';\nimport { GitHubClient } from './github.js';\nimport { WakaTimeClient } from './wakatime.js';\n\n/** Secrets for the activity sources. Never persisted; supplied per sync run. */\nexport interface ActivitySecrets {\n /** GitHub token. Optional for languages; REQUIRED for contributions. */\n githubToken?: string;\n /** WakaTime API key. REQUIRED to read coding time. */\n wakatimeKey?: string;\n}\n\nexport interface ActivityFetchDeps {\n /** HTTP client. Defaults to the runtime global `fetch` (Node 22+ / Workers). */\n fetch?: typeof fetch;\n /** Clock for `lastSyncedAt`; injectable for deterministic tests. */\n now?: () => Date;\n /** Reports a per-source failure so the caller can audit-log it. */\n onError?: (\n source: 'github-languages' | 'github-contributions' | 'wakatime',\n err: unknown,\n ) => void;\n}\n\n/**\n * Fetch and assemble the activity snapshot for the given config. Always\n * resolves (never rejects): failures surface through `onError` and leave the\n * corresponding field absent.\n */\nexport async function fetchActivitySnapshot(\n config: ActivitySettings,\n secrets: ActivitySecrets = {},\n deps: ActivityFetchDeps = {},\n): Promise<ActivitySnapshot> {\n const fetchImpl = deps.fetch ?? defaultFetch;\n const now = deps.now ?? ((): Date => new Date());\n const onError = deps.onError ?? ((): void => undefined);\n const github = new GitHubClient(fetchImpl);\n const wakatime = new WakaTimeClient(fetchImpl);\n\n const snapshot: ActivitySnapshot = { lastSyncedAt: now().toISOString() };\n\n let contributionsTotal: number | undefined;\n let codingSeconds: number | undefined;\n\n const githubUser = config.github?.username;\n if (githubUser !== undefined && githubUser !== '') {\n if (config.github?.showLanguages !== false) {\n try {\n const languages = computeLanguagePercentages(\n await github.fetchLanguages(githubUser, secrets.githubToken),\n );\n if (languages.length > 0) snapshot.languages = languages;\n } catch (err) {\n onError('github-languages', err);\n }\n }\n // Contributions require a token (GraphQL-only); skip silently without one.\n if (config.github?.showContributions !== false && secrets.githubToken) {\n try {\n const calendar = await github.fetchContributions(githubUser, secrets.githubToken);\n snapshot.contributions = calendar;\n contributionsTotal = calendar.total;\n } catch (err) {\n onError('github-contributions', err);\n }\n }\n }\n\n const wakatimeUser = config.wakatime?.username;\n if (\n wakatimeUser !== undefined &&\n wakatimeUser !== '' &&\n config.wakatime?.showCodingTime !== false &&\n secrets.wakatimeKey\n ) {\n try {\n codingSeconds = await wakatime.fetchCodingSeconds(wakatimeUser, secrets.wakatimeKey);\n snapshot.codingTime = formatCodingTime(codingSeconds);\n } catch (err) {\n onError('wakatime', err);\n }\n }\n\n // Only rank when at least one signal was gathered, so an unconfigured /\n // failed sync does not stamp a misleading \"D\".\n if (\n config.showRank !== false &&\n (contributionsTotal !== undefined || codingSeconds !== undefined)\n ) {\n snapshot.rank = deriveRankTier({ contributions: contributionsTotal, codingSeconds });\n }\n\n return snapshot;\n}\n\n/** True when the snapshot carries no metric data (only `lastSyncedAt`). */\nexport function isEmptySnapshot(snapshot: ActivitySnapshot): boolean {\n return (\n snapshot.languages === undefined &&\n snapshot.contributions === undefined &&\n snapshot.codingTime === undefined &&\n snapshot.rank === undefined\n );\n}\n","/**\n * The runtime global `fetch`, wrapped so it survives being stored as a field\n * and later invoked as a method.\n *\n * The activity clients keep their `fetch` implementation in an instance field\n * and call it as `this.fetchImpl(url)`. Using the bare global `fetch` as the\n * default would then invoke it with the client instance as its `this`, which\n * `workerd` (Cloudflare Workers) rejects at runtime with\n * \"Illegal invocation: function called with incorrect `this` reference\" — even\n * though the same call is fine under Node. Calling the global `fetch` as a free\n * function inside this wrapper keeps its `this` bound to the global scope, so it\n * works on every runtime regardless of how the field is invoked. The wrapper\n * also resolves `fetch` lazily (at call time), so a host that installs `fetch`\n * after this module is evaluated still works.\n */\nexport const defaultFetch: typeof fetch = (input, init) => fetch(input, init);\n","/**\n * GitHub fetchers for the activity snapshot.\n *\n * Two signals are read:\n * - **Language bytes** via the REST `repos/{owner}/{repo}/languages` endpoint,\n * aggregated across the user's owned (non-fork) repositories. REST works\n * unauthenticated (at a low rate limit); a token raises the limit.\n * - **Contribution calendar** via the GraphQL `contributionsCollection`. The\n * calendar is only exposed over GraphQL (the REST events API does not provide\n * it), so this REQUIRES a token.\n *\n * The `fetch` implementation is injected so tests can supply fakes; in\n * production it defaults to the runtime global (Node 22+ / Workers).\n */\n\nimport type { ContributionCalendar } from '@takuhon/core';\n\nimport { defaultFetch } from './fetch-impl.js';\n\nconst GITHUB_API = 'https://api.github.com';\n\n/** GitHub requires a User-Agent on every request. */\nconst USER_AGENT = 'takuhon-activity';\n\n/** Default cap on repositories scanned for languages, to bound fan-out. */\nexport const DEFAULT_MAX_REPOS = 50;\n\ninterface RepoSummary {\n name: string;\n fork: boolean;\n}\n\ninterface GraphQLContributionResponse {\n data?: {\n user?: {\n contributionsCollection?: {\n contributionCalendar?: {\n totalContributions: number;\n weeks: { contributionDays: { date: string; contributionCount: number }[] }[];\n };\n };\n };\n };\n errors?: { message: string }[];\n}\n\n/** Thrown when a GitHub request fails; carries no token or host-path detail. */\nexport class GitHubFetchError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'GitHubFetchError';\n }\n}\n\nexport class GitHubClient {\n constructor(private readonly fetchImpl: typeof fetch = defaultFetch) {}\n\n /**\n * Aggregate language byte counts across the user's owned, non-fork repos\n * (capped at `maxRepos`, most-recently-pushed first). Returns raw bytes per\n * language; the caller converts to percentages via `@takuhon/core`.\n */\n async fetchLanguages(\n username: string,\n token?: string,\n maxRepos = DEFAULT_MAX_REPOS,\n ): Promise<Record<string, number>> {\n const repos = await this.getJson<RepoSummary[]>(\n `${GITHUB_API}/users/${encodeURIComponent(username)}/repos?per_page=100&type=owner&sort=pushed`,\n token,\n );\n const owned = repos.filter((repo) => !repo.fork).slice(0, maxRepos);\n\n const totals: Record<string, number> = {};\n for (const repo of owned) {\n const langs = await this.getJson<Record<string, number>>(\n `${GITHUB_API}/repos/${encodeURIComponent(username)}/${encodeURIComponent(repo.name)}/languages`,\n token,\n );\n for (const [name, bytes] of Object.entries(langs)) {\n totals[name] = (totals[name] ?? 0) + bytes;\n }\n }\n return totals;\n }\n\n /**\n * Read the trailing-year contribution calendar. Requires a token (the\n * calendar is GraphQL-only).\n */\n async fetchContributions(username: string, token: string): Promise<ContributionCalendar> {\n const query =\n 'query($login:String!){user(login:$login){contributionsCollection{contributionCalendar{totalContributions weeks{contributionDays{date contributionCount}}}}}}';\n const res = await this.fetchImpl(`${GITHUB_API}/graphql`, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n 'user-agent': USER_AGENT,\n },\n body: JSON.stringify({ query, variables: { login: username } }),\n });\n if (!res.ok) {\n throw new GitHubFetchError(`GitHub GraphQL responded ${String(res.status)}.`);\n }\n const json = (await res.json()) as GraphQLContributionResponse;\n if (json.errors && json.errors.length > 0) {\n throw new GitHubFetchError(`GitHub GraphQL error: ${json.errors[0]?.message ?? 'unknown'}.`);\n }\n const calendar = json.data?.user?.contributionsCollection?.contributionCalendar;\n if (!calendar) {\n throw new GitHubFetchError('GitHub GraphQL response missing the contribution calendar.');\n }\n const days = calendar.weeks\n .flatMap((week) => week.contributionDays)\n .map((day) => ({ date: day.date, count: day.contributionCount }));\n return { total: calendar.totalContributions, days };\n }\n\n private async getJson<T>(url: string, token?: string): Promise<T> {\n const headers: Record<string, string> = {\n accept: 'application/vnd.github+json',\n 'user-agent': USER_AGENT,\n };\n if (token !== undefined && token !== '') headers.authorization = `Bearer ${token}`;\n const res = await this.fetchImpl(url, { headers });\n if (!res.ok) {\n throw new GitHubFetchError(`GitHub API responded ${String(res.status)}.`);\n }\n return (await res.json()) as T;\n }\n}\n","/**\n * WakaTime fetcher for the activity snapshot.\n *\n * Reads total coding seconds over a range from the `users/{user}/stats/{range}`\n * endpoint. WakaTime has no unauthenticated mode, so an API key is REQUIRED; it\n * is sent as HTTP Basic auth (the key base64-encoded) and only ever flows\n * through this sync step — never into any stored document.\n *\n * The `fetch` implementation is injected for tests; production defaults to the\n * runtime global.\n */\n\nimport { defaultFetch } from './fetch-impl.js';\n\nconst WAKATIME_API = 'https://wakatime.com/api/v1';\n\n/** Default stats range — recent activity rather than all-time. */\nexport const DEFAULT_WAKATIME_RANGE = 'last_year';\n\ninterface WakaTimeStatsResponse {\n data?: { total_seconds?: number };\n}\n\n/** Thrown when a WakaTime request fails; carries no key or host-path detail. */\nexport class WakaTimeFetchError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'WakaTimeFetchError';\n }\n}\n\nexport class WakaTimeClient {\n constructor(private readonly fetchImpl: typeof fetch = defaultFetch) {}\n\n /** Total coding seconds for `username` over `range` (default last year). */\n async fetchCodingSeconds(\n username: string,\n apiKey: string,\n range = DEFAULT_WAKATIME_RANGE,\n ): Promise<number> {\n const res = await this.fetchImpl(\n `${WAKATIME_API}/users/${encodeURIComponent(username)}/stats/${encodeURIComponent(range)}`,\n // WakaTime Basic auth: the API key is the username, base64-encoded.\n { headers: { authorization: `Basic ${btoa(apiKey)}` } },\n );\n if (!res.ok) {\n throw new WakaTimeFetchError(`WakaTime API responded ${String(res.status)}.`);\n }\n const json = (await res.json()) as WakaTimeStatsResponse;\n const seconds = json.data?.total_seconds;\n if (typeof seconds !== 'number') {\n throw new WakaTimeFetchError('WakaTime response missing data.total_seconds.');\n }\n return seconds;\n }\n}\n"],"mappings":";AAcA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;;;ACLA,IAAM,eAA6B,CAAC,OAAO,SAAS,MAAM,OAAO,IAAI;;;ACI5E,IAAM,aAAa;AAGnB,IAAM,aAAa;AAGZ,IAAM,oBAAoB;AAsB1B,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAC1C,YAAY,SAAiB,SAA+B;AAC1D,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,eAAN,MAAmB;AAAA,EACxB,YAA6B,YAA0B,cAAc;AAAxC;AAAA,EAAyC;AAAA,EAAzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7B,MAAM,eACJ,UACA,OACA,WAAW,mBACsB;AACjC,UAAM,QAAQ,MAAM,KAAK;AAAA,MACvB,GAAG,UAAU,UAAU,mBAAmB,QAAQ,CAAC;AAAA,MACnD;AAAA,IACF;AACA,UAAM,QAAQ,MAAM,OAAO,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,MAAM,GAAG,QAAQ;AAElE,UAAM,SAAiC,CAAC;AACxC,eAAW,QAAQ,OAAO;AACxB,YAAM,QAAQ,MAAM,KAAK;AAAA,QACvB,GAAG,UAAU,UAAU,mBAAmB,QAAQ,CAAC,IAAI,mBAAmB,KAAK,IAAI,CAAC;AAAA,QACpF;AAAA,MACF;AACA,iBAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AACjD,eAAO,IAAI,KAAK,OAAO,IAAI,KAAK,KAAK;AAAA,MACvC;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,mBAAmB,UAAkB,OAA8C;AACvF,UAAM,QACJ;AACF,UAAM,MAAM,MAAM,KAAK,UAAU,GAAG,UAAU,YAAY;AAAA,MACxD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,KAAK;AAAA,QAC9B,gBAAgB;AAAA,QAChB,cAAc;AAAA,MAChB;AAAA,MACA,MAAM,KAAK,UAAU,EAAE,OAAO,WAAW,EAAE,OAAO,SAAS,EAAE,CAAC;AAAA,IAChE,CAAC;AACD,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI,iBAAiB,4BAA4B,OAAO,IAAI,MAAM,CAAC,GAAG;AAAA,IAC9E;AACA,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,QAAI,KAAK,UAAU,KAAK,OAAO,SAAS,GAAG;AACzC,YAAM,IAAI,iBAAiB,yBAAyB,KAAK,OAAO,CAAC,GAAG,WAAW,SAAS,GAAG;AAAA,IAC7F;AACA,UAAM,WAAW,KAAK,MAAM,MAAM,yBAAyB;AAC3D,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,iBAAiB,4DAA4D;AAAA,IACzF;AACA,UAAM,OAAO,SAAS,MACnB,QAAQ,CAAC,SAAS,KAAK,gBAAgB,EACvC,IAAI,CAAC,SAAS,EAAE,MAAM,IAAI,MAAM,OAAO,IAAI,kBAAkB,EAAE;AAClE,WAAO,EAAE,OAAO,SAAS,oBAAoB,KAAK;AAAA,EACpD;AAAA,EAEA,MAAc,QAAW,KAAa,OAA4B;AAChE,UAAM,UAAkC;AAAA,MACtC,QAAQ;AAAA,MACR,cAAc;AAAA,IAChB;AACA,QAAI,UAAU,UAAa,UAAU,GAAI,SAAQ,gBAAgB,UAAU,KAAK;AAChF,UAAM,MAAM,MAAM,KAAK,UAAU,KAAK,EAAE,QAAQ,CAAC;AACjD,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI,iBAAiB,wBAAwB,OAAO,IAAI,MAAM,CAAC,GAAG;AAAA,IAC1E;AACA,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AACF;;;ACrHA,IAAM,eAAe;AAGd,IAAM,yBAAyB;AAO/B,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YAAY,SAAiB,SAA+B;AAC1D,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,iBAAN,MAAqB;AAAA,EAC1B,YAA6B,YAA0B,cAAc;AAAxC;AAAA,EAAyC;AAAA,EAAzC;AAAA;AAAA,EAG7B,MAAM,mBACJ,UACA,QACA,QAAQ,wBACS;AACjB,UAAM,MAAM,MAAM,KAAK;AAAA,MACrB,GAAG,YAAY,UAAU,mBAAmB,QAAQ,CAAC,UAAU,mBAAmB,KAAK,CAAC;AAAA;AAAA,MAExF,EAAE,SAAS,EAAE,eAAe,SAAS,KAAK,MAAM,CAAC,GAAG,EAAE;AAAA,IACxD;AACA,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI,mBAAmB,0BAA0B,OAAO,IAAI,MAAM,CAAC,GAAG;AAAA,IAC9E;AACA,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,UAAM,UAAU,KAAK,MAAM;AAC3B,QAAI,OAAO,YAAY,UAAU;AAC/B,YAAM,IAAI,mBAAmB,+CAA+C;AAAA,IAC9E;AACA,WAAO;AAAA,EACT;AACF;;;AHJA,eAAsB,sBACpB,QACA,UAA2B,CAAC,GAC5B,OAA0B,CAAC,GACA;AAC3B,QAAM,YAAY,KAAK,SAAS;AAChC,QAAM,MAAM,KAAK,QAAQ,MAAY,oBAAI,KAAK;AAC9C,QAAM,UAAU,KAAK,YAAY,MAAY;AAC7C,QAAM,SAAS,IAAI,aAAa,SAAS;AACzC,QAAM,WAAW,IAAI,eAAe,SAAS;AAE7C,QAAM,WAA6B,EAAE,cAAc,IAAI,EAAE,YAAY,EAAE;AAEvE,MAAI;AACJ,MAAI;AAEJ,QAAM,aAAa,OAAO,QAAQ;AAClC,MAAI,eAAe,UAAa,eAAe,IAAI;AACjD,QAAI,OAAO,QAAQ,kBAAkB,OAAO;AAC1C,UAAI;AACF,cAAM,YAAY;AAAA,UAChB,MAAM,OAAO,eAAe,YAAY,QAAQ,WAAW;AAAA,QAC7D;AACA,YAAI,UAAU,SAAS,EAAG,UAAS,YAAY;AAAA,MACjD,SAAS,KAAK;AACZ,gBAAQ,oBAAoB,GAAG;AAAA,MACjC;AAAA,IACF;AAEA,QAAI,OAAO,QAAQ,sBAAsB,SAAS,QAAQ,aAAa;AACrE,UAAI;AACF,cAAM,WAAW,MAAM,OAAO,mBAAmB,YAAY,QAAQ,WAAW;AAChF,iBAAS,gBAAgB;AACzB,6BAAqB,SAAS;AAAA,MAChC,SAAS,KAAK;AACZ,gBAAQ,wBAAwB,GAAG;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAEA,QAAM,eAAe,OAAO,UAAU;AACtC,MACE,iBAAiB,UACjB,iBAAiB,MACjB,OAAO,UAAU,mBAAmB,SACpC,QAAQ,aACR;AACA,QAAI;AACF,sBAAgB,MAAM,SAAS,mBAAmB,cAAc,QAAQ,WAAW;AACnF,eAAS,aAAa,iBAAiB,aAAa;AAAA,IACtD,SAAS,KAAK;AACZ,cAAQ,YAAY,GAAG;AAAA,IACzB;AAAA,EACF;AAIA,MACE,OAAO,aAAa,UACnB,uBAAuB,UAAa,kBAAkB,SACvD;AACA,aAAS,OAAO,eAAe,EAAE,eAAe,oBAAoB,cAAc,CAAC;AAAA,EACrF;AAEA,SAAO;AACT;AAGO,SAAS,gBAAgB,UAAqC;AACnE,SACE,SAAS,cAAc,UACvB,SAAS,kBAAkB,UAC3B,SAAS,eAAe,UACxB,SAAS,SAAS;AAEtB;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takuhon/activity",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Developer-activity fetchers (GitHub, WakaTime) that build a takuhon activity snapshot",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Takuhon contributors",
@@ -43,7 +43,7 @@
43
43
  "node": ">=22.0.0"
44
44
  },
45
45
  "dependencies": {
46
- "@takuhon/core": "0.15.0"
46
+ "@takuhon/core": "0.15.1"
47
47
  },
48
48
  "scripts": {
49
49
  "typecheck": "tsc",