mobbdev 1.4.27 → 1.4.28

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.
@@ -221,13 +221,18 @@ type RepoState = {
221
221
  * `GitService.getCurrentRepoState()` so detached-HEAD handling and SHA
222
222
  * normalization stay in lockstep across the two clients.
223
223
  *
224
- * Never throws non-existent dirs, missing git binaries, and unrecognized
225
- * remotes all resolve to nulls so the daemon hot path can rely on a value.
224
+ * The repository URL is the canonical form (parseScmURL().canonicalUrl) for any
225
+ * SCM host, including self-hosted / enterprise remotes (scmType "Unknown"); only
226
+ * an absent or genuinely unparseable remote resolves to `repositoryUrl: null`.
227
+ *
228
+ * Never throws — a non-existent dir, missing git binary, or absent/unparseable
229
+ * remote resolves to null so the daemon hot path can rely on a value.
226
230
  */
227
231
  declare function readRepoState(workingDir?: string): Promise<RepoState>;
228
232
  /**
229
- * Gets the normalized GitHub repository URL from the current working directory.
230
- * Returns null if not in a git repository or if not a GitHub repository.
233
+ * Gets the canonical remote repository URL for the working directory's repo, for
234
+ * any SCM host (GitHub, GitLab, Azure DevOps, Bitbucket, and self-hosted /
235
+ * enterprise). Returns null when there is no remote or it can't be parsed.
231
236
  */
232
237
  declare function getRepositoryUrl(workingDir?: string): Promise<string | null>;
233
238
  /**
@@ -7829,6 +7829,19 @@ import Debug8 from "debug";
7829
7829
  import Debug7 from "debug";
7830
7830
  import fetch3, { File, fileFrom, FormData } from "node-fetch";
7831
7831
  var debug8 = Debug7("mobbdev:upload-file");
7832
+ var MIN_UPLOAD_TIMEOUT_MS = 1e4;
7833
+ var MAX_UPLOAD_TIMEOUT_MS = 6e4;
7834
+ var SLOW_LINK_BYTES_PER_MS = 512;
7835
+ function computeUploadTimeoutMs(file) {
7836
+ if (typeof file === "string") {
7837
+ return MAX_UPLOAD_TIMEOUT_MS;
7838
+ }
7839
+ const sizeAllowanceMs = Math.ceil(file.byteLength / SLOW_LINK_BYTES_PER_MS);
7840
+ return Math.min(
7841
+ MAX_UPLOAD_TIMEOUT_MS,
7842
+ MIN_UPLOAD_TIMEOUT_MS + sizeAllowanceMs
7843
+ );
7844
+ }
7832
7845
  var S3UploadError = class extends Error {
7833
7846
  constructor(status, s3Code, s3Message) {
7834
7847
  super(`Failed to upload the file: ${status}`);
@@ -7887,11 +7900,12 @@ async function uploadFile({
7887
7900
  );
7888
7901
  }
7889
7902
  const agent = getProxyAgent(url);
7903
+ const effectiveSignal = signal ?? AbortSignal.timeout(computeUploadTimeoutMs(file));
7890
7904
  const response = await fetch3(url, {
7891
7905
  method: "POST",
7892
7906
  body: form,
7893
7907
  agent,
7894
- signal
7908
+ signal: effectiveSignal
7895
7909
  });
7896
7910
  if (!response.ok) {
7897
7911
  let bodyText = "";
@@ -8468,8 +8482,7 @@ async function readRepoState(workingDir) {
8468
8482
  const repoStatePromise = gitService.getCurrentRepoState().catch(() => ({ branch: null, commitSha: null }));
8469
8483
  const repositoryUrlPromise = gitService.getRemoteUrl().then((url) => {
8470
8484
  if (!url) return null;
8471
- const parsed = parseScmURL(url);
8472
- return parsed?.scmType && parsed.scmType !== "Unknown" ? url : null;
8485
+ return parseScmURL(url)?.canonicalUrl ?? null;
8473
8486
  }).catch(() => null);
8474
8487
  const [{ branch, commitSha }, repositoryUrl] = await Promise.all([
8475
8488
  repoStatePromise,
package/dist/index.mjs CHANGED
@@ -14209,6 +14209,41 @@ init_urlParser2();
14209
14209
  import Debug8 from "debug";
14210
14210
  import fetch3, { File, fileFrom, FormData } from "node-fetch";
14211
14211
  var debug9 = Debug8("mobbdev:upload-file");
14212
+ var MIN_UPLOAD_TIMEOUT_MS = 1e4;
14213
+ var MAX_UPLOAD_TIMEOUT_MS = 6e4;
14214
+ var SLOW_LINK_BYTES_PER_MS = 512;
14215
+ function computeUploadTimeoutMs(file) {
14216
+ if (typeof file === "string") {
14217
+ return MAX_UPLOAD_TIMEOUT_MS;
14218
+ }
14219
+ const sizeAllowanceMs = Math.ceil(file.byteLength / SLOW_LINK_BYTES_PER_MS);
14220
+ return Math.min(
14221
+ MAX_UPLOAD_TIMEOUT_MS,
14222
+ MIN_UPLOAD_TIMEOUT_MS + sizeAllowanceMs
14223
+ );
14224
+ }
14225
+ var TRANSIENT_ERROR_CODES = /* @__PURE__ */ new Set([
14226
+ "ECONNRESET",
14227
+ "ETIMEDOUT",
14228
+ "ECONNREFUSED",
14229
+ "EPIPE",
14230
+ "EAI_AGAIN",
14231
+ "ENETUNREACH",
14232
+ "ENOTFOUND"
14233
+ ]);
14234
+ function isTransientUploadError(err) {
14235
+ if (err instanceof S3UploadError) {
14236
+ return false;
14237
+ }
14238
+ if (typeof err !== "object" || err === null) {
14239
+ return false;
14240
+ }
14241
+ const e = err;
14242
+ if (e.code !== void 0 && TRANSIENT_ERROR_CODES.has(e.code)) {
14243
+ return true;
14244
+ }
14245
+ return e.type === "system" || e.name === "AbortError" || e.name === "TimeoutError";
14246
+ }
14212
14247
  var S3UploadError = class extends Error {
14213
14248
  constructor(status, s3Code, s3Message) {
14214
14249
  super(`Failed to upload the file: ${status}`);
@@ -14267,11 +14302,12 @@ async function uploadFile({
14267
14302
  );
14268
14303
  }
14269
14304
  const agent = getProxyAgent(url);
14305
+ const effectiveSignal = signal ?? AbortSignal.timeout(computeUploadTimeoutMs(file));
14270
14306
  const response = await fetch3(url, {
14271
14307
  method: "POST",
14272
14308
  body: form,
14273
14309
  agent,
14274
- signal
14310
+ signal: effectiveSignal
14275
14311
  });
14276
14312
  if (!response.ok) {
14277
14313
  let bodyText = "";
@@ -14653,8 +14689,7 @@ async function readRepoState(workingDir) {
14653
14689
  const repoStatePromise = gitService.getCurrentRepoState().catch(() => ({ branch: null, commitSha: null }));
14654
14690
  const repositoryUrlPromise = gitService.getRemoteUrl().then((url) => {
14655
14691
  if (!url) return null;
14656
- const parsed = parseScmURL(url);
14657
- return parsed?.scmType && parsed.scmType !== "Unknown" ? url : null;
14692
+ return parseScmURL(url)?.canonicalUrl ?? null;
14658
14693
  }).catch(() => null);
14659
14694
  const [{ branch, commitSha }, repositoryUrl] = await Promise.all([
14660
14695
  repoStatePromise,
@@ -17865,7 +17900,7 @@ import { spawn } from "child_process";
17865
17900
  import { readFileSync, writeFileSync as writeFileSync2 } from "fs";
17866
17901
  import * as os8 from "os";
17867
17902
  import path24 from "path";
17868
- import { setTimeout as sleep3 } from "timers/promises";
17903
+ import { setTimeout as sleep4 } from "timers/promises";
17869
17904
  import Configstore3 from "configstore";
17870
17905
 
17871
17906
  // src/features/analysis/skill_quarantine/runQuarantineCheck.ts
@@ -18144,23 +18179,61 @@ var SCAN_PATHS = {
18144
18179
  // src/features/analysis/context_file_scanner.ts
18145
18180
  var MAX_CONTEXT_FILE_SIZE = 20 * 1024 * 1024;
18146
18181
  var SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
18182
+ var FAILURE_BACKOFF_BASE_MS = 60 * 1e3;
18183
+ var FAILURE_BACKOFF_MAX_MS = 60 * 60 * 1e3;
18147
18184
  var sessionMtimes = /* @__PURE__ */ new Map();
18148
18185
  function markContextFilesUploaded(sessionId, files, skills) {
18149
- let entry = sessionMtimes.get(sessionId);
18150
- if (!entry) {
18151
- entry = { files: /* @__PURE__ */ new Map(), skills: /* @__PURE__ */ new Map(), lastUpdatedAt: Date.now() };
18152
- sessionMtimes.set(sessionId, entry);
18153
- }
18186
+ const entry = getOrCreateSessionEntry(sessionId);
18154
18187
  for (const f of files) {
18155
18188
  entry.files.set(f.path, f.mtimeMs);
18189
+ entry.failures.delete(f.path);
18156
18190
  }
18157
18191
  if (skills) {
18158
18192
  for (const sg of skills) {
18159
18193
  entry.skills.set(sg.sessionKey, sg.maxMtimeMs);
18194
+ entry.failures.delete(sg.sessionKey);
18160
18195
  }
18161
18196
  }
18162
18197
  entry.lastUpdatedAt = Date.now();
18163
18198
  }
18199
+ function markContextUploadFailed(sessionId, files, skills) {
18200
+ const entry = getOrCreateSessionEntry(sessionId);
18201
+ const now = Date.now();
18202
+ const recordFailure = (key) => {
18203
+ const attempts = (entry.failures.get(key)?.attempts ?? 0) + 1;
18204
+ const delay3 = Math.min(
18205
+ FAILURE_BACKOFF_BASE_MS * 2 ** (attempts - 1),
18206
+ FAILURE_BACKOFF_MAX_MS
18207
+ );
18208
+ entry.failures.set(key, { attempts, nextRetryAt: now + delay3 });
18209
+ };
18210
+ for (const f of files) {
18211
+ recordFailure(f.path);
18212
+ }
18213
+ if (skills) {
18214
+ for (const sg of skills) {
18215
+ recordFailure(sg.sessionKey);
18216
+ }
18217
+ }
18218
+ entry.lastUpdatedAt = now;
18219
+ }
18220
+ function getOrCreateSessionEntry(sessionId) {
18221
+ let entry = sessionMtimes.get(sessionId);
18222
+ if (!entry) {
18223
+ entry = {
18224
+ files: /* @__PURE__ */ new Map(),
18225
+ skills: /* @__PURE__ */ new Map(),
18226
+ failures: /* @__PURE__ */ new Map(),
18227
+ lastUpdatedAt: Date.now()
18228
+ };
18229
+ sessionMtimes.set(sessionId, entry);
18230
+ }
18231
+ return entry;
18232
+ }
18233
+ function isInUploadBackoff(entry, key, now) {
18234
+ const failure = entry.failures.get(key);
18235
+ return failure !== void 0 && failure.nextRetryAt > now;
18236
+ }
18164
18237
  var COPILOT_CUSTOM_LOCATION_SETTINGS = [
18165
18238
  {
18166
18239
  key: "chat.agentSkillsLocations",
@@ -18520,6 +18593,9 @@ async function scanContextFiles(workspaceRoot, platform2, sessionId) {
18520
18593
  if (prevMtime !== void 0 && fileStat.mtimeMs <= prevMtime) {
18521
18594
  continue;
18522
18595
  }
18596
+ if (sessionEntry && isInUploadBackoff(sessionEntry, filePath, now)) {
18597
+ continue;
18598
+ }
18523
18599
  allFiles.push(fileEntry);
18524
18600
  }
18525
18601
  } catch {
@@ -18535,6 +18611,9 @@ async function scanContextFiles(workspaceRoot, platform2, sessionId) {
18535
18611
  if (prevMtime !== void 0 && group.maxMtimeMs <= prevMtime) {
18536
18612
  continue;
18537
18613
  }
18614
+ if (isInUploadBackoff(sessionEntry, group.sessionKey, now)) {
18615
+ continue;
18616
+ }
18538
18617
  }
18539
18618
  allSkillGroups.push(group);
18540
18619
  }
@@ -19123,6 +19202,7 @@ var lastRunAt = /* @__PURE__ */ new Map();
19123
19202
  var lastDirsMtimeMs = /* @__PURE__ */ new Map();
19124
19203
  var seenSkillMd5s = /* @__PURE__ */ new Set();
19125
19204
  var killSwitchLogged = false;
19205
+ var optInDisabledLogged = false;
19126
19206
  async function runQuarantineCheckIfNeeded(opts) {
19127
19207
  const { sessionId, cwd, gqlClient, log: log2 } = opts;
19128
19208
  if (process.env[KILL_SWITCH_ENV] === "1") {
@@ -19149,14 +19229,14 @@ async function runQuarantineCheckIfNeeded(opts) {
19149
19229
  return;
19150
19230
  }
19151
19231
  lastDirsMtimeMs.set(sessionId, dirsMtime);
19152
- log2.info(
19232
+ log2.debug(
19153
19233
  { sessionId, metric: Metric.CHECK_TRIGGERED, hasNewSkills },
19154
19234
  "skill_quarantine: check start"
19155
19235
  );
19156
19236
  const t0 = Date.now();
19157
19237
  try {
19158
19238
  await reconcileAndSweep(log2);
19159
- log2.info(
19239
+ log2.debug(
19160
19240
  { sessionId, count: installed.length, metric: Metric.SKILLS_CHECKED },
19161
19241
  "skill_quarantine: skills enumerated"
19162
19242
  );
@@ -19176,10 +19256,13 @@ async function runQuarantineCheckIfNeeded(opts) {
19176
19256
  log2
19177
19257
  );
19178
19258
  if (!quarantineEnabled) {
19179
- log2.info(
19180
- { sessionId, metric: Metric.CHECK_DISABLED_ORG },
19181
- "skill_quarantine: opt-in not enabled for any org of caller; skipping enforcement"
19182
- );
19259
+ if (!optInDisabledLogged) {
19260
+ log2.info(
19261
+ { sessionId, metric: Metric.CHECK_DISABLED_ORG },
19262
+ "skill_quarantine: opt-in not enabled for any org of caller; skipping enforcement"
19263
+ );
19264
+ optInDisabledLogged = true;
19265
+ }
19183
19266
  return;
19184
19267
  }
19185
19268
  for (const skill of installed) {
@@ -19204,7 +19287,7 @@ async function runQuarantineCheckIfNeeded(opts) {
19204
19287
  }
19205
19288
  }
19206
19289
  } finally {
19207
- log2.info(
19290
+ log2.debug(
19208
19291
  {
19209
19292
  sessionId,
19210
19293
  duration_ms: Date.now() - t0,
@@ -19316,9 +19399,27 @@ import path21 from "path";
19316
19399
  import { promisify } from "util";
19317
19400
 
19318
19401
  // src/features/analysis/context_file_uploader.ts
19402
+ import { setTimeout as sleep3 } from "timers/promises";
19319
19403
  import pLimit7 from "p-limit";
19320
19404
  init_client_generates();
19321
19405
  var UPLOAD_CONCURRENCY = 5;
19406
+ var UPLOAD_MAX_ATTEMPTS = 2;
19407
+ var UPLOAD_RETRY_BASE_MS = 250;
19408
+ async function uploadWithRetry(args) {
19409
+ for (let attempt = 1; attempt <= UPLOAD_MAX_ATTEMPTS; attempt++) {
19410
+ try {
19411
+ await uploadFile(args);
19412
+ return;
19413
+ } catch (err) {
19414
+ const isLastAttempt = attempt === UPLOAD_MAX_ATTEMPTS;
19415
+ if (isLastAttempt || !isTransientUploadError(err)) {
19416
+ throw err;
19417
+ }
19418
+ const backoff = UPLOAD_RETRY_BASE_MS * 2 ** (attempt - 1) * (1 + Math.random());
19419
+ await sleep3(backoff);
19420
+ }
19421
+ }
19422
+ }
19322
19423
  async function uploadContextRecords(opts) {
19323
19424
  const {
19324
19425
  processedFiles,
@@ -19339,6 +19440,8 @@ async function uploadContextRecords(opts) {
19339
19440
  const records = [];
19340
19441
  const uploadedFiles = [];
19341
19442
  const uploadedSkillGroups = [];
19443
+ const failedFiles = [];
19444
+ const failedSkillGroups = [];
19342
19445
  const limit = pLimit7(UPLOAD_CONCURRENCY);
19343
19446
  const extraFields = {
19344
19447
  ...repositoryUrl !== void 0 && { repositoryUrl },
@@ -19351,13 +19454,14 @@ async function uploadContextRecords(opts) {
19351
19454
  (pf) => limit(async () => {
19352
19455
  const s3Key = `${keyPrefix}ctx-${pf.md5}.bin`;
19353
19456
  try {
19354
- await uploadFile({
19457
+ await uploadWithRetry({
19355
19458
  file: Buffer.from(pf.sanitizedContent, "utf-8"),
19356
19459
  url,
19357
19460
  uploadKey: s3Key,
19358
19461
  uploadFields
19359
19462
  });
19360
19463
  } catch (err) {
19464
+ failedFiles.push(pf.entry);
19361
19465
  onFileError?.(pf.entry.name, err);
19362
19466
  return;
19363
19467
  }
@@ -19385,13 +19489,14 @@ async function uploadContextRecords(opts) {
19385
19489
  (ps) => limit(async () => {
19386
19490
  const s3Key = `${keyPrefix}skill-${ps.md5}.zip`;
19387
19491
  try {
19388
- await uploadFile({
19492
+ await uploadWithRetry({
19389
19493
  file: ps.zipBuffer,
19390
19494
  url,
19391
19495
  uploadKey: s3Key,
19392
19496
  uploadFields
19393
19497
  });
19394
19498
  } catch (err) {
19499
+ failedSkillGroups.push(ps.group);
19395
19500
  onSkillError?.(ps.group.name, err);
19396
19501
  return;
19397
19502
  }
@@ -19417,7 +19522,13 @@ async function uploadContextRecords(opts) {
19417
19522
  )
19418
19523
  ];
19419
19524
  await Promise.allSettled(tasks);
19420
- return { records, uploadedFiles, uploadedSkillGroups };
19525
+ return {
19526
+ records,
19527
+ uploadedFiles,
19528
+ uploadedSkillGroups,
19529
+ failedFiles,
19530
+ failedSkillGroups
19531
+ };
19421
19532
  }
19422
19533
  async function runContextFileUploadPipeline(opts) {
19423
19534
  const {
@@ -19443,7 +19554,13 @@ async function runContextFileUploadPipeline(opts) {
19443
19554
  return null;
19444
19555
  }
19445
19556
  const now = (/* @__PURE__ */ new Date()).toISOString();
19446
- const { records, uploadedFiles, uploadedSkillGroups } = await uploadContextRecords({
19557
+ const {
19558
+ records,
19559
+ uploadedFiles,
19560
+ uploadedSkillGroups,
19561
+ failedFiles,
19562
+ failedSkillGroups
19563
+ } = await uploadContextRecords({
19447
19564
  processedFiles,
19448
19565
  processedSkills,
19449
19566
  keyPrefix,
@@ -19459,6 +19576,9 @@ async function runContextFileUploadPipeline(opts) {
19459
19576
  onFileError,
19460
19577
  onSkillError
19461
19578
  });
19579
+ if (failedFiles.length > 0 || failedSkillGroups.length > 0) {
19580
+ markContextUploadFailed(sessionId, failedFiles, failedSkillGroups);
19581
+ }
19462
19582
  if (records.length === 0) {
19463
19583
  return { fileCount: 0, skillCount: 0 };
19464
19584
  }
@@ -19810,7 +19930,7 @@ function createLogger(config2) {
19810
19930
 
19811
19931
  // src/features/claude_code/hook_logger.ts
19812
19932
  var DD_RUM_TOKEN = true ? "pubf59c0182545bfb4c299175119f1abf9b" : "";
19813
- var CLI_VERSION = true ? "1.4.27" : "unknown";
19933
+ var CLI_VERSION = true ? "1.4.28" : "unknown";
19814
19934
  var NAMESPACE = "mobbdev-claude-code-hook-logs";
19815
19935
  var claudeCodeVersion;
19816
19936
  function buildDdTags() {
@@ -19989,7 +20109,7 @@ async function readNewTranscriptEntries(transcriptPath, sessionId, sessionStore,
19989
20109
  const stat5 = await fh.stat();
19990
20110
  fileSize = stat5.size;
19991
20111
  if (cursor.byteOffset >= stat5.size) {
19992
- hookLog.info({ data: { sessionId } }, "No new data in transcript file");
20112
+ hookLog.debug({ data: { sessionId } }, "No new data in transcript file");
19993
20113
  return {
19994
20114
  entries: [],
19995
20115
  endByteOffset: fileSize,
@@ -20226,12 +20346,12 @@ async function processTranscript(input, sessionStore, log2, maxEntries = DAEMON_
20226
20346
  );
20227
20347
  const cursorKey = getCursorKey(resolvedTranscriptPath);
20228
20348
  if (rawEntries.length === 0) {
20229
- log2.info("No new entries to upload");
20349
+ log2.debug("No new entries to upload");
20230
20350
  return { entriesUploaded: 0, entriesSkipped: 0, errors: 0 };
20231
20351
  }
20232
20352
  const { filtered: entries, filteredOut } = filterEntries(rawEntries);
20233
20353
  if (filteredOut > 0) {
20234
- log2.info(
20354
+ log2.debug(
20235
20355
  { data: { filteredOut, remaining: entries.length } },
20236
20356
  "Filtered out noise entries"
20237
20357
  );
@@ -20914,7 +21034,7 @@ async function startDaemon() {
20914
21034
  },
20915
21035
  "daemon poll cycle"
20916
21036
  );
20917
- await sleep3(DAEMON_POLL_INTERVAL_MS);
21037
+ await sleep4(DAEMON_POLL_INTERVAL_MS);
20918
21038
  }
20919
21039
  }
20920
21040
  async function acquirePidFile() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobbdev",
3
- "version": "1.4.27",
3
+ "version": "1.4.28",
4
4
  "description": "Automated secure code remediation tool",
5
5
  "repository": "git+https://github.com/mobb-dev/bugsy.git",
6
6
  "main": "dist/index.mjs",