@spotpatch/dev-server 0.1.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/LICENSE +21 -0
- package/README.md +31 -0
- package/dist/index.cjs +2447 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +201 -0
- package/dist/index.d.ts +201 -0
- package/dist/index.js +2438 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2447 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
DEFAULT_EXCLUDE: () => DEFAULT_EXCLUDE,
|
|
34
|
+
DEFAULT_OPTIONS: () => DEFAULT_OPTIONS,
|
|
35
|
+
createAgentJobManager: () => createAgentJobManager,
|
|
36
|
+
createRuntimeAiConfig: () => createRuntimeAiConfig,
|
|
37
|
+
createSession: () => createSession,
|
|
38
|
+
createSourceRegistrationService: () => createSourceRegistrationService,
|
|
39
|
+
createSourceRegistry: () => createSourceRegistry,
|
|
40
|
+
createSpotPatchMiddleware: () => createSpotPatchMiddleware,
|
|
41
|
+
isLoopbackHostname: () => isLoopbackHostname,
|
|
42
|
+
parseSerializedSpotPatchOptions: () => parseSerializedSpotPatchOptions,
|
|
43
|
+
readJsonRequestBody: () => readJsonRequestBody,
|
|
44
|
+
readRuntimeBootstrap: () => readRuntimeBootstrap,
|
|
45
|
+
resolveCredentialEnvironment: () => resolveCredentialEnvironment,
|
|
46
|
+
resolveEnvironmentAiConfiguration: () => resolveEnvironmentAiConfiguration,
|
|
47
|
+
resolveOptions: () => resolveOptions,
|
|
48
|
+
resolveRuntimeBootstrapOptions: () => resolveRuntimeBootstrapOptions,
|
|
49
|
+
serializeResolvedSpotPatchOptions: () => serializeResolvedSpotPatchOptions
|
|
50
|
+
});
|
|
51
|
+
module.exports = __toCommonJS(index_exports);
|
|
52
|
+
|
|
53
|
+
// src/agent/job-manager.ts
|
|
54
|
+
var import_node_crypto = require("crypto");
|
|
55
|
+
var import_agent = require("@spotpatch/agent");
|
|
56
|
+
var import_shared = require("@spotpatch/shared");
|
|
57
|
+
var MAX_RETAINED_JOBS = 32;
|
|
58
|
+
var MAX_RETAINED_EVENTS = 512;
|
|
59
|
+
var JOB_ID_PATTERN = /^[A-Za-z0-9_-]{22,128}$/;
|
|
60
|
+
var ACTIVE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
61
|
+
"queued",
|
|
62
|
+
"preparing",
|
|
63
|
+
"running",
|
|
64
|
+
"validating",
|
|
65
|
+
"awaiting-review",
|
|
66
|
+
"applying",
|
|
67
|
+
"cancelling",
|
|
68
|
+
"reverting"
|
|
69
|
+
]);
|
|
70
|
+
var CANCELLABLE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
71
|
+
"queued",
|
|
72
|
+
"preparing",
|
|
73
|
+
"running",
|
|
74
|
+
"validating",
|
|
75
|
+
"awaiting-review"
|
|
76
|
+
]);
|
|
77
|
+
var PRUNABLE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
78
|
+
"completed",
|
|
79
|
+
"cancelled",
|
|
80
|
+
"reverted",
|
|
81
|
+
"failed"
|
|
82
|
+
]);
|
|
83
|
+
var DEFAULT_DEPENDENCIES = Object.freeze({
|
|
84
|
+
applyChange: import_agent.applyPreparedAgentChange,
|
|
85
|
+
createJobId: () => (0, import_node_crypto.randomBytes)(16).toString("base64url"),
|
|
86
|
+
executeChange: import_agent.executeAgentChange,
|
|
87
|
+
inspectWorkspace: import_agent.inspectAgentWorkspace,
|
|
88
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
89
|
+
probeCapability: import_agent.probeProviderCapability,
|
|
90
|
+
resolveCredential: import_agent.resolveProviderCredential,
|
|
91
|
+
revertChange: import_agent.revertPreparedAgentChange
|
|
92
|
+
});
|
|
93
|
+
function normalizeError(error) {
|
|
94
|
+
return error instanceof import_shared.SpotPatchError ? error : new import_shared.SpotPatchError(import_shared.ERROR_CODES.INTERNAL_ERROR, void 0, { cause: error });
|
|
95
|
+
}
|
|
96
|
+
function isActive(status) {
|
|
97
|
+
return ACTIVE_JOB_STATUSES.has(status);
|
|
98
|
+
}
|
|
99
|
+
function snapshot(job) {
|
|
100
|
+
const base = {
|
|
101
|
+
jobId: job.id,
|
|
102
|
+
status: job.status,
|
|
103
|
+
providerProfileId: job.provider.id,
|
|
104
|
+
providerLabel: job.provider.label,
|
|
105
|
+
modelProfileId: job.model.id,
|
|
106
|
+
modelLabel: job.model.label,
|
|
107
|
+
phaseMessage: job.phaseMessage,
|
|
108
|
+
createdAt: job.createdAt,
|
|
109
|
+
updatedAt: job.updatedAt,
|
|
110
|
+
canCancel: CANCELLABLE_JOB_STATUSES.has(job.status),
|
|
111
|
+
canApply: job.status === "awaiting-review" && job.preparedChange?.validationPassed === true && job.result !== void 0 && job.result.diff.length > 0,
|
|
112
|
+
canRevert: job.status === "applied"
|
|
113
|
+
};
|
|
114
|
+
return Object.freeze(
|
|
115
|
+
job.errorCode === void 0 ? base : { ...base, errorCode: job.errorCode }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
function capabilityCacheKey(provider, model) {
|
|
119
|
+
const configurationDigest = (0, import_node_crypto.createHash)("sha256").update(provider.baseURL).update("\0").update(provider.protocol).update("\0").update(provider.authentication).digest("hex");
|
|
120
|
+
return `${provider.id}:${model.id}:${configurationDigest}`;
|
|
121
|
+
}
|
|
122
|
+
function freezeEvent(event) {
|
|
123
|
+
return Object.freeze(event);
|
|
124
|
+
}
|
|
125
|
+
function createAgentJobManager(options) {
|
|
126
|
+
const dependencies = Object.freeze({
|
|
127
|
+
...DEFAULT_DEPENDENCIES,
|
|
128
|
+
...options.dependencies
|
|
129
|
+
});
|
|
130
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
131
|
+
const capabilityCache = /* @__PURE__ */ new Map();
|
|
132
|
+
const providerConsents = /* @__PURE__ */ new Set();
|
|
133
|
+
let closed = false;
|
|
134
|
+
const resolveSelection = (providerProfileId, modelProfileId) => {
|
|
135
|
+
const provider = options.ai.providers[providerProfileId];
|
|
136
|
+
if (provider === void 0) {
|
|
137
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.PROVIDER_NOT_CONFIGURED);
|
|
138
|
+
}
|
|
139
|
+
const model = provider.models[modelProfileId];
|
|
140
|
+
if (model === void 0) {
|
|
141
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.MODEL_NOT_ALLOWED);
|
|
142
|
+
}
|
|
143
|
+
const credential = dependencies.resolveCredential(
|
|
144
|
+
provider.apiKeyEnv,
|
|
145
|
+
options.environment
|
|
146
|
+
);
|
|
147
|
+
return Object.freeze({ credential, model, provider });
|
|
148
|
+
};
|
|
149
|
+
const requireJob = (jobId) => {
|
|
150
|
+
if (!JOB_ID_PATTERN.test(jobId)) {
|
|
151
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.INVALID_REQUEST);
|
|
152
|
+
}
|
|
153
|
+
const job = jobs.get(jobId);
|
|
154
|
+
if (job === void 0) {
|
|
155
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.INVALID_REQUEST);
|
|
156
|
+
}
|
|
157
|
+
return job;
|
|
158
|
+
};
|
|
159
|
+
const appendEvent = (job, event) => {
|
|
160
|
+
job.events.push(event);
|
|
161
|
+
if (job.events.length > MAX_RETAINED_EVENTS) {
|
|
162
|
+
job.events.splice(0, job.events.length - MAX_RETAINED_EVENTS);
|
|
163
|
+
}
|
|
164
|
+
for (const listener of job.listeners) {
|
|
165
|
+
listener(event);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const eventBase = (job) => {
|
|
169
|
+
job.sequence += 1;
|
|
170
|
+
return {
|
|
171
|
+
schemaVersion: 2,
|
|
172
|
+
sequence: job.sequence,
|
|
173
|
+
jobId: job.id,
|
|
174
|
+
status: job.status,
|
|
175
|
+
timestamp: dependencies.now()
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
const emitSnapshot = (job) => {
|
|
179
|
+
appendEvent(
|
|
180
|
+
job,
|
|
181
|
+
freezeEvent({
|
|
182
|
+
...eventBase(job),
|
|
183
|
+
type: "snapshot",
|
|
184
|
+
data: Object.freeze({ snapshot: snapshot(job) })
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
const emitPhase = (job, message) => {
|
|
189
|
+
appendEvent(
|
|
190
|
+
job,
|
|
191
|
+
freezeEvent({
|
|
192
|
+
...eventBase(job),
|
|
193
|
+
type: "phase",
|
|
194
|
+
data: Object.freeze({ message })
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
const emitError = (job, code) => {
|
|
199
|
+
appendEvent(
|
|
200
|
+
job,
|
|
201
|
+
freezeEvent({
|
|
202
|
+
...eventBase(job),
|
|
203
|
+
type: "error",
|
|
204
|
+
data: Object.freeze({ code, message: "The Agent job failed." })
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
const transition = (job, status, phaseMessage, errorCode) => {
|
|
209
|
+
job.status = status;
|
|
210
|
+
job.phaseMessage = phaseMessage;
|
|
211
|
+
job.errorCode = errorCode;
|
|
212
|
+
job.updatedAt = dependencies.now();
|
|
213
|
+
emitSnapshot(job);
|
|
214
|
+
emitPhase(job, phaseMessage);
|
|
215
|
+
};
|
|
216
|
+
const probeResolved = async (selection, signal) => {
|
|
217
|
+
const key = capabilityCacheKey(selection.provider, selection.model);
|
|
218
|
+
const cached = capabilityCache.get(key);
|
|
219
|
+
if (cached !== void 0) {
|
|
220
|
+
return cached;
|
|
221
|
+
}
|
|
222
|
+
const capability = await dependencies.probeCapability({
|
|
223
|
+
provider: selection.provider,
|
|
224
|
+
modelProfileId: selection.model.id,
|
|
225
|
+
limits: options.ai.execution.limits,
|
|
226
|
+
credential: selection.credential,
|
|
227
|
+
signal,
|
|
228
|
+
...options.fetch === void 0 ? {} : { fetch: options.fetch }
|
|
229
|
+
});
|
|
230
|
+
if (capability.state !== "agent-ready") {
|
|
231
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED);
|
|
232
|
+
}
|
|
233
|
+
capabilityCache.set(key, capability);
|
|
234
|
+
return capability;
|
|
235
|
+
};
|
|
236
|
+
const finishWithError = (job, error) => {
|
|
237
|
+
const normalized = normalizeError(error);
|
|
238
|
+
const cancelled = job.controller.signal.aborted || normalized.code === import_shared.ERROR_CODES.AGENT_CANCELLED;
|
|
239
|
+
transition(
|
|
240
|
+
job,
|
|
241
|
+
cancelled ? "cancelled" : "failed",
|
|
242
|
+
cancelled ? "Agent job cancelled." : "Agent job failed.",
|
|
243
|
+
cancelled ? import_shared.ERROR_CODES.AGENT_CANCELLED : normalized.code
|
|
244
|
+
);
|
|
245
|
+
if (!cancelled) {
|
|
246
|
+
emitError(job, normalized.code);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const applyChange = async (job, preparedChange) => {
|
|
250
|
+
transition(job, "applying", "Applying validated changes to the project.");
|
|
251
|
+
try {
|
|
252
|
+
await dependencies.applyChange(preparedChange);
|
|
253
|
+
transition(job, "applied", "Changes were applied to local project files.");
|
|
254
|
+
} catch (error) {
|
|
255
|
+
const normalized = normalizeError(error);
|
|
256
|
+
transition(job, "failed", "Agent change could not be applied.", normalized.code);
|
|
257
|
+
emitError(job, normalized.code);
|
|
258
|
+
throw normalized;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const runJob = async (job) => {
|
|
262
|
+
try {
|
|
263
|
+
transition(job, "preparing", "Verifying provider and model capabilities.");
|
|
264
|
+
await probeResolved(
|
|
265
|
+
Object.freeze({
|
|
266
|
+
credential: job.credential,
|
|
267
|
+
model: job.model,
|
|
268
|
+
provider: job.provider
|
|
269
|
+
}),
|
|
270
|
+
job.controller.signal
|
|
271
|
+
);
|
|
272
|
+
const callbacks = {
|
|
273
|
+
onCheck(result) {
|
|
274
|
+
appendEvent(
|
|
275
|
+
job,
|
|
276
|
+
freezeEvent({
|
|
277
|
+
...eventBase(job),
|
|
278
|
+
type: "check",
|
|
279
|
+
data: Object.freeze({ result })
|
|
280
|
+
})
|
|
281
|
+
);
|
|
282
|
+
},
|
|
283
|
+
onPhase(event) {
|
|
284
|
+
transition(job, event.phase, event.message);
|
|
285
|
+
},
|
|
286
|
+
onTool(event) {
|
|
287
|
+
appendEvent(
|
|
288
|
+
job,
|
|
289
|
+
freezeEvent({
|
|
290
|
+
...eventBase(job),
|
|
291
|
+
type: "tool",
|
|
292
|
+
data: Object.freeze({ ...event })
|
|
293
|
+
})
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const preparedChange = await dependencies.executeChange({
|
|
298
|
+
annotation: job.annotation,
|
|
299
|
+
callbacks,
|
|
300
|
+
credential: job.credential,
|
|
301
|
+
execution: options.ai.execution,
|
|
302
|
+
jobId: job.id,
|
|
303
|
+
model: job.model,
|
|
304
|
+
provider: job.provider,
|
|
305
|
+
root: options.root,
|
|
306
|
+
signal: job.controller.signal,
|
|
307
|
+
workingTreeMode: job.workingTreeMode,
|
|
308
|
+
...options.fetch === void 0 ? {} : { fetch: options.fetch }
|
|
309
|
+
});
|
|
310
|
+
job.preparedChange = preparedChange;
|
|
311
|
+
job.result = preparedChange.result;
|
|
312
|
+
appendEvent(
|
|
313
|
+
job,
|
|
314
|
+
freezeEvent({
|
|
315
|
+
...eventBase(job),
|
|
316
|
+
type: "result-ready",
|
|
317
|
+
data: Object.freeze({ hasResult: true })
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
if (!preparedChange.validationPassed) {
|
|
321
|
+
transition(
|
|
322
|
+
job,
|
|
323
|
+
"failed",
|
|
324
|
+
"Required validation checks failed.",
|
|
325
|
+
import_shared.ERROR_CODES.VALIDATION_FAILED
|
|
326
|
+
);
|
|
327
|
+
emitError(job, import_shared.ERROR_CODES.VALIDATION_FAILED);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (preparedChange.result.diff.length === 0) {
|
|
331
|
+
transition(job, "completed", "No source changes were proposed.");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (options.ai.execution.applyMode === "auto" && preparedChange.autoApplyEligible) {
|
|
335
|
+
try {
|
|
336
|
+
await applyChange(job, preparedChange);
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
transition(job, "awaiting-review", "Validated changes are ready for review.");
|
|
342
|
+
} catch (error) {
|
|
343
|
+
finishWithError(job, error);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const hasActiveJob = (excludedJobId) => [...jobs.values()].some((job) => job.id !== excludedJobId && isActive(job.status));
|
|
347
|
+
const pruneJobs = () => {
|
|
348
|
+
if (jobs.size < MAX_RETAINED_JOBS) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
for (const [jobId, job] of jobs) {
|
|
352
|
+
if (PRUNABLE_JOB_STATUSES.has(job.status)) {
|
|
353
|
+
jobs.delete(jobId);
|
|
354
|
+
}
|
|
355
|
+
if (jobs.size < MAX_RETAINED_JOBS) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
return Object.freeze({
|
|
361
|
+
async apply(jobId) {
|
|
362
|
+
const job = requireJob(jobId);
|
|
363
|
+
if (job.status !== "awaiting-review" || job.preparedChange === void 0 || !job.preparedChange.validationPassed || job.result?.diff.length === 0) {
|
|
364
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.PATCH_REJECTED);
|
|
365
|
+
}
|
|
366
|
+
await applyChange(job, job.preparedChange);
|
|
367
|
+
return snapshot(job);
|
|
368
|
+
},
|
|
369
|
+
cancel(jobId) {
|
|
370
|
+
const job = requireJob(jobId);
|
|
371
|
+
if (!CANCELLABLE_JOB_STATUSES.has(job.status)) {
|
|
372
|
+
return snapshot(job);
|
|
373
|
+
}
|
|
374
|
+
if (job.status === "awaiting-review") {
|
|
375
|
+
job.preparedChange = void 0;
|
|
376
|
+
job.controller.abort("agent-review-cancelled");
|
|
377
|
+
transition(
|
|
378
|
+
job,
|
|
379
|
+
"cancelled",
|
|
380
|
+
"Agent review was closed without applying changes.",
|
|
381
|
+
import_shared.ERROR_CODES.AGENT_CANCELLED
|
|
382
|
+
);
|
|
383
|
+
return snapshot(job);
|
|
384
|
+
}
|
|
385
|
+
transition(job, "cancelling", "Cancelling Agent job.");
|
|
386
|
+
job.controller.abort("agent-job-cancelled");
|
|
387
|
+
return snapshot(job);
|
|
388
|
+
},
|
|
389
|
+
async close() {
|
|
390
|
+
if (closed) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
closed = true;
|
|
394
|
+
for (const job of jobs.values()) {
|
|
395
|
+
if (CANCELLABLE_JOB_STATUSES.has(job.status)) {
|
|
396
|
+
job.controller.abort("vite-server-closed");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
await Promise.allSettled(
|
|
400
|
+
[...jobs.values()].map((job) => job.runPromise).filter((promise) => promise !== void 0)
|
|
401
|
+
);
|
|
402
|
+
capabilityCache.clear();
|
|
403
|
+
providerConsents.clear();
|
|
404
|
+
jobs.clear();
|
|
405
|
+
},
|
|
406
|
+
create(request) {
|
|
407
|
+
if (closed) {
|
|
408
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.AI_DISABLED);
|
|
409
|
+
}
|
|
410
|
+
if (hasActiveJob()) {
|
|
411
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.AGENT_BUSY);
|
|
412
|
+
}
|
|
413
|
+
pruneJobs();
|
|
414
|
+
if (jobs.size >= MAX_RETAINED_JOBS) {
|
|
415
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.AGENT_BUSY);
|
|
416
|
+
}
|
|
417
|
+
const selection = resolveSelection(
|
|
418
|
+
request.providerProfileId,
|
|
419
|
+
request.modelProfileId
|
|
420
|
+
);
|
|
421
|
+
providerConsents.add(selection.provider.id);
|
|
422
|
+
const id = dependencies.createJobId();
|
|
423
|
+
if (!JOB_ID_PATTERN.test(id) || jobs.has(id)) {
|
|
424
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.INTERNAL_ERROR);
|
|
425
|
+
}
|
|
426
|
+
const timestamp = dependencies.now();
|
|
427
|
+
const job = {
|
|
428
|
+
annotation: request.annotation,
|
|
429
|
+
controller: new AbortController(),
|
|
430
|
+
createdAt: timestamp,
|
|
431
|
+
credential: selection.credential,
|
|
432
|
+
errorCode: void 0,
|
|
433
|
+
events: [],
|
|
434
|
+
id,
|
|
435
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
436
|
+
model: selection.model,
|
|
437
|
+
phaseMessage: "Agent job queued.",
|
|
438
|
+
preparedChange: void 0,
|
|
439
|
+
provider: selection.provider,
|
|
440
|
+
result: void 0,
|
|
441
|
+
runPromise: void 0,
|
|
442
|
+
sequence: 0,
|
|
443
|
+
status: "queued",
|
|
444
|
+
updatedAt: timestamp,
|
|
445
|
+
workingTreeMode: request.workingTreeMode
|
|
446
|
+
};
|
|
447
|
+
jobs.set(id, job);
|
|
448
|
+
emitSnapshot(job);
|
|
449
|
+
emitPhase(job, job.phaseMessage);
|
|
450
|
+
job.runPromise = Promise.resolve().then(async () => runJob(job));
|
|
451
|
+
return snapshot(job);
|
|
452
|
+
},
|
|
453
|
+
events(jobId) {
|
|
454
|
+
return Object.freeze([...requireJob(jobId).events]);
|
|
455
|
+
},
|
|
456
|
+
async probe(request, signal) {
|
|
457
|
+
if (closed) {
|
|
458
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.AI_DISABLED);
|
|
459
|
+
}
|
|
460
|
+
return probeResolved(
|
|
461
|
+
resolveSelection(request.providerProfileId, request.modelProfileId),
|
|
462
|
+
signal
|
|
463
|
+
);
|
|
464
|
+
},
|
|
465
|
+
result(jobId) {
|
|
466
|
+
const job = requireJob(jobId);
|
|
467
|
+
const response = job.result === void 0 ? { snapshot: snapshot(job) } : { snapshot: snapshot(job), result: job.result };
|
|
468
|
+
return Object.freeze(response);
|
|
469
|
+
},
|
|
470
|
+
async revert(jobId) {
|
|
471
|
+
const job = requireJob(jobId);
|
|
472
|
+
if (job.status !== "applied" || job.preparedChange === void 0 || hasActiveJob(job.id)) {
|
|
473
|
+
throw new import_shared.SpotPatchError(
|
|
474
|
+
hasActiveJob(job.id) ? import_shared.ERROR_CODES.AGENT_BUSY : import_shared.ERROR_CODES.APPLY_CONFLICT
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
transition(job, "reverting", "Reverting the applied Agent change.");
|
|
478
|
+
try {
|
|
479
|
+
await dependencies.revertChange(job.preparedChange);
|
|
480
|
+
transition(job, "reverted", "The Agent change was safely reverted.");
|
|
481
|
+
} catch (error) {
|
|
482
|
+
const normalized = normalizeError(error);
|
|
483
|
+
transition(
|
|
484
|
+
job,
|
|
485
|
+
"applied",
|
|
486
|
+
"Revert was rejected because project files changed.",
|
|
487
|
+
normalized.code
|
|
488
|
+
);
|
|
489
|
+
emitError(job, normalized.code);
|
|
490
|
+
throw normalized;
|
|
491
|
+
}
|
|
492
|
+
return snapshot(job);
|
|
493
|
+
},
|
|
494
|
+
subscribe(jobId, listener) {
|
|
495
|
+
const job = requireJob(jobId);
|
|
496
|
+
job.listeners.add(listener);
|
|
497
|
+
return () => {
|
|
498
|
+
job.listeners.delete(listener);
|
|
499
|
+
};
|
|
500
|
+
},
|
|
501
|
+
workspaceHealth(signal) {
|
|
502
|
+
if (closed) {
|
|
503
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.AI_DISABLED);
|
|
504
|
+
}
|
|
505
|
+
return dependencies.inspectWorkspace(options.root, signal);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/environment-ai.ts
|
|
511
|
+
var AI_ENVIRONMENT_NAMES = Object.freeze({
|
|
512
|
+
authentication: "SPOTPATCH_AI_AUTHENTICATION",
|
|
513
|
+
baseURL: "SPOTPATCH_AI_BASE_URL",
|
|
514
|
+
credential: "SPOTPATCH_AI_API_KEY",
|
|
515
|
+
model: "SPOTPATCH_AI_MODEL",
|
|
516
|
+
protocol: "SPOTPATCH_AI_PROTOCOL"
|
|
517
|
+
});
|
|
518
|
+
function resolveCredentialEnvironment(options, environment) {
|
|
519
|
+
if (options.ai === false) {
|
|
520
|
+
return Object.freeze({});
|
|
521
|
+
}
|
|
522
|
+
const names = new Set(
|
|
523
|
+
Object.values(options.ai.providers).map((provider) => provider.apiKeyEnv)
|
|
524
|
+
);
|
|
525
|
+
const missing = [...names].filter((name) => {
|
|
526
|
+
const value = environment[name];
|
|
527
|
+
return value === void 0 || value.trim().length === 0;
|
|
528
|
+
});
|
|
529
|
+
if (missing.length > 0) {
|
|
530
|
+
throw new RangeError(
|
|
531
|
+
`SpotPatch AI credential environment is missing ${missing.join(", ")}.`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return Object.freeze(
|
|
535
|
+
Object.fromEntries(
|
|
536
|
+
[...names].map((name) => {
|
|
537
|
+
const value = environment[name];
|
|
538
|
+
if (value === void 0) {
|
|
539
|
+
throw new RangeError("SpotPatch AI credential resolution failed.");
|
|
540
|
+
}
|
|
541
|
+
return [name, value];
|
|
542
|
+
})
|
|
543
|
+
)
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
function normalizedValue(environment, name) {
|
|
547
|
+
const value = environment[name];
|
|
548
|
+
if (value === void 0 || value.trim().length === 0) {
|
|
549
|
+
return void 0;
|
|
550
|
+
}
|
|
551
|
+
return value.trim();
|
|
552
|
+
}
|
|
553
|
+
function resolveEnvironmentAiConfiguration(environment) {
|
|
554
|
+
const baseURL = normalizedValue(environment, AI_ENVIRONMENT_NAMES.baseURL);
|
|
555
|
+
const model = normalizedValue(environment, AI_ENVIRONMENT_NAMES.model);
|
|
556
|
+
const credential = normalizedValue(environment, AI_ENVIRONMENT_NAMES.credential);
|
|
557
|
+
const protocol = normalizedValue(environment, AI_ENVIRONMENT_NAMES.protocol);
|
|
558
|
+
const authentication = normalizedValue(
|
|
559
|
+
environment,
|
|
560
|
+
AI_ENVIRONMENT_NAMES.authentication
|
|
561
|
+
);
|
|
562
|
+
const configuredValues = [baseURL, model, credential, protocol, authentication];
|
|
563
|
+
if (configuredValues.every((value) => value === void 0)) {
|
|
564
|
+
return Object.freeze({ ai: false });
|
|
565
|
+
}
|
|
566
|
+
const missing = [
|
|
567
|
+
[AI_ENVIRONMENT_NAMES.baseURL, baseURL],
|
|
568
|
+
[AI_ENVIRONMENT_NAMES.model, model],
|
|
569
|
+
[AI_ENVIRONMENT_NAMES.credential, credential]
|
|
570
|
+
].filter((entry) => entry[1] === void 0).map(([name]) => name);
|
|
571
|
+
if (missing.length > 0) {
|
|
572
|
+
throw new RangeError(
|
|
573
|
+
`SpotPatch AI environment configuration is incomplete; missing ${missing.join(", ")}.`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
if (baseURL === void 0 || model === void 0 || credential === void 0) {
|
|
577
|
+
throw new RangeError("SpotPatch AI environment configuration is incomplete.");
|
|
578
|
+
}
|
|
579
|
+
if (protocol !== void 0 && protocol !== "responses" && protocol !== "chat-completions") {
|
|
580
|
+
throw new RangeError(
|
|
581
|
+
"SpotPatch SPOTPATCH_AI_PROTOCOL must be responses or chat-completions."
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
if (authentication !== void 0 && authentication !== "bearer" && authentication !== "x-api-key") {
|
|
585
|
+
throw new RangeError(
|
|
586
|
+
"SpotPatch SPOTPATCH_AI_AUTHENTICATION must be bearer or x-api-key."
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return Object.freeze({
|
|
590
|
+
ai: Object.freeze({
|
|
591
|
+
baseURL,
|
|
592
|
+
model,
|
|
593
|
+
...protocol === void 0 ? {} : { protocol },
|
|
594
|
+
...authentication === void 0 ? {} : { authentication }
|
|
595
|
+
})
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/options.ts
|
|
600
|
+
var import_shared2 = require("@spotpatch/shared");
|
|
601
|
+
var import_zod = require("zod");
|
|
602
|
+
var DEFAULT_EXCLUDE = Object.freeze([
|
|
603
|
+
/node_modules/,
|
|
604
|
+
/\.test\.[jt]sx$/,
|
|
605
|
+
/\.spec\.[jt]sx$/,
|
|
606
|
+
/\.stories\.[jt]sx$/,
|
|
607
|
+
/(?:^|\/)dist(?:\/|$)/,
|
|
608
|
+
/(?:^|\/)coverage(?:\/|$)/
|
|
609
|
+
]);
|
|
610
|
+
var DEFAULT_INCLUDE = Object.freeze([/(?:^|[/\\])src[/\\].+\.(?:jsx|tsx)$/]);
|
|
611
|
+
var DEFAULT_BUDGET = Object.freeze({
|
|
612
|
+
totalCharacters: 16e3,
|
|
613
|
+
domCharacters: 3e3,
|
|
614
|
+
cssCharacters: 4e3,
|
|
615
|
+
codeCharacters: 7e3,
|
|
616
|
+
maxCodeLines: 80,
|
|
617
|
+
maxComponentDepth: 8
|
|
618
|
+
});
|
|
619
|
+
var DEFAULT_OPTIONS = Object.freeze({
|
|
620
|
+
enabled: true,
|
|
621
|
+
include: DEFAULT_INCLUDE,
|
|
622
|
+
exclude: DEFAULT_EXCLUDE,
|
|
623
|
+
editor: "auto",
|
|
624
|
+
redact: true,
|
|
625
|
+
budget: DEFAULT_BUDGET,
|
|
626
|
+
shortcut: "Mod+Shift+S",
|
|
627
|
+
allowLan: false,
|
|
628
|
+
debug: false,
|
|
629
|
+
locale: "auto",
|
|
630
|
+
maxTargets: 8,
|
|
631
|
+
ai: false
|
|
632
|
+
});
|
|
633
|
+
var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
634
|
+
var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
|
|
635
|
+
var agentLimitsSchema = import_zod.z.strictObject({
|
|
636
|
+
maxTurns: import_zod.z.number().optional(),
|
|
637
|
+
maxToolCalls: import_zod.z.number().optional(),
|
|
638
|
+
maxChangedFiles: import_zod.z.number().optional(),
|
|
639
|
+
maxDiffBytes: import_zod.z.number().optional(),
|
|
640
|
+
maxReadBytesPerFile: import_zod.z.number().optional(),
|
|
641
|
+
maxToolOutputCharacters: import_zod.z.number().optional(),
|
|
642
|
+
maxProviderResponseBytes: import_zod.z.number().optional(),
|
|
643
|
+
providerConnectTimeoutMs: import_zod.z.number().optional(),
|
|
644
|
+
providerFirstByteTimeoutMs: import_zod.z.number().optional(),
|
|
645
|
+
providerIdleTimeoutMs: import_zod.z.number().optional(),
|
|
646
|
+
checkTimeoutMs: import_zod.z.number().optional(),
|
|
647
|
+
jobTimeoutMs: import_zod.z.number().optional()
|
|
648
|
+
}).optional();
|
|
649
|
+
var agentCheckSchema = import_zod.z.strictObject({
|
|
650
|
+
label: import_zod.z.string(),
|
|
651
|
+
command: import_zod.z.string(),
|
|
652
|
+
args: import_zod.z.array(import_zod.z.string()).optional(),
|
|
653
|
+
required: import_zod.z.boolean().optional(),
|
|
654
|
+
timeoutMs: import_zod.z.number().optional()
|
|
655
|
+
});
|
|
656
|
+
var aiOptionsSchema = import_zod.z.strictObject({
|
|
657
|
+
providers: import_zod.z.record(
|
|
658
|
+
import_zod.z.string(),
|
|
659
|
+
import_zod.z.strictObject({
|
|
660
|
+
type: import_zod.z.literal("openai-compatible"),
|
|
661
|
+
label: import_zod.z.string(),
|
|
662
|
+
protocol: import_zod.z.enum(["responses", "chat-completions"]),
|
|
663
|
+
authentication: import_zod.z.enum(["bearer", "x-api-key"]).optional(),
|
|
664
|
+
baseURL: import_zod.z.string(),
|
|
665
|
+
apiKeyEnv: import_zod.z.string(),
|
|
666
|
+
models: import_zod.z.record(
|
|
667
|
+
import_zod.z.string(),
|
|
668
|
+
import_zod.z.strictObject({ label: import_zod.z.string(), model: import_zod.z.string() })
|
|
669
|
+
),
|
|
670
|
+
defaultModel: import_zod.z.string()
|
|
671
|
+
})
|
|
672
|
+
),
|
|
673
|
+
defaultProvider: import_zod.z.string(),
|
|
674
|
+
execution: import_zod.z.strictObject({
|
|
675
|
+
isolation: import_zod.z.literal("git-worktree").optional(),
|
|
676
|
+
applyMode: import_zod.z.enum(["review", "auto"]).optional(),
|
|
677
|
+
checks: import_zod.z.record(import_zod.z.string(), agentCheckSchema).optional(),
|
|
678
|
+
limits: agentLimitsSchema
|
|
679
|
+
}).optional()
|
|
680
|
+
});
|
|
681
|
+
var simpleAiOptionsSchema = import_zod.z.strictObject({
|
|
682
|
+
baseURL: import_zod.z.string(),
|
|
683
|
+
model: import_zod.z.string(),
|
|
684
|
+
apiKeyEnv: import_zod.z.string().optional(),
|
|
685
|
+
protocol: import_zod.z.enum(["responses", "chat-completions"]).optional(),
|
|
686
|
+
authentication: import_zod.z.enum(["bearer", "x-api-key"]).optional(),
|
|
687
|
+
providerLabel: import_zod.z.string().optional(),
|
|
688
|
+
modelLabel: import_zod.z.string().optional(),
|
|
689
|
+
execution: aiOptionsSchema.shape.execution
|
|
690
|
+
});
|
|
691
|
+
function assertIdentifier(value, label) {
|
|
692
|
+
if (!PROFILE_ID_PATTERN.test(value)) {
|
|
693
|
+
throw new RangeError(
|
|
694
|
+
`SpotPatch ${label} must contain only letters, numbers, dot, underscore, or hyphen.`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function nonEmpty(value, label, maximum = 256) {
|
|
699
|
+
const normalized = value.trim();
|
|
700
|
+
if (normalized.length === 0 || normalized.length > maximum || value.includes("\0")) {
|
|
701
|
+
throw new RangeError(`SpotPatch ${label} is invalid.`);
|
|
702
|
+
}
|
|
703
|
+
return normalized;
|
|
704
|
+
}
|
|
705
|
+
function normalizeProviderBaseURL(value) {
|
|
706
|
+
let url;
|
|
707
|
+
try {
|
|
708
|
+
url = new URL(value);
|
|
709
|
+
} catch {
|
|
710
|
+
throw new RangeError("SpotPatch AI provider baseURL must be a valid URL.");
|
|
711
|
+
}
|
|
712
|
+
const loopbackHosts = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
713
|
+
const allowedProtocol = url.protocol === "https:" || url.protocol === "http:" && loopbackHosts.has(url.hostname);
|
|
714
|
+
if (!allowedProtocol || url.username.length > 0 || url.password.length > 0 || url.search.length > 0 || url.hash.length > 0) {
|
|
715
|
+
throw new RangeError("SpotPatch AI provider baseURL violates URL policy.");
|
|
716
|
+
}
|
|
717
|
+
url.pathname = url.pathname.replace(/\/{2,}/g, "/").replace(/\/$/, "");
|
|
718
|
+
return url.toString().replace(/\/$/, "");
|
|
719
|
+
}
|
|
720
|
+
function resolveLimits(limits) {
|
|
721
|
+
const resolved = Object.freeze({
|
|
722
|
+
maxTurns: limits?.maxTurns ?? import_shared2.DEFAULT_AGENT_LIMITS.maxTurns,
|
|
723
|
+
maxToolCalls: limits?.maxToolCalls ?? import_shared2.DEFAULT_AGENT_LIMITS.maxToolCalls,
|
|
724
|
+
maxChangedFiles: limits?.maxChangedFiles ?? import_shared2.DEFAULT_AGENT_LIMITS.maxChangedFiles,
|
|
725
|
+
maxDiffBytes: limits?.maxDiffBytes ?? import_shared2.DEFAULT_AGENT_LIMITS.maxDiffBytes,
|
|
726
|
+
maxReadBytesPerFile: limits?.maxReadBytesPerFile ?? import_shared2.DEFAULT_AGENT_LIMITS.maxReadBytesPerFile,
|
|
727
|
+
maxToolOutputCharacters: limits?.maxToolOutputCharacters ?? import_shared2.DEFAULT_AGENT_LIMITS.maxToolOutputCharacters,
|
|
728
|
+
maxProviderResponseBytes: limits?.maxProviderResponseBytes ?? import_shared2.DEFAULT_AGENT_LIMITS.maxProviderResponseBytes,
|
|
729
|
+
providerConnectTimeoutMs: limits?.providerConnectTimeoutMs ?? import_shared2.DEFAULT_AGENT_LIMITS.providerConnectTimeoutMs,
|
|
730
|
+
providerFirstByteTimeoutMs: limits?.providerFirstByteTimeoutMs ?? import_shared2.DEFAULT_AGENT_LIMITS.providerFirstByteTimeoutMs,
|
|
731
|
+
providerIdleTimeoutMs: limits?.providerIdleTimeoutMs ?? import_shared2.DEFAULT_AGENT_LIMITS.providerIdleTimeoutMs,
|
|
732
|
+
checkTimeoutMs: limits?.checkTimeoutMs ?? import_shared2.DEFAULT_AGENT_LIMITS.checkTimeoutMs,
|
|
733
|
+
jobTimeoutMs: limits?.jobTimeoutMs ?? import_shared2.DEFAULT_AGENT_LIMITS.jobTimeoutMs
|
|
734
|
+
});
|
|
735
|
+
for (const [name, value] of Object.entries(resolved)) {
|
|
736
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
737
|
+
throw new RangeError(`SpotPatch AI limit ${name} must be a positive integer.`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return resolved;
|
|
741
|
+
}
|
|
742
|
+
function resolveModels(models) {
|
|
743
|
+
const entries = Object.entries(models);
|
|
744
|
+
if (entries.length === 0) {
|
|
745
|
+
throw new RangeError("SpotPatch AI provider must declare at least one model.");
|
|
746
|
+
}
|
|
747
|
+
return Object.freeze(
|
|
748
|
+
Object.fromEntries(
|
|
749
|
+
entries.map(([id, model]) => {
|
|
750
|
+
assertIdentifier(id, "model profile id");
|
|
751
|
+
return [
|
|
752
|
+
id,
|
|
753
|
+
Object.freeze({
|
|
754
|
+
id,
|
|
755
|
+
label: nonEmpty(model.label, "model label", 100),
|
|
756
|
+
model: nonEmpty(model.model, "provider model name")
|
|
757
|
+
})
|
|
758
|
+
];
|
|
759
|
+
})
|
|
760
|
+
)
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
function resolveProviders(providers) {
|
|
764
|
+
const entries = Object.entries(providers);
|
|
765
|
+
if (entries.length === 0) {
|
|
766
|
+
throw new RangeError("SpotPatch AI must declare at least one provider.");
|
|
767
|
+
}
|
|
768
|
+
return Object.freeze(
|
|
769
|
+
Object.fromEntries(
|
|
770
|
+
entries.map(([id, provider]) => {
|
|
771
|
+
assertIdentifier(id, "provider profile id");
|
|
772
|
+
if (!ENV_NAME_PATTERN.test(provider.apiKeyEnv) || provider.apiKeyEnv.startsWith("VITE_")) {
|
|
773
|
+
throw new RangeError(
|
|
774
|
+
"SpotPatch AI apiKeyEnv must be an uppercase non-VITE environment name."
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
const models = resolveModels(provider.models);
|
|
778
|
+
if (!(provider.defaultModel in models)) {
|
|
779
|
+
throw new RangeError(
|
|
780
|
+
"SpotPatch AI provider defaultModel must reference a configured model."
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
return [
|
|
784
|
+
id,
|
|
785
|
+
Object.freeze({
|
|
786
|
+
id,
|
|
787
|
+
type: provider.type,
|
|
788
|
+
label: nonEmpty(provider.label, "provider label", 100),
|
|
789
|
+
protocol: provider.protocol,
|
|
790
|
+
authentication: provider.authentication ?? "bearer",
|
|
791
|
+
baseURL: normalizeProviderBaseURL(provider.baseURL),
|
|
792
|
+
apiKeyEnv: provider.apiKeyEnv,
|
|
793
|
+
models,
|
|
794
|
+
defaultModel: provider.defaultModel
|
|
795
|
+
})
|
|
796
|
+
];
|
|
797
|
+
})
|
|
798
|
+
)
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
function resolveChecks(checks, defaultTimeoutMs) {
|
|
802
|
+
return Object.freeze(
|
|
803
|
+
Object.fromEntries(
|
|
804
|
+
Object.entries(checks ?? {}).map(([id, check]) => {
|
|
805
|
+
assertIdentifier(id, "check id");
|
|
806
|
+
const timeoutMs = check.timeoutMs ?? defaultTimeoutMs;
|
|
807
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
808
|
+
throw new RangeError("SpotPatch AI check timeout must be positive.");
|
|
809
|
+
}
|
|
810
|
+
const args = Object.freeze(
|
|
811
|
+
[...check.args ?? []].map(
|
|
812
|
+
(argument) => nonEmpty(argument, "check argument", 4096)
|
|
813
|
+
)
|
|
814
|
+
);
|
|
815
|
+
return [
|
|
816
|
+
id,
|
|
817
|
+
Object.freeze({
|
|
818
|
+
id,
|
|
819
|
+
label: nonEmpty(check.label, "check label", 100),
|
|
820
|
+
command: nonEmpty(check.command, "check command", 1024),
|
|
821
|
+
args,
|
|
822
|
+
required: check.required ?? true,
|
|
823
|
+
timeoutMs
|
|
824
|
+
})
|
|
825
|
+
];
|
|
826
|
+
})
|
|
827
|
+
)
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
function resolveAiOptions(options) {
|
|
831
|
+
if (options === void 0 || options === false) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
const expanded = "providers" in options ? options : (() => {
|
|
835
|
+
const simple = simpleAiOptionsSchema.safeParse(options);
|
|
836
|
+
if (!simple.success) {
|
|
837
|
+
throw new RangeError("SpotPatch AI configuration is invalid.");
|
|
838
|
+
}
|
|
839
|
+
const providerId = "default";
|
|
840
|
+
const modelId = "default";
|
|
841
|
+
return {
|
|
842
|
+
providers: {
|
|
843
|
+
[providerId]: {
|
|
844
|
+
type: "openai-compatible",
|
|
845
|
+
label: simple.data.providerLabel ?? "AI provider",
|
|
846
|
+
protocol: simple.data.protocol ?? "chat-completions",
|
|
847
|
+
authentication: simple.data.authentication ?? "bearer",
|
|
848
|
+
baseURL: simple.data.baseURL,
|
|
849
|
+
apiKeyEnv: simple.data.apiKeyEnv ?? "SPOTPATCH_AI_API_KEY",
|
|
850
|
+
models: {
|
|
851
|
+
[modelId]: {
|
|
852
|
+
label: simple.data.modelLabel ?? "AI model",
|
|
853
|
+
model: simple.data.model
|
|
854
|
+
}
|
|
855
|
+
},
|
|
856
|
+
defaultModel: modelId
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
defaultProvider: providerId,
|
|
860
|
+
...simple.data.execution === void 0 ? {} : { execution: simple.data.execution }
|
|
861
|
+
};
|
|
862
|
+
})();
|
|
863
|
+
const parsed = aiOptionsSchema.safeParse(expanded);
|
|
864
|
+
if (!parsed.success) {
|
|
865
|
+
throw new RangeError("SpotPatch AI configuration is invalid.");
|
|
866
|
+
}
|
|
867
|
+
const validated = parsed.data;
|
|
868
|
+
const limits = resolveLimits(validated.execution?.limits);
|
|
869
|
+
const checks = resolveChecks(validated.execution?.checks, limits.checkTimeoutMs);
|
|
870
|
+
const applyMode = validated.execution?.applyMode ?? "review";
|
|
871
|
+
if (applyMode === "auto" && !Object.values(checks).some((check) => check.required)) {
|
|
872
|
+
throw new RangeError("SpotPatch AI auto mode requires a required check.");
|
|
873
|
+
}
|
|
874
|
+
const providers = resolveProviders(validated.providers);
|
|
875
|
+
if (!(validated.defaultProvider in providers)) {
|
|
876
|
+
throw new RangeError(
|
|
877
|
+
"SpotPatch AI defaultProvider must reference a configured provider."
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
return Object.freeze({
|
|
881
|
+
providers,
|
|
882
|
+
defaultProvider: validated.defaultProvider,
|
|
883
|
+
execution: Object.freeze({
|
|
884
|
+
isolation: "git-worktree",
|
|
885
|
+
applyMode,
|
|
886
|
+
checks,
|
|
887
|
+
limits
|
|
888
|
+
})
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
function createRuntimeAiConfig(options) {
|
|
892
|
+
if (options === false) {
|
|
893
|
+
return Object.freeze({ enabled: false });
|
|
894
|
+
}
|
|
895
|
+
return Object.freeze({
|
|
896
|
+
enabled: true,
|
|
897
|
+
defaultProvider: options.defaultProvider,
|
|
898
|
+
applyMode: options.execution.applyMode,
|
|
899
|
+
providers: Object.freeze(
|
|
900
|
+
Object.values(options.providers).map(
|
|
901
|
+
(provider) => Object.freeze({
|
|
902
|
+
id: provider.id,
|
|
903
|
+
label: provider.label,
|
|
904
|
+
protocol: provider.protocol,
|
|
905
|
+
defaultModel: provider.defaultModel,
|
|
906
|
+
models: Object.freeze(
|
|
907
|
+
Object.values(provider.models).map(
|
|
908
|
+
(model) => Object.freeze({ id: model.id, label: model.label })
|
|
909
|
+
)
|
|
910
|
+
)
|
|
911
|
+
})
|
|
912
|
+
)
|
|
913
|
+
)
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
function assertPositiveBudget(budget) {
|
|
917
|
+
for (const [name, value] of Object.entries(budget)) {
|
|
918
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
919
|
+
throw new RangeError(`SpotPatch budget ${name} must be a positive integer.`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function resolveOptions(options = {}, environmentAi) {
|
|
924
|
+
const budget = Object.freeze({
|
|
925
|
+
...DEFAULT_OPTIONS.budget,
|
|
926
|
+
...options.budget
|
|
927
|
+
});
|
|
928
|
+
assertPositiveBudget(budget);
|
|
929
|
+
const maxTargets = options.maxTargets ?? DEFAULT_OPTIONS.maxTargets;
|
|
930
|
+
const locale = options.locale ?? DEFAULT_OPTIONS.locale;
|
|
931
|
+
const editor = options.editor ?? DEFAULT_OPTIONS.editor;
|
|
932
|
+
if (!import_shared2.SPOTPATCH_LOCALE_PREFERENCES.includes(locale)) {
|
|
933
|
+
throw new RangeError("SpotPatch locale must be auto, en-US, or zh-CN.");
|
|
934
|
+
}
|
|
935
|
+
if (!import_shared2.SPOTPATCH_EDITOR_PREFERENCES.includes(editor)) {
|
|
936
|
+
throw new RangeError("SpotPatch editor must be auto, vscode, or cursor.");
|
|
937
|
+
}
|
|
938
|
+
if (!Number.isSafeInteger(maxTargets) || maxTargets < 1 || maxTargets > import_shared2.MAX_ANNOTATION_TARGETS) {
|
|
939
|
+
throw new RangeError(
|
|
940
|
+
`SpotPatch maxTargets must be an integer between 1 and ${String(import_shared2.MAX_ANNOTATION_TARGETS)}.`
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const resolved = {
|
|
944
|
+
enabled: options.enabled ?? DEFAULT_OPTIONS.enabled,
|
|
945
|
+
include: Object.freeze([...options.include ?? DEFAULT_OPTIONS.include]),
|
|
946
|
+
exclude: Object.freeze([...options.exclude ?? DEFAULT_OPTIONS.exclude]),
|
|
947
|
+
editor,
|
|
948
|
+
redact: options.redact ?? DEFAULT_OPTIONS.redact,
|
|
949
|
+
budget,
|
|
950
|
+
shortcut: options.shortcut ?? DEFAULT_OPTIONS.shortcut,
|
|
951
|
+
allowLan: options.allowLan ?? DEFAULT_OPTIONS.allowLan,
|
|
952
|
+
debug: options.debug ?? DEFAULT_OPTIONS.debug,
|
|
953
|
+
locale,
|
|
954
|
+
maxTargets,
|
|
955
|
+
ai: resolveAiOptions(options.ai ?? environmentAi)
|
|
956
|
+
};
|
|
957
|
+
if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
|
|
958
|
+
throw new RangeError("SpotPatch shortcut is invalid.");
|
|
959
|
+
}
|
|
960
|
+
return Object.freeze(resolved);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/registry/source-registry.ts
|
|
964
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
965
|
+
|
|
966
|
+
// src/registry/source-id.ts
|
|
967
|
+
var import_node_crypto2 = require("crypto");
|
|
968
|
+
var SOURCE_ID_BYTES = 8;
|
|
969
|
+
var createRandomSourceId = () => (0, import_node_crypto2.randomBytes)(SOURCE_ID_BYTES).toString("base64url");
|
|
970
|
+
|
|
971
|
+
// src/registry/source-registry.ts
|
|
972
|
+
function normalizeAbsolutePath(absolutePath) {
|
|
973
|
+
return import_node_path.default.normalize(import_node_path.default.resolve(absolutePath));
|
|
974
|
+
}
|
|
975
|
+
function createSourceRegistry(options = {}) {
|
|
976
|
+
const createId = options.createId ?? createRandomSourceId;
|
|
977
|
+
const pathToId = /* @__PURE__ */ new Map();
|
|
978
|
+
const idToPath = /* @__PURE__ */ new Map();
|
|
979
|
+
return Object.freeze({
|
|
980
|
+
register(absolutePath) {
|
|
981
|
+
const normalizedPath = normalizeAbsolutePath(absolutePath);
|
|
982
|
+
const existingId = pathToId.get(normalizedPath);
|
|
983
|
+
if (existingId !== void 0) {
|
|
984
|
+
return existingId;
|
|
985
|
+
}
|
|
986
|
+
let fileId = createId();
|
|
987
|
+
while (idToPath.has(fileId)) {
|
|
988
|
+
fileId = createId();
|
|
989
|
+
}
|
|
990
|
+
pathToId.set(normalizedPath, fileId);
|
|
991
|
+
idToPath.set(fileId, normalizedPath);
|
|
992
|
+
return fileId;
|
|
993
|
+
},
|
|
994
|
+
resolve(fileId) {
|
|
995
|
+
return idToPath.get(fileId);
|
|
996
|
+
},
|
|
997
|
+
clear() {
|
|
998
|
+
pathToId.clear();
|
|
999
|
+
idToPath.clear();
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/server/middleware.ts
|
|
1005
|
+
var import_shared10 = require("@spotpatch/shared");
|
|
1006
|
+
|
|
1007
|
+
// src/server/agent-http.ts
|
|
1008
|
+
var import_shared7 = require("@spotpatch/shared");
|
|
1009
|
+
|
|
1010
|
+
// src/server/agent-request.ts
|
|
1011
|
+
var import_promises3 = require("fs/promises");
|
|
1012
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
1013
|
+
var import_shared5 = require("@spotpatch/shared");
|
|
1014
|
+
|
|
1015
|
+
// src/server/source-context.ts
|
|
1016
|
+
var import_promises2 = require("fs/promises");
|
|
1017
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
1018
|
+
var import_shared4 = require("@spotpatch/shared");
|
|
1019
|
+
|
|
1020
|
+
// src/server/extract-code-context.ts
|
|
1021
|
+
var import_oxc_parser = require("oxc-parser");
|
|
1022
|
+
function isComponentName(name) {
|
|
1023
|
+
return /^[A-Z]/u.test(name);
|
|
1024
|
+
}
|
|
1025
|
+
function unwrapTypeExpression(expression) {
|
|
1026
|
+
switch (expression.type) {
|
|
1027
|
+
case "TSAsExpression":
|
|
1028
|
+
case "TSSatisfiesExpression":
|
|
1029
|
+
case "TSTypeAssertion":
|
|
1030
|
+
case "TSNonNullExpression":
|
|
1031
|
+
case "TSInstantiationExpression":
|
|
1032
|
+
return unwrapTypeExpression(expression.expression);
|
|
1033
|
+
default:
|
|
1034
|
+
return expression;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function calleeName(expression) {
|
|
1038
|
+
const unwrapped = unwrapTypeExpression(expression);
|
|
1039
|
+
if (unwrapped.type === "Identifier") {
|
|
1040
|
+
return unwrapped.name;
|
|
1041
|
+
}
|
|
1042
|
+
if (unwrapped.type === "MemberExpression" && !unwrapped.computed) {
|
|
1043
|
+
return unwrapped.property.type === "Identifier" ? unwrapped.property.name : void 0;
|
|
1044
|
+
}
|
|
1045
|
+
return void 0;
|
|
1046
|
+
}
|
|
1047
|
+
function isFunctionExpression(expression) {
|
|
1048
|
+
const unwrapped = unwrapTypeExpression(expression);
|
|
1049
|
+
return unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "FunctionExpression";
|
|
1050
|
+
}
|
|
1051
|
+
function isSupportedComponentInitializer(expression) {
|
|
1052
|
+
const unwrapped = unwrapTypeExpression(expression);
|
|
1053
|
+
if (isFunctionExpression(unwrapped)) {
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
if (unwrapped.type !== "CallExpression") {
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
const name = calleeName(unwrapped.callee);
|
|
1060
|
+
if (name !== "memo" && name !== "forwardRef") {
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
const firstArgument = unwrapped.arguments[0];
|
|
1064
|
+
return firstArgument !== void 0 && firstArgument.type !== "SpreadElement" && (isFunctionExpression(firstArgument) || isSupportedComponentInitializer(firstArgument));
|
|
1065
|
+
}
|
|
1066
|
+
function variableComponent(node) {
|
|
1067
|
+
if (node.id.type !== "Identifier" || !isComponentName(node.id.name) || node.init === null || !isSupportedComponentInitializer(node.init)) {
|
|
1068
|
+
return void 0;
|
|
1069
|
+
}
|
|
1070
|
+
return Object.freeze({ start: node.start, end: node.end, name: node.id.name });
|
|
1071
|
+
}
|
|
1072
|
+
function functionComponent(node) {
|
|
1073
|
+
return node.id !== null && isComponentName(node.id.name) && node.body !== null ? Object.freeze({ start: node.start, end: node.end, name: node.id.name }) : void 0;
|
|
1074
|
+
}
|
|
1075
|
+
function isReactComponentSuperclass(expression) {
|
|
1076
|
+
if (expression === null) {
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
const unwrapped = unwrapTypeExpression(expression);
|
|
1080
|
+
if (unwrapped.type === "Identifier") {
|
|
1081
|
+
return unwrapped.name === "Component" || unwrapped.name === "PureComponent";
|
|
1082
|
+
}
|
|
1083
|
+
return unwrapped.type === "MemberExpression" && !unwrapped.computed && unwrapped.object.type === "Identifier" && unwrapped.object.name === "React" && (unwrapped.property.name === "Component" || unwrapped.property.name === "PureComponent");
|
|
1084
|
+
}
|
|
1085
|
+
function classComponent(node) {
|
|
1086
|
+
return node.id !== null && isComponentName(node.id.name) && isReactComponentSuperclass(node.superClass) ? Object.freeze({ start: node.start, end: node.end, name: node.id.name }) : void 0;
|
|
1087
|
+
}
|
|
1088
|
+
function selectedOffset(source, line, column) {
|
|
1089
|
+
const lines = source.split(/\r?\n/u);
|
|
1090
|
+
if (line < 1 || line > lines.length) {
|
|
1091
|
+
return void 0;
|
|
1092
|
+
}
|
|
1093
|
+
const lineStart = lines.slice(0, line - 1).reduce((total, value) => total + value.length + 1, 0);
|
|
1094
|
+
const lineLength = lines[line - 1]?.length ?? 0;
|
|
1095
|
+
return lineStart + Math.min(Math.max(0, column - 1), lineLength);
|
|
1096
|
+
}
|
|
1097
|
+
function findComponentSpan(options) {
|
|
1098
|
+
const offset = selectedOffset(options.source, options.line, options.column);
|
|
1099
|
+
if (offset === void 0) {
|
|
1100
|
+
return void 0;
|
|
1101
|
+
}
|
|
1102
|
+
let parseResult;
|
|
1103
|
+
try {
|
|
1104
|
+
parseResult = (0, import_oxc_parser.parseSync)(options.sourcePath, options.source, {
|
|
1105
|
+
sourceType: "module"
|
|
1106
|
+
});
|
|
1107
|
+
} catch {
|
|
1108
|
+
return void 0;
|
|
1109
|
+
}
|
|
1110
|
+
if (parseResult.errors.length > 0) {
|
|
1111
|
+
return void 0;
|
|
1112
|
+
}
|
|
1113
|
+
const jsxNodes = [];
|
|
1114
|
+
const components = [];
|
|
1115
|
+
const visitor = new import_oxc_parser.Visitor({
|
|
1116
|
+
JSXElement(node) {
|
|
1117
|
+
jsxNodes.push(node);
|
|
1118
|
+
},
|
|
1119
|
+
JSXFragment(node) {
|
|
1120
|
+
jsxNodes.push(node);
|
|
1121
|
+
},
|
|
1122
|
+
FunctionDeclaration(node) {
|
|
1123
|
+
const candidate = functionComponent(node);
|
|
1124
|
+
if (candidate !== void 0) {
|
|
1125
|
+
components.push(candidate);
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
VariableDeclarator(node) {
|
|
1129
|
+
const candidate = variableComponent(node);
|
|
1130
|
+
if (candidate !== void 0) {
|
|
1131
|
+
components.push(candidate);
|
|
1132
|
+
}
|
|
1133
|
+
},
|
|
1134
|
+
ClassDeclaration(node) {
|
|
1135
|
+
const candidate = classComponent(node);
|
|
1136
|
+
if (candidate !== void 0) {
|
|
1137
|
+
components.push(candidate);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
visitor.visit(parseResult.program);
|
|
1142
|
+
const selectedJsx = jsxNodes.filter((node) => node.start <= offset && node.end >= offset).sort((left, right) => left.end - left.start - (right.end - right.start))[0];
|
|
1143
|
+
if (selectedJsx === void 0) {
|
|
1144
|
+
return void 0;
|
|
1145
|
+
}
|
|
1146
|
+
return components.filter(
|
|
1147
|
+
(component) => component.start <= selectedJsx.start && component.end >= selectedJsx.end
|
|
1148
|
+
).sort((left, right) => left.end - left.start - (right.end - right.start))[0];
|
|
1149
|
+
}
|
|
1150
|
+
function lineAtOffset(source, offset) {
|
|
1151
|
+
let line = 1;
|
|
1152
|
+
for (let index = 0; index < offset; index += 1) {
|
|
1153
|
+
if (source[index] === "\n") {
|
|
1154
|
+
line += 1;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return line;
|
|
1158
|
+
}
|
|
1159
|
+
function componentRange(source, component) {
|
|
1160
|
+
return Object.freeze({
|
|
1161
|
+
startLine: lineAtOffset(source, component.start),
|
|
1162
|
+
endLine: lineAtOffset(source, Math.max(component.start, component.end - 1))
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
function truncateSelectedLine(line, column, maxCharacters) {
|
|
1166
|
+
if (line.length <= maxCharacters) {
|
|
1167
|
+
return line;
|
|
1168
|
+
}
|
|
1169
|
+
if (maxCharacters === 1) {
|
|
1170
|
+
return "\u2026";
|
|
1171
|
+
}
|
|
1172
|
+
const contentCharacters = maxCharacters - 2;
|
|
1173
|
+
const desiredStart = Math.max(0, column - 1 - Math.floor(contentCharacters / 2));
|
|
1174
|
+
const start = Math.min(desiredStart, line.length - contentCharacters);
|
|
1175
|
+
const end = start + contentCharacters;
|
|
1176
|
+
return `${start > 0 ? "\u2026" : ""}${line.slice(start, end)}${end < line.length ? "\u2026" : ""}`.slice(
|
|
1177
|
+
0,
|
|
1178
|
+
maxCharacters
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
function boundedRange(lines, selectedLine, column, initialStart, initialEnd, maxCharacters) {
|
|
1182
|
+
let startLine = initialStart;
|
|
1183
|
+
let endLine = initialEnd;
|
|
1184
|
+
let excerpt = lines.slice(startLine - 1, endLine).join("\n");
|
|
1185
|
+
while (excerpt.length > maxCharacters && startLine < endLine) {
|
|
1186
|
+
if (endLine - selectedLine >= selectedLine - startLine) {
|
|
1187
|
+
endLine -= 1;
|
|
1188
|
+
} else {
|
|
1189
|
+
startLine += 1;
|
|
1190
|
+
}
|
|
1191
|
+
excerpt = lines.slice(startLine - 1, endLine).join("\n");
|
|
1192
|
+
}
|
|
1193
|
+
if (excerpt.length > maxCharacters) {
|
|
1194
|
+
startLine = selectedLine;
|
|
1195
|
+
endLine = selectedLine;
|
|
1196
|
+
excerpt = truncateSelectedLine(
|
|
1197
|
+
lines[selectedLine - 1] ?? "",
|
|
1198
|
+
column,
|
|
1199
|
+
maxCharacters
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
return Object.freeze({ startLine, endLine, excerpt });
|
|
1203
|
+
}
|
|
1204
|
+
function nearbyContext(options) {
|
|
1205
|
+
const lines = options.source.split(/\r?\n/u);
|
|
1206
|
+
const initialStart = Math.max(1, options.line - Math.floor(options.maxLines / 2));
|
|
1207
|
+
const initialEnd = Math.min(lines.length, initialStart + options.maxLines - 1);
|
|
1208
|
+
const startLine = Math.max(1, initialEnd - options.maxLines + 1);
|
|
1209
|
+
const bounded = boundedRange(
|
|
1210
|
+
lines,
|
|
1211
|
+
options.line,
|
|
1212
|
+
options.column,
|
|
1213
|
+
startLine,
|
|
1214
|
+
initialEnd,
|
|
1215
|
+
options.maxCharacters
|
|
1216
|
+
);
|
|
1217
|
+
return Object.freeze({
|
|
1218
|
+
relativePath: options.relativePath,
|
|
1219
|
+
language: options.language,
|
|
1220
|
+
startLine: bounded.startLine,
|
|
1221
|
+
endLine: bounded.endLine,
|
|
1222
|
+
excerpt: bounded.excerpt,
|
|
1223
|
+
boundary: "nearby-lines"
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
function extractCodeContext(options) {
|
|
1227
|
+
const component = findComponentSpan(options);
|
|
1228
|
+
if (component !== void 0) {
|
|
1229
|
+
const range = componentRange(options.source, component);
|
|
1230
|
+
const lineCount = range.endLine - range.startLine + 1;
|
|
1231
|
+
const excerpt = options.source.split(/\r?\n/u).slice(range.startLine - 1, range.endLine).join("\n");
|
|
1232
|
+
if (lineCount <= options.maxLines && excerpt.length <= options.maxCharacters) {
|
|
1233
|
+
return Object.freeze({
|
|
1234
|
+
relativePath: options.relativePath,
|
|
1235
|
+
language: options.language,
|
|
1236
|
+
startLine: range.startLine,
|
|
1237
|
+
endLine: range.endLine,
|
|
1238
|
+
excerpt,
|
|
1239
|
+
boundary: "component"
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return nearbyContext(options);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// src/server/source-file.ts
|
|
1247
|
+
var import_promises = require("fs/promises");
|
|
1248
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
1249
|
+
var import_shared3 = require("@spotpatch/shared");
|
|
1250
|
+
|
|
1251
|
+
// src/server/constants.ts
|
|
1252
|
+
var MAX_REQUEST_BODY_BYTES = 32 * 1024;
|
|
1253
|
+
var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
|
|
1254
|
+
var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
|
|
1255
|
+
|
|
1256
|
+
// src/server/source-file.ts
|
|
1257
|
+
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
|
|
1258
|
+
function isMissingFileError(error) {
|
|
1259
|
+
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
1260
|
+
}
|
|
1261
|
+
async function assertInsideRoot(root, candidate) {
|
|
1262
|
+
let realRoot;
|
|
1263
|
+
let realCandidate;
|
|
1264
|
+
try {
|
|
1265
|
+
[realRoot, realCandidate] = await Promise.all([
|
|
1266
|
+
(0, import_promises.realpath)(root),
|
|
1267
|
+
(0, import_promises.realpath)(candidate)
|
|
1268
|
+
]);
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
if (isMissingFileError(error)) {
|
|
1271
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
1272
|
+
cause: error
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
throw error;
|
|
1276
|
+
}
|
|
1277
|
+
const relative = import_node_path2.default.relative(realRoot, realCandidate);
|
|
1278
|
+
const outside = relative.startsWith(`..${import_node_path2.default.sep}`) || relative === ".." || import_node_path2.default.isAbsolute(relative);
|
|
1279
|
+
if (outside) {
|
|
1280
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_OUTSIDE_ROOT);
|
|
1281
|
+
}
|
|
1282
|
+
return realCandidate;
|
|
1283
|
+
}
|
|
1284
|
+
async function resolveSourceFile(options) {
|
|
1285
|
+
const registeredPath = options.registry.resolve(options.fileId);
|
|
1286
|
+
if (registeredPath === void 0) {
|
|
1287
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
1288
|
+
}
|
|
1289
|
+
const sourcePath = await assertInsideRoot(options.root, registeredPath);
|
|
1290
|
+
if (!ALLOWED_EXTENSIONS.has(import_node_path2.default.extname(sourcePath).toLowerCase())) {
|
|
1291
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
1292
|
+
}
|
|
1293
|
+
let sourceStat;
|
|
1294
|
+
try {
|
|
1295
|
+
sourceStat = await (0, import_promises.stat)(sourcePath);
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
if (isMissingFileError(error)) {
|
|
1298
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
1299
|
+
cause: error
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
throw error;
|
|
1303
|
+
}
|
|
1304
|
+
if (!sourceStat.isFile()) {
|
|
1305
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
1306
|
+
}
|
|
1307
|
+
if (sourceStat.size > MAX_SOURCE_FILE_BYTES) {
|
|
1308
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_TOO_LARGE);
|
|
1309
|
+
}
|
|
1310
|
+
return sourcePath;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/server/source-context.ts
|
|
1314
|
+
function toDisplayPath(root, sourcePath) {
|
|
1315
|
+
return import_node_path3.default.relative(root, sourcePath).split(import_node_path3.default.sep).join("/");
|
|
1316
|
+
}
|
|
1317
|
+
async function readSourceContext(options) {
|
|
1318
|
+
const sourcePath = await resolveSourceFile({
|
|
1319
|
+
fileId: options.request.fileId,
|
|
1320
|
+
registry: options.registry,
|
|
1321
|
+
root: options.root
|
|
1322
|
+
});
|
|
1323
|
+
let source;
|
|
1324
|
+
try {
|
|
1325
|
+
source = await (0, import_promises2.readFile)(sourcePath, "utf8");
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1328
|
+
throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
1329
|
+
cause: error
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
throw error;
|
|
1333
|
+
}
|
|
1334
|
+
const lines = source.split(/\r?\n/);
|
|
1335
|
+
if (options.request.line > lines.length) {
|
|
1336
|
+
throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
1337
|
+
}
|
|
1338
|
+
const extension = import_node_path3.default.extname(sourcePath).toLowerCase();
|
|
1339
|
+
return extractCodeContext({
|
|
1340
|
+
source,
|
|
1341
|
+
sourcePath,
|
|
1342
|
+
relativePath: toDisplayPath(await (0, import_promises2.realpath)(options.root), sourcePath),
|
|
1343
|
+
language: extension === ".tsx" ? "tsx" : "jsx",
|
|
1344
|
+
line: options.request.line,
|
|
1345
|
+
column: options.request.column,
|
|
1346
|
+
maxLines: Math.min(options.request.maxLines, options.maxLines),
|
|
1347
|
+
maxCharacters: options.maxCharacters
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/server/agent-request.ts
|
|
1352
|
+
function compactSourceRef(source) {
|
|
1353
|
+
return Object.freeze({
|
|
1354
|
+
origin: source.origin,
|
|
1355
|
+
confidence: source.confidence,
|
|
1356
|
+
...source.fileId === void 0 ? {} : { fileId: source.fileId },
|
|
1357
|
+
...source.relativePath === void 0 ? {} : { relativePath: source.relativePath },
|
|
1358
|
+
...source.line === void 0 ? {} : { line: source.line },
|
|
1359
|
+
...source.column === void 0 ? {} : { column: source.column }
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
async function authorizeSourceRef(source, registry, root) {
|
|
1363
|
+
const markerOrigin = source.origin === "jsx-host" || source.origin === "dom-ancestor";
|
|
1364
|
+
if (markerOrigin && (source.fileId === void 0 || source.line === void 0 || source.column === void 0)) {
|
|
1365
|
+
throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
|
|
1366
|
+
}
|
|
1367
|
+
if (source.fileId === void 0) {
|
|
1368
|
+
if (source.origin === "none" && source.relativePath !== void 0) {
|
|
1369
|
+
throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
|
|
1370
|
+
}
|
|
1371
|
+
return compactSourceRef(source);
|
|
1372
|
+
}
|
|
1373
|
+
const sourcePath = await resolveSourceFile({
|
|
1374
|
+
fileId: source.fileId,
|
|
1375
|
+
registry,
|
|
1376
|
+
root
|
|
1377
|
+
});
|
|
1378
|
+
const relativePath = import_node_path4.default.relative(await (0, import_promises3.realpath)(root), sourcePath).split(import_node_path4.default.sep).join("/");
|
|
1379
|
+
if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
|
|
1380
|
+
throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
|
|
1381
|
+
}
|
|
1382
|
+
return Object.freeze({
|
|
1383
|
+
...compactSourceRef(source),
|
|
1384
|
+
relativePath
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
function freezeMatchedRule(rule) {
|
|
1388
|
+
return Object.freeze({
|
|
1389
|
+
selector: rule.selector,
|
|
1390
|
+
declarations: rule.declarations,
|
|
1391
|
+
...rule.source === void 0 ? {} : { source: rule.source },
|
|
1392
|
+
...rule.media === void 0 ? {} : { media: rule.media }
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
function targetIdentity(target) {
|
|
1396
|
+
const source = target.source;
|
|
1397
|
+
if (source.fileId !== void 0 && source.line !== void 0 && source.column !== void 0) {
|
|
1398
|
+
return `source:${source.fileId}:${String(source.line)}:${String(source.column)}`;
|
|
1399
|
+
}
|
|
1400
|
+
return [
|
|
1401
|
+
"element",
|
|
1402
|
+
source.origin,
|
|
1403
|
+
source.relativePath ?? "",
|
|
1404
|
+
target.element.selector,
|
|
1405
|
+
target.element.sanitizedHtml
|
|
1406
|
+
].join("\0");
|
|
1407
|
+
}
|
|
1408
|
+
async function authorizeTarget(target, input) {
|
|
1409
|
+
const source = await authorizeSourceRef(target.source, input.registry, input.root);
|
|
1410
|
+
const reactSourceInput = target.react.source;
|
|
1411
|
+
const reactSource = reactSourceInput === void 0 ? void 0 : await authorizeSourceRef(reactSourceInput, input.registry, input.root);
|
|
1412
|
+
const marker = source.fileId === void 0 || source.line === void 0 || source.column === void 0 ? void 0 : Object.freeze({
|
|
1413
|
+
fileId: source.fileId,
|
|
1414
|
+
line: source.line,
|
|
1415
|
+
column: source.column,
|
|
1416
|
+
maxLines: input.options.budget.maxCodeLines
|
|
1417
|
+
});
|
|
1418
|
+
if (marker === void 0 && target.code !== void 0) {
|
|
1419
|
+
throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
|
|
1420
|
+
}
|
|
1421
|
+
const code = marker === void 0 ? void 0 : await readSourceContext({
|
|
1422
|
+
request: marker,
|
|
1423
|
+
registry: input.registry,
|
|
1424
|
+
root: input.root,
|
|
1425
|
+
maxCharacters: input.options.budget.codeCharacters,
|
|
1426
|
+
maxLines: input.options.budget.maxCodeLines
|
|
1427
|
+
});
|
|
1428
|
+
if (target.code !== void 0 && target.code.relativePath !== code?.relativePath) {
|
|
1429
|
+
throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
|
|
1430
|
+
}
|
|
1431
|
+
return Object.freeze({
|
|
1432
|
+
instruction: target.instruction,
|
|
1433
|
+
source,
|
|
1434
|
+
react: Object.freeze({
|
|
1435
|
+
supported: target.react.supported,
|
|
1436
|
+
...target.react.version === void 0 ? {} : { version: target.react.version },
|
|
1437
|
+
...target.react.componentName === void 0 ? {} : { componentName: target.react.componentName },
|
|
1438
|
+
componentStack: Object.freeze([...target.react.componentStack]),
|
|
1439
|
+
...reactSource === void 0 ? {} : { source: reactSource }
|
|
1440
|
+
}),
|
|
1441
|
+
element: Object.freeze({
|
|
1442
|
+
tagName: target.element.tagName,
|
|
1443
|
+
selector: target.element.selector,
|
|
1444
|
+
sanitizedHtml: target.element.sanitizedHtml,
|
|
1445
|
+
...target.element.textPreview === void 0 ? {} : { textPreview: target.element.textPreview },
|
|
1446
|
+
...target.element.role === void 0 ? {} : { role: target.element.role },
|
|
1447
|
+
rect: Object.freeze({ ...target.element.rect })
|
|
1448
|
+
}),
|
|
1449
|
+
styles: Object.freeze({
|
|
1450
|
+
classNames: Object.freeze([...target.styles.classNames]),
|
|
1451
|
+
...target.styles.inlineStyle === void 0 ? {} : { inlineStyle: target.styles.inlineStyle },
|
|
1452
|
+
matchedRules: Object.freeze(target.styles.matchedRules.map(freezeMatchedRule)),
|
|
1453
|
+
computed: Object.freeze({ ...target.styles.computed }),
|
|
1454
|
+
warnings: Object.freeze([...target.styles.warnings])
|
|
1455
|
+
}),
|
|
1456
|
+
...code === void 0 ? {} : { code: Object.freeze({ ...code }) },
|
|
1457
|
+
warnings: Object.freeze([...target.warnings])
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
async function authorizeAgentJobRequest(input) {
|
|
1461
|
+
const requestedTargets = input.request.annotation.targets;
|
|
1462
|
+
if (requestedTargets.length > input.options.maxTargets) {
|
|
1463
|
+
throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
|
|
1464
|
+
}
|
|
1465
|
+
const identities = requestedTargets.map(targetIdentity);
|
|
1466
|
+
if (new Set(identities).size !== identities.length) {
|
|
1467
|
+
throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
|
|
1468
|
+
}
|
|
1469
|
+
const targets = Object.freeze(
|
|
1470
|
+
await Promise.all(requestedTargets.map((target) => authorizeTarget(target, input)))
|
|
1471
|
+
);
|
|
1472
|
+
const annotation = Object.freeze({
|
|
1473
|
+
schemaVersion: 3,
|
|
1474
|
+
id: input.request.annotation.id,
|
|
1475
|
+
locale: input.request.annotation.locale,
|
|
1476
|
+
page: Object.freeze({ ...input.request.annotation.page }),
|
|
1477
|
+
targets,
|
|
1478
|
+
createdAt: input.request.annotation.createdAt
|
|
1479
|
+
});
|
|
1480
|
+
return Object.freeze({
|
|
1481
|
+
annotation,
|
|
1482
|
+
providerProfileId: input.request.providerProfileId,
|
|
1483
|
+
modelProfileId: input.request.modelProfileId,
|
|
1484
|
+
providerDataConsent: true,
|
|
1485
|
+
workingTreeMode: input.request.workingTreeMode
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// src/server/request-body.ts
|
|
1490
|
+
var import_shared6 = require("@spotpatch/shared");
|
|
1491
|
+
function isJsonContentType(value) {
|
|
1492
|
+
return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
1493
|
+
}
|
|
1494
|
+
async function readJsonRequestBody(request, maximumBytes = MAX_REQUEST_BODY_BYTES) {
|
|
1495
|
+
if (!isJsonContentType(request.headers["content-type"])) {
|
|
1496
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.INVALID_REQUEST);
|
|
1497
|
+
}
|
|
1498
|
+
const declaredLength = Number(request.headers["content-length"]);
|
|
1499
|
+
if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
|
|
1500
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.INVALID_REQUEST);
|
|
1501
|
+
}
|
|
1502
|
+
const chunks = [];
|
|
1503
|
+
let byteLength = 0;
|
|
1504
|
+
let exceededLimit = false;
|
|
1505
|
+
for await (const rawChunk of request) {
|
|
1506
|
+
const chunk = rawChunk;
|
|
1507
|
+
if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
|
|
1508
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.INVALID_REQUEST);
|
|
1509
|
+
}
|
|
1510
|
+
const buffer = Buffer.from(chunk);
|
|
1511
|
+
byteLength += buffer.byteLength;
|
|
1512
|
+
if (byteLength > maximumBytes) {
|
|
1513
|
+
exceededLimit = true;
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
chunks.push(buffer);
|
|
1517
|
+
}
|
|
1518
|
+
if (exceededLimit || byteLength === 0) {
|
|
1519
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.INVALID_REQUEST);
|
|
1520
|
+
}
|
|
1521
|
+
try {
|
|
1522
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.INVALID_REQUEST, void 0, {
|
|
1525
|
+
cause: error
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// src/server/agent-http.ts
|
|
1531
|
+
var AGENT_JOB_ID_PATTERN = /^[A-Za-z0-9_-]{22,128}$/;
|
|
1532
|
+
var AGENT_JOB_ACTIONS = /* @__PURE__ */ new Set([
|
|
1533
|
+
"events",
|
|
1534
|
+
"result",
|
|
1535
|
+
"cancel",
|
|
1536
|
+
"apply",
|
|
1537
|
+
"revert"
|
|
1538
|
+
]);
|
|
1539
|
+
var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
|
|
1540
|
+
"awaiting-review",
|
|
1541
|
+
"applied",
|
|
1542
|
+
"completed",
|
|
1543
|
+
"cancelled",
|
|
1544
|
+
"reverted",
|
|
1545
|
+
"failed"
|
|
1546
|
+
]);
|
|
1547
|
+
function matchAgentRequestPath(path6) {
|
|
1548
|
+
if (path6 === import_shared7.SPOTPATCH_ENDPOINTS.agentCapability) {
|
|
1549
|
+
return Object.freeze({ kind: "capability" });
|
|
1550
|
+
}
|
|
1551
|
+
if (path6 === import_shared7.SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
|
|
1552
|
+
return Object.freeze({ kind: "workspace-health" });
|
|
1553
|
+
}
|
|
1554
|
+
if (path6 === import_shared7.SPOTPATCH_ENDPOINTS.agentJobs) {
|
|
1555
|
+
return Object.freeze({ kind: "create-job" });
|
|
1556
|
+
}
|
|
1557
|
+
const prefix = `${import_shared7.SPOTPATCH_ENDPOINTS.agentJobs}/`;
|
|
1558
|
+
if (!path6.startsWith(prefix)) {
|
|
1559
|
+
return void 0;
|
|
1560
|
+
}
|
|
1561
|
+
const segments = path6.slice(prefix.length).split("/");
|
|
1562
|
+
const jobId = segments[0];
|
|
1563
|
+
const action = segments[1];
|
|
1564
|
+
if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
|
|
1565
|
+
return void 0;
|
|
1566
|
+
}
|
|
1567
|
+
return Object.freeze({
|
|
1568
|
+
kind: "job-action",
|
|
1569
|
+
action,
|
|
1570
|
+
jobId
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
function requireAgentManager(options) {
|
|
1574
|
+
if (options.agentManager === void 0 || options.options.ai === false) {
|
|
1575
|
+
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.AI_DISABLED);
|
|
1576
|
+
}
|
|
1577
|
+
return options.agentManager;
|
|
1578
|
+
}
|
|
1579
|
+
function writeNdjsonEvent(response, event) {
|
|
1580
|
+
response.write(`${JSON.stringify(event)}
|
|
1581
|
+
`);
|
|
1582
|
+
}
|
|
1583
|
+
function streamAgentJobEvents(response, manager, jobId) {
|
|
1584
|
+
const events = manager.events(jobId);
|
|
1585
|
+
const current = manager.result(jobId).snapshot;
|
|
1586
|
+
response.statusCode = 200;
|
|
1587
|
+
response.setHeader("Cache-Control", "no-store");
|
|
1588
|
+
response.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
1589
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
1590
|
+
for (const event of events) {
|
|
1591
|
+
writeNdjsonEvent(response, event);
|
|
1592
|
+
}
|
|
1593
|
+
if (EVENT_STREAM_END_STATUSES.has(current.status)) {
|
|
1594
|
+
response.end();
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
let settled = false;
|
|
1598
|
+
let unsubscribe = () => void 0;
|
|
1599
|
+
const heartbeat = setInterval(() => {
|
|
1600
|
+
if (!settled) {
|
|
1601
|
+
response.write("\n");
|
|
1602
|
+
}
|
|
1603
|
+
}, 15e3);
|
|
1604
|
+
heartbeat.unref();
|
|
1605
|
+
const cleanup = () => {
|
|
1606
|
+
if (settled) {
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
settled = true;
|
|
1610
|
+
clearInterval(heartbeat);
|
|
1611
|
+
unsubscribe();
|
|
1612
|
+
};
|
|
1613
|
+
unsubscribe = manager.subscribe(jobId, (event) => {
|
|
1614
|
+
if (settled) {
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
writeNdjsonEvent(response, event);
|
|
1618
|
+
if (event.type === "snapshot" && EVENT_STREAM_END_STATUSES.has(event.data.snapshot.status)) {
|
|
1619
|
+
cleanup();
|
|
1620
|
+
response.end();
|
|
1621
|
+
}
|
|
1622
|
+
});
|
|
1623
|
+
response.once("close", cleanup);
|
|
1624
|
+
response.once("error", cleanup);
|
|
1625
|
+
}
|
|
1626
|
+
async function handleCapability(request, response, options, writeSuccess) {
|
|
1627
|
+
if (request.method !== "POST") {
|
|
1628
|
+
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
|
|
1629
|
+
}
|
|
1630
|
+
const parsed = import_shared7.agentCapabilityRequestSchema.safeParse(
|
|
1631
|
+
await readJsonRequestBody(request)
|
|
1632
|
+
);
|
|
1633
|
+
if (!parsed.success) {
|
|
1634
|
+
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
|
|
1635
|
+
}
|
|
1636
|
+
const controller = new AbortController();
|
|
1637
|
+
const abort = () => {
|
|
1638
|
+
controller.abort("agent-capability-client-disconnected");
|
|
1639
|
+
};
|
|
1640
|
+
response.once("close", abort);
|
|
1641
|
+
try {
|
|
1642
|
+
const data = await requireAgentManager(options).probe(
|
|
1643
|
+
parsed.data,
|
|
1644
|
+
controller.signal
|
|
1645
|
+
);
|
|
1646
|
+
writeSuccess(response, 200, data);
|
|
1647
|
+
} finally {
|
|
1648
|
+
response.removeListener("close", abort);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
async function handleCreateJob(request, response, options, writeSuccess) {
|
|
1652
|
+
if (request.method !== "POST") {
|
|
1653
|
+
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
|
|
1654
|
+
}
|
|
1655
|
+
const parsed = import_shared7.agentJobCreateRequestSchema.safeParse(
|
|
1656
|
+
await readJsonRequestBody(request, MAX_AGENT_REQUEST_BODY_BYTES)
|
|
1657
|
+
);
|
|
1658
|
+
if (!parsed.success) {
|
|
1659
|
+
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
|
|
1660
|
+
}
|
|
1661
|
+
const authorizedRequest = await authorizeAgentJobRequest({
|
|
1662
|
+
request: parsed.data,
|
|
1663
|
+
options: options.options,
|
|
1664
|
+
registry: options.registry,
|
|
1665
|
+
root: options.root
|
|
1666
|
+
});
|
|
1667
|
+
const data = requireAgentManager(options).create(authorizedRequest);
|
|
1668
|
+
writeSuccess(response, 202, data);
|
|
1669
|
+
}
|
|
1670
|
+
async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
1671
|
+
if (request.method !== "POST") {
|
|
1672
|
+
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
|
|
1673
|
+
}
|
|
1674
|
+
const parsed = import_shared7.agentWorkspaceHealthRequestSchema.safeParse(
|
|
1675
|
+
await readJsonRequestBody(request)
|
|
1676
|
+
);
|
|
1677
|
+
if (!parsed.success) {
|
|
1678
|
+
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
|
|
1679
|
+
}
|
|
1680
|
+
const controller = new AbortController();
|
|
1681
|
+
const abort = () => {
|
|
1682
|
+
controller.abort("agent-workspace-health-client-disconnected");
|
|
1683
|
+
};
|
|
1684
|
+
response.once("close", abort);
|
|
1685
|
+
try {
|
|
1686
|
+
const data = await requireAgentManager(options).workspaceHealth(controller.signal);
|
|
1687
|
+
writeSuccess(response, 200, data);
|
|
1688
|
+
} finally {
|
|
1689
|
+
response.removeListener("close", abort);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
async function handleJobAction(request, response, options, route, writeSuccess) {
|
|
1693
|
+
const manager = requireAgentManager(options);
|
|
1694
|
+
if (request.method !== "POST") {
|
|
1695
|
+
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
|
|
1696
|
+
}
|
|
1697
|
+
const parsed = import_shared7.agentJobActionRequestSchema.safeParse(
|
|
1698
|
+
await readJsonRequestBody(request)
|
|
1699
|
+
);
|
|
1700
|
+
if (!parsed.success) {
|
|
1701
|
+
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
|
|
1702
|
+
}
|
|
1703
|
+
if (route.action === "events") {
|
|
1704
|
+
streamAgentJobEvents(response, manager, route.jobId);
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
if (route.action === "result") {
|
|
1708
|
+
writeSuccess(response, 200, manager.result(route.jobId));
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
const data = route.action === "cancel" ? manager.cancel(route.jobId) : route.action === "apply" ? await manager.apply(route.jobId) : await manager.revert(route.jobId);
|
|
1712
|
+
writeSuccess(response, 200, data);
|
|
1713
|
+
}
|
|
1714
|
+
async function handleAgentRequest(request, response, options, route, writeSuccess) {
|
|
1715
|
+
if (route.kind === "capability") {
|
|
1716
|
+
await handleCapability(request, response, options, writeSuccess);
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
if (route.kind === "create-job") {
|
|
1720
|
+
await handleCreateJob(request, response, options, writeSuccess);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
if (route.kind === "workspace-health") {
|
|
1724
|
+
await handleWorkspaceHealth(request, response, options, writeSuccess);
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
await handleJobAction(request, response, options, route, writeSuccess);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/server/editor.ts
|
|
1731
|
+
var import_node_child_process = require("child_process");
|
|
1732
|
+
var import_launch_editor = __toESM(require("launch-editor"), 1);
|
|
1733
|
+
var EDITOR_STARTUP_GRACE_MS = 300;
|
|
1734
|
+
function normalizedEditorEnvironment(environment) {
|
|
1735
|
+
return [
|
|
1736
|
+
environment.TERM_PROGRAM,
|
|
1737
|
+
environment.VSCODE_GIT_ASKPASS_NODE,
|
|
1738
|
+
environment.VSCODE_GIT_ASKPASS_MAIN,
|
|
1739
|
+
environment.GIT_ASKPASS
|
|
1740
|
+
].filter((value) => typeof value === "string").join("\n").replaceAll("\\", "/").toLowerCase();
|
|
1741
|
+
}
|
|
1742
|
+
function detectIntegratedEditor(environment) {
|
|
1743
|
+
const signature = normalizedEditorEnvironment(environment);
|
|
1744
|
+
if (signature === "cursor" || signature.includes("/cursor")) {
|
|
1745
|
+
return "cursor";
|
|
1746
|
+
}
|
|
1747
|
+
if (signature === "vscode" || signature.includes("visual studio code") || /(^|\/)(code|code-insiders)(\.exe)?($|\/)/u.test(signature)) {
|
|
1748
|
+
return "vscode";
|
|
1749
|
+
}
|
|
1750
|
+
return void 0;
|
|
1751
|
+
}
|
|
1752
|
+
function editorCommand(editor) {
|
|
1753
|
+
return editor === "cursor" ? "cursor" : "code";
|
|
1754
|
+
}
|
|
1755
|
+
var DEFAULT_DEPENDENCIES2 = Object.freeze({
|
|
1756
|
+
environment: process.env,
|
|
1757
|
+
fallbackLauncher: import_launch_editor.default,
|
|
1758
|
+
processSpawner: (command, arguments_, options) => (0, import_node_child_process.spawn)(command, [...arguments_], options),
|
|
1759
|
+
startupGraceMs: EDITOR_STARTUP_GRACE_MS
|
|
1760
|
+
});
|
|
1761
|
+
function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
1762
|
+
return (target, configuredEditor) => {
|
|
1763
|
+
const integratedEditor = detectIntegratedEditor(dependencies.environment);
|
|
1764
|
+
const resolvedEditor = configuredEditor === "auto" ? integratedEditor : configuredEditor;
|
|
1765
|
+
return new Promise((resolve, reject) => {
|
|
1766
|
+
let settled = false;
|
|
1767
|
+
const settle = (error, editor = resolvedEditor ?? "auto") => {
|
|
1768
|
+
if (settled) {
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
settled = true;
|
|
1772
|
+
clearTimeout(startupTimer);
|
|
1773
|
+
if (error === void 0) {
|
|
1774
|
+
resolve(editor);
|
|
1775
|
+
} else {
|
|
1776
|
+
reject(error);
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
const startupTimer = setTimeout(settle, dependencies.startupGraceMs);
|
|
1780
|
+
const rejectStartup = () => {
|
|
1781
|
+
settle(new Error("The configured editor could not be started."));
|
|
1782
|
+
};
|
|
1783
|
+
try {
|
|
1784
|
+
if (resolvedEditor === void 0) {
|
|
1785
|
+
dependencies.fallbackLauncher(target, void 0, rejectStartup);
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
const child = dependencies.processSpawner(
|
|
1789
|
+
editorCommand(resolvedEditor),
|
|
1790
|
+
["--goto", target],
|
|
1791
|
+
{
|
|
1792
|
+
env: dependencies.environment,
|
|
1793
|
+
stdio: "ignore",
|
|
1794
|
+
windowsHide: true
|
|
1795
|
+
}
|
|
1796
|
+
);
|
|
1797
|
+
child.once("error", rejectStartup);
|
|
1798
|
+
child.once("exit", (code, signal) => {
|
|
1799
|
+
if (code === 0) {
|
|
1800
|
+
settle(void 0, resolvedEditor);
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
if (code !== null || signal !== null) {
|
|
1804
|
+
rejectStartup();
|
|
1805
|
+
}
|
|
1806
|
+
});
|
|
1807
|
+
} catch {
|
|
1808
|
+
rejectStartup();
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
var launchConfiguredEditor = createEditorLauncher();
|
|
1814
|
+
|
|
1815
|
+
// src/server/request-security.ts
|
|
1816
|
+
var import_node_crypto3 = require("crypto");
|
|
1817
|
+
var import_node_net = require("net");
|
|
1818
|
+
var import_shared8 = require("@spotpatch/shared");
|
|
1819
|
+
function getSingleHeader(request, name) {
|
|
1820
|
+
const value = request.headers[name.toLowerCase()];
|
|
1821
|
+
return Array.isArray(value) ? value[0] : value;
|
|
1822
|
+
}
|
|
1823
|
+
function tokensMatch(actual, expected) {
|
|
1824
|
+
if (actual === void 0) {
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1827
|
+
const actualBytes = Buffer.from(actual);
|
|
1828
|
+
const expectedBytes = Buffer.from(expected);
|
|
1829
|
+
return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto3.timingSafeEqual)(actualBytes, expectedBytes);
|
|
1830
|
+
}
|
|
1831
|
+
function isLoopbackHostname(hostname) {
|
|
1832
|
+
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
1833
|
+
if (normalized === "localhost" || normalized.endsWith(".localhost")) {
|
|
1834
|
+
return true;
|
|
1835
|
+
}
|
|
1836
|
+
if (normalized === "::1") {
|
|
1837
|
+
return true;
|
|
1838
|
+
}
|
|
1839
|
+
if ((0, import_node_net.isIP)(normalized) === 4) {
|
|
1840
|
+
return normalized.split(".")[0] === "127";
|
|
1841
|
+
}
|
|
1842
|
+
return normalized.startsWith("::ffff:127.");
|
|
1843
|
+
}
|
|
1844
|
+
function parseHost(value) {
|
|
1845
|
+
try {
|
|
1846
|
+
return new URL(`http://${value}`);
|
|
1847
|
+
} catch {
|
|
1848
|
+
return void 0;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
function parseOrigin(value) {
|
|
1852
|
+
try {
|
|
1853
|
+
const origin = new URL(value);
|
|
1854
|
+
if (origin.protocol !== "http:" && origin.protocol !== "https:" || origin.username.length > 0 || origin.password.length > 0 || origin.origin === "null") {
|
|
1855
|
+
return void 0;
|
|
1856
|
+
}
|
|
1857
|
+
return origin;
|
|
1858
|
+
} catch {
|
|
1859
|
+
return void 0;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
function assertRequestAuthorized(request, options) {
|
|
1863
|
+
const actualToken = getSingleHeader(request, import_shared8.SPOTPATCH_TOKEN_HEADER);
|
|
1864
|
+
if (!tokensMatch(actualToken, options.sessionToken)) {
|
|
1865
|
+
throw new import_shared8.SpotPatchError(import_shared8.ERROR_CODES.INVALID_TOKEN);
|
|
1866
|
+
}
|
|
1867
|
+
const hostHeader = getSingleHeader(request, "host");
|
|
1868
|
+
const originHeader = getSingleHeader(request, "origin");
|
|
1869
|
+
const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
|
|
1870
|
+
const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
|
|
1871
|
+
if (host === void 0 || origin === void 0) {
|
|
1872
|
+
throw new import_shared8.SpotPatchError(import_shared8.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
1873
|
+
}
|
|
1874
|
+
const hostIsLoopback = isLoopbackHostname(host.hostname);
|
|
1875
|
+
const originIsLoopback = isLoopbackHostname(origin.hostname);
|
|
1876
|
+
if (!options.allowLan) {
|
|
1877
|
+
if (!hostIsLoopback || !originIsLoopback) {
|
|
1878
|
+
throw new import_shared8.SpotPatchError(import_shared8.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
1879
|
+
}
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
|
|
1883
|
+
throw new import_shared8.SpotPatchError(import_shared8.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// src/server/runtime-bootstrap.ts
|
|
1888
|
+
var import_shared9 = require("@spotpatch/shared");
|
|
1889
|
+
function getSingleHeader2(request, name) {
|
|
1890
|
+
const value = request.headers[name.toLowerCase()];
|
|
1891
|
+
return Array.isArray(value) ? value[0] : value;
|
|
1892
|
+
}
|
|
1893
|
+
function resolveRuntimeBootstrapOptions(options) {
|
|
1894
|
+
let expectedOrigin;
|
|
1895
|
+
try {
|
|
1896
|
+
expectedOrigin = new URL(options.expectedOrigin);
|
|
1897
|
+
} catch {
|
|
1898
|
+
throw new TypeError("The SpotPatch bootstrap origin is invalid.");
|
|
1899
|
+
}
|
|
1900
|
+
if (expectedOrigin.origin !== options.expectedOrigin || expectedOrigin.protocol !== "http:" || !isLoopbackHostname(expectedOrigin.hostname)) {
|
|
1901
|
+
throw new TypeError("The SpotPatch bootstrap origin must be a loopback origin.");
|
|
1902
|
+
}
|
|
1903
|
+
const parsedConfig = import_shared9.runtimeConfigSchema.safeParse(options.runtimeConfig);
|
|
1904
|
+
if (!parsedConfig.success) {
|
|
1905
|
+
throw new TypeError("The SpotPatch Runtime configuration is invalid.");
|
|
1906
|
+
}
|
|
1907
|
+
return Object.freeze({
|
|
1908
|
+
expectedOrigin: expectedOrigin.origin,
|
|
1909
|
+
runtimeConfig: parsedConfig.data
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
1913
|
+
const contentType = getSingleHeader2(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1914
|
+
if (request.method !== "POST" || contentType !== "application/json") {
|
|
1915
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.INVALID_REQUEST);
|
|
1916
|
+
}
|
|
1917
|
+
const host = getSingleHeader2(request, "host");
|
|
1918
|
+
let hostIsLoopback = false;
|
|
1919
|
+
if (host !== void 0) {
|
|
1920
|
+
try {
|
|
1921
|
+
hostIsLoopback = isLoopbackHostname(new URL(`http://${host}`).hostname);
|
|
1922
|
+
} catch {
|
|
1923
|
+
hostIsLoopback = false;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
if (!hostIsLoopback || getSingleHeader2(request, "origin") !== expectedOrigin || getSingleHeader2(request, "sec-fetch-site") !== "same-origin") {
|
|
1927
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
async function readRuntimeBootstrap(request, options) {
|
|
1931
|
+
assertRuntimeBootstrapRequest(request, options.expectedOrigin);
|
|
1932
|
+
const parsedBody = import_shared9.runtimeBootstrapRequestSchema.safeParse(
|
|
1933
|
+
await readJsonRequestBody(request)
|
|
1934
|
+
);
|
|
1935
|
+
if (!parsedBody.success) {
|
|
1936
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.INVALID_REQUEST);
|
|
1937
|
+
}
|
|
1938
|
+
return options.runtimeConfig;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// src/server/middleware.ts
|
|
1942
|
+
var STATUS_BY_ERROR = Object.freeze({
|
|
1943
|
+
[import_shared10.ERROR_CODES.INVALID_REQUEST]: 400,
|
|
1944
|
+
[import_shared10.ERROR_CODES.INVALID_TOKEN]: 401,
|
|
1945
|
+
[import_shared10.ERROR_CODES.ORIGIN_NOT_ALLOWED]: 403,
|
|
1946
|
+
[import_shared10.ERROR_CODES.SOURCE_NOT_FOUND]: 404,
|
|
1947
|
+
[import_shared10.ERROR_CODES.SOURCE_OUTSIDE_ROOT]: 403,
|
|
1948
|
+
[import_shared10.ERROR_CODES.SOURCE_TOO_LARGE]: 413,
|
|
1949
|
+
[import_shared10.ERROR_CODES.EDITOR_OPEN_FAILED]: 500,
|
|
1950
|
+
[import_shared10.ERROR_CODES.AI_DISABLED]: 404,
|
|
1951
|
+
[import_shared10.ERROR_CODES.PROVIDER_NOT_CONFIGURED]: 503,
|
|
1952
|
+
[import_shared10.ERROR_CODES.PROVIDER_AUTH_FAILED]: 502,
|
|
1953
|
+
[import_shared10.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
|
|
1954
|
+
[import_shared10.ERROR_CODES.MODEL_NOT_ALLOWED]: 400,
|
|
1955
|
+
[import_shared10.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
|
|
1956
|
+
[import_shared10.ERROR_CODES.PROVIDER_RATE_LIMITED]: 429,
|
|
1957
|
+
[import_shared10.ERROR_CODES.AGENT_BUSY]: 409,
|
|
1958
|
+
[import_shared10.ERROR_CODES.AGENT_LIMIT_EXCEEDED]: 413,
|
|
1959
|
+
[import_shared10.ERROR_CODES.AGENT_CANCELLED]: 409,
|
|
1960
|
+
[import_shared10.ERROR_CODES.WORKTREE_DIRTY]: 409,
|
|
1961
|
+
[import_shared10.ERROR_CODES.WORKTREE_NOT_REPOSITORY]: 409,
|
|
1962
|
+
[import_shared10.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS]: 409,
|
|
1963
|
+
[import_shared10.ERROR_CODES.WORKTREE_CONFLICTED]: 409,
|
|
1964
|
+
[import_shared10.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
|
|
1965
|
+
[import_shared10.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
|
|
1966
|
+
[import_shared10.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
|
|
1967
|
+
[import_shared10.ERROR_CODES.TOOL_DENIED]: 403,
|
|
1968
|
+
[import_shared10.ERROR_CODES.TOOL_INPUT_INVALID]: 422,
|
|
1969
|
+
[import_shared10.ERROR_CODES.TOOL_ARGUMENTS_INVALID]: 422,
|
|
1970
|
+
[import_shared10.ERROR_CODES.TOOL_CALL_ID_CONFLICT]: 422,
|
|
1971
|
+
[import_shared10.ERROR_CODES.TOOL_PATH_DENIED]: 403,
|
|
1972
|
+
[import_shared10.ERROR_CODES.PATCH_REJECTED]: 422,
|
|
1973
|
+
[import_shared10.ERROR_CODES.VALIDATION_FAILED]: 422,
|
|
1974
|
+
[import_shared10.ERROR_CODES.APPLY_CONFLICT]: 409,
|
|
1975
|
+
[import_shared10.ERROR_CODES.INTERNAL_ERROR]: 500
|
|
1976
|
+
});
|
|
1977
|
+
var PUBLIC_MESSAGES = Object.freeze({
|
|
1978
|
+
[import_shared10.ERROR_CODES.INVALID_REQUEST]: "The request is invalid.",
|
|
1979
|
+
[import_shared10.ERROR_CODES.INVALID_TOKEN]: "The session token is invalid.",
|
|
1980
|
+
[import_shared10.ERROR_CODES.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
|
|
1981
|
+
[import_shared10.ERROR_CODES.SOURCE_NOT_FOUND]: "The source file is unavailable.",
|
|
1982
|
+
[import_shared10.ERROR_CODES.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
|
|
1983
|
+
[import_shared10.ERROR_CODES.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
|
|
1984
|
+
[import_shared10.ERROR_CODES.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
|
|
1985
|
+
[import_shared10.ERROR_CODES.AI_DISABLED]: "AI execution is not enabled.",
|
|
1986
|
+
[import_shared10.ERROR_CODES.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
|
|
1987
|
+
[import_shared10.ERROR_CODES.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
|
|
1988
|
+
[import_shared10.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
|
|
1989
|
+
[import_shared10.ERROR_CODES.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
|
|
1990
|
+
[import_shared10.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
|
|
1991
|
+
[import_shared10.ERROR_CODES.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
|
|
1992
|
+
[import_shared10.ERROR_CODES.AGENT_BUSY]: "Another Agent job is already running.",
|
|
1993
|
+
[import_shared10.ERROR_CODES.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
|
|
1994
|
+
[import_shared10.ERROR_CODES.AGENT_CANCELLED]: "The Agent job was cancelled.",
|
|
1995
|
+
[import_shared10.ERROR_CODES.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
|
|
1996
|
+
[import_shared10.ERROR_CODES.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
|
|
1997
|
+
[import_shared10.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
|
|
1998
|
+
[import_shared10.ERROR_CODES.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
|
|
1999
|
+
[import_shared10.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
|
|
2000
|
+
[import_shared10.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
|
|
2001
|
+
[import_shared10.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
|
|
2002
|
+
[import_shared10.ERROR_CODES.TOOL_DENIED]: "The Agent tool request was denied.",
|
|
2003
|
+
[import_shared10.ERROR_CODES.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
|
|
2004
|
+
[import_shared10.ERROR_CODES.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
|
|
2005
|
+
[import_shared10.ERROR_CODES.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
|
|
2006
|
+
[import_shared10.ERROR_CODES.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
|
|
2007
|
+
[import_shared10.ERROR_CODES.PATCH_REJECTED]: "The proposed patch was rejected.",
|
|
2008
|
+
[import_shared10.ERROR_CODES.VALIDATION_FAILED]: "The proposed change failed validation.",
|
|
2009
|
+
[import_shared10.ERROR_CODES.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
|
|
2010
|
+
[import_shared10.ERROR_CODES.INTERNAL_ERROR]: "The request could not be completed."
|
|
2011
|
+
});
|
|
2012
|
+
function writeJson(response, status, payload) {
|
|
2013
|
+
response.statusCode = status;
|
|
2014
|
+
response.setHeader("Cache-Control", "no-store");
|
|
2015
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2016
|
+
response.end(JSON.stringify(payload));
|
|
2017
|
+
}
|
|
2018
|
+
function asSpotPatchError(error) {
|
|
2019
|
+
return error instanceof import_shared10.SpotPatchError ? error : new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INTERNAL_ERROR, void 0, { cause: error });
|
|
2020
|
+
}
|
|
2021
|
+
function writeError(response, error, logger) {
|
|
2022
|
+
const normalized = asSpotPatchError(error);
|
|
2023
|
+
if (normalized.code === import_shared10.ERROR_CODES.INTERNAL_ERROR) {
|
|
2024
|
+
logger?.warn("[spotpatch:server] Internal request failure.");
|
|
2025
|
+
}
|
|
2026
|
+
writeJson(response, STATUS_BY_ERROR[normalized.code], {
|
|
2027
|
+
ok: false,
|
|
2028
|
+
error: {
|
|
2029
|
+
code: normalized.code,
|
|
2030
|
+
message: PUBLIC_MESSAGES[normalized.code]
|
|
2031
|
+
}
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
function requestPath(request) {
|
|
2035
|
+
try {
|
|
2036
|
+
return new URL(request.url ?? "/", "http://spotpatch.invalid").pathname;
|
|
2037
|
+
} catch {
|
|
2038
|
+
return "";
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
async function handleSourceContext(request, options) {
|
|
2042
|
+
const parsed = import_shared10.sourceContextRequestSchema.safeParse(
|
|
2043
|
+
await readJsonRequestBody(request)
|
|
2044
|
+
);
|
|
2045
|
+
if (!parsed.success) {
|
|
2046
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
|
|
2047
|
+
}
|
|
2048
|
+
return readSourceContext({
|
|
2049
|
+
request: parsed.data,
|
|
2050
|
+
registry: options.registry,
|
|
2051
|
+
root: options.root,
|
|
2052
|
+
maxCharacters: options.options.budget.codeCharacters,
|
|
2053
|
+
maxLines: options.options.budget.maxCodeLines
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
async function handleOpenEditor(request, options) {
|
|
2057
|
+
const parsed = import_shared10.openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
|
|
2058
|
+
if (!parsed.success) {
|
|
2059
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
|
|
2060
|
+
}
|
|
2061
|
+
const body = parsed.data;
|
|
2062
|
+
const sourcePath = await resolveSourceFile({
|
|
2063
|
+
fileId: body.fileId,
|
|
2064
|
+
registry: options.registry,
|
|
2065
|
+
root: options.root
|
|
2066
|
+
});
|
|
2067
|
+
const target = `${sourcePath}:${String(body.line)}:${String(body.column)}`;
|
|
2068
|
+
const editorLauncher = options.editorLauncher ?? launchConfiguredEditor;
|
|
2069
|
+
try {
|
|
2070
|
+
const editor = await editorLauncher(target, options.options.editor);
|
|
2071
|
+
return Object.freeze({ editor });
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
options.logger?.warn(
|
|
2074
|
+
`[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
|
|
2075
|
+
);
|
|
2076
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.EDITOR_OPEN_FAILED, void 0, {
|
|
2077
|
+
cause: error
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
function createSpotPatchMiddleware(options) {
|
|
2082
|
+
const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
|
|
2083
|
+
return (request, response, next) => {
|
|
2084
|
+
const path6 = requestPath(request);
|
|
2085
|
+
const agentRoute = matchAgentRequestPath(path6);
|
|
2086
|
+
if (path6 !== import_shared10.SPOTPATCH_ENDPOINTS.sourceContext && path6 !== import_shared10.SPOTPATCH_ENDPOINTS.openEditor && agentRoute === void 0 && !path6.startsWith(`${import_shared10.SPOTPATCH_API_BASE}/`)) {
|
|
2087
|
+
next();
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
const handle = async () => {
|
|
2091
|
+
if (path6 === import_shared10.SPOTPATCH_ENDPOINTS.bootstrap && bootstrap !== void 0) {
|
|
2092
|
+
const data = await readRuntimeBootstrap(
|
|
2093
|
+
request,
|
|
2094
|
+
bootstrap
|
|
2095
|
+
);
|
|
2096
|
+
writeJson(response, 200, { ok: true, data });
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
assertRequestAuthorized(request, {
|
|
2100
|
+
allowLan: options.options.allowLan,
|
|
2101
|
+
sessionToken: options.session.token
|
|
2102
|
+
});
|
|
2103
|
+
if (path6 === import_shared10.SPOTPATCH_ENDPOINTS.sourceContext) {
|
|
2104
|
+
if (request.method !== "POST") {
|
|
2105
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
|
|
2106
|
+
}
|
|
2107
|
+
const data = await handleSourceContext(request, options);
|
|
2108
|
+
writeJson(response, 200, { ok: true, data });
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
if (path6 === import_shared10.SPOTPATCH_ENDPOINTS.openEditor) {
|
|
2112
|
+
if (request.method !== "POST") {
|
|
2113
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
|
|
2114
|
+
}
|
|
2115
|
+
const data = await handleOpenEditor(request, options);
|
|
2116
|
+
writeJson(response, 200, { ok: true, data });
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
if (agentRoute === void 0) {
|
|
2120
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
|
|
2121
|
+
}
|
|
2122
|
+
await handleAgentRequest(
|
|
2123
|
+
request,
|
|
2124
|
+
response,
|
|
2125
|
+
options,
|
|
2126
|
+
agentRoute,
|
|
2127
|
+
(target, status, data) => {
|
|
2128
|
+
writeJson(target, status, { ok: true, data });
|
|
2129
|
+
}
|
|
2130
|
+
);
|
|
2131
|
+
};
|
|
2132
|
+
void handle().catch((error) => {
|
|
2133
|
+
writeError(response, error, options.logger);
|
|
2134
|
+
});
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// src/server/source-registration.ts
|
|
2139
|
+
var import_node_crypto4 = require("crypto");
|
|
2140
|
+
var import_promises4 = require("fs/promises");
|
|
2141
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
2142
|
+
var import_compiler = require("@spotpatch/compiler");
|
|
2143
|
+
var import_zod2 = require("zod");
|
|
2144
|
+
var REGISTRATION_BODY_LIMIT_BYTES = 4096;
|
|
2145
|
+
var REGISTRATION_IDENTITY_PATTERN = /^[A-Za-z0-9_-]{16,128}$/u;
|
|
2146
|
+
var INTERNAL_SECRET_HEADER = "x-spotpatch-internal";
|
|
2147
|
+
var FORBIDDEN_SOURCE_SEGMENTS = /* @__PURE__ */ new Set([".next", "node_modules"]);
|
|
2148
|
+
var registrationRequestSchema = import_zod2.z.strictObject({
|
|
2149
|
+
epoch: import_zod2.z.string().regex(REGISTRATION_IDENTITY_PATTERN),
|
|
2150
|
+
resourcePath: import_zod2.z.string().min(1).max(3072)
|
|
2151
|
+
});
|
|
2152
|
+
function getSingleHeader3(request, name) {
|
|
2153
|
+
const value = request.headers[name.toLowerCase()];
|
|
2154
|
+
return Array.isArray(value) ? value[0] : value;
|
|
2155
|
+
}
|
|
2156
|
+
function identitiesMatch(actual, expected) {
|
|
2157
|
+
if (actual === void 0) {
|
|
2158
|
+
return false;
|
|
2159
|
+
}
|
|
2160
|
+
const actualBytes = Buffer.from(actual);
|
|
2161
|
+
const expectedBytes = Buffer.from(expected);
|
|
2162
|
+
return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto4.timingSafeEqual)(actualBytes, expectedBytes);
|
|
2163
|
+
}
|
|
2164
|
+
function isWithinRoot(root, candidate) {
|
|
2165
|
+
const relative = import_node_path5.default.relative(root, candidate);
|
|
2166
|
+
return relative === "" || !relative.startsWith(`..${import_node_path5.default.sep}`) && relative !== ".." && !import_node_path5.default.isAbsolute(relative);
|
|
2167
|
+
}
|
|
2168
|
+
function hasForbiddenSegment(root, candidate) {
|
|
2169
|
+
return import_node_path5.default.relative(root, candidate).split(import_node_path5.default.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
|
|
2170
|
+
}
|
|
2171
|
+
function writeJson2(response, statusCode, payload) {
|
|
2172
|
+
const body = JSON.stringify(payload);
|
|
2173
|
+
response.statusCode = statusCode;
|
|
2174
|
+
response.setHeader("Cache-Control", "no-store");
|
|
2175
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2176
|
+
response.setHeader("Content-Length", Buffer.byteLength(body));
|
|
2177
|
+
response.end(body);
|
|
2178
|
+
}
|
|
2179
|
+
function requestComesFromLoopbackWorker(request) {
|
|
2180
|
+
const host = getSingleHeader3(request, "host");
|
|
2181
|
+
if (host === void 0 || getSingleHeader3(request, "origin") !== void 0) {
|
|
2182
|
+
return false;
|
|
2183
|
+
}
|
|
2184
|
+
try {
|
|
2185
|
+
return isLoopbackHostname(new URL(`http://${host}`).hostname);
|
|
2186
|
+
} catch {
|
|
2187
|
+
return false;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
|
|
2191
|
+
if (!import_node_path5.default.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
|
|
2192
|
+
return void 0;
|
|
2193
|
+
}
|
|
2194
|
+
try {
|
|
2195
|
+
const sourceStat = await (0, import_promises4.lstat)(requestedPath);
|
|
2196
|
+
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
2197
|
+
return void 0;
|
|
2198
|
+
}
|
|
2199
|
+
const resolvedPath = await (0, import_promises4.realpath)(requestedPath);
|
|
2200
|
+
if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
|
|
2201
|
+
return void 0;
|
|
2202
|
+
}
|
|
2203
|
+
return resolvedPath;
|
|
2204
|
+
} catch {
|
|
2205
|
+
return void 0;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
async function createSourceRegistrationService(input) {
|
|
2209
|
+
if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
|
|
2210
|
+
throw new TypeError("The source registration identity is invalid.");
|
|
2211
|
+
}
|
|
2212
|
+
const root = await (0, import_promises4.realpath)(input.root);
|
|
2213
|
+
const sourceFilter = (0, import_compiler.createSourceFilter)(root, input.options);
|
|
2214
|
+
const handler = (request, response) => {
|
|
2215
|
+
const handle = async () => {
|
|
2216
|
+
const contentType = getSingleHeader3(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
2217
|
+
if (request.method !== "POST" || contentType !== "application/json" || !requestComesFromLoopbackWorker(request) || !identitiesMatch(
|
|
2218
|
+
getSingleHeader3(request, INTERNAL_SECRET_HEADER),
|
|
2219
|
+
input.internalSecret
|
|
2220
|
+
)) {
|
|
2221
|
+
writeJson2(response, 403, { ok: false });
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
const parsed = registrationRequestSchema.safeParse(
|
|
2225
|
+
await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
|
|
2226
|
+
);
|
|
2227
|
+
if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
|
|
2228
|
+
writeJson2(response, 400, { ok: false });
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
const sourcePath = await resolveAuthorizedSource(
|
|
2232
|
+
root,
|
|
2233
|
+
parsed.data.resourcePath,
|
|
2234
|
+
(absolutePath) => sourceFilter.shouldTransform(absolutePath, "<")
|
|
2235
|
+
);
|
|
2236
|
+
if (sourcePath === void 0) {
|
|
2237
|
+
writeJson2(response, 403, { ok: false });
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
writeJson2(response, 200, {
|
|
2241
|
+
epoch: input.registryEpoch,
|
|
2242
|
+
fileId: input.registry.register(sourcePath)
|
|
2243
|
+
});
|
|
2244
|
+
};
|
|
2245
|
+
void handle().catch(() => {
|
|
2246
|
+
if (!response.headersSent) {
|
|
2247
|
+
writeJson2(response, 400, { ok: false });
|
|
2248
|
+
} else {
|
|
2249
|
+
response.destroy();
|
|
2250
|
+
}
|
|
2251
|
+
});
|
|
2252
|
+
};
|
|
2253
|
+
return Object.freeze({ handler, root });
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// src/session/session.ts
|
|
2257
|
+
var import_node_crypto5 = require("crypto");
|
|
2258
|
+
function createSession() {
|
|
2259
|
+
return Object.freeze({
|
|
2260
|
+
token: (0, import_node_crypto5.randomBytes)(16).toString("base64url")
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// src/transport-options.ts
|
|
2265
|
+
var OPTION_KEYS = Object.freeze([
|
|
2266
|
+
"ai",
|
|
2267
|
+
"allowLan",
|
|
2268
|
+
"budget",
|
|
2269
|
+
"debug",
|
|
2270
|
+
"editor",
|
|
2271
|
+
"enabled",
|
|
2272
|
+
"exclude",
|
|
2273
|
+
"include",
|
|
2274
|
+
"locale",
|
|
2275
|
+
"maxTargets",
|
|
2276
|
+
"redact",
|
|
2277
|
+
"shortcut"
|
|
2278
|
+
]);
|
|
2279
|
+
var BUDGET_KEYS = Object.freeze([
|
|
2280
|
+
"totalCharacters",
|
|
2281
|
+
"domCharacters",
|
|
2282
|
+
"cssCharacters",
|
|
2283
|
+
"codeCharacters",
|
|
2284
|
+
"maxCodeLines",
|
|
2285
|
+
"maxComponentDepth"
|
|
2286
|
+
]);
|
|
2287
|
+
var REGEXP_FLAGS_PATTERN = /^(?!.*(.).*\1)[dgimsuvy]*$/u;
|
|
2288
|
+
function isRecord(value) {
|
|
2289
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2290
|
+
}
|
|
2291
|
+
function hasExactKeys(value, keys) {
|
|
2292
|
+
const actual = Object.keys(value).sort();
|
|
2293
|
+
const expected = [...keys].sort();
|
|
2294
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
2295
|
+
}
|
|
2296
|
+
function serializeFilter(entry) {
|
|
2297
|
+
return typeof entry === "string" ? Object.freeze({ kind: "string", value: entry }) : Object.freeze({
|
|
2298
|
+
flags: entry.flags,
|
|
2299
|
+
kind: "regexp",
|
|
2300
|
+
source: entry.source
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
function serializeAiOptions(options) {
|
|
2304
|
+
return Object.freeze({
|
|
2305
|
+
providers: Object.freeze(
|
|
2306
|
+
Object.fromEntries(
|
|
2307
|
+
Object.entries(options.providers).map(([id, provider]) => [
|
|
2308
|
+
id,
|
|
2309
|
+
Object.freeze({
|
|
2310
|
+
type: provider.type,
|
|
2311
|
+
label: provider.label,
|
|
2312
|
+
protocol: provider.protocol,
|
|
2313
|
+
authentication: provider.authentication,
|
|
2314
|
+
baseURL: provider.baseURL,
|
|
2315
|
+
apiKeyEnv: provider.apiKeyEnv,
|
|
2316
|
+
models: Object.freeze(
|
|
2317
|
+
Object.fromEntries(
|
|
2318
|
+
Object.entries(provider.models).map(([modelId, model]) => [
|
|
2319
|
+
modelId,
|
|
2320
|
+
Object.freeze({ label: model.label, model: model.model })
|
|
2321
|
+
])
|
|
2322
|
+
)
|
|
2323
|
+
),
|
|
2324
|
+
defaultModel: provider.defaultModel
|
|
2325
|
+
})
|
|
2326
|
+
])
|
|
2327
|
+
)
|
|
2328
|
+
),
|
|
2329
|
+
defaultProvider: options.defaultProvider,
|
|
2330
|
+
execution: Object.freeze({
|
|
2331
|
+
isolation: options.execution.isolation,
|
|
2332
|
+
applyMode: options.execution.applyMode,
|
|
2333
|
+
checks: Object.freeze(
|
|
2334
|
+
Object.fromEntries(
|
|
2335
|
+
Object.entries(options.execution.checks).map(([id, check]) => [
|
|
2336
|
+
id,
|
|
2337
|
+
Object.freeze({
|
|
2338
|
+
label: check.label,
|
|
2339
|
+
command: check.command,
|
|
2340
|
+
args: check.args,
|
|
2341
|
+
required: check.required,
|
|
2342
|
+
timeoutMs: check.timeoutMs
|
|
2343
|
+
})
|
|
2344
|
+
])
|
|
2345
|
+
)
|
|
2346
|
+
),
|
|
2347
|
+
limits: options.execution.limits
|
|
2348
|
+
})
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
function serializeResolvedSpotPatchOptions(options) {
|
|
2352
|
+
return Object.freeze({
|
|
2353
|
+
ai: options.ai === false ? false : serializeAiOptions(options.ai),
|
|
2354
|
+
allowLan: options.allowLan,
|
|
2355
|
+
budget: options.budget,
|
|
2356
|
+
debug: options.debug,
|
|
2357
|
+
editor: options.editor,
|
|
2358
|
+
enabled: options.enabled,
|
|
2359
|
+
exclude: Object.freeze(options.exclude.map(serializeFilter)),
|
|
2360
|
+
include: Object.freeze(options.include.map(serializeFilter)),
|
|
2361
|
+
locale: options.locale,
|
|
2362
|
+
maxTargets: options.maxTargets,
|
|
2363
|
+
redact: options.redact,
|
|
2364
|
+
shortcut: options.shortcut
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
function parseFilterList(value) {
|
|
2368
|
+
if (!Array.isArray(value) || value.length > 256) {
|
|
2369
|
+
throw new TypeError("The SpotPatch filter transport is invalid.");
|
|
2370
|
+
}
|
|
2371
|
+
return Object.freeze(
|
|
2372
|
+
value.map((entry) => {
|
|
2373
|
+
if (!isRecord(entry)) {
|
|
2374
|
+
throw new TypeError("The SpotPatch filter transport is invalid.");
|
|
2375
|
+
}
|
|
2376
|
+
if (entry.kind === "string" && hasExactKeys(entry, ["kind", "value"]) && typeof entry.value === "string" && entry.value.length > 0 && entry.value.length <= 1024 && !entry.value.includes("\0")) {
|
|
2377
|
+
return entry.value;
|
|
2378
|
+
}
|
|
2379
|
+
if (entry.kind === "regexp" && hasExactKeys(entry, ["flags", "kind", "source"]) && typeof entry.source === "string" && entry.source.length <= 1024 && typeof entry.flags === "string" && REGEXP_FLAGS_PATTERN.test(entry.flags)) {
|
|
2380
|
+
try {
|
|
2381
|
+
return new RegExp(entry.source, entry.flags);
|
|
2382
|
+
} catch {
|
|
2383
|
+
throw new TypeError("The SpotPatch filter transport is invalid.");
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
throw new TypeError("The SpotPatch filter transport is invalid.");
|
|
2387
|
+
})
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
function parseBudget(value) {
|
|
2391
|
+
if (!isRecord(value) || !hasExactKeys(value, BUDGET_KEYS)) {
|
|
2392
|
+
throw new TypeError("The SpotPatch budget transport is invalid.");
|
|
2393
|
+
}
|
|
2394
|
+
const budget = Object.fromEntries(
|
|
2395
|
+
BUDGET_KEYS.map((key) => [key, value[key]])
|
|
2396
|
+
);
|
|
2397
|
+
return Object.freeze(budget);
|
|
2398
|
+
}
|
|
2399
|
+
function parseSerializedSpotPatchOptions(value) {
|
|
2400
|
+
if (!isRecord(value) || !hasExactKeys(value, OPTION_KEYS)) {
|
|
2401
|
+
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
2402
|
+
}
|
|
2403
|
+
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)) {
|
|
2404
|
+
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
2405
|
+
}
|
|
2406
|
+
try {
|
|
2407
|
+
return resolveOptions({
|
|
2408
|
+
ai: value.ai,
|
|
2409
|
+
allowLan: value.allowLan,
|
|
2410
|
+
budget: parseBudget(value.budget),
|
|
2411
|
+
debug: value.debug,
|
|
2412
|
+
editor: value.editor,
|
|
2413
|
+
enabled: value.enabled,
|
|
2414
|
+
exclude: parseFilterList(value.exclude),
|
|
2415
|
+
include: parseFilterList(value.include),
|
|
2416
|
+
locale: value.locale,
|
|
2417
|
+
maxTargets: value.maxTargets,
|
|
2418
|
+
redact: value.redact,
|
|
2419
|
+
shortcut: value.shortcut
|
|
2420
|
+
});
|
|
2421
|
+
} catch (error) {
|
|
2422
|
+
throw new TypeError("The SpotPatch options transport is invalid.", {
|
|
2423
|
+
cause: error
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2428
|
+
0 && (module.exports = {
|
|
2429
|
+
DEFAULT_EXCLUDE,
|
|
2430
|
+
DEFAULT_OPTIONS,
|
|
2431
|
+
createAgentJobManager,
|
|
2432
|
+
createRuntimeAiConfig,
|
|
2433
|
+
createSession,
|
|
2434
|
+
createSourceRegistrationService,
|
|
2435
|
+
createSourceRegistry,
|
|
2436
|
+
createSpotPatchMiddleware,
|
|
2437
|
+
isLoopbackHostname,
|
|
2438
|
+
parseSerializedSpotPatchOptions,
|
|
2439
|
+
readJsonRequestBody,
|
|
2440
|
+
readRuntimeBootstrap,
|
|
2441
|
+
resolveCredentialEnvironment,
|
|
2442
|
+
resolveEnvironmentAiConfiguration,
|
|
2443
|
+
resolveOptions,
|
|
2444
|
+
resolveRuntimeBootstrapOptions,
|
|
2445
|
+
serializeResolvedSpotPatchOptions
|
|
2446
|
+
});
|
|
2447
|
+
//# sourceMappingURL=index.cjs.map
|