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/args/commands/upload_ai_blame.mjs +36 -6
- package/dist/index.mjs +670 -301
- package/package.json +1 -1
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().
|
|
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
|
|
6725
|
+
origin
|
|
6699
6726
|
}) {
|
|
6700
|
-
const orgUrl = `${
|
|
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
|
|
6789
|
+
const { owner, origin, prefixPath } = parseAdoOwnerAndRepo(url);
|
|
6763
6790
|
return {
|
|
6764
6791
|
org: owner,
|
|
6765
|
-
origin: prefixPath ? `${
|
|
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
|
|
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(
|
|
6794
|
-
return _getPublicAdoClient({ orgName, origin
|
|
6820
|
+
params.tokenType === "PAT" /* PAT */ && params.patTokenOrg !== orgName && isAdoOnCloud(origin)) {
|
|
6821
|
+
return _getPublicAdoClient({ orgName, origin });
|
|
6795
6822
|
}
|
|
6796
|
-
const orgUrl = `${
|
|
6823
|
+
const orgUrl = `${origin}/${orgName}`;
|
|
6797
6824
|
if (params.tokenType === "OAUTH" /* OAUTH */) {
|
|
6798
|
-
if (!isAdoOnCloud(
|
|
6825
|
+
if (!isAdoOnCloud(origin)) {
|
|
6799
6826
|
throw new Error(
|
|
6800
|
-
`Oauth token is not supported for ADO on prem - ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}`,
|
|
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
|
-
|
|
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 &&
|
|
7354
|
-
orgs = [
|
|
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
|
|
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
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
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
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
}
|
|
10161
|
-
}
|
|
10162
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
13682
|
-
|
|
13683
|
-
|
|
13684
|
-
|
|
13685
|
-
|
|
13686
|
-
|
|
13687
|
-
|
|
13688
|
-
|
|
13689
|
-
...rest
|
|
13690
|
-
|
|
13691
|
-
|
|
13692
|
-
|
|
13693
|
-
|
|
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(
|
|
13700
|
-
|
|
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: [
|
|
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 {
|
|
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
|
|
13723
|
-
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
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
|
|
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
|
|
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
|
|
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: [
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
16662
|
-
|
|
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(
|
|
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"
|
|
16704
|
-
return {
|
|
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(
|
|
16714
|
-
|
|
16715
|
-
|
|
16716
|
-
|
|
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(
|
|
16723
|
-
transcriptPath,
|
|
16724
|
-
|
|
16725
|
-
|
|
17047
|
+
hookLog.debug(
|
|
17048
|
+
{ data: { transcriptPath, totalBytes: fileSize } },
|
|
17049
|
+
"Read full transcript file"
|
|
17050
|
+
);
|
|
16726
17051
|
}
|
|
16727
|
-
const
|
|
17052
|
+
const startOffset = cursor?.byteOffset ?? 0;
|
|
17053
|
+
const allLines = content.split("\n");
|
|
16728
17054
|
const parsed = [];
|
|
16729
17055
|
let malformedLines = 0;
|
|
16730
|
-
|
|
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(
|
|
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 +
|
|
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(
|
|
17081
|
+
hookLog.warn(
|
|
17082
|
+
{ data: { malformedLines, transcriptPath } },
|
|
17083
|
+
"Skipped malformed lines"
|
|
17084
|
+
);
|
|
16746
17085
|
}
|
|
16747
|
-
if (
|
|
16748
|
-
hookLog.info(
|
|
16749
|
-
|
|
16750
|
-
|
|
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(
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
|
|
16757
|
-
|
|
17103
|
+
hookLog.info(
|
|
17104
|
+
{
|
|
17105
|
+
data: { sessionId, byteOffset: startOffset, newEntries: parsed.length }
|
|
17106
|
+
},
|
|
17107
|
+
"Resuming from byte offset"
|
|
17108
|
+
);
|
|
16758
17109
|
}
|
|
16759
|
-
return {
|
|
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 =
|
|
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 =
|
|
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"
|
|
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 &&
|
|
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(
|
|
17258
|
+
sessionStore.set(ACTIVE_KEY, now);
|
|
17259
|
+
sessionStore.set(COOLDOWN_KEY, now);
|
|
16871
17260
|
const log2 = createScopedHookLog(hookData.cwd);
|
|
16872
|
-
log2.info(
|
|
16873
|
-
|
|
16874
|
-
|
|
16875
|
-
|
|
16876
|
-
|
|
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
|
|
16886
|
-
|
|
16887
|
-
|
|
16888
|
-
|
|
16889
|
-
|
|
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(
|
|
16898
|
-
filteredOut,
|
|
16899
|
-
|
|
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:
|
|
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
|
-
|
|
16920
|
-
|
|
16921
|
-
|
|
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
|
-
|
|
16947
|
-
|
|
16948
|
-
|
|
16949
|
-
|
|
16950
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
17119
|
-
error: String(error),
|
|
17120
|
-
|
|
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(
|
|
17126
|
-
|
|
17127
|
-
|
|
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
|
-
|
|
17135
|
-
|
|
17136
|
-
|
|
17137
|
-
|
|
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(
|
|
17142
|
-
|
|
17143
|
-
|
|
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
|
};
|