mobbdev 1.2.56 → 1.2.58

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.mjs CHANGED
@@ -1660,8 +1660,16 @@ var init_analysis = __esm({
1660
1660
  originalUrl: z8.string(),
1661
1661
  reference: z8.string(),
1662
1662
  commitSha: z8.string(),
1663
- isKnownBranch: z8.boolean().nullable()
1664
- }),
1663
+ isKnownBranch: z8.boolean().nullish().default(true)
1664
+ }).nullable().transform(
1665
+ (repo) => repo ?? {
1666
+ name: null,
1667
+ originalUrl: "",
1668
+ reference: "",
1669
+ commitSha: "",
1670
+ isKnownBranch: true
1671
+ }
1672
+ ),
1665
1673
  vulnerabilityReport: z8.object({
1666
1674
  id: z8.string().uuid(),
1667
1675
  vendor: z8.nativeEnum(Vulnerability_Report_Vendor_Enum),
@@ -1982,7 +1990,15 @@ var init_types = __esm({
1982
1990
  reference: z11.string(),
1983
1991
  commitSha: z11.string(),
1984
1992
  isKnownBranch: z11.boolean().nullish().default(true)
1985
- }),
1993
+ }).nullable().transform(
1994
+ (repo) => repo ?? {
1995
+ name: null,
1996
+ originalUrl: "",
1997
+ reference: "",
1998
+ commitSha: "",
1999
+ isKnownBranch: true
2000
+ }
2001
+ ),
1986
2002
  vulnerabilityReportIssuesFixedCount: z11.object({
1987
2003
  vulnerabilityReportIssues_aggregate: z11.object({
1988
2004
  aggregate: z11.object({ count: z11.number() })
@@ -2003,7 +2019,7 @@ var init_types = __esm({
2003
2019
  file: z11.object({
2004
2020
  id: z11.string().uuid(),
2005
2021
  path: z11.string()
2006
- }),
2022
+ }).nullable(),
2007
2023
  pending: z11.object({
2008
2024
  aggregate: z11.object({
2009
2025
  count: z11.number()
@@ -2204,7 +2220,13 @@ var init_types = __esm({
2204
2220
  originalUrl: z11.string(),
2205
2221
  reference: z11.string(),
2206
2222
  name: z11.string()
2207
- }),
2223
+ }).nullable().transform(
2224
+ (repo) => repo ?? {
2225
+ originalUrl: "",
2226
+ reference: "",
2227
+ name: ""
2228
+ }
2229
+ ),
2208
2230
  createdByUser: z11.object({
2209
2231
  email: z11.string()
2210
2232
  }).nullable(),
@@ -2752,7 +2774,7 @@ function parseCommitLine(line) {
2752
2774
  message: parts[6]
2753
2775
  };
2754
2776
  }
2755
- var PrepareGitBlameMessageZ, PrepareGitBlameResponseMessageZ, CommitMetadataZ, LineToCommitMapZ, CommitMetadataMapZ, BlameInfoZ, LineRangeZ, PrContextZ, PrepareCommitBlameMessageZ, BlameLineInfoZ, FileBlameDataZ, ChunkFetchResultZ, FileBlameResponseEntryZ, CommitBlameDataZ, CommitInfoZ, GitIdentityZ, COMMIT_LOG_FORMAT, CommitDataZ, PrDiffDataZ, PrStatsZ, CommitsManifestZ, BlameLineEntryZ, BlameLinesDataZ, PrepareCommitBlameResponseMessageZ;
2777
+ var PrepareGitBlameMessageZ, PrepareGitBlameResponseMessageZ, CommitMetadataZ, LineToCommitMapZ, CommitMetadataMapZ, BlameInfoZ, LineRangeZ, PrContextZ, PrepareCommitBlameMessageZ, BlameLineInfoZ, FileBlameDataZ, ChunkFetchResultZ, FileBlameResponseEntryZ, CommitBlameDataZ, CommitInfoZ, GitIdentityZ, COMMIT_LOG_FORMAT, CommitDataZ, PrDiffDataZ, PrStatsZ, CommitsManifestZ, BlameLineEntryZ, BlameLinesDataZ, PrepareCommitBlameResponseMessageZ, VulnerabilityAttributionMessageZ;
2756
2778
  var init_gitBlameTypes = __esm({
2757
2779
  "src/utils/blame/gitBlameTypes.ts"() {
2758
2780
  "use strict";
@@ -2927,6 +2949,11 @@ var init_gitBlameTypes = __esm({
2927
2949
  /** User email passed through from request for single commit blame attribution analysis trigger. */
2928
2950
  userEmail: z18.string().optional()
2929
2951
  });
2952
+ VulnerabilityAttributionMessageZ = z18.object({
2953
+ fixReportId: z18.string().uuid(),
2954
+ vulnerabilityAttributionRequestId: z18.string().uuid(),
2955
+ userEmail: z18.string()
2956
+ });
2930
2957
  }
2931
2958
  });
2932
2959
 
@@ -6695,9 +6722,9 @@ function transformVisualStudioUrl(url) {
6695
6722
  }
6696
6723
  function _getPublicAdoClient({
6697
6724
  orgName,
6698
- origin: origin2
6725
+ origin
6699
6726
  }) {
6700
- const orgUrl = `${origin2}/${orgName}`;
6727
+ const orgUrl = `${origin}/${orgName}`;
6701
6728
  const authHandler = api.getPersonalAccessTokenHandler("");
6702
6729
  authHandler.canHandleAuthentication = () => false;
6703
6730
  authHandler.prepareRequest = (_options) => {
@@ -6759,10 +6786,10 @@ async function getAdoConnectData({
6759
6786
  org: tokenOrg
6760
6787
  };
6761
6788
  }
6762
- const { owner, origin: origin2, prefixPath } = parseAdoOwnerAndRepo(url);
6789
+ const { owner, origin, prefixPath } = parseAdoOwnerAndRepo(url);
6763
6790
  return {
6764
6791
  org: owner,
6765
- origin: prefixPath ? `${origin2}/${prefixPath}` : origin2
6792
+ origin: prefixPath ? `${origin}/${prefixPath}` : origin
6766
6793
  };
6767
6794
  }
6768
6795
  if (!tokenOrg) {
@@ -6787,17 +6814,17 @@ function isAdoOnCloud(url) {
6787
6814
  return urlObj.origin.toLowerCase() === DEFUALT_ADO_ORIGIN || urlObj.hostname.toLowerCase().endsWith(".visualstudio.com");
6788
6815
  }
6789
6816
  async function getAdoApiClient(params) {
6790
- const { origin: origin2 = DEFUALT_ADO_ORIGIN, orgName } = params;
6817
+ const { origin = DEFUALT_ADO_ORIGIN, orgName } = params;
6791
6818
  if (params.tokenType === "NONE" /* NONE */ || // note: move to public client if the token is not associated with the PAT org
6792
6819
  // we're only doing it the ado on the cloud
6793
- params.tokenType === "PAT" /* PAT */ && params.patTokenOrg !== orgName && isAdoOnCloud(origin2)) {
6794
- return _getPublicAdoClient({ orgName, origin: origin2 });
6820
+ params.tokenType === "PAT" /* PAT */ && params.patTokenOrg !== orgName && isAdoOnCloud(origin)) {
6821
+ return _getPublicAdoClient({ orgName, origin });
6795
6822
  }
6796
- const orgUrl = `${origin2}/${orgName}`;
6823
+ const orgUrl = `${origin}/${orgName}`;
6797
6824
  if (params.tokenType === "OAUTH" /* OAUTH */) {
6798
- if (!isAdoOnCloud(origin2)) {
6825
+ if (!isAdoOnCloud(origin)) {
6799
6826
  throw new Error(
6800
- `Oauth token is not supported for ADO on prem - ${origin2} `
6827
+ `Oauth token is not supported for ADO on prem - ${origin} `
6801
6828
  );
6802
6829
  }
6803
6830
  const connection2 = new api.WebApi(
@@ -6833,7 +6860,7 @@ function getAdoTokenInfo(token) {
6833
6860
  async function getAdoClientParams(params) {
6834
6861
  const { url, accessToken, tokenOrg } = params;
6835
6862
  const adoTokenInfo = getAdoTokenInfo(accessToken);
6836
- const { org, origin: origin2 } = await getAdoConnectData({
6863
+ const { org, origin } = await getAdoConnectData({
6837
6864
  url,
6838
6865
  tokenOrg,
6839
6866
  adoTokenInfo
@@ -6842,14 +6869,14 @@ async function getAdoClientParams(params) {
6842
6869
  case "NONE" /* NONE */:
6843
6870
  return {
6844
6871
  tokenType: "NONE" /* NONE */,
6845
- origin: origin2,
6872
+ origin,
6846
6873
  orgName: org.toLowerCase()
6847
6874
  };
6848
6875
  case "OAUTH" /* OAUTH */: {
6849
6876
  return {
6850
6877
  tokenType: "OAUTH" /* OAUTH */,
6851
6878
  accessToken: adoTokenInfo.accessToken,
6852
- origin: origin2,
6879
+ origin,
6853
6880
  orgName: org.toLowerCase()
6854
6881
  };
6855
6882
  }
@@ -6858,7 +6885,7 @@ async function getAdoClientParams(params) {
6858
6885
  tokenType: "PAT" /* PAT */,
6859
6886
  accessToken: adoTokenInfo.accessToken,
6860
6887
  patTokenOrg: z17.string().parse(tokenOrg).toLowerCase(),
6861
- origin: origin2,
6888
+ origin,
6862
6889
  orgName: org.toLowerCase()
6863
6890
  };
6864
6891
  }
@@ -7044,7 +7071,7 @@ async function getAdoSdk(params) {
7044
7071
  }) {
7045
7072
  const { owner, repo, projectName, prefixPath } = parseAdoOwnerAndRepo(repoUrl);
7046
7073
  const url = new URL(repoUrl);
7047
- const origin2 = url.origin.toLowerCase().endsWith(".visualstudio.com") ? DEFUALT_ADO_ORIGIN : url.origin.toLowerCase();
7074
+ const origin = url.origin.toLowerCase().endsWith(".visualstudio.com") ? DEFUALT_ADO_ORIGIN : url.origin.toLowerCase();
7048
7075
  const params2 = `path=/&versionDescriptor[versionOptions]=0&versionDescriptor[versionType]=commit&versionDescriptor[version]=${branch}&resolveLfs=true&$format=zip&api-version=5.0&download=true`;
7049
7076
  const path27 = [
7050
7077
  prefixPath,
@@ -7057,7 +7084,7 @@ async function getAdoSdk(params) {
7057
7084
  "items",
7058
7085
  "items"
7059
7086
  ].filter(Boolean).join("/");
7060
- return new URL(`${path27}?${params2}`, origin2).toString();
7087
+ return new URL(`${path27}?${params2}`, origin).toString();
7061
7088
  },
7062
7089
  async getAdoBranchList({ repoUrl }) {
7063
7090
  try {
@@ -7338,7 +7365,8 @@ async function getAdoSdk(params) {
7338
7365
  async function getAdoRepoList({
7339
7366
  orgName,
7340
7367
  tokenOrg,
7341
- accessToken
7368
+ accessToken,
7369
+ url
7342
7370
  }) {
7343
7371
  let orgs = [];
7344
7372
  const adoTokenInfo = getAdoTokenInfo(accessToken);
@@ -7348,10 +7376,11 @@ async function getAdoRepoList({
7348
7376
  if (adoTokenInfo.type === "OAUTH" /* OAUTH */) {
7349
7377
  orgs = await getOrgsForOauthToken({ oauthToken: accessToken });
7350
7378
  }
7351
- if (orgs.length === 0 && !orgName) {
7379
+ const effectiveOrgName = orgName || tokenOrg;
7380
+ if (orgs.length === 0 && !effectiveOrgName) {
7352
7381
  throw new Error(`no orgs for ADO`);
7353
- } else if (orgs.length === 0 && orgName) {
7354
- orgs = [orgName];
7382
+ } else if (orgs.length === 0 && effectiveOrgName) {
7383
+ orgs = [effectiveOrgName];
7355
7384
  }
7356
7385
  const repos = (await Promise.allSettled(
7357
7386
  orgs.map(async (org) => {
@@ -7359,7 +7388,7 @@ async function getAdoRepoList({
7359
7388
  ...await getAdoClientParams({
7360
7389
  accessToken,
7361
7390
  tokenOrg: tokenOrg || org,
7362
- url: void 0
7391
+ url
7363
7392
  }),
7364
7393
  orgName: org
7365
7394
  });
@@ -7627,15 +7656,11 @@ var AdoSCMLib = class extends SCMLib {
7627
7656
  }
7628
7657
  async getRepoList(scmOrg) {
7629
7658
  this._validateAccessToken();
7630
- if (this.url && new URL(this.url).origin !== scmCloudUrl.Ado) {
7631
- throw new Error(
7632
- `Oauth token is not supported for ADO on prem - ${origin} `
7633
- );
7634
- }
7635
7659
  return getAdoRepoList({
7636
7660
  orgName: scmOrg,
7637
7661
  accessToken: this.accessToken,
7638
- tokenOrg: this.scmOrg
7662
+ tokenOrg: this.scmOrg,
7663
+ url: this.url
7639
7664
  });
7640
7665
  }
7641
7666
  async getBranchList() {
@@ -7840,15 +7865,11 @@ var AdoSCMLib = class extends SCMLib {
7840
7865
  // getRepositoriesPaged() API per-project for true server-side pagination.
7841
7866
  async searchRepos(params) {
7842
7867
  this._validateAccessToken();
7843
- if (this.url && new URL(this.url).origin !== scmCloudUrl.Ado) {
7844
- throw new Error(
7845
- `Oauth token is not supported for ADO on prem - ${this.url}`
7846
- );
7847
- }
7848
7868
  const allRepos = await getAdoRepoList({
7849
7869
  orgName: params.scmOrg,
7850
7870
  accessToken: this.accessToken,
7851
- tokenOrg: this.scmOrg
7871
+ tokenOrg: this.scmOrg,
7872
+ url: this.url
7852
7873
  });
7853
7874
  allRepos.sort((a, b) => {
7854
7875
  const dateA = a.repoUpdatedAt ? new Date(a.repoUpdatedAt).getTime() : 0;
@@ -8945,17 +8966,30 @@ function getGithubSdk(params = {}) {
8945
8966
  },
8946
8967
  async getGithubRepoList() {
8947
8968
  try {
8948
- const githubRepos = await octokit.request(GET_USER_REPOS, {
8949
- sort: "updated"
8950
- });
8951
- return githubRepos.data.map((repo) => ({
8952
- repoName: repo.name,
8953
- repoUrl: repo.html_url,
8954
- repoOwner: repo.owner.login,
8955
- repoLanguages: repo.language ? [repo.language] : [],
8956
- repoIsPublic: !repo.private,
8957
- repoUpdatedAt: repo.updated_at
8958
- }));
8969
+ const allRepos = [];
8970
+ let page = 1;
8971
+ const perPage = 100;
8972
+ let hasMore = true;
8973
+ while (hasMore) {
8974
+ const githubRepos = await octokit.request(GET_USER_REPOS, {
8975
+ sort: "updated",
8976
+ per_page: perPage,
8977
+ page
8978
+ });
8979
+ for (const repo of githubRepos.data) {
8980
+ allRepos.push({
8981
+ repoName: repo.name,
8982
+ repoUrl: repo.html_url,
8983
+ repoOwner: repo.owner.login,
8984
+ repoLanguages: repo.language ? [repo.language] : [],
8985
+ repoIsPublic: !repo.private,
8986
+ repoUpdatedAt: repo.updated_at
8987
+ });
8988
+ }
8989
+ hasMore = githubRepos.data.length >= perPage;
8990
+ page++;
8991
+ }
8992
+ return allRepos;
8959
8993
  } catch (e) {
8960
8994
  if (e instanceof RequestError && e.status === 401) {
8961
8995
  console.warn(
@@ -9378,7 +9412,7 @@ function getGithubSdk(params = {}) {
9378
9412
  if (!org) {
9379
9413
  throw new Error("Organization is required for repository search");
9380
9414
  }
9381
- const query = `org:${org}`;
9415
+ const query = `org:${org} fork:true`;
9382
9416
  const githubSortField = sort.field === "name" ? void 0 : "updated";
9383
9417
  const response = await octokit.rest.search.repos({
9384
9418
  q: query,
@@ -10139,7 +10173,9 @@ async function searchGitlabProjects({
10139
10173
  url,
10140
10174
  accessToken,
10141
10175
  perPage = 20,
10142
- page = 1
10176
+ page = 1,
10177
+ orderBy = "created_at",
10178
+ sort = "desc"
10143
10179
  }) {
10144
10180
  if (perPage > GITLAB_MAX_PER_PAGE) {
10145
10181
  throw new Error(
@@ -10147,31 +10183,28 @@ async function searchGitlabProjects({
10147
10183
  );
10148
10184
  }
10149
10185
  const api2 = getGitBeaker({ url, gitlabAuthToken: accessToken });
10186
+ const fetchProjects = (effectiveOrderBy) => api2.Projects.all({
10187
+ membership: true,
10188
+ orderBy: effectiveOrderBy,
10189
+ sort,
10190
+ pagination: "offset",
10191
+ perPage,
10192
+ page,
10193
+ showExpanded: true
10194
+ });
10150
10195
  let response;
10151
- try {
10152
- response = await api2.Projects.all({
10153
- membership: true,
10154
- orderBy: "last_activity_at",
10155
- sort: "desc",
10156
- pagination: "offset",
10157
- perPage,
10158
- page,
10159
- showExpanded: true
10160
- });
10161
- } catch (e) {
10162
- debug4(
10163
- "[searchGitlabProjects] order_by=last_activity_at failed, falling back to created_at: %s",
10164
- e instanceof Error ? e.message : String(e)
10165
- );
10166
- response = await api2.Projects.all({
10167
- membership: true,
10168
- orderBy: "created_at",
10169
- sort: "desc",
10170
- pagination: "offset",
10171
- perPage,
10172
- page,
10173
- showExpanded: true
10174
- });
10196
+ if (orderBy === "last_activity_at") {
10197
+ try {
10198
+ response = await fetchProjects("last_activity_at");
10199
+ } catch (e) {
10200
+ debug4(
10201
+ "[searchGitlabProjects] order_by=last_activity_at failed, falling back to created_at: %s",
10202
+ e instanceof Error ? e.message : String(e)
10203
+ );
10204
+ response = await fetchProjects("created_at");
10205
+ }
10206
+ } else {
10207
+ response = await fetchProjects(orderBy);
10175
10208
  }
10176
10209
  const projects = response.data.map((p) => ({
10177
10210
  id: p.id,
@@ -10933,11 +10966,15 @@ var GitlabSCMLib = class extends SCMLib {
10933
10966
  }
10934
10967
  const page = parseCursorSafe(params.cursor, 1);
10935
10968
  const perPage = params.limit || 10;
10969
+ const sort = params.sort || { field: "updated", order: "desc" };
10970
+ const orderBy = sort.field === "updated" ? "last_activity_at" : "created_at";
10936
10971
  const { projects, hasMore } = await searchGitlabProjects({
10937
10972
  url: this.url,
10938
10973
  accessToken: this.accessToken,
10939
10974
  perPage,
10940
- page
10975
+ page,
10976
+ orderBy,
10977
+ sort: sort.order
10941
10978
  });
10942
10979
  const includeLanguages = params.includeLanguages !== false;
10943
10980
  const languageMap = /* @__PURE__ */ new Map();
@@ -11127,29 +11164,29 @@ var StubSCMLib = class extends SCMLib {
11127
11164
  };
11128
11165
 
11129
11166
  // src/features/analysis/scm/scmFactory.ts
11130
- async function createScmLib({ url, accessToken, scmType, scmOrg }, { propagateExceptions = false } = {}) {
11167
+ async function createScmLib({ url, accessToken, scmType, scmOrg }, { propagateExceptions = false, skipValidation = false } = {}) {
11131
11168
  const trimmedUrl = url ? url.trim().replace(/\/$/, "").replace(/.git$/i, "") : void 0;
11132
11169
  try {
11133
11170
  switch (scmType) {
11134
11171
  case "GITHUB" /* GITHUB */: {
11135
11172
  const scm = new GithubSCMLib(trimmedUrl, accessToken, scmOrg);
11136
- await scm.validateParams();
11173
+ if (!skipValidation) await scm.validateParams();
11137
11174
  return scm;
11138
11175
  }
11139
11176
  case "GITLAB" /* GITLAB */: {
11140
11177
  const scm = new GitlabSCMLib(trimmedUrl, accessToken, scmOrg);
11141
- await scm.validateParams();
11178
+ if (!skipValidation) await scm.validateParams();
11142
11179
  return scm;
11143
11180
  }
11144
11181
  case "ADO" /* ADO */: {
11145
11182
  const scm = new AdoSCMLib(trimmedUrl, accessToken, scmOrg);
11146
11183
  await scm.getAdoSdk();
11147
- await scm.validateParams();
11184
+ if (!skipValidation) await scm.validateParams();
11148
11185
  return scm;
11149
11186
  }
11150
11187
  case "BITBUCKET" /* BITBUCKET */: {
11151
11188
  const scm = new BitbucketSCMLib(trimmedUrl, accessToken, scmOrg);
11152
- await scm.validateParams();
11189
+ if (!skipValidation) await scm.validateParams();
11153
11190
  return scm;
11154
11191
  }
11155
11192
  }
@@ -13659,12 +13696,47 @@ async function uploadAiBlameCommandHandler(args) {
13659
13696
  await uploadAiBlameHandler({ args });
13660
13697
  }
13661
13698
 
13699
+ // src/utils/with-timeout.ts
13700
+ import { setTimeout as delay } from "timers/promises";
13701
+ function withTimeout(promise, ms, label) {
13702
+ const ac = new AbortController();
13703
+ return Promise.race([
13704
+ promise.finally(() => ac.abort()),
13705
+ delay(ms, void 0, { signal: ac.signal }).then(() => {
13706
+ throw new Error(`${label} timed out after ${ms}ms`);
13707
+ })
13708
+ ]);
13709
+ }
13710
+
13662
13711
  // src/features/analysis/graphql/tracy-batch-upload.ts
13663
13712
  var debug10 = Debug9("mobbdev:tracy-batch-upload");
13713
+ function timedStep(label, fn) {
13714
+ const start = Date.now();
13715
+ const result = fn();
13716
+ const maybePromise = result instanceof Promise ? result : Promise.resolve(result);
13717
+ return maybePromise.then(
13718
+ (val) => {
13719
+ debug10("[perf] %s: %dms", label, Date.now() - start);
13720
+ return val;
13721
+ },
13722
+ (err) => {
13723
+ debug10("[perf] %s FAILED: %dms", label, Date.now() - start);
13724
+ throw err;
13725
+ }
13726
+ );
13727
+ }
13728
+ var BATCH_TIMEOUT_MS = 3e4;
13664
13729
  async function sanitizeRawData(rawData) {
13730
+ const start = Date.now();
13665
13731
  try {
13666
13732
  const sanitized = await sanitizeData(rawData);
13667
- return JSON.stringify(sanitized);
13733
+ const serialized = JSON.stringify(sanitized);
13734
+ debug10(
13735
+ "[perf] sanitizeRawData: %dms (%d bytes)",
13736
+ Date.now() - start,
13737
+ serialized.length
13738
+ );
13739
+ return serialized;
13668
13740
  } catch (err) {
13669
13741
  console.warn(
13670
13742
  "[tracy] sanitizeRawData failed, falling back to unsanitized:",
@@ -13673,44 +13745,80 @@ async function sanitizeRawData(rawData) {
13673
13745
  return JSON.stringify(rawData);
13674
13746
  }
13675
13747
  }
13676
- async function prepareAndSendTracyRecords(client, rawRecords, workingDir) {
13677
- const repositoryUrl = await getRepositoryUrl(workingDir);
13748
+ async function prepareAndSendTracyRecords(client, rawRecords, workingDir, options) {
13678
13749
  const { computerName, userName } = getSystemInfo();
13679
- const clientVersion = packageJson.version;
13750
+ const defaultClientVersion = packageJson.version;
13751
+ const shouldSanitize = options?.sanitize ?? true;
13752
+ const defaultRepoUrl = rawRecords[0]?.repositoryUrl ? void 0 : await getRepositoryUrl(workingDir) ?? void 0;
13753
+ debug10(
13754
+ "[step:sanitize] %s %d records",
13755
+ shouldSanitize ? "Sanitizing" : "Serializing",
13756
+ rawRecords.length
13757
+ );
13680
13758
  const serializedRawDataByIndex = /* @__PURE__ */ new Map();
13681
- const records = await Promise.all(
13682
- rawRecords.map(async (record, index) => {
13683
- if (record.rawData != null) {
13684
- const serialized = await sanitizeRawData(record.rawData);
13685
- serializedRawDataByIndex.set(index, serialized);
13686
- }
13687
- const { rawData: _rawData, ...rest } = record;
13688
- return {
13689
- ...rest,
13690
- repositoryUrl: repositoryUrl ?? void 0,
13691
- computerName,
13692
- userName,
13693
- clientVersion
13694
- };
13695
- })
13759
+ const records = await timedStep(
13760
+ `${shouldSanitize ? "sanitize" : "serialize"} ${rawRecords.length} records`,
13761
+ () => Promise.all(
13762
+ rawRecords.map(async (record, index) => {
13763
+ if (record.rawData != null) {
13764
+ const serialized = shouldSanitize ? await sanitizeRawData(record.rawData) : JSON.stringify(record.rawData);
13765
+ serializedRawDataByIndex.set(index, serialized);
13766
+ }
13767
+ const { rawData: _rawData, ...rest } = record;
13768
+ return {
13769
+ ...rest,
13770
+ repositoryUrl: record.repositoryUrl ?? defaultRepoUrl,
13771
+ computerName,
13772
+ userName,
13773
+ clientVersion: record.clientVersion ?? defaultClientVersion
13774
+ };
13775
+ })
13776
+ )
13696
13777
  );
13697
13778
  const recordsWithRawData = rawRecords.map((r, i) => ({ recordId: r.recordId, index: i })).filter((entry) => serializedRawDataByIndex.has(entry.index));
13698
13779
  if (recordsWithRawData.length > 0) {
13699
- debug10("Uploading %d rawData files to S3...", recordsWithRawData.length);
13700
- const uploadUrlResult = await client.getTracyRawDataUploadUrl();
13780
+ debug10(
13781
+ "[step:s3-url] Requesting presigned URL for %d rawData files",
13782
+ recordsWithRawData.length
13783
+ );
13784
+ let uploadUrlResult;
13785
+ try {
13786
+ uploadUrlResult = await withTimeout(
13787
+ client.getTracyRawDataUploadUrl(),
13788
+ BATCH_TIMEOUT_MS,
13789
+ "[step:s3-url] getTracyRawDataUploadUrl"
13790
+ );
13791
+ } catch (err) {
13792
+ return {
13793
+ ok: false,
13794
+ errors: [
13795
+ `[step:s3-url] Failed to fetch S3 upload URL: ${err.message}`
13796
+ ]
13797
+ };
13798
+ }
13701
13799
  const { url, uploadFieldsJSON, keyPrefix } = uploadUrlResult.getTracyRawDataUploadUrl;
13702
13800
  if (!url || !uploadFieldsJSON || !keyPrefix) {
13703
13801
  return {
13704
13802
  ok: false,
13705
- errors: ["Failed to get S3 upload URL for rawData"]
13803
+ errors: [
13804
+ `[step:s3-url] Missing S3 upload fields (url=${!!url}, fields=${!!uploadFieldsJSON}, prefix=${!!keyPrefix})`
13805
+ ]
13706
13806
  };
13707
13807
  }
13708
13808
  let uploadFields;
13709
13809
  try {
13710
13810
  uploadFields = JSON.parse(uploadFieldsJSON);
13711
13811
  } catch {
13712
- return { ok: false, errors: ["Malformed uploadFieldsJSON from server"] };
13812
+ return {
13813
+ ok: false,
13814
+ errors: ["[step:s3-url] Malformed uploadFieldsJSON from server"]
13815
+ };
13713
13816
  }
13817
+ debug10(
13818
+ "[step:s3-upload] Uploading %d files to S3",
13819
+ recordsWithRawData.length
13820
+ );
13821
+ const s3Start = Date.now();
13714
13822
  const uploadResults = await Promise.allSettled(
13715
13823
  recordsWithRawData.map(async (entry) => {
13716
13824
  const rawDataJson = serializedRawDataByIndex.get(entry.index);
@@ -13719,48 +13827,67 @@ async function prepareAndSendTracyRecords(client, rawRecords, workingDir) {
13719
13827
  return;
13720
13828
  }
13721
13829
  const uploadKey = `${keyPrefix}${entry.recordId}.json`;
13722
- await uploadFile({
13723
- file: Buffer.from(rawDataJson, "utf-8"),
13724
- url,
13725
- uploadKey,
13726
- uploadFields
13727
- });
13830
+ await withTimeout(
13831
+ uploadFile({
13832
+ file: Buffer.from(rawDataJson, "utf-8"),
13833
+ url,
13834
+ uploadKey,
13835
+ uploadFields
13836
+ }),
13837
+ BATCH_TIMEOUT_MS,
13838
+ `[step:s3-upload] uploadFile ${entry.recordId}`
13839
+ );
13728
13840
  records[entry.index].rawDataS3Key = uploadKey;
13729
13841
  })
13730
13842
  );
13843
+ debug10(
13844
+ "[perf] s3-upload %d files: %dms",
13845
+ recordsWithRawData.length,
13846
+ Date.now() - s3Start
13847
+ );
13731
13848
  const uploadErrors = uploadResults.filter((r) => r.status === "rejected").map((r) => r.reason.message);
13732
13849
  if (uploadErrors.length > 0) {
13733
- debug10("S3 upload errors: %O", uploadErrors);
13850
+ debug10("[step:s3-upload] S3 upload errors: %O", uploadErrors);
13734
13851
  }
13735
13852
  const missingS3Keys = recordsWithRawData.filter(
13736
13853
  (entry) => !records[entry.index].rawDataS3Key
13737
13854
  );
13738
13855
  if (missingS3Keys.length > 0) {
13739
13856
  const missingIds = missingS3Keys.map((e) => e.recordId);
13740
- debug10("Records missing S3 keys after upload: %O", missingIds);
13857
+ debug10("[step:s3-upload] Records missing S3 keys: %O", missingIds);
13741
13858
  return {
13742
13859
  ok: false,
13743
13860
  errors: [
13744
- `Failed to upload rawData to S3 for ${missingS3Keys.length} record(s): ${missingIds.join(", ")}`,
13861
+ `[step:s3-upload] Failed to upload rawData for ${missingS3Keys.length} record(s): ${missingIds.join(", ")}`,
13745
13862
  ...uploadErrors
13746
13863
  ]
13747
13864
  };
13748
13865
  }
13749
- debug10("S3 uploads complete");
13866
+ debug10("[step:s3-upload] S3 uploads complete");
13750
13867
  }
13868
+ debug10("[step:gql-submit] Submitting %d records via GraphQL", records.length);
13751
13869
  try {
13752
- const result = await client.uploadTracyRecords({ records });
13870
+ const result = await timedStep(
13871
+ `gql-submit ${records.length} records`,
13872
+ () => withTimeout(
13873
+ client.uploadTracyRecords({ records }),
13874
+ BATCH_TIMEOUT_MS,
13875
+ "[step:gql-submit] uploadTracyRecords"
13876
+ )
13877
+ );
13753
13878
  if (result.uploadTracyRecords.status !== "OK") {
13754
13879
  return {
13755
13880
  ok: false,
13756
- errors: [result.uploadTracyRecords.error ?? "Unknown server error"]
13881
+ errors: [
13882
+ `[step:gql-submit] Server rejected: ${result.uploadTracyRecords.error ?? "Unknown server error"}`
13883
+ ]
13757
13884
  };
13758
13885
  }
13759
13886
  } catch (err) {
13760
- debug10("Upload failed: %s", err.message);
13887
+ debug10("[step:gql-submit] Upload failed: %s", err.message);
13761
13888
  return {
13762
13889
  ok: false,
13763
- errors: [err.message]
13890
+ errors: [`[step:gql-submit] ${err.message}`]
13764
13891
  };
13765
13892
  }
13766
13893
  return { ok: true, errors: null };
@@ -16297,8 +16424,8 @@ async function analyzeHandler(args) {
16297
16424
 
16298
16425
  // src/features/claude_code/data_collector.ts
16299
16426
  import { createHash as createHash2 } from "crypto";
16300
- import { open as open4, readdir, readFile, unlink } from "fs/promises";
16301
- import path13 from "path";
16427
+ import { access, open as open4, readdir, readFile, unlink } from "fs/promises";
16428
+ import path14 from "path";
16302
16429
  import { z as z33 } from "zod";
16303
16430
  init_client_generates();
16304
16431
 
@@ -16583,7 +16710,7 @@ function createLogger(config2) {
16583
16710
 
16584
16711
  // src/features/claude_code/hook_logger.ts
16585
16712
  var DD_RUM_TOKEN = true ? "pubf59c0182545bfb4c299175119f1abf9b" : "";
16586
- var CLI_VERSION = true ? "1.2.56" : "unknown";
16713
+ var CLI_VERSION = true ? "1.2.58" : "unknown";
16587
16714
  var NAMESPACE = "mobbdev-claude-code-hook-logs";
16588
16715
  function createHookLogger(scopePath) {
16589
16716
  return createLogger({
@@ -16607,13 +16734,13 @@ function flushLogs() {
16607
16734
  for (const scoped of activeScopedLoggers) {
16608
16735
  scoped.flushLogs();
16609
16736
  }
16610
- activeScopedLoggers.length = 0;
16611
16737
  }
16612
16738
  async function flushDdLogs() {
16613
16739
  await logger.flushDdAsync();
16614
16740
  for (const scoped of activeScopedLoggers) {
16615
16741
  await scoped.flushDdAsync();
16616
16742
  }
16743
+ activeScopedLoggers.length = 0;
16617
16744
  }
16618
16745
  function createScopedHookLog(scopePath) {
16619
16746
  const scoped = createHookLogger(scopePath);
@@ -16621,11 +16748,138 @@ function createScopedHookLog(scopePath) {
16621
16748
  return scoped;
16622
16749
  }
16623
16750
 
16751
+ // src/features/claude_code/install_hook.ts
16752
+ import fsPromises4 from "fs/promises";
16753
+ import os5 from "os";
16754
+ import path13 from "path";
16755
+ import chalk11 from "chalk";
16756
+ var CLAUDE_SETTINGS_PATH = path13.join(os5.homedir(), ".claude", "settings.json");
16757
+ var RECOMMENDED_MATCHER = "Bash|Write|Edit|Agent|Read";
16758
+ var STALE_MATCHERS = /* @__PURE__ */ new Set(["", "*", "Edit|Write"]);
16759
+ async function claudeSettingsExists() {
16760
+ try {
16761
+ await fsPromises4.access(CLAUDE_SETTINGS_PATH);
16762
+ return true;
16763
+ } catch {
16764
+ return false;
16765
+ }
16766
+ }
16767
+ async function readClaudeSettings() {
16768
+ const settingsContent = await fsPromises4.readFile(
16769
+ CLAUDE_SETTINGS_PATH,
16770
+ "utf-8"
16771
+ );
16772
+ return JSON.parse(settingsContent);
16773
+ }
16774
+ async function writeClaudeSettings(settings) {
16775
+ await fsPromises4.writeFile(
16776
+ CLAUDE_SETTINGS_PATH,
16777
+ JSON.stringify(settings, null, 2),
16778
+ "utf-8"
16779
+ );
16780
+ }
16781
+ async function autoUpgradeMatcherIfStale() {
16782
+ try {
16783
+ if (!await claudeSettingsExists()) return false;
16784
+ const settings = await readClaudeSettings();
16785
+ const hooks = settings.hooks?.PostToolUse;
16786
+ if (!hooks) return false;
16787
+ let upgraded = false;
16788
+ for (const hook of hooks) {
16789
+ const isMobbHook = hook.hooks.some(
16790
+ (h) => h.command?.includes("claude-code-process-hook") && (h.command?.includes("mobbdev") || h.command?.includes("mobbdev@"))
16791
+ );
16792
+ if (isMobbHook && STALE_MATCHERS.has(hook.matcher)) {
16793
+ hook.matcher = RECOMMENDED_MATCHER;
16794
+ upgraded = true;
16795
+ }
16796
+ }
16797
+ if (upgraded) {
16798
+ await writeClaudeSettings(settings);
16799
+ }
16800
+ return upgraded;
16801
+ } catch {
16802
+ return false;
16803
+ }
16804
+ }
16805
+ async function installMobbHooks(options = {}) {
16806
+ console.log(chalk11.blue("Installing Mobb hooks in Claude Code settings..."));
16807
+ if (!await claudeSettingsExists()) {
16808
+ console.log(chalk11.red("\u274C Claude Code settings file not found"));
16809
+ console.log(chalk11.yellow(`Expected location: ${CLAUDE_SETTINGS_PATH}`));
16810
+ console.log(chalk11.yellow("Is Claude Code installed on your system?"));
16811
+ console.log(chalk11.yellow("Please install Claude Code and try again."));
16812
+ throw new Error(
16813
+ "Claude Code settings file not found. Is Claude Code installed?"
16814
+ );
16815
+ }
16816
+ const settings = await readClaudeSettings();
16817
+ if (!settings.hooks) {
16818
+ settings.hooks = {};
16819
+ }
16820
+ if (!settings.hooks.PostToolUse) {
16821
+ settings.hooks.PostToolUse = [];
16822
+ }
16823
+ let command = "npx --yes mobbdev@latest claude-code-process-hook";
16824
+ if (options.saveEnv) {
16825
+ const envVars = [];
16826
+ if (process.env["WEB_APP_URL"]) {
16827
+ envVars.push(`WEB_APP_URL="${process.env["WEB_APP_URL"]}"`);
16828
+ }
16829
+ if (process.env["API_URL"]) {
16830
+ envVars.push(`API_URL="${process.env["API_URL"]}"`);
16831
+ }
16832
+ if (envVars.length > 0) {
16833
+ command = `${envVars.join(" ")} ${command}`;
16834
+ console.log(
16835
+ chalk11.blue(
16836
+ `Adding environment variables to hook command: ${envVars.join(", ")}`
16837
+ )
16838
+ );
16839
+ }
16840
+ }
16841
+ const mobbHookConfig = {
16842
+ // Only fire on tools that indicate meaningful work — skip high-frequency
16843
+ // read-only tools (Grep, Glob, WebSearch, WebFetch) to reduce CPU overhead
16844
+ // from process startup (~1.7s user CPU per invocation).
16845
+ matcher: RECOMMENDED_MATCHER,
16846
+ hooks: [
16847
+ {
16848
+ type: "command",
16849
+ command
16850
+ }
16851
+ ]
16852
+ };
16853
+ const existingHookIndex = settings.hooks.PostToolUse.findIndex(
16854
+ (hook) => hook.hooks.some(
16855
+ (h) => h.command?.includes("mobbdev@latest claude-code-process-hook")
16856
+ )
16857
+ );
16858
+ if (existingHookIndex >= 0) {
16859
+ console.log(chalk11.yellow("Mobb hook already exists, updating..."));
16860
+ settings.hooks.PostToolUse[existingHookIndex] = mobbHookConfig;
16861
+ } else {
16862
+ console.log(chalk11.green("Adding new Mobb hook..."));
16863
+ settings.hooks.PostToolUse.push(mobbHookConfig);
16864
+ }
16865
+ await writeClaudeSettings(settings);
16866
+ console.log(
16867
+ chalk11.green(
16868
+ `\u2705 Mobb hooks ${options.saveEnv ? "and environment variables " : ""}installed successfully in ${CLAUDE_SETTINGS_PATH}`
16869
+ )
16870
+ );
16871
+ }
16872
+
16624
16873
  // src/features/claude_code/data_collector.ts
16625
- var HOOK_COOLDOWN_MS = 1e4;
16874
+ var GLOBAL_COOLDOWN_MS = 5e3;
16875
+ var HOOK_COOLDOWN_MS = 15e3;
16876
+ var ACTIVE_LOCK_TTL_MS = 6e4;
16877
+ var GQL_AUTH_TIMEOUT_MS = 15e3;
16626
16878
  var STALE_KEY_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
16627
16879
  var CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1e3;
16880
+ var MAX_ENTRIES_PER_INVOCATION = 50;
16628
16881
  var COOLDOWN_KEY = "lastHookRunAt";
16882
+ var ACTIVE_KEY = "hookActiveAt";
16629
16883
  var HookDataSchema = z33.object({
16630
16884
  session_id: z33.string(),
16631
16885
  transcript_path: z33.string(),
@@ -16658,9 +16912,12 @@ async function readStdinData() {
16658
16912
  clearTimeout(timer);
16659
16913
  try {
16660
16914
  const parsedData = JSON.parse(inputData);
16661
- hookLog.debug("Parsed stdin data", {
16662
- keys: Object.keys(parsedData)
16663
- });
16915
+ hookLog.debug(
16916
+ {
16917
+ data: { keys: Object.keys(parsedData) }
16918
+ },
16919
+ "Parsed stdin data"
16920
+ );
16664
16921
  resolve(parsedData);
16665
16922
  } catch (error) {
16666
16923
  const msg = `Failed to parse JSON from stdin: ${error.message}`;
@@ -16672,7 +16929,10 @@ async function readStdinData() {
16672
16929
  if (settled) return;
16673
16930
  settled = true;
16674
16931
  clearTimeout(timer);
16675
- hookLog.error("Error reading from stdin", { error: error.message });
16932
+ hookLog.error(
16933
+ { data: { error: error.message } },
16934
+ "Error reading from stdin"
16935
+ );
16676
16936
  reject(new Error(`Error reading from stdin: ${error.message}`));
16677
16937
  });
16678
16938
  });
@@ -16689,7 +16949,63 @@ function getCursorKey(transcriptPath) {
16689
16949
  const hash = createHash2("sha256").update(transcriptPath).digest("hex").slice(0, 12);
16690
16950
  return `cursor.${hash}`;
16691
16951
  }
16952
+ async function resolveTranscriptPath(transcriptPath, sessionId) {
16953
+ try {
16954
+ await access(transcriptPath);
16955
+ return transcriptPath;
16956
+ } catch {
16957
+ }
16958
+ const filename = path14.basename(transcriptPath);
16959
+ const dirName = path14.basename(path14.dirname(transcriptPath));
16960
+ const projectsDir = path14.dirname(path14.dirname(transcriptPath));
16961
+ const baseDirName = dirName.replace(/[-.]claude-worktrees-.+$/, "");
16962
+ if (baseDirName !== dirName) {
16963
+ const candidate = path14.join(projectsDir, baseDirName, filename);
16964
+ try {
16965
+ await access(candidate);
16966
+ hookLog.info(
16967
+ {
16968
+ data: {
16969
+ original: transcriptPath,
16970
+ resolved: candidate,
16971
+ sessionId,
16972
+ method: "worktree-strip"
16973
+ }
16974
+ },
16975
+ "Transcript path resolved via fallback"
16976
+ );
16977
+ return candidate;
16978
+ } catch {
16979
+ }
16980
+ }
16981
+ try {
16982
+ const dirs = await readdir(projectsDir);
16983
+ for (const dir of dirs) {
16984
+ if (dir === dirName) continue;
16985
+ const candidate = path14.join(projectsDir, dir, filename);
16986
+ try {
16987
+ await access(candidate);
16988
+ hookLog.info(
16989
+ {
16990
+ data: {
16991
+ original: transcriptPath,
16992
+ resolved: candidate,
16993
+ sessionId,
16994
+ method: "sibling-scan"
16995
+ }
16996
+ },
16997
+ "Transcript path resolved via fallback"
16998
+ );
16999
+ return candidate;
17000
+ } catch {
17001
+ }
17002
+ }
17003
+ } catch {
17004
+ }
17005
+ return transcriptPath;
17006
+ }
16692
17007
  async function readNewTranscriptEntries(transcriptPath, sessionId, sessionStore) {
17008
+ transcriptPath = await resolveTranscriptPath(transcriptPath, sessionId);
16693
17009
  const cursor = sessionStore.get(getCursorKey(transcriptPath));
16694
17010
  let content;
16695
17011
  let fileSize;
@@ -16700,8 +17016,12 @@ async function readNewTranscriptEntries(transcriptPath, sessionId, sessionStore)
16700
17016
  const stat = await fh.stat();
16701
17017
  fileSize = stat.size;
16702
17018
  if (cursor.byteOffset >= stat.size) {
16703
- hookLog.info("No new data in transcript file", { sessionId });
16704
- return { entries: [], fileSize };
17019
+ hookLog.info({ data: { sessionId } }, "No new data in transcript file");
17020
+ return {
17021
+ entries: [],
17022
+ endByteOffset: fileSize,
17023
+ resolvedTranscriptPath: transcriptPath
17024
+ };
16705
17025
  }
16706
17026
  const buf = Buffer.alloc(stat.size - cursor.byteOffset);
16707
17027
  await fh.read(buf, 0, buf.length, cursor.byteOffset);
@@ -16710,53 +17030,88 @@ async function readNewTranscriptEntries(transcriptPath, sessionId, sessionStore)
16710
17030
  await fh.close();
16711
17031
  }
16712
17032
  lineIndexOffset = cursor.byteOffset;
16713
- hookLog.debug("Read transcript file from offset", {
16714
- transcriptPath,
16715
- byteOffset: cursor.byteOffset,
16716
- bytesRead: content.length
16717
- });
17033
+ hookLog.debug(
17034
+ {
17035
+ data: {
17036
+ transcriptPath,
17037
+ byteOffset: cursor.byteOffset,
17038
+ bytesRead: content.length
17039
+ }
17040
+ },
17041
+ "Read transcript file from offset"
17042
+ );
16718
17043
  } else {
16719
17044
  content = await readFile(transcriptPath, "utf-8");
16720
17045
  fileSize = Buffer.byteLength(content, "utf-8");
16721
17046
  lineIndexOffset = 0;
16722
- hookLog.debug("Read full transcript file", {
16723
- transcriptPath,
16724
- totalBytes: fileSize
16725
- });
17047
+ hookLog.debug(
17048
+ { data: { transcriptPath, totalBytes: fileSize } },
17049
+ "Read full transcript file"
17050
+ );
16726
17051
  }
16727
- const lines = content.split("\n").filter((line) => line.trim().length > 0);
17052
+ const startOffset = cursor?.byteOffset ?? 0;
17053
+ const allLines = content.split("\n");
16728
17054
  const parsed = [];
16729
17055
  let malformedLines = 0;
16730
- for (let i = 0; i < lines.length; i++) {
17056
+ let bytesConsumed = 0;
17057
+ let parsedLineIndex = 0;
17058
+ for (let i = 0; i < allLines.length; i++) {
17059
+ const line = allLines[i];
17060
+ const lineBytes = Buffer.byteLength(line, "utf-8") + (i < allLines.length - 1 ? 1 : 0);
17061
+ if (parsed.length >= MAX_ENTRIES_PER_INVOCATION) break;
17062
+ bytesConsumed += lineBytes;
17063
+ if (line.trim().length === 0) continue;
16731
17064
  try {
16732
- const entry = JSON.parse(lines[i]);
17065
+ const entry = JSON.parse(line);
16733
17066
  const recordId = entry.uuid ?? generateSyntheticId(
16734
17067
  entry.sessionId,
16735
17068
  entry.timestamp,
16736
17069
  entry.type,
16737
- lineIndexOffset + i
17070
+ lineIndexOffset + parsedLineIndex
16738
17071
  );
16739
17072
  parsed.push({ ...entry, _recordId: recordId });
16740
17073
  } catch {
16741
17074
  malformedLines++;
16742
17075
  }
17076
+ parsedLineIndex++;
16743
17077
  }
17078
+ const endByteOffset = startOffset + bytesConsumed;
17079
+ const capped = parsed.length >= MAX_ENTRIES_PER_INVOCATION;
16744
17080
  if (malformedLines > 0) {
16745
- hookLog.warn("Skipped malformed lines", { malformedLines, transcriptPath });
17081
+ hookLog.warn(
17082
+ { data: { malformedLines, transcriptPath } },
17083
+ "Skipped malformed lines"
17084
+ );
16746
17085
  }
16747
- if (!cursor) {
16748
- hookLog.info("First invocation for session \u2014 uploading all entries", {
16749
- sessionId,
16750
- totalEntries: parsed.length
16751
- });
17086
+ if (capped) {
17087
+ hookLog.info(
17088
+ {
17089
+ data: {
17090
+ sessionId,
17091
+ entriesParsed: parsed.length,
17092
+ totalLines: allLines.length
17093
+ }
17094
+ },
17095
+ "Capped at MAX_ENTRIES_PER_INVOCATION, remaining entries deferred"
17096
+ );
17097
+ } else if (!cursor) {
17098
+ hookLog.info(
17099
+ { data: { sessionId, totalEntries: parsed.length } },
17100
+ "First invocation for session \u2014 uploading all entries"
17101
+ );
16752
17102
  } else {
16753
- hookLog.info("Resuming from byte offset", {
16754
- sessionId,
16755
- byteOffset: cursor.byteOffset,
16756
- newEntries: parsed.length
16757
- });
17103
+ hookLog.info(
17104
+ {
17105
+ data: { sessionId, byteOffset: startOffset, newEntries: parsed.length }
17106
+ },
17107
+ "Resuming from byte offset"
17108
+ );
16758
17109
  }
16759
- return { entries: parsed, fileSize };
17110
+ return {
17111
+ entries: parsed,
17112
+ endByteOffset,
17113
+ resolvedTranscriptPath: transcriptPath
17114
+ };
16760
17115
  }
16761
17116
  var FILTERED_PROGRESS_SUBTYPES = /* @__PURE__ */ new Set([
16762
17117
  // Incremental streaming output from running bash commands, emitted every
@@ -16824,13 +17179,13 @@ async function cleanupStaleSessions(sessionStore) {
16824
17179
  }
16825
17180
  const now = Date.now();
16826
17181
  const prefix = getSessionFilePrefix();
16827
- const configDir = path13.dirname(sessionStore.path);
17182
+ const configDir = path14.dirname(sessionStore.path);
16828
17183
  try {
16829
17184
  const files = await readdir(configDir);
16830
17185
  let deletedCount = 0;
16831
17186
  for (const file of files) {
16832
17187
  if (!file.startsWith(prefix) || !file.endsWith(".json")) continue;
16833
- const filePath = path13.join(configDir, file);
17188
+ const filePath = path14.join(configDir, file);
16834
17189
  try {
16835
17190
  const content = JSON.parse(await readFile(filePath, "utf-8"));
16836
17191
  let newest = 0;
@@ -16851,7 +17206,7 @@ async function cleanupStaleSessions(sessionStore) {
16851
17206
  }
16852
17207
  }
16853
17208
  if (deletedCount > 0) {
16854
- hookLog.info("Cleaned up stale session files", { deletedCount });
17209
+ hookLog.info({ data: { deletedCount } }, "Cleaned up stale session files");
16855
17210
  }
16856
17211
  } catch {
16857
17212
  }
@@ -16859,45 +17214,92 @@ async function cleanupStaleSessions(sessionStore) {
16859
17214
  }
16860
17215
  async function processAndUploadTranscriptEntries() {
16861
17216
  hookLog.info("Hook invoked");
17217
+ const globalLastRun = configStore.get("claudeCode.globalLastHookRunAt");
17218
+ const globalNow = Date.now();
17219
+ if (globalLastRun && globalNow - globalLastRun < GLOBAL_COOLDOWN_MS) {
17220
+ return { entriesUploaded: 0, entriesSkipped: 0, errors: 0 };
17221
+ }
17222
+ configStore.set("claudeCode.globalLastHookRunAt", globalNow);
17223
+ const lastUpgradeVersion = configStore.get(
17224
+ "claudeCode.matcherUpgradeVersion"
17225
+ );
17226
+ if (lastUpgradeVersion !== packageJson.version) {
17227
+ const upgraded = await autoUpgradeMatcherIfStale();
17228
+ configStore.set("claudeCode.matcherUpgradeVersion", packageJson.version);
17229
+ if (upgraded) {
17230
+ hookLog.info("Auto-upgraded hook matcher to reduce CPU usage");
17231
+ }
17232
+ }
16862
17233
  const rawData = await readStdinData();
16863
17234
  const hookData = validateHookData(rawData);
16864
17235
  const sessionStore = createSessionConfigStore(hookData.session_id);
16865
17236
  await cleanupStaleSessions(sessionStore);
17237
+ const now = Date.now();
16866
17238
  const lastRunAt = sessionStore.get(COOLDOWN_KEY);
16867
- if (lastRunAt && Date.now() - lastRunAt < HOOK_COOLDOWN_MS) {
17239
+ if (lastRunAt && now - lastRunAt < HOOK_COOLDOWN_MS) {
17240
+ return { entriesUploaded: 0, entriesSkipped: 0, errors: 0 };
17241
+ }
17242
+ const activeAt = sessionStore.get(ACTIVE_KEY);
17243
+ if (activeAt && now - activeAt < ACTIVE_LOCK_TTL_MS) {
17244
+ const activeDuration = now - activeAt;
17245
+ if (activeDuration > HOOK_COOLDOWN_MS) {
17246
+ hookLog.warn(
17247
+ {
17248
+ data: {
17249
+ activeDurationMs: activeDuration,
17250
+ sessionId: hookData.session_id
17251
+ }
17252
+ },
17253
+ "Hook still active \u2014 possible slow upload or hung process"
17254
+ );
17255
+ }
16868
17256
  return { entriesUploaded: 0, entriesSkipped: 0, errors: 0 };
16869
17257
  }
16870
- sessionStore.set(COOLDOWN_KEY, Date.now());
17258
+ sessionStore.set(ACTIVE_KEY, now);
17259
+ sessionStore.set(COOLDOWN_KEY, now);
16871
17260
  const log2 = createScopedHookLog(hookData.cwd);
16872
- log2.info("Hook data validated", {
16873
- sessionId: hookData.session_id,
16874
- toolName: hookData.tool_name,
16875
- hookEvent: hookData.hook_event_name,
16876
- cwd: hookData.cwd
16877
- });
17261
+ log2.info(
17262
+ {
17263
+ data: {
17264
+ sessionId: hookData.session_id,
17265
+ toolName: hookData.tool_name,
17266
+ hookEvent: hookData.hook_event_name,
17267
+ cwd: hookData.cwd
17268
+ }
17269
+ },
17270
+ "Hook data validated"
17271
+ );
16878
17272
  try {
16879
17273
  return await processTranscript(hookData, sessionStore, log2);
16880
17274
  } finally {
17275
+ sessionStore.delete(ACTIVE_KEY);
16881
17276
  log2.flushLogs();
16882
17277
  }
16883
17278
  }
16884
17279
  async function processTranscript(hookData, sessionStore, log2) {
16885
- const cursorKey = getCursorKey(hookData.transcript_path);
16886
- const { entries: rawEntries, fileSize } = await readNewTranscriptEntries(
16887
- hookData.transcript_path,
16888
- hookData.session_id,
16889
- sessionStore
17280
+ const {
17281
+ entries: rawEntries,
17282
+ endByteOffset,
17283
+ resolvedTranscriptPath
17284
+ } = await log2.timed(
17285
+ "Read transcript",
17286
+ () => readNewTranscriptEntries(
17287
+ hookData.transcript_path,
17288
+ hookData.session_id,
17289
+ sessionStore
17290
+ )
16890
17291
  );
17292
+ const cursorKey = getCursorKey(resolvedTranscriptPath);
16891
17293
  if (rawEntries.length === 0) {
16892
17294
  log2.info("No new entries to upload");
16893
17295
  return { entriesUploaded: 0, entriesSkipped: 0, errors: 0 };
16894
17296
  }
16895
17297
  const { filtered: entries, filteredOut } = filterEntries(rawEntries);
16896
17298
  if (filteredOut > 0) {
16897
- log2.info("Filtered out noise entries", {
16898
- filteredOut,
16899
- remaining: entries.length
16900
- });
17299
+ log2.info(
17300
+ { data: { filteredOut, remaining: entries.length } },
17301
+ "Filtered out noise entries"
17302
+ );
16901
17303
  }
16902
17304
  if (entries.length === 0) {
16903
17305
  log2.info("All entries filtered out, nothing to upload");
@@ -16905,7 +17307,7 @@ async function processTranscript(hookData, sessionStore, log2) {
16905
17307
  const prevCursor = sessionStore.get(cursorKey);
16906
17308
  const cursor = {
16907
17309
  id: lastEntry._recordId,
16908
- byteOffset: fileSize,
17310
+ byteOffset: endByteOffset,
16909
17311
  updatedAt: Date.now(),
16910
17312
  lastModel: prevCursor?.lastModel
16911
17313
  };
@@ -16916,10 +17318,32 @@ async function processTranscript(hookData, sessionStore, log2) {
16916
17318
  errors: 0
16917
17319
  };
16918
17320
  }
16919
- const gqlClient = await log2.timed(
16920
- "GQL auth",
16921
- () => getAuthenticatedGQLClient({ isSkipPrompts: true })
16922
- );
17321
+ let gqlClient;
17322
+ try {
17323
+ gqlClient = await log2.timed(
17324
+ "GQL auth",
17325
+ () => withTimeout(
17326
+ getAuthenticatedGQLClient({ isSkipPrompts: true }),
17327
+ GQL_AUTH_TIMEOUT_MS,
17328
+ "GQL auth"
17329
+ )
17330
+ );
17331
+ } catch (err) {
17332
+ log2.error(
17333
+ {
17334
+ data: {
17335
+ error: String(err),
17336
+ stack: err instanceof Error ? err.stack : void 0
17337
+ }
17338
+ },
17339
+ "GQL auth failed"
17340
+ );
17341
+ return {
17342
+ entriesUploaded: 0,
17343
+ entriesSkipped: filteredOut,
17344
+ errors: entries.length
17345
+ };
17346
+ }
16923
17347
  const cursorForModel = sessionStore.get(cursorKey);
16924
17348
  let lastSeenModel = cursorForModel?.lastModel ?? null;
16925
17349
  const records = entries.map((entry) => {
@@ -16943,21 +17367,33 @@ async function processTranscript(hookData, sessionStore, log2) {
16943
17367
  rawData: rawEntry
16944
17368
  };
16945
17369
  });
16946
- log2.info("Uploading batch", {
16947
- count: records.length,
16948
- skipped: filteredOut,
16949
- firstRecordId: records[0]?.recordId,
16950
- lastRecordId: records[records.length - 1]?.recordId
16951
- });
17370
+ const totalRawDataBytes = records.reduce((sum, r) => {
17371
+ return sum + (r.rawData ? JSON.stringify(r.rawData).length : 0);
17372
+ }, 0);
17373
+ log2.info(
17374
+ {
17375
+ data: {
17376
+ count: records.length,
17377
+ skipped: filteredOut,
17378
+ rawDataBytes: totalRawDataBytes,
17379
+ firstRecordId: records[0]?.recordId,
17380
+ lastRecordId: records[records.length - 1]?.recordId
17381
+ }
17382
+ },
17383
+ "Uploading batch"
17384
+ );
17385
+ const sanitize = process.env["MOBBDEV_HOOK_SANITIZE"] === "1";
16952
17386
  const result = await log2.timed(
16953
17387
  "Batch upload",
16954
- () => prepareAndSendTracyRecords(gqlClient, records, hookData.cwd)
17388
+ () => prepareAndSendTracyRecords(gqlClient, records, hookData.cwd, {
17389
+ sanitize
17390
+ })
16955
17391
  );
16956
17392
  if (result.ok) {
16957
17393
  const lastRawEntry = rawEntries[rawEntries.length - 1];
16958
17394
  const cursor = {
16959
17395
  id: lastRawEntry._recordId,
16960
- byteOffset: fileSize,
17396
+ byteOffset: endByteOffset,
16961
17397
  updatedAt: Date.now(),
16962
17398
  lastModel: lastSeenModel ?? void 0
16963
17399
  };
@@ -16972,7 +17408,10 @@ async function processTranscript(hookData, sessionStore, log2) {
16972
17408
  errors: 0
16973
17409
  };
16974
17410
  }
16975
- log2.error("Batch upload had errors", { errors: result.errors });
17411
+ log2.error(
17412
+ { data: { errors: result.errors, recordCount: entries.length } },
17413
+ "Batch upload had errors"
17414
+ );
16976
17415
  return {
16977
17416
  entriesUploaded: 0,
16978
17417
  entriesSkipped: filteredOut,
@@ -16980,100 +17419,6 @@ async function processTranscript(hookData, sessionStore, log2) {
16980
17419
  };
16981
17420
  }
16982
17421
 
16983
- // src/features/claude_code/install_hook.ts
16984
- import fsPromises4 from "fs/promises";
16985
- import os5 from "os";
16986
- import path14 from "path";
16987
- import chalk11 from "chalk";
16988
- var CLAUDE_SETTINGS_PATH = path14.join(os5.homedir(), ".claude", "settings.json");
16989
- async function claudeSettingsExists() {
16990
- try {
16991
- await fsPromises4.access(CLAUDE_SETTINGS_PATH);
16992
- return true;
16993
- } catch {
16994
- return false;
16995
- }
16996
- }
16997
- async function readClaudeSettings() {
16998
- const settingsContent = await fsPromises4.readFile(
16999
- CLAUDE_SETTINGS_PATH,
17000
- "utf-8"
17001
- );
17002
- return JSON.parse(settingsContent);
17003
- }
17004
- async function writeClaudeSettings(settings) {
17005
- await fsPromises4.writeFile(
17006
- CLAUDE_SETTINGS_PATH,
17007
- JSON.stringify(settings, null, 2),
17008
- "utf-8"
17009
- );
17010
- }
17011
- async function installMobbHooks(options = {}) {
17012
- console.log(chalk11.blue("Installing Mobb hooks in Claude Code settings..."));
17013
- if (!await claudeSettingsExists()) {
17014
- console.log(chalk11.red("\u274C Claude Code settings file not found"));
17015
- console.log(chalk11.yellow(`Expected location: ${CLAUDE_SETTINGS_PATH}`));
17016
- console.log(chalk11.yellow("Is Claude Code installed on your system?"));
17017
- console.log(chalk11.yellow("Please install Claude Code and try again."));
17018
- throw new Error(
17019
- "Claude Code settings file not found. Is Claude Code installed?"
17020
- );
17021
- }
17022
- const settings = await readClaudeSettings();
17023
- if (!settings.hooks) {
17024
- settings.hooks = {};
17025
- }
17026
- if (!settings.hooks.PostToolUse) {
17027
- settings.hooks.PostToolUse = [];
17028
- }
17029
- let command = "npx --yes mobbdev@latest claude-code-process-hook";
17030
- if (options.saveEnv) {
17031
- const envVars = [];
17032
- if (process.env["WEB_APP_URL"]) {
17033
- envVars.push(`WEB_APP_URL="${process.env["WEB_APP_URL"]}"`);
17034
- }
17035
- if (process.env["API_URL"]) {
17036
- envVars.push(`API_URL="${process.env["API_URL"]}"`);
17037
- }
17038
- if (envVars.length > 0) {
17039
- command = `${envVars.join(" ")} ${command}`;
17040
- console.log(
17041
- chalk11.blue(
17042
- `Adding environment variables to hook command: ${envVars.join(", ")}`
17043
- )
17044
- );
17045
- }
17046
- }
17047
- const mobbHookConfig = {
17048
- // Empty matcher = match all tools (Claude Code hook spec: empty string matches every PostToolUse event)
17049
- matcher: "",
17050
- hooks: [
17051
- {
17052
- type: "command",
17053
- command
17054
- }
17055
- ]
17056
- };
17057
- const existingHookIndex = settings.hooks.PostToolUse.findIndex(
17058
- (hook) => hook.hooks.some(
17059
- (h) => h.command?.includes("mobbdev@latest claude-code-process-hook")
17060
- )
17061
- );
17062
- if (existingHookIndex >= 0) {
17063
- console.log(chalk11.yellow("Mobb hook already exists, updating..."));
17064
- settings.hooks.PostToolUse[existingHookIndex] = mobbHookConfig;
17065
- } else {
17066
- console.log(chalk11.green("Adding new Mobb hook..."));
17067
- settings.hooks.PostToolUse.push(mobbHookConfig);
17068
- }
17069
- await writeClaudeSettings(settings);
17070
- console.log(
17071
- chalk11.green(
17072
- `\u2705 Mobb hooks ${options.saveEnv ? "and environment variables " : ""}installed successfully in ${CLAUDE_SETTINGS_PATH}`
17073
- )
17074
- );
17075
- }
17076
-
17077
17422
  // src/args/commands/claude_code.ts
17078
17423
  var claudeCodeInstallHookBuilder = (yargs2) => {
17079
17424
  return yargs2.option("save-env", {
@@ -17105,43 +17450,67 @@ var claudeCodeInstallHookHandler = async (argv) => {
17105
17450
  }
17106
17451
  };
17107
17452
  var claudeCodeProcessHookHandler = async () => {
17453
+ const startupMs = Math.round(process.uptime() * 1e3);
17454
+ const debugMode = process.env["MOBBDEV_HOOK_DEBUG"] === "1";
17108
17455
  async function flushAndExit(code) {
17109
17456
  try {
17110
17457
  flushLogs();
17111
17458
  await flushDdLogs();
17112
17459
  } catch {
17113
17460
  } finally {
17114
- process.exit(code);
17461
+ process.exit(debugMode ? code : 0);
17115
17462
  }
17116
17463
  }
17117
17464
  process.on("uncaughtException", (error) => {
17118
- hookLog.error("Uncaught exception in hook", {
17119
- error: String(error),
17120
- stack: error.stack
17121
- });
17465
+ hookLog.error(
17466
+ { data: { error: String(error), stack: error.stack } },
17467
+ "Uncaught exception in hook"
17468
+ );
17122
17469
  void flushAndExit(1);
17123
17470
  });
17124
17471
  process.on("unhandledRejection", (reason) => {
17125
- hookLog.error("Unhandled rejection in hook", {
17126
- error: String(reason),
17127
- stack: reason instanceof Error ? reason.stack : void 0
17128
- });
17472
+ hookLog.error(
17473
+ {
17474
+ data: {
17475
+ error: String(reason),
17476
+ stack: reason instanceof Error ? reason.stack : void 0
17477
+ }
17478
+ },
17479
+ "Unhandled rejection in hook"
17480
+ );
17129
17481
  void flushAndExit(1);
17130
17482
  });
17131
17483
  let exitCode = 0;
17484
+ const hookStart = Date.now();
17132
17485
  try {
17133
17486
  const result = await processAndUploadTranscriptEntries();
17134
- hookLog.info("Claude Code upload complete", {
17135
- entriesUploaded: result.entriesUploaded,
17136
- entriesSkipped: result.entriesSkipped,
17137
- errors: result.errors
17138
- });
17487
+ if (result.errors > 0) {
17488
+ exitCode = 1;
17489
+ }
17490
+ hookLog.info(
17491
+ {
17492
+ data: {
17493
+ entriesUploaded: result.entriesUploaded,
17494
+ entriesSkipped: result.entriesSkipped,
17495
+ errors: result.errors,
17496
+ startupMs,
17497
+ durationMs: Date.now() - hookStart
17498
+ }
17499
+ },
17500
+ "Claude Code upload complete"
17501
+ );
17139
17502
  } catch (error) {
17140
17503
  exitCode = 1;
17141
- hookLog.error("Failed to process Claude Code hook", {
17142
- error: String(error),
17143
- stack: error instanceof Error ? error.stack : void 0
17144
- });
17504
+ hookLog.error(
17505
+ {
17506
+ data: {
17507
+ error: String(error),
17508
+ stack: error instanceof Error ? error.stack : void 0,
17509
+ durationMs: Date.now() - hookStart
17510
+ }
17511
+ },
17512
+ "Failed to process Claude Code hook"
17513
+ );
17145
17514
  }
17146
17515
  await flushAndExit(exitCode);
17147
17516
  };