@spotpatch/dev-server 0.2.0 → 0.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/index.cjs CHANGED
@@ -32,19 +32,25 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  DEFAULT_EXCLUDE: () => DEFAULT_EXCLUDE,
34
34
  DEFAULT_OPTIONS: () => DEFAULT_OPTIONS,
35
+ applyIntegrationPlan: () => applyIntegrationPlan,
35
36
  createAgentJobManager: () => createAgentJobManager,
37
+ createIntegrationFileChange: () => createIntegrationFileChange,
36
38
  createRuntimeAiConfig: () => createRuntimeAiConfig,
37
39
  createSession: () => createSession,
38
40
  createSourceRegistrationService: () => createSourceRegistrationService,
39
41
  createSourceRegistry: () => createSourceRegistry,
40
42
  createSpotPatchMiddleware: () => createSpotPatchMiddleware,
43
+ discoverProjectValidationCheck: () => discoverProjectValidationCheck,
44
+ integrationPathExists: () => integrationPathExists,
41
45
  isLoopbackHostname: () => isLoopbackHostname,
42
46
  parseSerializedSpotPatchOptions: () => parseSerializedSpotPatchOptions,
47
+ readIntegrationFile: () => readIntegrationFile,
43
48
  readJsonRequestBody: () => readJsonRequestBody,
44
49
  readRuntimeBootstrap: () => readRuntimeBootstrap,
45
50
  resolveCredentialEnvironment: () => resolveCredentialEnvironment,
46
51
  resolveEnvironmentAiConfiguration: () => resolveEnvironmentAiConfiguration,
47
52
  resolveOptions: () => resolveOptions,
53
+ resolveProjectOptions: () => resolveProjectOptions,
48
54
  resolveRuntimeBootstrapOptions: () => resolveRuntimeBootstrapOptions,
49
55
  serializeResolvedSpotPatchOptions: () => serializeResolvedSpotPatchOptions
50
56
  });
@@ -246,7 +252,11 @@ function createAgentJobManager(options) {
246
252
  }
247
253
  };
248
254
  const applyChange = async (job, preparedChange) => {
249
- transition(job, "applying", "Applying validated changes to the project.");
255
+ transition(
256
+ job,
257
+ "applying",
258
+ job.applyMode === "trusted-auto" ? "Applying trusted change directly to the project." : "Applying validated changes to the project."
259
+ );
250
260
  try {
251
261
  await dependencies.applyChange(preparedChange);
252
262
  transition(job, "applied", "Changes were applied to local project files.");
@@ -285,11 +295,15 @@ function createAgentJobManager(options) {
285
295
  );
286
296
  }
287
297
  };
298
+ const execution = Object.freeze({
299
+ ...options.ai.execution,
300
+ applyMode: job.applyMode
301
+ });
288
302
  const preparedChange = await dependencies.executeChange({
289
303
  annotation: job.annotation,
290
304
  callbacks,
291
305
  credential: job.credential,
292
- execution: options.ai.execution,
306
+ execution,
293
307
  jobId: job.id,
294
308
  model: job.model,
295
309
  provider: job.provider,
@@ -322,7 +336,7 @@ function createAgentJobManager(options) {
322
336
  transition(job, "completed", "No source changes were proposed.");
323
337
  return;
324
338
  }
325
- const shouldApplyDirectly = options.ai.execution.applyMode === "auto" && preparedChange.autoApplyEligible || options.ai.execution.applyMode === "trusted-auto" && job.trustedFastModeConsent;
339
+ const shouldApplyDirectly = job.applyMode === "auto" && preparedChange.autoApplyEligible || job.applyMode === "trusted-auto" && job.trustedFastModeConsent;
326
340
  if (shouldApplyDirectly) {
327
341
  try {
328
342
  await applyChange(job, preparedChange);
@@ -398,8 +412,11 @@ function createAgentJobManager(options) {
398
412
  if (closed) {
399
413
  throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.AI_DISABLED);
400
414
  }
401
- const trustedFastModeConfigured = options.ai.execution.applyMode === "trusted-auto";
402
- if (trustedFastModeConfigured !== (request.trustedFastModeConsent === true)) {
415
+ const configuredApplyMode = options.ai.execution.applyMode;
416
+ const requestedApplyMode = request.applyMode ?? (request.trustedFastModeConsent === true ? "trusted-auto" : configuredApplyMode);
417
+ const applyModeAllowed = configuredApplyMode === "trusted-auto" ? requestedApplyMode === "review" || requestedApplyMode === "trusted-auto" : requestedApplyMode === configuredApplyMode;
418
+ const trustedConsentMatches = requestedApplyMode === "trusted-auto" === (request.trustedFastModeConsent === true);
419
+ if (!applyModeAllowed || !trustedConsentMatches) {
403
420
  throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.INVALID_REQUEST);
404
421
  }
405
422
  if (hasActiveJob()) {
@@ -420,6 +437,7 @@ function createAgentJobManager(options) {
420
437
  const timestamp = dependencies.now();
421
438
  const job = {
422
439
  annotation: request.annotation,
440
+ applyMode: requestedApplyMode,
423
441
  controller: new AbortController(),
424
442
  createdAt: timestamp,
425
443
  credential: selection.credential,
@@ -591,6 +609,171 @@ function resolveEnvironmentAiConfiguration(environment) {
591
609
  });
592
610
  }
593
611
 
612
+ // src/integration/file-plan.ts
613
+ var import_node_crypto2 = require("crypto");
614
+ var import_promises = require("fs/promises");
615
+ var import_node_path = __toESM(require("path"), 1);
616
+ function isMissingPathError(error) {
617
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
618
+ }
619
+ function isPathWithin(root, target) {
620
+ const relative = import_node_path.default.relative(root, target);
621
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(relative);
622
+ }
623
+ function relativePathWithin(root, target) {
624
+ const relative = import_node_path.default.relative(root, target);
625
+ if (relative.length === 0 || !isPathWithin(root, target)) {
626
+ throw new Error("SpotPatch init refuses to modify a path outside the app root.");
627
+ }
628
+ return relative.split(import_node_path.default.sep).join("/");
629
+ }
630
+ async function integrationPathExists(absolutePath) {
631
+ try {
632
+ await (0, import_promises.access)(absolutePath);
633
+ return true;
634
+ } catch {
635
+ return false;
636
+ }
637
+ }
638
+ async function readIntegrationFile(absolutePath) {
639
+ const metadata = await (0, import_promises.lstat)(absolutePath);
640
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
641
+ throw new Error(
642
+ `SpotPatch refuses to modify the non-regular file ${import_node_path.default.basename(absolutePath)}.`
643
+ );
644
+ }
645
+ return (0, import_promises.readFile)(absolutePath, "utf8");
646
+ }
647
+ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previousContent) {
648
+ if (previousContent === nextContent) {
649
+ return void 0;
650
+ }
651
+ const root = import_node_path.default.resolve(appRoot);
652
+ const target = import_node_path.default.resolve(absolutePath);
653
+ return Object.freeze({
654
+ absolutePath: target,
655
+ nextContent,
656
+ ...previousContent === void 0 ? {} : { previousContent },
657
+ relativePath: relativePathWithin(root, target)
658
+ });
659
+ }
660
+ function temporaryPath(absolutePath, label) {
661
+ return import_node_path.default.join(
662
+ import_node_path.default.dirname(absolutePath),
663
+ `.${import_node_path.default.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${(0, import_node_crypto2.randomBytes)(8).toString("hex")}`
664
+ );
665
+ }
666
+ async function writeAtomic(absolutePath, content, mode) {
667
+ await (0, import_promises.mkdir)(import_node_path.default.dirname(absolutePath), { recursive: true });
668
+ const stagedPath = temporaryPath(absolutePath, "stage");
669
+ try {
670
+ await (0, import_promises.writeFile)(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
671
+ await (0, import_promises.rename)(stagedPath, absolutePath);
672
+ } catch (error) {
673
+ await (0, import_promises.unlink)(stagedPath).catch(() => void 0);
674
+ throw error;
675
+ }
676
+ }
677
+ async function rollbackChange(change) {
678
+ const currentContent = await readIntegrationFile(change.absolutePath);
679
+ if (currentContent !== change.nextContent) {
680
+ throw new Error(
681
+ `SpotPatch init cannot restore ${change.relativePath} because it changed during initialization.`
682
+ );
683
+ }
684
+ if (change.previousContent === void 0) {
685
+ await (0, import_promises.unlink)(change.absolutePath);
686
+ return;
687
+ }
688
+ const mode = (await (0, import_promises.stat)(change.absolutePath)).mode & 511;
689
+ await writeAtomic(change.absolutePath, change.previousContent, mode);
690
+ }
691
+ async function assertSafeTarget(appRoot, realAppRoot, change) {
692
+ const target = import_node_path.default.resolve(change.absolutePath);
693
+ const relativePath = relativePathWithin(appRoot, target);
694
+ if (target !== change.absolutePath || relativePath !== change.relativePath || import_node_path.default.dirname(target) === target) {
695
+ throw new Error("SpotPatch init received an invalid integration file plan.");
696
+ }
697
+ let targetMetadata;
698
+ try {
699
+ targetMetadata = await (0, import_promises.lstat)(target);
700
+ } catch (error) {
701
+ if (!isMissingPathError(error)) {
702
+ throw error;
703
+ }
704
+ }
705
+ if (targetMetadata?.isSymbolicLink()) {
706
+ throw new Error(
707
+ `SpotPatch refuses to modify the symbolic link ${change.relativePath}.`
708
+ );
709
+ }
710
+ const containmentAnchor = await (0, import_promises.realpath)(
711
+ targetMetadata === void 0 ? import_node_path.default.dirname(target) : target
712
+ );
713
+ if (!isPathWithin(realAppRoot, containmentAnchor)) {
714
+ throw new Error("SpotPatch init refuses to modify a path outside the app root.");
715
+ }
716
+ }
717
+ async function assertCurrentBaseline(change) {
718
+ if (change.previousContent === void 0) {
719
+ try {
720
+ await (0, import_promises.lstat)(change.absolutePath);
721
+ } catch (error) {
722
+ if (isMissingPathError(error)) {
723
+ return;
724
+ }
725
+ throw error;
726
+ }
727
+ throw new Error(
728
+ `SpotPatch init cannot create ${change.relativePath} because it now exists.`
729
+ );
730
+ }
731
+ const currentContent = await readIntegrationFile(change.absolutePath);
732
+ if (currentContent !== change.previousContent) {
733
+ throw new Error(
734
+ `SpotPatch init cannot update ${change.relativePath} because it changed after the preview.`
735
+ );
736
+ }
737
+ }
738
+ async function applyIntegrationPlan(plan) {
739
+ if (plan.changes.length === 0) {
740
+ return;
741
+ }
742
+ const appRoot = import_node_path.default.resolve(plan.appRoot);
743
+ const realAppRoot = await (0, import_promises.realpath)(appRoot);
744
+ const targets = /* @__PURE__ */ new Set();
745
+ for (const change of plan.changes) {
746
+ if (targets.has(change.absolutePath)) {
747
+ throw new Error("SpotPatch init received duplicate integration file changes.");
748
+ }
749
+ targets.add(change.absolutePath);
750
+ await assertSafeTarget(appRoot, realAppRoot, change);
751
+ await assertCurrentBaseline(change);
752
+ }
753
+ const applied = [];
754
+ try {
755
+ for (const change of plan.changes) {
756
+ await assertCurrentBaseline(change);
757
+ const mode = change.previousContent === void 0 ? 384 : (await (0, import_promises.stat)(change.absolutePath)).mode & 511;
758
+ await writeAtomic(change.absolutePath, change.nextContent, mode);
759
+ applied.push(change);
760
+ }
761
+ } catch (error) {
762
+ const rollbackResults = await Promise.allSettled(
763
+ applied.reverse().map(rollbackChange)
764
+ );
765
+ if (rollbackResults.some((result) => result.status === "rejected")) {
766
+ throw new Error(
767
+ "SpotPatch init failed and could not completely restore the previous files.",
768
+ { cause: error }
769
+ );
770
+ }
771
+ throw new Error("SpotPatch init failed; all written files were restored.", {
772
+ cause: error
773
+ });
774
+ }
775
+ }
776
+
594
777
  // src/options.ts
595
778
  var import_shared2 = require("@spotpatch/shared");
596
779
  var import_zod = require("zod");
@@ -916,6 +1099,9 @@ function assertPositiveBudget(budget) {
916
1099
  }
917
1100
  }
918
1101
  function resolveOptions(options = {}, environmentAi) {
1102
+ if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
1103
+ throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
1104
+ }
919
1105
  const budget = Object.freeze({
920
1106
  ...DEFAULT_OPTIONS.budget,
921
1107
  ...options.budget
@@ -955,17 +1141,179 @@ function resolveOptions(options = {}, environmentAi) {
955
1141
  return Object.freeze(resolved);
956
1142
  }
957
1143
 
1144
+ // src/project-validation.ts
1145
+ var import_node_child_process = require("child_process");
1146
+ var import_promises2 = require("fs/promises");
1147
+ var import_node_module = require("module");
1148
+ var import_node_path2 = __toESM(require("path"), 1);
1149
+ var import_node_util = require("util");
1150
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
1151
+ var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
1152
+ var TYPESCRIPT_CHECK_LABEL = "TypeScript";
1153
+ function isRecord(value) {
1154
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1155
+ }
1156
+ async function isRegularFile(absolutePath) {
1157
+ try {
1158
+ const metadata = await (0, import_promises2.lstat)(absolutePath);
1159
+ return metadata.isFile() && !metadata.isSymbolicLink();
1160
+ } catch {
1161
+ return false;
1162
+ }
1163
+ }
1164
+ async function readManifest(appRoot) {
1165
+ const manifestPath = import_node_path2.default.join(appRoot, "package.json");
1166
+ if (!await isRegularFile(manifestPath)) {
1167
+ return void 0;
1168
+ }
1169
+ try {
1170
+ const value = JSON.parse(await (0, import_promises2.readFile)(manifestPath, "utf8"));
1171
+ return isRecord(value) ? value : void 0;
1172
+ } catch {
1173
+ return void 0;
1174
+ }
1175
+ }
1176
+ function declaresTypeScript(manifest) {
1177
+ return [
1178
+ manifest.dependencies,
1179
+ manifest.devDependencies,
1180
+ manifest.peerDependencies
1181
+ ].some(
1182
+ (dependencies) => isRecord(dependencies) && typeof dependencies.typescript === "string"
1183
+ );
1184
+ }
1185
+ async function findGitRoot(appRoot) {
1186
+ try {
1187
+ const result = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
1188
+ cwd: appRoot,
1189
+ encoding: "utf8",
1190
+ timeout: 5e3,
1191
+ windowsHide: true
1192
+ });
1193
+ const root = await (0, import_promises2.realpath)(result.stdout.trim());
1194
+ const relative = import_node_path2.default.relative(root, appRoot);
1195
+ if (relative === "" || !relative.startsWith(`..${import_node_path2.default.sep}`) && relative !== ".." && !import_node_path2.default.isAbsolute(relative)) {
1196
+ return root;
1197
+ }
1198
+ } catch {
1199
+ return void 0;
1200
+ }
1201
+ return void 0;
1202
+ }
1203
+ async function resolveTypeScriptCli(appRoot) {
1204
+ const resolveFromApplication = (0, import_node_module.createRequire)(import_node_path2.default.join(appRoot, "package.json"));
1205
+ try {
1206
+ const packagePath = resolveFromApplication.resolve("typescript/package.json");
1207
+ const cliPath = import_node_path2.default.join(import_node_path2.default.dirname(packagePath), "bin", "tsc");
1208
+ await (0, import_promises2.access)(cliPath);
1209
+ return await (0, import_promises2.realpath)(cliPath);
1210
+ } catch {
1211
+ return void 0;
1212
+ }
1213
+ }
1214
+ function portableRelativePath(from, to) {
1215
+ return import_node_path2.default.relative(from, to).split(import_node_path2.default.sep).join("/");
1216
+ }
1217
+ async function discoverProjectValidationCheck(options) {
1218
+ const appRoot = await (0, import_promises2.realpath)(options.appRoot);
1219
+ const tsconfigPath = import_node_path2.default.join(appRoot, "tsconfig.json");
1220
+ const [manifest, projectRoot, hasTsconfig] = await Promise.all([
1221
+ readManifest(appRoot),
1222
+ findGitRoot(appRoot),
1223
+ isRegularFile(tsconfigPath)
1224
+ ]);
1225
+ if (manifest === void 0 || projectRoot === void 0 || !hasTsconfig || !declaresTypeScript(manifest)) {
1226
+ return void 0;
1227
+ }
1228
+ const cliPath = await resolveTypeScriptCli(appRoot);
1229
+ if (cliPath === void 0) {
1230
+ return void 0;
1231
+ }
1232
+ const projectPath = portableRelativePath(projectRoot, tsconfigPath);
1233
+ if (projectPath.length === 0 || projectPath.startsWith("../")) {
1234
+ return void 0;
1235
+ }
1236
+ return Object.freeze({
1237
+ id: TYPESCRIPT_CHECK_ID,
1238
+ label: TYPESCRIPT_CHECK_LABEL,
1239
+ command: process.execPath,
1240
+ args: Object.freeze([
1241
+ cliPath,
1242
+ "--noEmit",
1243
+ "--pretty",
1244
+ "false",
1245
+ "--project",
1246
+ projectPath
1247
+ ]),
1248
+ required: true,
1249
+ timeoutMs: options.timeoutMs
1250
+ });
1251
+ }
1252
+
1253
+ // src/project-options.ts
1254
+ function hasRequiredCheck(ai) {
1255
+ return Object.values(ai.execution.checks).some((check) => check.required);
1256
+ }
1257
+ function availableCheckId(checks, preferred) {
1258
+ if (checks[preferred] === void 0) {
1259
+ return preferred;
1260
+ }
1261
+ let suffix = 2;
1262
+ while (checks[`${preferred}-${String(suffix)}`] !== void 0) {
1263
+ suffix += 1;
1264
+ }
1265
+ return `${preferred}-${String(suffix)}`;
1266
+ }
1267
+ async function resolveProjectOptions(input) {
1268
+ const userOptions = input.options ?? {};
1269
+ const resolved = resolveOptions(userOptions, input.environmentAi);
1270
+ if (!userOptions.trustedFastMode || resolved.ai === false) {
1271
+ return resolved;
1272
+ }
1273
+ if (resolved.ai.execution.applyMode === "auto") {
1274
+ throw new RangeError(
1275
+ "SpotPatch trustedFastMode cannot be combined with applyMode auto."
1276
+ );
1277
+ }
1278
+ let checks = resolved.ai.execution.checks;
1279
+ if (!hasRequiredCheck(resolved.ai)) {
1280
+ const discovered = await discoverProjectValidationCheck({
1281
+ appRoot: input.appRoot,
1282
+ timeoutMs: resolved.ai.execution.limits.checkTimeoutMs
1283
+ });
1284
+ if (discovered === void 0) {
1285
+ throw new RangeError(
1286
+ "SpotPatch trustedFastMode requires a configured required check or a local TypeScript project with tsconfig.json."
1287
+ );
1288
+ }
1289
+ const id = availableCheckId(checks, discovered.id);
1290
+ checks = Object.freeze({
1291
+ ...checks,
1292
+ [id]: Object.freeze({ ...discovered, id })
1293
+ });
1294
+ }
1295
+ const ai = Object.freeze({
1296
+ ...resolved.ai,
1297
+ execution: Object.freeze({
1298
+ ...resolved.ai.execution,
1299
+ applyMode: "trusted-auto",
1300
+ checks
1301
+ })
1302
+ });
1303
+ return Object.freeze({ ...resolved, ai });
1304
+ }
1305
+
958
1306
  // src/registry/source-registry.ts
959
- var import_node_path = __toESM(require("path"), 1);
1307
+ var import_node_path3 = __toESM(require("path"), 1);
960
1308
 
961
1309
  // src/registry/source-id.ts
962
- var import_node_crypto2 = require("crypto");
1310
+ var import_node_crypto3 = require("crypto");
963
1311
  var SOURCE_ID_BYTES = 8;
964
- var createRandomSourceId = () => (0, import_node_crypto2.randomBytes)(SOURCE_ID_BYTES).toString("base64url");
1312
+ var createRandomSourceId = () => (0, import_node_crypto3.randomBytes)(SOURCE_ID_BYTES).toString("base64url");
965
1313
 
966
1314
  // src/registry/source-registry.ts
967
1315
  function normalizeAbsolutePath(absolutePath) {
968
- return import_node_path.default.normalize(import_node_path.default.resolve(absolutePath));
1316
+ return import_node_path3.default.normalize(import_node_path3.default.resolve(absolutePath));
969
1317
  }
970
1318
  function createSourceRegistry(options = {}) {
971
1319
  const createId = options.createId ?? createRandomSourceId;
@@ -1003,13 +1351,13 @@ var import_shared10 = require("@spotpatch/shared");
1003
1351
  var import_shared7 = require("@spotpatch/shared");
1004
1352
 
1005
1353
  // src/server/agent-request.ts
1006
- var import_promises3 = require("fs/promises");
1007
- var import_node_path4 = __toESM(require("path"), 1);
1354
+ var import_promises5 = require("fs/promises");
1355
+ var import_node_path6 = __toESM(require("path"), 1);
1008
1356
  var import_shared5 = require("@spotpatch/shared");
1009
1357
 
1010
1358
  // src/server/source-context.ts
1011
- var import_promises2 = require("fs/promises");
1012
- var import_node_path3 = __toESM(require("path"), 1);
1359
+ var import_promises4 = require("fs/promises");
1360
+ var import_node_path5 = __toESM(require("path"), 1);
1013
1361
  var import_shared4 = require("@spotpatch/shared");
1014
1362
 
1015
1363
  // src/server/extract-code-context.ts
@@ -1239,8 +1587,8 @@ function extractCodeContext(options) {
1239
1587
  }
1240
1588
 
1241
1589
  // src/server/source-file.ts
1242
- var import_promises = require("fs/promises");
1243
- var import_node_path2 = __toESM(require("path"), 1);
1590
+ var import_promises3 = require("fs/promises");
1591
+ var import_node_path4 = __toESM(require("path"), 1);
1244
1592
  var import_shared3 = require("@spotpatch/shared");
1245
1593
 
1246
1594
  // src/server/constants.ts
@@ -1258,8 +1606,8 @@ async function assertInsideRoot(root, candidate) {
1258
1606
  let realCandidate;
1259
1607
  try {
1260
1608
  [realRoot, realCandidate] = await Promise.all([
1261
- (0, import_promises.realpath)(root),
1262
- (0, import_promises.realpath)(candidate)
1609
+ (0, import_promises3.realpath)(root),
1610
+ (0, import_promises3.realpath)(candidate)
1263
1611
  ]);
1264
1612
  } catch (error) {
1265
1613
  if (isMissingFileError(error)) {
@@ -1269,8 +1617,8 @@ async function assertInsideRoot(root, candidate) {
1269
1617
  }
1270
1618
  throw error;
1271
1619
  }
1272
- const relative = import_node_path2.default.relative(realRoot, realCandidate);
1273
- const outside = relative.startsWith(`..${import_node_path2.default.sep}`) || relative === ".." || import_node_path2.default.isAbsolute(relative);
1620
+ const relative = import_node_path4.default.relative(realRoot, realCandidate);
1621
+ const outside = relative.startsWith(`..${import_node_path4.default.sep}`) || relative === ".." || import_node_path4.default.isAbsolute(relative);
1274
1622
  if (outside) {
1275
1623
  throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_OUTSIDE_ROOT);
1276
1624
  }
@@ -1282,12 +1630,12 @@ async function resolveSourceFile(options) {
1282
1630
  throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND);
1283
1631
  }
1284
1632
  const sourcePath = await assertInsideRoot(options.root, registeredPath);
1285
- if (!ALLOWED_EXTENSIONS.has(import_node_path2.default.extname(sourcePath).toLowerCase())) {
1633
+ if (!ALLOWED_EXTENSIONS.has(import_node_path4.default.extname(sourcePath).toLowerCase())) {
1286
1634
  throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND);
1287
1635
  }
1288
1636
  let sourceStat;
1289
1637
  try {
1290
- sourceStat = await (0, import_promises.stat)(sourcePath);
1638
+ sourceStat = await (0, import_promises3.stat)(sourcePath);
1291
1639
  } catch (error) {
1292
1640
  if (isMissingFileError(error)) {
1293
1641
  throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
@@ -1307,7 +1655,7 @@ async function resolveSourceFile(options) {
1307
1655
 
1308
1656
  // src/server/source-context.ts
1309
1657
  function toDisplayPath(root, sourcePath) {
1310
- return import_node_path3.default.relative(root, sourcePath).split(import_node_path3.default.sep).join("/");
1658
+ return import_node_path5.default.relative(root, sourcePath).split(import_node_path5.default.sep).join("/");
1311
1659
  }
1312
1660
  async function readSourceContext(options) {
1313
1661
  const sourcePath = await resolveSourceFile({
@@ -1317,7 +1665,7 @@ async function readSourceContext(options) {
1317
1665
  });
1318
1666
  let source;
1319
1667
  try {
1320
- source = await (0, import_promises2.readFile)(sourcePath, "utf8");
1668
+ source = await (0, import_promises4.readFile)(sourcePath, "utf8");
1321
1669
  } catch (error) {
1322
1670
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1323
1671
  throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
@@ -1330,11 +1678,11 @@ async function readSourceContext(options) {
1330
1678
  if (options.request.line > lines.length) {
1331
1679
  throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
1332
1680
  }
1333
- const extension = import_node_path3.default.extname(sourcePath).toLowerCase();
1681
+ const extension = import_node_path5.default.extname(sourcePath).toLowerCase();
1334
1682
  return extractCodeContext({
1335
1683
  source,
1336
1684
  sourcePath,
1337
- relativePath: toDisplayPath(await (0, import_promises2.realpath)(options.root), sourcePath),
1685
+ relativePath: toDisplayPath(await (0, import_promises4.realpath)(options.root), sourcePath),
1338
1686
  language: extension === ".tsx" ? "tsx" : "jsx",
1339
1687
  line: options.request.line,
1340
1688
  column: options.request.column,
@@ -1370,7 +1718,7 @@ async function authorizeSourceRef(source, registry, root) {
1370
1718
  registry,
1371
1719
  root
1372
1720
  });
1373
- const relativePath = import_node_path4.default.relative(await (0, import_promises3.realpath)(root), sourcePath).split(import_node_path4.default.sep).join("/");
1721
+ const relativePath = import_node_path6.default.relative(await (0, import_promises5.realpath)(root), sourcePath).split(import_node_path6.default.sep).join("/");
1374
1722
  if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
1375
1723
  throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
1376
1724
  }
@@ -1475,6 +1823,7 @@ async function authorizeAgentJobRequest(input) {
1475
1823
  });
1476
1824
  return Object.freeze({
1477
1825
  annotation,
1826
+ ...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
1478
1827
  providerProfileId: input.request.providerProfileId,
1479
1828
  modelProfileId: input.request.modelProfileId,
1480
1829
  providerDataConsent: true,
@@ -1541,21 +1890,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
1541
1890
  "reverted",
1542
1891
  "failed"
1543
1892
  ]);
1544
- function matchAgentRequestPath(path6) {
1545
- if (path6 === import_shared7.SPOTPATCH_ENDPOINTS.agentCapability) {
1893
+ function matchAgentRequestPath(path8) {
1894
+ if (path8 === import_shared7.SPOTPATCH_ENDPOINTS.agentCapability) {
1546
1895
  return Object.freeze({ kind: "capability" });
1547
1896
  }
1548
- if (path6 === import_shared7.SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
1897
+ if (path8 === import_shared7.SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
1549
1898
  return Object.freeze({ kind: "workspace-health" });
1550
1899
  }
1551
- if (path6 === import_shared7.SPOTPATCH_ENDPOINTS.agentJobs) {
1900
+ if (path8 === import_shared7.SPOTPATCH_ENDPOINTS.agentJobs) {
1552
1901
  return Object.freeze({ kind: "create-job" });
1553
1902
  }
1554
1903
  const prefix = `${import_shared7.SPOTPATCH_ENDPOINTS.agentJobs}/`;
1555
- if (!path6.startsWith(prefix)) {
1904
+ if (!path8.startsWith(prefix)) {
1556
1905
  return void 0;
1557
1906
  }
1558
- const segments = path6.slice(prefix.length).split("/");
1907
+ const segments = path8.slice(prefix.length).split("/");
1559
1908
  const jobId = segments[0];
1560
1909
  const action = segments[1];
1561
1910
  if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
@@ -1725,7 +2074,7 @@ async function handleAgentRequest(request, response, options, route, writeSucces
1725
2074
  }
1726
2075
 
1727
2076
  // src/server/editor.ts
1728
- var import_node_child_process = require("child_process");
2077
+ var import_node_child_process2 = require("child_process");
1729
2078
  var import_launch_editor = __toESM(require("launch-editor"), 1);
1730
2079
  var EDITOR_STARTUP_GRACE_MS = 300;
1731
2080
  function normalizedEditorEnvironment(environment) {
@@ -1752,7 +2101,7 @@ function editorCommand(editor) {
1752
2101
  var DEFAULT_DEPENDENCIES2 = Object.freeze({
1753
2102
  environment: process.env,
1754
2103
  fallbackLauncher: import_launch_editor.default,
1755
- processSpawner: (command, arguments_, options) => (0, import_node_child_process.spawn)(command, [...arguments_], options),
2104
+ processSpawner: (command, arguments_, options) => (0, import_node_child_process2.spawn)(command, [...arguments_], options),
1756
2105
  startupGraceMs: EDITOR_STARTUP_GRACE_MS
1757
2106
  });
1758
2107
  function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
@@ -1810,7 +2159,7 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
1810
2159
  var launchConfiguredEditor = createEditorLauncher();
1811
2160
 
1812
2161
  // src/server/request-security.ts
1813
- var import_node_crypto3 = require("crypto");
2162
+ var import_node_crypto4 = require("crypto");
1814
2163
  var import_node_net = require("net");
1815
2164
  var import_shared8 = require("@spotpatch/shared");
1816
2165
  function getSingleHeader(request, name) {
@@ -1823,7 +2172,7 @@ function tokensMatch(actual, expected) {
1823
2172
  }
1824
2173
  const actualBytes = Buffer.from(actual);
1825
2174
  const expectedBytes = Buffer.from(expected);
1826
- return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto3.timingSafeEqual)(actualBytes, expectedBytes);
2175
+ return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto4.timingSafeEqual)(actualBytes, expectedBytes);
1827
2176
  }
1828
2177
  function isLoopbackHostname(hostname) {
1829
2178
  const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
@@ -2078,14 +2427,14 @@ async function handleOpenEditor(request, options) {
2078
2427
  function createSpotPatchMiddleware(options) {
2079
2428
  const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
2080
2429
  return (request, response, next) => {
2081
- const path6 = requestPath(request);
2082
- const agentRoute = matchAgentRequestPath(path6);
2083
- if (path6 !== import_shared10.SPOTPATCH_ENDPOINTS.sourceContext && path6 !== import_shared10.SPOTPATCH_ENDPOINTS.openEditor && agentRoute === void 0 && !path6.startsWith(`${import_shared10.SPOTPATCH_API_BASE}/`)) {
2430
+ const path8 = requestPath(request);
2431
+ const agentRoute = matchAgentRequestPath(path8);
2432
+ if (path8 !== import_shared10.SPOTPATCH_ENDPOINTS.sourceContext && path8 !== import_shared10.SPOTPATCH_ENDPOINTS.openEditor && agentRoute === void 0 && !path8.startsWith(`${import_shared10.SPOTPATCH_API_BASE}/`)) {
2084
2433
  next();
2085
2434
  return;
2086
2435
  }
2087
2436
  const handle = async () => {
2088
- if (path6 === import_shared10.SPOTPATCH_ENDPOINTS.bootstrap && bootstrap !== void 0) {
2437
+ if (path8 === import_shared10.SPOTPATCH_ENDPOINTS.bootstrap && bootstrap !== void 0) {
2089
2438
  const data = await readRuntimeBootstrap(
2090
2439
  request,
2091
2440
  bootstrap
@@ -2097,7 +2446,7 @@ function createSpotPatchMiddleware(options) {
2097
2446
  allowLan: options.options.allowLan,
2098
2447
  sessionToken: options.session.token
2099
2448
  });
2100
- if (path6 === import_shared10.SPOTPATCH_ENDPOINTS.sourceContext) {
2449
+ if (path8 === import_shared10.SPOTPATCH_ENDPOINTS.sourceContext) {
2101
2450
  if (request.method !== "POST") {
2102
2451
  throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
2103
2452
  }
@@ -2105,7 +2454,7 @@ function createSpotPatchMiddleware(options) {
2105
2454
  writeJson(response, 200, { ok: true, data });
2106
2455
  return;
2107
2456
  }
2108
- if (path6 === import_shared10.SPOTPATCH_ENDPOINTS.openEditor) {
2457
+ if (path8 === import_shared10.SPOTPATCH_ENDPOINTS.openEditor) {
2109
2458
  if (request.method !== "POST") {
2110
2459
  throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
2111
2460
  }
@@ -2133,9 +2482,9 @@ function createSpotPatchMiddleware(options) {
2133
2482
  }
2134
2483
 
2135
2484
  // src/server/source-registration.ts
2136
- var import_node_crypto4 = require("crypto");
2137
- var import_promises4 = require("fs/promises");
2138
- var import_node_path5 = __toESM(require("path"), 1);
2485
+ var import_node_crypto5 = require("crypto");
2486
+ var import_promises6 = require("fs/promises");
2487
+ var import_node_path7 = __toESM(require("path"), 1);
2139
2488
  var import_compiler = require("@spotpatch/compiler");
2140
2489
  var import_zod2 = require("zod");
2141
2490
  var REGISTRATION_BODY_LIMIT_BYTES = 4096;
@@ -2156,14 +2505,14 @@ function identitiesMatch(actual, expected) {
2156
2505
  }
2157
2506
  const actualBytes = Buffer.from(actual);
2158
2507
  const expectedBytes = Buffer.from(expected);
2159
- return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto4.timingSafeEqual)(actualBytes, expectedBytes);
2508
+ return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto5.timingSafeEqual)(actualBytes, expectedBytes);
2160
2509
  }
2161
2510
  function isWithinRoot(root, candidate) {
2162
- const relative = import_node_path5.default.relative(root, candidate);
2163
- return relative === "" || !relative.startsWith(`..${import_node_path5.default.sep}`) && relative !== ".." && !import_node_path5.default.isAbsolute(relative);
2511
+ const relative = import_node_path7.default.relative(root, candidate);
2512
+ return relative === "" || !relative.startsWith(`..${import_node_path7.default.sep}`) && relative !== ".." && !import_node_path7.default.isAbsolute(relative);
2164
2513
  }
2165
2514
  function hasForbiddenSegment(root, candidate) {
2166
- return import_node_path5.default.relative(root, candidate).split(import_node_path5.default.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
2515
+ return import_node_path7.default.relative(root, candidate).split(import_node_path7.default.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
2167
2516
  }
2168
2517
  function writeJson2(response, statusCode, payload) {
2169
2518
  const body = JSON.stringify(payload);
@@ -2185,15 +2534,15 @@ function requestComesFromLoopbackWorker(request) {
2185
2534
  }
2186
2535
  }
2187
2536
  async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
2188
- if (!import_node_path5.default.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
2537
+ if (!import_node_path7.default.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
2189
2538
  return void 0;
2190
2539
  }
2191
2540
  try {
2192
- const sourceStat = await (0, import_promises4.lstat)(requestedPath);
2541
+ const sourceStat = await (0, import_promises6.lstat)(requestedPath);
2193
2542
  if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
2194
2543
  return void 0;
2195
2544
  }
2196
- const resolvedPath = await (0, import_promises4.realpath)(requestedPath);
2545
+ const resolvedPath = await (0, import_promises6.realpath)(requestedPath);
2197
2546
  if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
2198
2547
  return void 0;
2199
2548
  }
@@ -2206,7 +2555,7 @@ async function createSourceRegistrationService(input) {
2206
2555
  if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
2207
2556
  throw new TypeError("The source registration identity is invalid.");
2208
2557
  }
2209
- const root = await (0, import_promises4.realpath)(input.root);
2558
+ const root = await (0, import_promises6.realpath)(input.root);
2210
2559
  const sourceFilter = (0, import_compiler.createSourceFilter)(root, input.options);
2211
2560
  const handler = (request, response) => {
2212
2561
  const handle = async () => {
@@ -2251,11 +2600,11 @@ async function createSourceRegistrationService(input) {
2251
2600
  }
2252
2601
 
2253
2602
  // src/session/session.ts
2254
- var import_node_crypto5 = require("crypto");
2603
+ var import_node_crypto6 = require("crypto");
2255
2604
  function createSession() {
2256
2605
  return Object.freeze({
2257
- id: (0, import_node_crypto5.randomBytes)(16).toString("base64url"),
2258
- token: (0, import_node_crypto5.randomBytes)(16).toString("base64url")
2606
+ id: (0, import_node_crypto6.randomBytes)(16).toString("base64url"),
2607
+ token: (0, import_node_crypto6.randomBytes)(16).toString("base64url")
2259
2608
  });
2260
2609
  }
2261
2610
 
@@ -2283,7 +2632,7 @@ var BUDGET_KEYS = Object.freeze([
2283
2632
  "maxComponentDepth"
2284
2633
  ]);
2285
2634
  var REGEXP_FLAGS_PATTERN = /^(?!.*(.).*\1)[dgimsuvy]*$/u;
2286
- function isRecord(value) {
2635
+ function isRecord2(value) {
2287
2636
  return typeof value === "object" && value !== null && !Array.isArray(value);
2288
2637
  }
2289
2638
  function hasExactKeys(value, keys) {
@@ -2368,7 +2717,7 @@ function parseFilterList(value) {
2368
2717
  }
2369
2718
  return Object.freeze(
2370
2719
  value.map((entry) => {
2371
- if (!isRecord(entry)) {
2720
+ if (!isRecord2(entry)) {
2372
2721
  throw new TypeError("The SpotPatch filter transport is invalid.");
2373
2722
  }
2374
2723
  if (entry.kind === "string" && hasExactKeys(entry, ["kind", "value"]) && typeof entry.value === "string" && entry.value.length > 0 && entry.value.length <= 1024 && !entry.value.includes("\0")) {
@@ -2386,7 +2735,7 @@ function parseFilterList(value) {
2386
2735
  );
2387
2736
  }
2388
2737
  function parseBudget(value) {
2389
- if (!isRecord(value) || !hasExactKeys(value, BUDGET_KEYS)) {
2738
+ if (!isRecord2(value) || !hasExactKeys(value, BUDGET_KEYS)) {
2390
2739
  throw new TypeError("The SpotPatch budget transport is invalid.");
2391
2740
  }
2392
2741
  const budget = Object.fromEntries(
@@ -2395,10 +2744,10 @@ function parseBudget(value) {
2395
2744
  return Object.freeze(budget);
2396
2745
  }
2397
2746
  function parseSerializedSpotPatchOptions(value) {
2398
- if (!isRecord(value) || !hasExactKeys(value, OPTION_KEYS)) {
2747
+ if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
2399
2748
  throw new TypeError("The SpotPatch options transport is invalid.");
2400
2749
  }
2401
- if (typeof value.enabled !== "boolean" || typeof value.redact !== "boolean" || typeof value.allowLan !== "boolean" || typeof value.debug !== "boolean" || typeof value.shortcut !== "string" || typeof value.maxTargets !== "number" || typeof value.editor !== "string" || typeof value.locale !== "string" || value.ai !== false && !isRecord(value.ai)) {
2750
+ if (typeof value.enabled !== "boolean" || typeof value.redact !== "boolean" || typeof value.allowLan !== "boolean" || typeof value.debug !== "boolean" || typeof value.shortcut !== "string" || typeof value.maxTargets !== "number" || typeof value.editor !== "string" || typeof value.locale !== "string" || value.ai !== false && !isRecord2(value.ai)) {
2402
2751
  throw new TypeError("The SpotPatch options transport is invalid.");
2403
2752
  }
2404
2753
  try {
@@ -2426,19 +2775,25 @@ function parseSerializedSpotPatchOptions(value) {
2426
2775
  0 && (module.exports = {
2427
2776
  DEFAULT_EXCLUDE,
2428
2777
  DEFAULT_OPTIONS,
2778
+ applyIntegrationPlan,
2429
2779
  createAgentJobManager,
2780
+ createIntegrationFileChange,
2430
2781
  createRuntimeAiConfig,
2431
2782
  createSession,
2432
2783
  createSourceRegistrationService,
2433
2784
  createSourceRegistry,
2434
2785
  createSpotPatchMiddleware,
2786
+ discoverProjectValidationCheck,
2787
+ integrationPathExists,
2435
2788
  isLoopbackHostname,
2436
2789
  parseSerializedSpotPatchOptions,
2790
+ readIntegrationFile,
2437
2791
  readJsonRequestBody,
2438
2792
  readRuntimeBootstrap,
2439
2793
  resolveCredentialEnvironment,
2440
2794
  resolveEnvironmentAiConfiguration,
2441
2795
  resolveOptions,
2796
+ resolveProjectOptions,
2442
2797
  resolveRuntimeBootstrapOptions,
2443
2798
  serializeResolvedSpotPatchOptions
2444
2799
  });