@coderule/mcp 1.2.0 → 1.4.0

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/cli.cjs CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  'use strict';
2
3
 
3
4
  var fs4 = require('fs/promises');
@@ -43,7 +44,7 @@ var DEFAULT_HEARTBEAT_CHECK_INTERVAL_MS = 5e3;
43
44
  var DEFAULT_QUEUE_POLL_INTERVAL_MS = 500;
44
45
  var DEFAULT_HASH_BATCH_SIZE = 32;
45
46
  var DEFAULT_MAX_SNAPSHOT_ATTEMPTS = 5;
46
- var DEFAULT_HTTP_TIMEOUT_MS = 3e4;
47
+ var DEFAULT_HTTP_TIMEOUT_MS = 12e4;
47
48
 
48
49
  // src/config/Configurator.ts
49
50
  var DEFAULT_RETRIEVAL_FORMATTER = "standard";
@@ -548,6 +549,16 @@ var Outbox = class {
548
549
  if (purged > 0) {
549
550
  this.log.warn({ purged }, "Purged legacy fs_control jobs without kind");
550
551
  }
552
+ try {
553
+ const counts = {
554
+ pending: this.queue.countByStatus(qulite.JobStatus.Pending),
555
+ processing: this.queue.countByStatus(qulite.JobStatus.Processing),
556
+ done: this.queue.countByStatus(qulite.JobStatus.Done),
557
+ failed: this.queue.countByStatus(qulite.JobStatus.Failed)
558
+ };
559
+ this.log.debug({ counts }, "Outbox initialized");
560
+ } catch {
561
+ }
551
562
  }
552
563
  getQueue() {
553
564
  return this.queue;
@@ -1059,44 +1070,109 @@ function computeSnapshot(filesRepo) {
1059
1070
  totalSize
1060
1071
  };
1061
1072
  }
1062
- async function uploadMissing(rootPath, missing, syncClient, logger2) {
1063
- if (!missing || missing.length === 0) return;
1064
- const map = /* @__PURE__ */ new Map();
1065
- for (const missingFile of missing) {
1066
- const absPath = path__default.default.join(rootPath, missingFile.file_path);
1073
+ async function withRetries(op, logger2, context, maxAttempts) {
1074
+ let attempt = 0;
1075
+ while (true) {
1067
1076
  try {
1068
- const buffer = await fs4__default.default.readFile(absPath);
1069
- map.set(missingFile.file_hash, {
1070
- path: missingFile.file_path,
1071
- content: buffer
1072
- });
1073
- } catch (error) {
1077
+ return await op();
1078
+ } catch (err) {
1079
+ attempt += 1;
1080
+ if (attempt >= maxAttempts) {
1081
+ logger2.error(
1082
+ { err, ...context, attempt },
1083
+ "Operation failed after retries"
1084
+ );
1085
+ throw err;
1086
+ }
1087
+ const delay = Math.min(15e3, 1e3 * 2 ** (attempt - 1));
1074
1088
  logger2.warn(
1075
- { err: error, relPath: missingFile.file_path },
1076
- "Failed to read missing file content"
1089
+ { err, ...context, attempt, delay },
1090
+ "Operation failed; retrying"
1077
1091
  );
1092
+ await sleep(delay);
1093
+ }
1094
+ }
1095
+ }
1096
+ async function uploadMissing(rootPath, missing, syncClient, logger2, maxAttempts, chunkSize = 64) {
1097
+ if (!missing || missing.length === 0) return;
1098
+ const total = missing.length;
1099
+ const chunks = [];
1100
+ for (let i = 0; i < total; i += chunkSize) {
1101
+ chunks.push(missing.slice(i, i + chunkSize));
1102
+ }
1103
+ for (let idx = 0; idx < chunks.length; idx++) {
1104
+ const list = chunks[idx];
1105
+ const map = /* @__PURE__ */ new Map();
1106
+ for (const missingFile of list) {
1107
+ const absPath = path__default.default.join(rootPath, missingFile.file_path);
1108
+ try {
1109
+ const buffer = await fs4__default.default.readFile(absPath);
1110
+ map.set(missingFile.file_hash, {
1111
+ path: missingFile.file_path,
1112
+ content: buffer
1113
+ });
1114
+ } catch (error) {
1115
+ logger2.warn(
1116
+ { err: error, relPath: missingFile.file_path },
1117
+ "Failed to read missing file content"
1118
+ );
1119
+ }
1078
1120
  }
1121
+ if (map.size === 0) continue;
1122
+ await withRetries(
1123
+ () => syncClient.uploadFileContent(map),
1124
+ logger2,
1125
+ {
1126
+ op: "uploadFileContent",
1127
+ chunkIndex: idx + 1,
1128
+ chunks: chunks.length,
1129
+ files: map.size
1130
+ },
1131
+ maxAttempts
1132
+ );
1079
1133
  }
1080
- if (map.size === 0) return;
1081
- await syncClient.uploadFileContent(map);
1082
1134
  }
1083
- async function ensureSnapshotCreated(rootPath, computation, syncClient, logger2) {
1135
+ async function ensureSnapshotCreated(rootPath, computation, syncClient, logger2, options) {
1084
1136
  const { snapshotHash, files } = computation;
1085
- let status = await syncClient.checkSnapshotStatus(snapshotHash);
1137
+ const maxAttempts = options?.maxAttempts ?? 5;
1138
+ const uploadChunkSize = options?.uploadChunkSize ?? 64;
1139
+ let status = await withRetries(
1140
+ () => syncClient.checkSnapshotStatus(snapshotHash),
1141
+ logger2,
1142
+ { op: "checkSnapshotStatus", snapshotHash },
1143
+ maxAttempts
1144
+ );
1086
1145
  if (status.status === "READY") {
1087
1146
  logger2.info({ snapshotHash }, "Snapshot already READY");
1088
1147
  return;
1089
1148
  }
1090
1149
  if (status.status === "NOT_FOUND" || status.status === "MISSING_CONTENT") {
1091
- status = await syncClient.createSnapshot(snapshotHash, files);
1150
+ status = await withRetries(
1151
+ () => syncClient.createSnapshot(snapshotHash, files),
1152
+ logger2,
1153
+ { op: "createSnapshot", snapshotHash },
1154
+ maxAttempts
1155
+ );
1092
1156
  }
1093
1157
  if (status.status === "MISSING_CONTENT" && status.missing_files?.length) {
1094
1158
  logger2.info(
1095
1159
  { missing: status.missing_files.length },
1096
1160
  "Uploading missing file content"
1097
1161
  );
1098
- await uploadMissing(rootPath, status.missing_files, syncClient, logger2);
1099
- status = await syncClient.createSnapshot(snapshotHash, files);
1162
+ await uploadMissing(
1163
+ rootPath,
1164
+ status.missing_files,
1165
+ syncClient,
1166
+ logger2,
1167
+ maxAttempts,
1168
+ uploadChunkSize
1169
+ );
1170
+ status = await withRetries(
1171
+ () => syncClient.createSnapshot(snapshotHash, files),
1172
+ logger2,
1173
+ { op: "createSnapshot", snapshotHash },
1174
+ maxAttempts
1175
+ );
1100
1176
  }
1101
1177
  let attempt = 0;
1102
1178
  while (status.status !== "READY") {
@@ -1106,13 +1182,24 @@ async function ensureSnapshotCreated(rootPath, computation, syncClient, logger2)
1106
1182
  const delay = Math.min(5e3, 1e3 * Math.max(1, 2 ** attempt));
1107
1183
  await sleep(delay);
1108
1184
  attempt += 1;
1109
- status = await syncClient.checkSnapshotStatus(snapshotHash);
1185
+ status = await withRetries(
1186
+ () => syncClient.checkSnapshotStatus(snapshotHash),
1187
+ logger2,
1188
+ { op: "checkSnapshotStatus", snapshotHash },
1189
+ maxAttempts
1190
+ );
1110
1191
  }
1111
1192
  logger2.info({ snapshotHash }, "Snapshot READY");
1112
1193
  }
1113
- async function publishSnapshot(rootPath, filesRepo, snapshotsRepo, syncClient, logger2) {
1194
+ async function publishSnapshot(rootPath, filesRepo, snapshotsRepo, syncClient, logger2, options) {
1114
1195
  const computation = computeSnapshot(filesRepo);
1115
- await ensureSnapshotCreated(rootPath, computation, syncClient, logger2);
1196
+ await ensureSnapshotCreated(
1197
+ rootPath,
1198
+ computation,
1199
+ syncClient,
1200
+ logger2,
1201
+ options
1202
+ );
1116
1203
  const createdAt = Date.now();
1117
1204
  snapshotsRepo.insert(
1118
1205
  computation.snapshotHash,
@@ -1152,7 +1239,8 @@ async function runInitialSyncPipeline(runtime) {
1152
1239
  runtime.filesRepo,
1153
1240
  runtime.snapshotsRepo,
1154
1241
  runtime.clients.sync,
1155
- syncLogger
1242
+ syncLogger,
1243
+ { maxAttempts: runtime.config.maxSnapshotAttempts }
1156
1244
  );
1157
1245
  return result;
1158
1246
  }
@@ -1555,7 +1643,8 @@ var ServiceRunner = class {
1555
1643
  this.runtime.filesRepo,
1556
1644
  this.runtime.snapshotsRepo,
1557
1645
  this.runtime.clients.sync,
1558
- log
1646
+ log,
1647
+ { maxAttempts: this.runtime.config.maxSnapshotAttempts }
1559
1648
  );
1560
1649
  this.runtime.outbox.ack(job.id, this.fsControlLeaseOwner);
1561
1650
  this.state.updateSnapshotReady(result.createdAt);
@@ -1636,15 +1725,27 @@ async function runService(params) {
1636
1725
  try {
1637
1726
  runner = new ServiceRunner(runtime);
1638
1727
  await runner.prepareWatcher(true);
1639
- const initial = await runInitialSyncPipeline(runtime);
1640
- runtime.logger.info(
1641
- {
1642
- snapshotHash: initial.snapshotHash,
1643
- filesCount: initial.filesCount
1644
- },
1645
- "Initial sync completed; entering continuous mode"
1646
- );
1647
- runner.recordInitialSnapshot(initial.createdAt);
1728
+ let initialCreatedAt;
1729
+ try {
1730
+ const initial = await runInitialSyncPipeline(runtime);
1731
+ runtime.logger.info(
1732
+ {
1733
+ snapshotHash: initial.snapshotHash,
1734
+ filesCount: initial.filesCount
1735
+ },
1736
+ "Initial sync completed; entering continuous mode"
1737
+ );
1738
+ initialCreatedAt = initial.createdAt;
1739
+ } catch (error) {
1740
+ runtime.logger.warn(
1741
+ { err: error },
1742
+ "Initial sync failed; enqueuing snapshot job and continuing"
1743
+ );
1744
+ runtime.outbox.enqueueSnapshot(runtime.config.rootId, 0);
1745
+ }
1746
+ if (initialCreatedAt) {
1747
+ runner.recordInitialSnapshot(initialCreatedAt);
1748
+ }
1648
1749
  await runner.startLoops();
1649
1750
  await runner.enableWatcherProcessing();
1650
1751
  runtime.logger.info("Coderule scanner service is running");