@spotpatch/dev-server 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1919 -459
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +109 -4
- package/dist/index.d.ts +109 -4
- package/dist/index.js +1587 -107
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -169,6 +169,10 @@ function createAgentJobManager(options) {
|
|
|
169
169
|
job.updatedAt = dependencies.now();
|
|
170
170
|
emitSnapshot(job);
|
|
171
171
|
emitPhase(job, phaseMessage);
|
|
172
|
+
if (!isActive(status)) {
|
|
173
|
+
job.lease?.release();
|
|
174
|
+
job.lease = void 0;
|
|
175
|
+
}
|
|
172
176
|
};
|
|
173
177
|
const probeResolved = async (selection, signal) => {
|
|
174
178
|
const key = capabilityCacheKey(selection.provider, selection.model);
|
|
@@ -358,6 +362,7 @@ function createAgentJobManager(options) {
|
|
|
358
362
|
[...jobs.values()].map((job) => job.runPromise).filter((promise) => promise !== void 0)
|
|
359
363
|
);
|
|
360
364
|
capabilityCache.clear();
|
|
365
|
+
for (const job of jobs.values()) job.lease?.release();
|
|
361
366
|
jobs.clear();
|
|
362
367
|
},
|
|
363
368
|
create(request) {
|
|
@@ -382,8 +387,13 @@ function createAgentJobManager(options) {
|
|
|
382
387
|
request.providerProfileId,
|
|
383
388
|
request.modelProfileId
|
|
384
389
|
);
|
|
390
|
+
const lease = options.coordinator?.acquire("change");
|
|
391
|
+
if (options.coordinator !== void 0 && lease === void 0) {
|
|
392
|
+
throw new SpotPatchError(ERROR_CODES.AGENT_BUSY);
|
|
393
|
+
}
|
|
385
394
|
const id = dependencies.createJobId();
|
|
386
395
|
if (!JOB_ID_PATTERN.test(id) || jobs.has(id)) {
|
|
396
|
+
lease?.release();
|
|
387
397
|
throw new SpotPatchError(ERROR_CODES.INTERNAL_ERROR);
|
|
388
398
|
}
|
|
389
399
|
const timestamp = dependencies.now();
|
|
@@ -397,6 +407,7 @@ function createAgentJobManager(options) {
|
|
|
397
407
|
events: [],
|
|
398
408
|
id,
|
|
399
409
|
listeners: /* @__PURE__ */ new Set(),
|
|
410
|
+
lease,
|
|
400
411
|
model: selection.model,
|
|
401
412
|
phaseMessage: "Agent job queued.",
|
|
402
413
|
preparedChange: void 0,
|
|
@@ -439,6 +450,11 @@ function createAgentJobManager(options) {
|
|
|
439
450
|
hasActiveJob(job.id) ? ERROR_CODES.AGENT_BUSY : ERROR_CODES.APPLY_CONFLICT
|
|
440
451
|
);
|
|
441
452
|
}
|
|
453
|
+
const lease = options.coordinator?.acquire("change");
|
|
454
|
+
if (options.coordinator !== void 0 && lease === void 0) {
|
|
455
|
+
throw new SpotPatchError(ERROR_CODES.AGENT_BUSY);
|
|
456
|
+
}
|
|
457
|
+
job.lease = lease;
|
|
442
458
|
transition(job, "reverting", "Reverting the applied Agent change.");
|
|
443
459
|
try {
|
|
444
460
|
await dependencies.revertChange(job.preparedChange);
|
|
@@ -1249,12 +1265,12 @@ function createExternalHandoffStore(options) {
|
|
|
1249
1265
|
}
|
|
1250
1266
|
return current.snapshot;
|
|
1251
1267
|
};
|
|
1252
|
-
const findReplay = (requestId,
|
|
1268
|
+
const findReplay = (requestId, fingerprint2) => {
|
|
1253
1269
|
requireOpen();
|
|
1254
1270
|
sweep();
|
|
1255
1271
|
const record = idempotency.get(requestId);
|
|
1256
1272
|
if (record === void 0) return void 0;
|
|
1257
|
-
if (record.fingerprint !==
|
|
1273
|
+
if (record.fingerprint !== fingerprint2) {
|
|
1258
1274
|
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_VALIDATION_FAILED);
|
|
1259
1275
|
}
|
|
1260
1276
|
return replayResult(record);
|
|
@@ -1499,12 +1515,12 @@ function createExternalHandoffService(options) {
|
|
|
1499
1515
|
},
|
|
1500
1516
|
async publish(request, authorize) {
|
|
1501
1517
|
requireReady();
|
|
1502
|
-
const
|
|
1503
|
-
const replayed = store.replay(request.requestId,
|
|
1518
|
+
const fingerprint2 = fingerprintExternalHandoffAnnotation(request.annotation);
|
|
1519
|
+
const replayed = store.replay(request.requestId, fingerprint2);
|
|
1504
1520
|
if (replayed !== void 0) return replayed;
|
|
1505
1521
|
const pending = inFlight.get(request.requestId);
|
|
1506
1522
|
if (pending !== void 0) {
|
|
1507
|
-
if (pending.fingerprint !==
|
|
1523
|
+
if (pending.fingerprint !== fingerprint2) {
|
|
1508
1524
|
throw new SpotPatchError6(ERROR_CODES6.HANDOFF_VALIDATION_FAILED);
|
|
1509
1525
|
}
|
|
1510
1526
|
return asReplay(await pending.promise);
|
|
@@ -1514,12 +1530,12 @@ function createExternalHandoffService(options) {
|
|
|
1514
1530
|
const annotation = await authorize(request.annotation);
|
|
1515
1531
|
return store.publish({
|
|
1516
1532
|
annotation,
|
|
1517
|
-
fingerprint,
|
|
1533
|
+
fingerprint: fingerprint2,
|
|
1518
1534
|
requestId: request.requestId,
|
|
1519
1535
|
reserve: activeRegistry.reserve
|
|
1520
1536
|
});
|
|
1521
1537
|
})();
|
|
1522
|
-
const activePublish = Object.freeze({ fingerprint, promise });
|
|
1538
|
+
const activePublish = Object.freeze({ fingerprint: fingerprint2, promise });
|
|
1523
1539
|
inFlight.set(request.requestId, activePublish);
|
|
1524
1540
|
try {
|
|
1525
1541
|
return await promise;
|
|
@@ -1840,6 +1856,8 @@ import {
|
|
|
1840
1856
|
MAX_ANNOTATION_TARGETS,
|
|
1841
1857
|
SPOTPATCH_EDITOR_PREFERENCES,
|
|
1842
1858
|
SPOTPATCH_LOCALE_PREFERENCES,
|
|
1859
|
+
contextualAskOptionsSchema,
|
|
1860
|
+
CONTEXTUAL_ASK_LIMITS,
|
|
1843
1861
|
DEFAULT_DATA_FLOW_LIMITS
|
|
1844
1862
|
} from "@spotpatch/shared";
|
|
1845
1863
|
import { z } from "zod";
|
|
@@ -1878,7 +1896,8 @@ var DEFAULT_OPTIONS = Object.freeze({
|
|
|
1878
1896
|
runtime: "dispatch",
|
|
1879
1897
|
limits: DEFAULT_DATA_FLOW_LIMITS
|
|
1880
1898
|
}),
|
|
1881
|
-
externalAgent: Object.freeze({ enabled: false })
|
|
1899
|
+
externalAgent: Object.freeze({ enabled: false }),
|
|
1900
|
+
contextualAsk: Object.freeze({ enabled: false })
|
|
1882
1901
|
});
|
|
1883
1902
|
var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
1884
1903
|
var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
|
|
@@ -2188,6 +2207,35 @@ function resolveDataFlowOptions(options) {
|
|
|
2188
2207
|
limits: DEFAULT_DATA_FLOW_LIMITS
|
|
2189
2208
|
});
|
|
2190
2209
|
}
|
|
2210
|
+
function resolveContextualAskOptions(value, ai) {
|
|
2211
|
+
if (value === void 0 || value === false) return DEFAULT_OPTIONS.contextualAsk;
|
|
2212
|
+
const parsed = contextualAskOptionsSchema.safeParse(value);
|
|
2213
|
+
if (!parsed.success) {
|
|
2214
|
+
throw new RangeError("SpotPatch contextualAsk configuration is invalid.");
|
|
2215
|
+
}
|
|
2216
|
+
const configured = typeof parsed.data === "boolean" ? {} : parsed.data;
|
|
2217
|
+
const preference = configured.defaultExecutor;
|
|
2218
|
+
if (preference?.kind === "configured-key") {
|
|
2219
|
+
const provider = ai === false ? void 0 : ai.providers[preference.providerProfileId];
|
|
2220
|
+
if (provider?.models[preference.modelProfileId] === void 0) {
|
|
2221
|
+
throw new RangeError(
|
|
2222
|
+
"SpotPatch contextualAsk default Key executor must reference a configured AI model."
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (ai !== false && Object.values(ai.providers).reduce(
|
|
2227
|
+
(total, provider) => total + Object.keys(provider.models).length,
|
|
2228
|
+
0
|
|
2229
|
+
) >= CONTEXTUAL_ASK_LIMITS.maximumExecutors) {
|
|
2230
|
+
throw new RangeError(
|
|
2231
|
+
"SpotPatch contextualAsk configuration exceeds the executor limit."
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
return Object.freeze({
|
|
2235
|
+
enabled: true,
|
|
2236
|
+
...preference === void 0 ? {} : { defaultExecutor: Object.freeze({ ...preference }) }
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2191
2239
|
function createRuntimeDataFlowConfig(options) {
|
|
2192
2240
|
return Object.freeze({
|
|
2193
2241
|
enabled: options.enabled,
|
|
@@ -2226,6 +2274,7 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
2226
2274
|
`SpotPatch maxTargets must be an integer between 1 and ${String(MAX_ANNOTATION_TARGETS)}.`
|
|
2227
2275
|
);
|
|
2228
2276
|
}
|
|
2277
|
+
const ai = resolveAiOptions(options.ai ?? environmentAi);
|
|
2229
2278
|
const resolved = {
|
|
2230
2279
|
enabled: options.enabled ?? DEFAULT_OPTIONS.enabled,
|
|
2231
2280
|
include: Object.freeze([...options.include ?? DEFAULT_OPTIONS.include]),
|
|
@@ -2238,11 +2287,12 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
2238
2287
|
debug: options.debug ?? DEFAULT_OPTIONS.debug,
|
|
2239
2288
|
locale,
|
|
2240
2289
|
maxTargets,
|
|
2241
|
-
ai
|
|
2290
|
+
ai,
|
|
2242
2291
|
dataFlow: resolveDataFlowOptions(options.dataFlow),
|
|
2243
2292
|
externalAgent: Object.freeze({
|
|
2244
2293
|
enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
|
|
2245
|
-
})
|
|
2294
|
+
}),
|
|
2295
|
+
contextualAsk: resolveContextualAskOptions(options.contextualAsk, ai)
|
|
2246
2296
|
};
|
|
2247
2297
|
if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
|
|
2248
2298
|
throw new RangeError("SpotPatch shortcut is invalid.");
|
|
@@ -2478,10 +2528,14 @@ function createSourceRegistry(options = {}) {
|
|
|
2478
2528
|
);
|
|
2479
2529
|
}
|
|
2480
2530
|
componentIdsByPath.set(normalizedPath, currentIds);
|
|
2531
|
+
return fileId;
|
|
2481
2532
|
},
|
|
2482
2533
|
resolve(fileId) {
|
|
2483
2534
|
return idToPath.get(fileId);
|
|
2484
2535
|
},
|
|
2536
|
+
findRegisteredId(absolutePath) {
|
|
2537
|
+
return pathToId.get(normalizeAbsolutePath(absolutePath));
|
|
2538
|
+
},
|
|
2485
2539
|
resolveDataFlowComponent(componentSourceId) {
|
|
2486
2540
|
return componentAnchors.get(componentSourceId);
|
|
2487
2541
|
},
|
|
@@ -2494,21 +2548,1342 @@ function createSourceRegistry(options = {}) {
|
|
|
2494
2548
|
});
|
|
2495
2549
|
}
|
|
2496
2550
|
|
|
2551
|
+
// src/contextual-ask/manager.ts
|
|
2552
|
+
import { createHash as createHash4, randomBytes as randomBytes9 } from "crypto";
|
|
2553
|
+
import {
|
|
2554
|
+
askAnswerDraftSchema,
|
|
2555
|
+
askAnswerResultSchema,
|
|
2556
|
+
askJobCreateRequestSchema,
|
|
2557
|
+
askJobEventSchema,
|
|
2558
|
+
askJobResultResponseSchema,
|
|
2559
|
+
askJobSnapshotSchema,
|
|
2560
|
+
contextualAskCapabilitySchema,
|
|
2561
|
+
contextualAskExecutorCapabilitySchema,
|
|
2562
|
+
CONTEXTUAL_ASK_LIMITS as CONTEXTUAL_ASK_LIMITS3,
|
|
2563
|
+
CONTEXTUAL_ASK_SCHEMA_VERSION
|
|
2564
|
+
} from "@spotpatch/shared";
|
|
2565
|
+
|
|
2566
|
+
// src/contextual-ask/error.ts
|
|
2567
|
+
import { ContextualAskExecutorError } from "@spotpatch/agent";
|
|
2568
|
+
var ContextualAskError = class extends Error {
|
|
2569
|
+
code;
|
|
2570
|
+
constructor(code, options = {}) {
|
|
2571
|
+
super(code, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
2572
|
+
this.name = "ContextualAskError";
|
|
2573
|
+
this.code = code;
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
function asContextualAskError(error) {
|
|
2577
|
+
if (error instanceof ContextualAskError) return error;
|
|
2578
|
+
if (error instanceof ContextualAskExecutorError) {
|
|
2579
|
+
return new ContextualAskError(error.code, { cause: error });
|
|
2580
|
+
}
|
|
2581
|
+
return new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", { cause: error });
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/contextual-ask/read-snapshot.ts
|
|
2585
|
+
import { createHash as createHash3, randomBytes as randomBytes8 } from "crypto";
|
|
2586
|
+
import { constants } from "fs";
|
|
2587
|
+
import { lstat as lstat4, open as open2, realpath as realpath3 } from "fs/promises";
|
|
2588
|
+
import path5 from "path";
|
|
2589
|
+
import {
|
|
2590
|
+
CONTEXTUAL_ASK_LIMITS as CONTEXTUAL_ASK_LIMITS2
|
|
2591
|
+
} from "@spotpatch/shared";
|
|
2592
|
+
import { parseSync, Visitor } from "oxc-parser";
|
|
2593
|
+
var SOURCE_EXTENSIONS = Object.freeze([
|
|
2594
|
+
".ts",
|
|
2595
|
+
".tsx",
|
|
2596
|
+
".js",
|
|
2597
|
+
".jsx",
|
|
2598
|
+
".mjs",
|
|
2599
|
+
".cjs",
|
|
2600
|
+
".css",
|
|
2601
|
+
".scss",
|
|
2602
|
+
".sass",
|
|
2603
|
+
".less"
|
|
2604
|
+
]);
|
|
2605
|
+
var PROTECTED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
2606
|
+
".git",
|
|
2607
|
+
".ssh",
|
|
2608
|
+
".spotpatch",
|
|
2609
|
+
"coverage",
|
|
2610
|
+
"dist",
|
|
2611
|
+
"node_modules"
|
|
2612
|
+
]);
|
|
2613
|
+
var PROTECTED_FILES = /* @__PURE__ */ new Set([
|
|
2614
|
+
".gitmodules",
|
|
2615
|
+
".git-credentials",
|
|
2616
|
+
".netrc",
|
|
2617
|
+
".npmrc",
|
|
2618
|
+
".pnpmrc",
|
|
2619
|
+
".pypirc",
|
|
2620
|
+
".yarnrc",
|
|
2621
|
+
"bun.lock",
|
|
2622
|
+
"bun.lockb",
|
|
2623
|
+
"npm-shrinkwrap.json",
|
|
2624
|
+
"package-lock.json",
|
|
2625
|
+
"pnpm-lock.yaml",
|
|
2626
|
+
"yarn.lock"
|
|
2627
|
+
]);
|
|
2628
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
2629
|
+
function deny(code) {
|
|
2630
|
+
throw new ContextualAskError(code);
|
|
2631
|
+
}
|
|
2632
|
+
function toPosixRelative(realRoot, absolutePath) {
|
|
2633
|
+
const relative = path5.relative(realRoot, absolutePath);
|
|
2634
|
+
if (relative.length === 0 || relative === ".." || relative.startsWith(`..${path5.sep}`) || path5.isAbsolute(relative)) {
|
|
2635
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2636
|
+
}
|
|
2637
|
+
const normalized = relative.split(path5.sep).join("/");
|
|
2638
|
+
const segments = normalized.toLowerCase().split("/");
|
|
2639
|
+
const fileName = segments.at(-1) ?? "";
|
|
2640
|
+
if (segments.some((segment) => PROTECTED_DIRECTORIES.has(segment)) || PROTECTED_FILES.has(fileName) || fileName === ".env" || fileName === ".envrc" || fileName === ".dev.vars" || fileName.startsWith(".env.") || fileName.endsWith(".pem") || fileName.endsWith(".key") || fileName.endsWith(".p12") || fileName.endsWith(".pfx") || fileName.startsWith("id_rsa")) {
|
|
2641
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2642
|
+
}
|
|
2643
|
+
return normalized;
|
|
2644
|
+
}
|
|
2645
|
+
async function assertNoSymlinkPath(realRoot, absolutePath) {
|
|
2646
|
+
const relative = toPosixRelative(realRoot, absolutePath);
|
|
2647
|
+
let current = realRoot;
|
|
2648
|
+
for (const segment of relative.split("/")) {
|
|
2649
|
+
current = path5.join(current, segment);
|
|
2650
|
+
const metadata = await lstat4(current).catch(() => void 0);
|
|
2651
|
+
if (metadata === void 0 || metadata.isSymbolicLink()) {
|
|
2652
|
+
deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
async function readSecureSource(realRoot, absolutePath) {
|
|
2657
|
+
const normalizedAbsolute = path5.normalize(path5.resolve(absolutePath));
|
|
2658
|
+
const relativePath = toPosixRelative(realRoot, normalizedAbsolute);
|
|
2659
|
+
await assertNoSymlinkPath(realRoot, normalizedAbsolute);
|
|
2660
|
+
const canonical = await realpath3(normalizedAbsolute).catch(() => void 0);
|
|
2661
|
+
if (canonical === void 0 || canonical !== normalizedAbsolute) {
|
|
2662
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2663
|
+
}
|
|
2664
|
+
let handle;
|
|
2665
|
+
try {
|
|
2666
|
+
handle = await open2(normalizedAbsolute, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
2667
|
+
const before = await handle.stat();
|
|
2668
|
+
if (!before.isFile() || before.nlink !== 1) {
|
|
2669
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2670
|
+
}
|
|
2671
|
+
if (before.size > CONTEXTUAL_ASK_LIMITS2.maximumReadBytesPerFile) {
|
|
2672
|
+
throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
2673
|
+
}
|
|
2674
|
+
const bytes = await handle.readFile();
|
|
2675
|
+
const after = await handle.stat();
|
|
2676
|
+
if (bytes.byteLength !== before.size || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || bytes.includes(0)) {
|
|
2677
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2678
|
+
}
|
|
2679
|
+
let content;
|
|
2680
|
+
try {
|
|
2681
|
+
content = decoder.decode(bytes);
|
|
2682
|
+
} catch (error) {
|
|
2683
|
+
throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED", { cause: error });
|
|
2684
|
+
}
|
|
2685
|
+
return Object.freeze({
|
|
2686
|
+
content,
|
|
2687
|
+
contentHash: createHash3("sha256").update(bytes).digest("hex"),
|
|
2688
|
+
relativePath,
|
|
2689
|
+
size: bytes.byteLength
|
|
2690
|
+
});
|
|
2691
|
+
} finally {
|
|
2692
|
+
await handle?.close().catch(() => void 0);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
function confidence(value) {
|
|
2696
|
+
return value === "exact" || value === "probable" ? value : "approximate";
|
|
2697
|
+
}
|
|
2698
|
+
async function authorizeSeeds(selection, registry, root, realRoot) {
|
|
2699
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
2700
|
+
for (const target of selection.targets) {
|
|
2701
|
+
let fileId = target.react.source?.fileId ?? target.source.fileId;
|
|
2702
|
+
let sourceVersion;
|
|
2703
|
+
if (target.react.componentSourceId !== void 0) {
|
|
2704
|
+
const anchor = registry.resolveDataFlowComponent(target.react.componentSourceId);
|
|
2705
|
+
if (anchor === void 0 || target.react.sourceVersion !== anchor.sourceVersion) {
|
|
2706
|
+
deny("ASK_SELECTION_STALE");
|
|
2707
|
+
}
|
|
2708
|
+
fileId = anchor.fileId;
|
|
2709
|
+
sourceVersion = anchor.sourceVersion;
|
|
2710
|
+
}
|
|
2711
|
+
if (fileId === void 0) continue;
|
|
2712
|
+
const registered = registry.resolve(fileId);
|
|
2713
|
+
if (registered === void 0) deny("ASK_SELECTION_STALE");
|
|
2714
|
+
const canonical = await realpath3(registered).catch(() => void 0);
|
|
2715
|
+
if (canonical === void 0) deny("ASK_SELECTION_STALE");
|
|
2716
|
+
const absolutePath = path5.normalize(canonical);
|
|
2717
|
+
const registeredRelative = toPosixRelative(realRoot, absolutePath);
|
|
2718
|
+
const claimedRelative = target.react.source?.relativePath ?? target.source.relativePath;
|
|
2719
|
+
if (claimedRelative !== void 0 && claimedRelative.split("\\").join("/") !== registeredRelative) {
|
|
2720
|
+
deny("ASK_SELECTION_STALE");
|
|
2721
|
+
}
|
|
2722
|
+
const existing = byPath.get(absolutePath);
|
|
2723
|
+
const targetIds = Object.freeze([
|
|
2724
|
+
...existing?.targetIds ?? [],
|
|
2725
|
+
...existing?.targetIds.includes(target.targetId) === true ? [] : [target.targetId]
|
|
2726
|
+
]);
|
|
2727
|
+
byPath.set(
|
|
2728
|
+
absolutePath,
|
|
2729
|
+
Object.freeze({
|
|
2730
|
+
absolutePath,
|
|
2731
|
+
fileId,
|
|
2732
|
+
confidence: confidence(
|
|
2733
|
+
target.react.source?.confidence ?? target.source.confidence
|
|
2734
|
+
),
|
|
2735
|
+
targetIds,
|
|
2736
|
+
...sourceVersion === void 0 ? {} : { sourceVersion }
|
|
2737
|
+
})
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
for (const target of selection.targets) {
|
|
2741
|
+
for (const rule of target.styles.matchedRules) {
|
|
2742
|
+
const claimed = rule.source;
|
|
2743
|
+
if (claimed === void 0 || claimed.startsWith("/") || claimed.includes("\\") || claimed.includes("\0") || claimed.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
2744
|
+
continue;
|
|
2745
|
+
}
|
|
2746
|
+
const registeredPath = path5.resolve(root, ...claimed.split("/"));
|
|
2747
|
+
const fileId = registry.findRegisteredId(registeredPath);
|
|
2748
|
+
if (fileId === void 0) continue;
|
|
2749
|
+
const canonical = await realpath3(registeredPath).catch(() => void 0);
|
|
2750
|
+
if (canonical === void 0) continue;
|
|
2751
|
+
const absolutePath = path5.normalize(canonical);
|
|
2752
|
+
toPosixRelative(realRoot, absolutePath);
|
|
2753
|
+
const existing = byPath.get(absolutePath);
|
|
2754
|
+
const targetIds = Object.freeze([
|
|
2755
|
+
...existing?.targetIds ?? [],
|
|
2756
|
+
...existing?.targetIds.includes(target.targetId) === true ? [] : [target.targetId]
|
|
2757
|
+
]);
|
|
2758
|
+
byPath.set(
|
|
2759
|
+
absolutePath,
|
|
2760
|
+
Object.freeze({
|
|
2761
|
+
absolutePath,
|
|
2762
|
+
fileId,
|
|
2763
|
+
confidence: existing?.confidence ?? "probable",
|
|
2764
|
+
targetIds,
|
|
2765
|
+
...existing?.sourceVersion === void 0 ? {} : { sourceVersion: existing.sourceVersion }
|
|
2766
|
+
})
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
if (byPath.size === 0) deny("ASK_SELECTION_REQUIRED");
|
|
2771
|
+
return [...byPath.values()];
|
|
2772
|
+
}
|
|
2773
|
+
function localSpecifiers(filePath, content) {
|
|
2774
|
+
const found = /* @__PURE__ */ new Set();
|
|
2775
|
+
if (/\.(?:css|scss|sass|less)$/iu.test(filePath)) {
|
|
2776
|
+
const pattern = /@import\s+(?:url\(\s*)?["']([^"']+)["']/gu;
|
|
2777
|
+
for (const match of content.matchAll(pattern)) {
|
|
2778
|
+
if (match[1]?.startsWith(".")) found.add(match[1]);
|
|
2779
|
+
}
|
|
2780
|
+
return [...found].sort();
|
|
2781
|
+
}
|
|
2782
|
+
try {
|
|
2783
|
+
const parsed = parseSync(filePath, content, { sourceType: "module" });
|
|
2784
|
+
if (parsed.errors.length > 0) return [];
|
|
2785
|
+
const add = (value) => {
|
|
2786
|
+
if (typeof value === "string" && value.startsWith(".")) found.add(value);
|
|
2787
|
+
};
|
|
2788
|
+
const visitor = new Visitor({
|
|
2789
|
+
ImportDeclaration(node) {
|
|
2790
|
+
add(node.source.value);
|
|
2791
|
+
},
|
|
2792
|
+
ExportNamedDeclaration(node) {
|
|
2793
|
+
add(node.source?.value);
|
|
2794
|
+
},
|
|
2795
|
+
ExportAllDeclaration(node) {
|
|
2796
|
+
add(node.source.value);
|
|
2797
|
+
},
|
|
2798
|
+
ImportExpression(node) {
|
|
2799
|
+
if ("value" in node.source) add(node.source.value);
|
|
2800
|
+
}
|
|
2801
|
+
});
|
|
2802
|
+
visitor.visit(parsed.program);
|
|
2803
|
+
} catch {
|
|
2804
|
+
return [];
|
|
2805
|
+
}
|
|
2806
|
+
return [...found].sort();
|
|
2807
|
+
}
|
|
2808
|
+
function importCandidates(importer, specifier) {
|
|
2809
|
+
const base = path5.resolve(path5.dirname(importer), specifier);
|
|
2810
|
+
if (path5.extname(base) !== "") return Object.freeze([base]);
|
|
2811
|
+
return Object.freeze([
|
|
2812
|
+
...SOURCE_EXTENSIONS.map((extension) => `${base}${extension}`),
|
|
2813
|
+
...SOURCE_EXTENSIONS.map((extension) => path5.join(base, `index${extension}`))
|
|
2814
|
+
]);
|
|
2815
|
+
}
|
|
2816
|
+
function countLines(content) {
|
|
2817
|
+
return content.length === 0 ? 1 : content.split("\n").length;
|
|
2818
|
+
}
|
|
2819
|
+
function publicSourceEntry(source) {
|
|
2820
|
+
return Object.freeze({
|
|
2821
|
+
handleId: source.handleId,
|
|
2822
|
+
fileId: source.fileId,
|
|
2823
|
+
relativePath: source.relativePath,
|
|
2824
|
+
label: source.label,
|
|
2825
|
+
lineCount: source.lineCount,
|
|
2826
|
+
size: source.size,
|
|
2827
|
+
contentHash: source.contentHash,
|
|
2828
|
+
confidence: source.confidence,
|
|
2829
|
+
targetIds: source.targetIds,
|
|
2830
|
+
...source.sourceVersion === void 0 ? {} : { sourceVersion: source.sourceVersion }
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
function assertSelectionEvidenceCurrent(selection, captured) {
|
|
2834
|
+
for (const target of selection.targets) {
|
|
2835
|
+
if (target.code === void 0) continue;
|
|
2836
|
+
const source = captured.find(
|
|
2837
|
+
(candidate) => candidate.relativePath === target.code?.relativePath
|
|
2838
|
+
);
|
|
2839
|
+
if (source === void 0) deny("ASK_SELECTION_STALE");
|
|
2840
|
+
const lines = source.content.split(/\r?\n/u);
|
|
2841
|
+
if (target.code.endLine > lines.length) deny("ASK_SELECTION_STALE");
|
|
2842
|
+
const currentExcerpt = lines.slice(target.code.startLine - 1, target.code.endLine).join("\n");
|
|
2843
|
+
if (currentExcerpt !== target.code.excerpt) deny("ASK_SELECTION_STALE");
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
async function captureAskReadSnapshot(options) {
|
|
2847
|
+
options.signal?.throwIfAborted();
|
|
2848
|
+
const realRoot = await realpath3(options.root).catch(() => void 0);
|
|
2849
|
+
if (realRoot === void 0) deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2850
|
+
const seeds = await authorizeSeeds(
|
|
2851
|
+
options.selection,
|
|
2852
|
+
options.registry,
|
|
2853
|
+
options.root,
|
|
2854
|
+
realRoot
|
|
2855
|
+
);
|
|
2856
|
+
const createHandleId = options.createHandleId ?? (() => randomBytes8(16).toString("base64url"));
|
|
2857
|
+
const captured = [];
|
|
2858
|
+
const capturedPaths = /* @__PURE__ */ new Set();
|
|
2859
|
+
const queue = [...seeds];
|
|
2860
|
+
let totalBytes = 0;
|
|
2861
|
+
let truncated = false;
|
|
2862
|
+
while (queue.length > 0) {
|
|
2863
|
+
options.signal?.throwIfAborted();
|
|
2864
|
+
const candidate = queue.shift();
|
|
2865
|
+
if (candidate === void 0 || capturedPaths.has(candidate.absolutePath)) continue;
|
|
2866
|
+
if (captured.length >= CONTEXTUAL_ASK_LIMITS2.maximumReadFiles) {
|
|
2867
|
+
truncated = true;
|
|
2868
|
+
break;
|
|
2869
|
+
}
|
|
2870
|
+
let read;
|
|
2871
|
+
try {
|
|
2872
|
+
read = await readSecureSource(realRoot, candidate.absolutePath);
|
|
2873
|
+
options.signal?.throwIfAborted();
|
|
2874
|
+
} catch (error) {
|
|
2875
|
+
if (captured.length < seeds.length) throw error;
|
|
2876
|
+
truncated = true;
|
|
2877
|
+
continue;
|
|
2878
|
+
}
|
|
2879
|
+
if (totalBytes + read.size > CONTEXTUAL_ASK_LIMITS2.maximumReadBytesTotal) {
|
|
2880
|
+
if (captured.length < seeds.length)
|
|
2881
|
+
throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
2882
|
+
truncated = true;
|
|
2883
|
+
continue;
|
|
2884
|
+
}
|
|
2885
|
+
capturedPaths.add(candidate.absolutePath);
|
|
2886
|
+
totalBytes += read.size;
|
|
2887
|
+
const entry = Object.freeze({
|
|
2888
|
+
handleId: createHandleId(),
|
|
2889
|
+
fileId: candidate.fileId,
|
|
2890
|
+
relativePath: read.relativePath,
|
|
2891
|
+
label: path5.basename(read.relativePath),
|
|
2892
|
+
lineCount: countLines(read.content),
|
|
2893
|
+
size: read.size,
|
|
2894
|
+
contentHash: read.contentHash,
|
|
2895
|
+
confidence: candidate.confidence,
|
|
2896
|
+
targetIds: Object.freeze([...candidate.targetIds]),
|
|
2897
|
+
...candidate.sourceVersion === void 0 ? {} : { sourceVersion: candidate.sourceVersion },
|
|
2898
|
+
absolutePath: candidate.absolutePath,
|
|
2899
|
+
content: read.content
|
|
2900
|
+
});
|
|
2901
|
+
captured.push(entry);
|
|
2902
|
+
for (const specifier of localSpecifiers(candidate.absolutePath, read.content)) {
|
|
2903
|
+
options.signal?.throwIfAborted();
|
|
2904
|
+
for (const importPath of importCandidates(candidate.absolutePath, specifier)) {
|
|
2905
|
+
if (capturedPaths.has(importPath)) continue;
|
|
2906
|
+
const metadata = await lstat4(importPath).catch(() => void 0);
|
|
2907
|
+
if (metadata?.isFile() !== true || metadata.isSymbolicLink()) continue;
|
|
2908
|
+
const fileId = options.registry.findRegisteredId(importPath) ?? options.registry.register(importPath);
|
|
2909
|
+
queue.push(
|
|
2910
|
+
Object.freeze({
|
|
2911
|
+
absolutePath: path5.normalize(importPath),
|
|
2912
|
+
fileId,
|
|
2913
|
+
confidence: "probable",
|
|
2914
|
+
targetIds: candidate.targetIds
|
|
2915
|
+
})
|
|
2916
|
+
);
|
|
2917
|
+
break;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
assertSelectionEvidenceCurrent(options.selection, captured);
|
|
2922
|
+
const sourceByHandle = new Map(captured.map((source) => [source.handleId, source]));
|
|
2923
|
+
const publicEntries = Object.freeze(
|
|
2924
|
+
captured.map((source) => publicSourceEntry(source))
|
|
2925
|
+
);
|
|
2926
|
+
const contextHash = createHash3("sha256").update(
|
|
2927
|
+
JSON.stringify({
|
|
2928
|
+
selectionId: options.selection.selectionId,
|
|
2929
|
+
sources: publicEntries.map((source) => ({
|
|
2930
|
+
relativePath: source.relativePath,
|
|
2931
|
+
contentHash: source.contentHash,
|
|
2932
|
+
targetIds: source.targetIds,
|
|
2933
|
+
sourceVersion: source.sourceVersion ?? null
|
|
2934
|
+
}))
|
|
2935
|
+
})
|
|
2936
|
+
).digest("hex");
|
|
2937
|
+
let disposed = false;
|
|
2938
|
+
const requireSource = (handleId) => {
|
|
2939
|
+
if (disposed) throw new ContextualAskError("ASK_RESULT_EXPIRED");
|
|
2940
|
+
const source = sourceByHandle.get(handleId);
|
|
2941
|
+
if (source === void 0) throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED");
|
|
2942
|
+
return source;
|
|
2943
|
+
};
|
|
2944
|
+
const snapshot2 = Object.freeze({
|
|
2945
|
+
manifest: () => {
|
|
2946
|
+
if (disposed) throw new ContextualAskError("ASK_RESULT_EXPIRED");
|
|
2947
|
+
return publicEntries;
|
|
2948
|
+
},
|
|
2949
|
+
read(handleId, range = {}) {
|
|
2950
|
+
const source = requireSource(handleId);
|
|
2951
|
+
const startLine = range.startLine ?? 1;
|
|
2952
|
+
const endLine = range.endLine ?? source.lineCount;
|
|
2953
|
+
if (!Number.isSafeInteger(startLine) || !Number.isSafeInteger(endLine) || startLine < 1 || endLine < startLine || endLine > source.lineCount) {
|
|
2954
|
+
throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED");
|
|
2955
|
+
}
|
|
2956
|
+
return Object.freeze({
|
|
2957
|
+
handleId,
|
|
2958
|
+
startLine,
|
|
2959
|
+
endLine,
|
|
2960
|
+
content: source.content.split("\n").slice(startLine - 1, endLine).join("\n")
|
|
2961
|
+
});
|
|
2962
|
+
},
|
|
2963
|
+
search(query) {
|
|
2964
|
+
if (query.length === 0 || query.length > CONTEXTUAL_ASK_LIMITS2.maximumSearchQueryCharacters || query.includes("\0")) {
|
|
2965
|
+
throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED");
|
|
2966
|
+
}
|
|
2967
|
+
const normalized = query.toLocaleLowerCase("en-US");
|
|
2968
|
+
const matches = [];
|
|
2969
|
+
for (const source of captured) {
|
|
2970
|
+
for (const [index, line] of source.content.split("\n").entries()) {
|
|
2971
|
+
if (line.toLocaleLowerCase("en-US").includes(normalized)) {
|
|
2972
|
+
matches.push(
|
|
2973
|
+
Object.freeze({
|
|
2974
|
+
handleId: source.handleId,
|
|
2975
|
+
line: index + 1,
|
|
2976
|
+
preview: line.slice(
|
|
2977
|
+
0,
|
|
2978
|
+
CONTEXTUAL_ASK_LIMITS2.maximumSearchPreviewCharacters
|
|
2979
|
+
)
|
|
2980
|
+
})
|
|
2981
|
+
);
|
|
2982
|
+
if (matches.length >= CONTEXTUAL_ASK_LIMITS2.maximumSearchResults) {
|
|
2983
|
+
return Object.freeze(matches);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return Object.freeze(matches);
|
|
2989
|
+
}
|
|
2990
|
+
});
|
|
2991
|
+
return Object.freeze({
|
|
2992
|
+
grant: Object.freeze({ contextHash, truncated, sources: publicEntries }),
|
|
2993
|
+
snapshot: snapshot2,
|
|
2994
|
+
dispose() {
|
|
2995
|
+
disposed = true;
|
|
2996
|
+
sourceByHandle.clear();
|
|
2997
|
+
captured.splice(0, captured.length);
|
|
2998
|
+
},
|
|
2999
|
+
async isStale() {
|
|
3000
|
+
for (const source of captured) {
|
|
3001
|
+
try {
|
|
3002
|
+
const current = await readSecureSource(realRoot, source.absolutePath);
|
|
3003
|
+
if (current.contentHash !== source.contentHash) return true;
|
|
3004
|
+
} catch {
|
|
3005
|
+
return true;
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
return false;
|
|
3009
|
+
},
|
|
3010
|
+
projectCitation(handleId, startLine, endLine) {
|
|
3011
|
+
const source = requireSource(handleId);
|
|
3012
|
+
if (!Number.isSafeInteger(startLine) || !Number.isSafeInteger(endLine) || startLine < 1 || endLine < startLine || endLine > source.lineCount) {
|
|
3013
|
+
throw new ContextualAskError("ASK_ANSWER_INVALID");
|
|
3014
|
+
}
|
|
3015
|
+
return Object.freeze({ ...publicSourceEntry(source), startLine, endLine });
|
|
3016
|
+
}
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
// src/contextual-ask/manager.ts
|
|
3021
|
+
var ACTIVE_STATUSES = /* @__PURE__ */ new Set([
|
|
3022
|
+
"queued",
|
|
3023
|
+
"authorizing",
|
|
3024
|
+
"running",
|
|
3025
|
+
"cancelling"
|
|
3026
|
+
]);
|
|
3027
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["answered", "cancelled", "failed"]);
|
|
3028
|
+
var DEFAULT_DEPENDENCIES2 = Object.freeze({
|
|
3029
|
+
captureSnapshot: captureAskReadSnapshot,
|
|
3030
|
+
createId: () => randomBytes9(16).toString("base64url"),
|
|
3031
|
+
now: () => /* @__PURE__ */ new Date()
|
|
3032
|
+
});
|
|
3033
|
+
function fingerprint(request) {
|
|
3034
|
+
return createHash4("sha256").update(
|
|
3035
|
+
JSON.stringify({
|
|
3036
|
+
envelope: request.envelope,
|
|
3037
|
+
executorId: request.executorId,
|
|
3038
|
+
providerDataConsent: request.providerDataConsent
|
|
3039
|
+
})
|
|
3040
|
+
).digest("hex");
|
|
3041
|
+
}
|
|
3042
|
+
function fileCountBucket(count) {
|
|
3043
|
+
if (count <= 1) return "one";
|
|
3044
|
+
if (count <= 5) return "few";
|
|
3045
|
+
return "many";
|
|
3046
|
+
}
|
|
3047
|
+
function raceWithAbort(operation, signal) {
|
|
3048
|
+
const reason = () => signal.reason instanceof Error ? signal.reason : new ContextualAskError("ASK_CANCELLED");
|
|
3049
|
+
if (signal.aborted) return Promise.reject(reason());
|
|
3050
|
+
return new Promise((resolve, reject) => {
|
|
3051
|
+
const onAbort = () => {
|
|
3052
|
+
reject(reason());
|
|
3053
|
+
};
|
|
3054
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3055
|
+
operation.then(
|
|
3056
|
+
(value) => {
|
|
3057
|
+
signal.removeEventListener("abort", onAbort);
|
|
3058
|
+
resolve(value);
|
|
3059
|
+
},
|
|
3060
|
+
(error) => {
|
|
3061
|
+
signal.removeEventListener("abort", onAbort);
|
|
3062
|
+
reject(
|
|
3063
|
+
error instanceof Error ? error : new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", { cause: error })
|
|
3064
|
+
);
|
|
3065
|
+
}
|
|
3066
|
+
);
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
function createContextualAskManager(options) {
|
|
3070
|
+
const dependencies = Object.freeze({
|
|
3071
|
+
...DEFAULT_DEPENDENCIES2,
|
|
3072
|
+
...options.dependencies
|
|
3073
|
+
});
|
|
3074
|
+
const executors = /* @__PURE__ */ new Map();
|
|
3075
|
+
for (const executor of options.executors ?? []) {
|
|
3076
|
+
if (executors.has(executor.executorId)) {
|
|
3077
|
+
throw new TypeError("Contextual Ask executor IDs must be unique.");
|
|
3078
|
+
}
|
|
3079
|
+
executors.set(executor.executorId, executor);
|
|
3080
|
+
}
|
|
3081
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
3082
|
+
const requests = /* @__PURE__ */ new Map();
|
|
3083
|
+
const pendingRequests = /* @__PURE__ */ new Map();
|
|
3084
|
+
let closed = false;
|
|
3085
|
+
const isClosed = () => closed;
|
|
3086
|
+
const cleanupExpired = () => {
|
|
3087
|
+
const now = dependencies.now().getTime();
|
|
3088
|
+
for (const [jobId, job] of jobs) {
|
|
3089
|
+
if (job.expiresAt !== void 0 && now >= job.expiresAt) {
|
|
3090
|
+
job.snapshot?.dispose();
|
|
3091
|
+
jobs.delete(jobId);
|
|
3092
|
+
const request = requests.get(job.request.requestId);
|
|
3093
|
+
if (request?.jobId === jobId) requests.delete(job.request.requestId);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
const requireJob = (jobId) => {
|
|
3098
|
+
cleanupExpired();
|
|
3099
|
+
const job = jobs.get(jobId);
|
|
3100
|
+
if (job === void 0) throw new ContextualAskError("ASK_RESULT_EXPIRED");
|
|
3101
|
+
return job;
|
|
3102
|
+
};
|
|
3103
|
+
const publicSnapshot = (job) => askJobSnapshotSchema.parse({
|
|
3104
|
+
schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3105
|
+
jobId: job.id,
|
|
3106
|
+
selectionId: job.request.envelope.selection.selectionId,
|
|
3107
|
+
status: job.status,
|
|
3108
|
+
executor: {
|
|
3109
|
+
executorId: job.executorCapability.executorId,
|
|
3110
|
+
kind: job.executorCapability.kind,
|
|
3111
|
+
label: job.executorCapability.label,
|
|
3112
|
+
modelLabel: job.executorCapability.effectiveModelLabel
|
|
3113
|
+
},
|
|
3114
|
+
...job.phaseMessage === void 0 ? {} : { phaseMessage: job.phaseMessage },
|
|
3115
|
+
createdAt: job.createdAt,
|
|
3116
|
+
updatedAt: job.updatedAt,
|
|
3117
|
+
canCancel: ["queued", "authorizing", "running"].includes(job.status),
|
|
3118
|
+
...job.errorCode === void 0 ? {} : { errorCode: job.errorCode }
|
|
3119
|
+
});
|
|
3120
|
+
const appendEvent = (job, event) => {
|
|
3121
|
+
job.events.push(askJobEventSchema.parse(event));
|
|
3122
|
+
if (job.events.length > CONTEXTUAL_ASK_LIMITS3.maximumRetainedEvents) {
|
|
3123
|
+
job.events.splice(
|
|
3124
|
+
0,
|
|
3125
|
+
job.events.length - CONTEXTUAL_ASK_LIMITS3.maximumRetainedEvents
|
|
3126
|
+
);
|
|
3127
|
+
}
|
|
3128
|
+
for (const listener of job.listeners) listener(event);
|
|
3129
|
+
};
|
|
3130
|
+
const eventBase = (job) => {
|
|
3131
|
+
job.sequence += 1;
|
|
3132
|
+
return {
|
|
3133
|
+
schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3134
|
+
sequence: job.sequence,
|
|
3135
|
+
jobId: job.id,
|
|
3136
|
+
status: job.status,
|
|
3137
|
+
timestamp: dependencies.now().toISOString()
|
|
3138
|
+
};
|
|
3139
|
+
};
|
|
3140
|
+
const emitSnapshot = (job) => {
|
|
3141
|
+
appendEvent(job, {
|
|
3142
|
+
...eventBase(job),
|
|
3143
|
+
type: "snapshot",
|
|
3144
|
+
snapshot: publicSnapshot(job)
|
|
3145
|
+
});
|
|
3146
|
+
};
|
|
3147
|
+
const emitPhase = (job, message) => {
|
|
3148
|
+
appendEvent(job, { ...eventBase(job), type: "phase", message });
|
|
3149
|
+
};
|
|
3150
|
+
const emitReadActivity = (job, entry, state) => {
|
|
3151
|
+
appendEvent(job, {
|
|
3152
|
+
...eventBase(job),
|
|
3153
|
+
type: "read-activity",
|
|
3154
|
+
activity: entry === void 0 ? {
|
|
3155
|
+
kind: "file-count",
|
|
3156
|
+
bucket: fileCountBucket(job.snapshot?.grant.sources.length ?? 0)
|
|
3157
|
+
} : {
|
|
3158
|
+
kind: "source",
|
|
3159
|
+
sourceId: entry.fileId,
|
|
3160
|
+
relativePath: entry.relativePath
|
|
3161
|
+
},
|
|
3162
|
+
state
|
|
3163
|
+
});
|
|
3164
|
+
};
|
|
3165
|
+
const transition = (job, status, phaseMessage, errorCode) => {
|
|
3166
|
+
job.status = status;
|
|
3167
|
+
job.phaseMessage = phaseMessage;
|
|
3168
|
+
job.errorCode = errorCode;
|
|
3169
|
+
job.updatedAt = dependencies.now().toISOString();
|
|
3170
|
+
emitSnapshot(job);
|
|
3171
|
+
if (!TERMINAL_STATUSES.has(status)) emitPhase(job, phaseMessage);
|
|
3172
|
+
};
|
|
3173
|
+
const finish = (job) => {
|
|
3174
|
+
job.expiresAt = dependencies.now().getTime() + CONTEXTUAL_ASK_LIMITS3.resultTtlMs;
|
|
3175
|
+
job.lease.release();
|
|
3176
|
+
job.snapshot?.dispose();
|
|
3177
|
+
job.snapshot = void 0;
|
|
3178
|
+
};
|
|
3179
|
+
const fail = (job, error) => {
|
|
3180
|
+
if (TERMINAL_STATUSES.has(job.status)) return;
|
|
3181
|
+
const normalized = asContextualAskError(error);
|
|
3182
|
+
const abortReason = job.controller.signal.reason;
|
|
3183
|
+
const cancelled = normalized.code === "ASK_CANCELLED" || abortReason instanceof ContextualAskError && abortReason.code === "ASK_CANCELLED";
|
|
3184
|
+
transition(
|
|
3185
|
+
job,
|
|
3186
|
+
cancelled ? "cancelled" : "failed",
|
|
3187
|
+
cancelled ? "Question cancelled." : "Question failed.",
|
|
3188
|
+
cancelled ? "ASK_CANCELLED" : normalized.code
|
|
3189
|
+
);
|
|
3190
|
+
appendEvent(job, {
|
|
3191
|
+
...eventBase(job),
|
|
3192
|
+
type: "error",
|
|
3193
|
+
errorCode: cancelled ? "ASK_CANCELLED" : normalized.code
|
|
3194
|
+
});
|
|
3195
|
+
finish(job);
|
|
3196
|
+
};
|
|
3197
|
+
const instrumentSnapshot = (job, snapshot2, onLimitExceeded) => {
|
|
3198
|
+
const byHandle = new Map(
|
|
3199
|
+
snapshot2.manifest().map((entry) => [entry.handleId, entry])
|
|
3200
|
+
);
|
|
3201
|
+
let readOperations = 0;
|
|
3202
|
+
const beginOperation = () => {
|
|
3203
|
+
readOperations += 1;
|
|
3204
|
+
if (readOperations > CONTEXTUAL_ASK_LIMITS3.maximumToolCalls) {
|
|
3205
|
+
onLimitExceeded();
|
|
3206
|
+
throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
3207
|
+
}
|
|
3208
|
+
};
|
|
3209
|
+
return Object.freeze({
|
|
3210
|
+
manifest: () => snapshot2.manifest(),
|
|
3211
|
+
read(handleId, range) {
|
|
3212
|
+
beginOperation();
|
|
3213
|
+
const entry = byHandle.get(handleId);
|
|
3214
|
+
if (job.status === "running") emitReadActivity(job, entry, "started");
|
|
3215
|
+
try {
|
|
3216
|
+
const result = snapshot2.read(handleId, range);
|
|
3217
|
+
if (job.status === "running") emitReadActivity(job, entry, "succeeded");
|
|
3218
|
+
return result;
|
|
3219
|
+
} catch (error) {
|
|
3220
|
+
if (job.status === "running") emitReadActivity(job, entry, "failed");
|
|
3221
|
+
throw error;
|
|
3222
|
+
}
|
|
3223
|
+
},
|
|
3224
|
+
search(query) {
|
|
3225
|
+
beginOperation();
|
|
3226
|
+
if (job.status === "running") emitReadActivity(job, void 0, "started");
|
|
3227
|
+
try {
|
|
3228
|
+
const result = snapshot2.search(query);
|
|
3229
|
+
if (job.status === "running") {
|
|
3230
|
+
emitReadActivity(job, void 0, "succeeded");
|
|
3231
|
+
}
|
|
3232
|
+
return result;
|
|
3233
|
+
} catch (error) {
|
|
3234
|
+
if (job.status === "running") emitReadActivity(job, void 0, "failed");
|
|
3235
|
+
throw error;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
});
|
|
3239
|
+
};
|
|
3240
|
+
const projectAnswer = async (job, draft, captured) => {
|
|
3241
|
+
const sourceIds = /* @__PURE__ */ new Map();
|
|
3242
|
+
const sources = /* @__PURE__ */ new Map();
|
|
3243
|
+
const projectCitations = (citations) => citations.map((citation) => {
|
|
3244
|
+
const key = `${citation.handleId}:${String(citation.startLine)}:${String(citation.endLine)}`;
|
|
3245
|
+
let sourceId = sourceIds.get(key);
|
|
3246
|
+
if (sourceId === void 0) {
|
|
3247
|
+
sourceId = dependencies.createId();
|
|
3248
|
+
sourceIds.set(key, sourceId);
|
|
3249
|
+
sources.set(
|
|
3250
|
+
sourceId,
|
|
3251
|
+
captured.projectCitation(
|
|
3252
|
+
citation.handleId,
|
|
3253
|
+
citation.startLine,
|
|
3254
|
+
citation.endLine
|
|
3255
|
+
)
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
return sourceId;
|
|
3259
|
+
});
|
|
3260
|
+
const blocks = draft.blocks.map((block) => {
|
|
3261
|
+
if (block.kind === "list") {
|
|
3262
|
+
return {
|
|
3263
|
+
kind: "list",
|
|
3264
|
+
items: block.items.map((item) => ({
|
|
3265
|
+
text: item.text,
|
|
3266
|
+
sourceIds: projectCitations(item.citations)
|
|
3267
|
+
}))
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
return Object.freeze({
|
|
3271
|
+
kind: block.kind,
|
|
3272
|
+
...block.kind === "paragraph" ? { text: block.text } : { code: block.code },
|
|
3273
|
+
...block.kind === "code" && block.language !== void 0 ? { language: block.language } : {},
|
|
3274
|
+
sourceIds: projectCitations(block.citations)
|
|
3275
|
+
});
|
|
3276
|
+
});
|
|
3277
|
+
const warnings = [...draft.warnings];
|
|
3278
|
+
if (captured.grant.truncated && !warnings.some((warning) => warning.code === "source-truncated")) {
|
|
3279
|
+
warnings.push(Object.freeze({ code: "source-truncated" }));
|
|
3280
|
+
}
|
|
3281
|
+
if (await captured.isStale() && !warnings.some((warning) => warning.code === "source-stale")) {
|
|
3282
|
+
warnings.push(Object.freeze({ code: "source-stale" }));
|
|
3283
|
+
}
|
|
3284
|
+
const created = dependencies.now();
|
|
3285
|
+
return askAnswerResultSchema.parse({
|
|
3286
|
+
schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3287
|
+
jobId: job.id,
|
|
3288
|
+
selectionId: job.request.envelope.selection.selectionId,
|
|
3289
|
+
contextHash: captured.grant.contextHash,
|
|
3290
|
+
executor: {
|
|
3291
|
+
executorId: job.executorCapability.executorId,
|
|
3292
|
+
kind: job.executorCapability.kind,
|
|
3293
|
+
label: job.executorCapability.label,
|
|
3294
|
+
modelLabel: job.executorCapability.effectiveModelLabel
|
|
3295
|
+
},
|
|
3296
|
+
blocks,
|
|
3297
|
+
sources: [...sources].map(([sourceId, source]) => ({
|
|
3298
|
+
sourceId,
|
|
3299
|
+
label: source.label,
|
|
3300
|
+
relativePath: source.relativePath,
|
|
3301
|
+
fileId: source.fileId,
|
|
3302
|
+
startLine: source.startLine,
|
|
3303
|
+
endLine: source.endLine,
|
|
3304
|
+
confidence: source.confidence,
|
|
3305
|
+
targetIds: source.targetIds,
|
|
3306
|
+
...source.sourceVersion === void 0 ? {} : { sourceVersion: source.sourceVersion },
|
|
3307
|
+
contentHash: source.contentHash
|
|
3308
|
+
})),
|
|
3309
|
+
warnings,
|
|
3310
|
+
createdAt: created.toISOString(),
|
|
3311
|
+
expiresAt: new Date(
|
|
3312
|
+
created.getTime() + CONTEXTUAL_ASK_LIMITS3.resultTtlMs
|
|
3313
|
+
).toISOString()
|
|
3314
|
+
});
|
|
3315
|
+
};
|
|
3316
|
+
const run = async (job) => {
|
|
3317
|
+
const timeout = setTimeout(() => {
|
|
3318
|
+
job.controller.abort(new ContextualAskError("ASK_TIMEOUT"));
|
|
3319
|
+
}, CONTEXTUAL_ASK_LIMITS3.jobTimeoutMs);
|
|
3320
|
+
timeout.unref();
|
|
3321
|
+
try {
|
|
3322
|
+
transition(job, "authorizing", "Authorizing selected sources.");
|
|
3323
|
+
const captured = await dependencies.captureSnapshot({
|
|
3324
|
+
root: options.root,
|
|
3325
|
+
registry: options.registry,
|
|
3326
|
+
selection: job.request.envelope.selection,
|
|
3327
|
+
signal: job.controller.signal,
|
|
3328
|
+
createHandleId: dependencies.createId
|
|
3329
|
+
});
|
|
3330
|
+
if (job.controller.signal.aborted) {
|
|
3331
|
+
captured.dispose();
|
|
3332
|
+
}
|
|
3333
|
+
job.controller.signal.throwIfAborted();
|
|
3334
|
+
job.snapshot = captured;
|
|
3335
|
+
transition(job, "running", "Analyzing the authorized snapshot.");
|
|
3336
|
+
emitReadActivity(job, void 0, "succeeded");
|
|
3337
|
+
const readLimit = { exceeded: false };
|
|
3338
|
+
const execution = await raceWithAbort(
|
|
3339
|
+
job.executor.execute(
|
|
3340
|
+
{
|
|
3341
|
+
jobId: job.id,
|
|
3342
|
+
envelope: job.request.envelope,
|
|
3343
|
+
grant: captured.grant,
|
|
3344
|
+
snapshot: instrumentSnapshot(job, captured.snapshot, () => {
|
|
3345
|
+
readLimit.exceeded = true;
|
|
3346
|
+
})
|
|
3347
|
+
},
|
|
3348
|
+
job.controller.signal
|
|
3349
|
+
),
|
|
3350
|
+
job.controller.signal
|
|
3351
|
+
);
|
|
3352
|
+
const draft = askAnswerDraftSchema.parse(execution);
|
|
3353
|
+
const effectiveModelLabel = job.executor.effectiveModelLabel?.();
|
|
3354
|
+
if (effectiveModelLabel !== void 0) {
|
|
3355
|
+
job.executorCapability = contextualAskExecutorCapabilitySchema.parse({
|
|
3356
|
+
...job.executorCapability,
|
|
3357
|
+
effectiveModelLabel
|
|
3358
|
+
});
|
|
3359
|
+
}
|
|
3360
|
+
if (readLimit.exceeded) throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
3361
|
+
job.controller.signal.throwIfAborted();
|
|
3362
|
+
const result = await projectAnswer(job, draft, captured);
|
|
3363
|
+
job.controller.signal.throwIfAborted();
|
|
3364
|
+
job.result = result;
|
|
3365
|
+
transition(job, "answered", "Answer ready.");
|
|
3366
|
+
appendEvent(job, { ...eventBase(job), type: "answer-ready" });
|
|
3367
|
+
finish(job);
|
|
3368
|
+
} catch (error) {
|
|
3369
|
+
fail(
|
|
3370
|
+
job,
|
|
3371
|
+
error instanceof Error && error.name === "ZodError" ? new ContextualAskError("ASK_ANSWER_INVALID", { cause: error }) : asContextualAskError(error)
|
|
3372
|
+
);
|
|
3373
|
+
} finally {
|
|
3374
|
+
clearTimeout(timeout);
|
|
3375
|
+
}
|
|
3376
|
+
};
|
|
3377
|
+
return Object.freeze({
|
|
3378
|
+
async capability(signal) {
|
|
3379
|
+
if (!options.enabled || closed) {
|
|
3380
|
+
return contextualAskCapabilitySchema.parse({
|
|
3381
|
+
schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3382
|
+
enabled: false,
|
|
3383
|
+
executors: [],
|
|
3384
|
+
safety: {
|
|
3385
|
+
selectionRequired: true,
|
|
3386
|
+
singleTurn: true,
|
|
3387
|
+
writesAllowed: false,
|
|
3388
|
+
historyStored: false
|
|
3389
|
+
},
|
|
3390
|
+
checkedAt: dependencies.now().toISOString()
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
const capabilities = (await Promise.all(
|
|
3394
|
+
[...executors.values()].map(async (executor) => {
|
|
3395
|
+
try {
|
|
3396
|
+
return contextualAskExecutorCapabilitySchema.parse(
|
|
3397
|
+
await raceWithAbort(executor.capability(signal), signal)
|
|
3398
|
+
);
|
|
3399
|
+
} catch {
|
|
3400
|
+
return void 0;
|
|
3401
|
+
}
|
|
3402
|
+
})
|
|
3403
|
+
)).filter((capability) => capability !== void 0);
|
|
3404
|
+
return contextualAskCapabilitySchema.parse({
|
|
3405
|
+
schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3406
|
+
enabled: true,
|
|
3407
|
+
executors: capabilities,
|
|
3408
|
+
safety: {
|
|
3409
|
+
selectionRequired: true,
|
|
3410
|
+
singleTurn: true,
|
|
3411
|
+
writesAllowed: false,
|
|
3412
|
+
historyStored: false
|
|
3413
|
+
},
|
|
3414
|
+
checkedAt: dependencies.now().toISOString()
|
|
3415
|
+
});
|
|
3416
|
+
},
|
|
3417
|
+
async create(rawRequest) {
|
|
3418
|
+
cleanupExpired();
|
|
3419
|
+
if (!options.enabled || closed) throw new ContextualAskError("ASK_DISABLED");
|
|
3420
|
+
const parsed = askJobCreateRequestSchema.safeParse(rawRequest);
|
|
3421
|
+
if (!parsed.success) throw new ContextualAskError("ASK_QUESTION_INVALID");
|
|
3422
|
+
const request = parsed.data;
|
|
3423
|
+
const requestFingerprint = fingerprint(request);
|
|
3424
|
+
const previous = requests.get(request.requestId);
|
|
3425
|
+
if (previous !== void 0) {
|
|
3426
|
+
if (previous.fingerprint !== requestFingerprint) {
|
|
3427
|
+
throw new ContextualAskError("ASK_IDEMPOTENCY_CONFLICT");
|
|
3428
|
+
}
|
|
3429
|
+
return publicSnapshot(requireJob(previous.jobId));
|
|
3430
|
+
}
|
|
3431
|
+
const pending = pendingRequests.get(request.requestId);
|
|
3432
|
+
if (pending !== void 0) {
|
|
3433
|
+
if (pending.fingerprint !== requestFingerprint) {
|
|
3434
|
+
throw new ContextualAskError("ASK_IDEMPOTENCY_CONFLICT");
|
|
3435
|
+
}
|
|
3436
|
+
return await pending.promise;
|
|
3437
|
+
}
|
|
3438
|
+
if ([...jobs.values()].some((job) => ACTIVE_STATUSES.has(job.status))) {
|
|
3439
|
+
throw new ContextualAskError("ASK_BUSY");
|
|
3440
|
+
}
|
|
3441
|
+
const executor = executors.get(request.executorId);
|
|
3442
|
+
if (executor === void 0)
|
|
3443
|
+
throw new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE");
|
|
3444
|
+
const lease = options.coordinator.acquire("ask");
|
|
3445
|
+
if (lease === void 0) throw new ContextualAskError("ASK_BUSY");
|
|
3446
|
+
const controller = new AbortController();
|
|
3447
|
+
let leaseTransferred = false;
|
|
3448
|
+
const creation = (async () => {
|
|
3449
|
+
try {
|
|
3450
|
+
const capabilitySignal = AbortSignal.any([
|
|
3451
|
+
controller.signal,
|
|
3452
|
+
AbortSignal.timeout(CONTEXTUAL_ASK_LIMITS3.capabilityTimeoutMs)
|
|
3453
|
+
]);
|
|
3454
|
+
const capability = contextualAskExecutorCapabilitySchema.parse(
|
|
3455
|
+
await raceWithAbort(
|
|
3456
|
+
executor.capability(capabilitySignal),
|
|
3457
|
+
capabilitySignal
|
|
3458
|
+
)
|
|
3459
|
+
);
|
|
3460
|
+
if (isClosed()) throw new ContextualAskError("ASK_DISABLED");
|
|
3461
|
+
if (capability.executorId !== executor.executorId || capability.state !== "ready" || !capability.readOnlyProven) {
|
|
3462
|
+
throw new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE");
|
|
3463
|
+
}
|
|
3464
|
+
for (const [jobId, existing] of jobs) {
|
|
3465
|
+
if (TERMINAL_STATUSES.has(existing.status)) {
|
|
3466
|
+
existing.snapshot?.dispose();
|
|
3467
|
+
jobs.delete(jobId);
|
|
3468
|
+
requests.delete(existing.request.requestId);
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
const createdAt = dependencies.now().toISOString();
|
|
3472
|
+
const job = {
|
|
3473
|
+
controller: new AbortController(),
|
|
3474
|
+
createdAt,
|
|
3475
|
+
events: [],
|
|
3476
|
+
executor,
|
|
3477
|
+
executorCapability: capability,
|
|
3478
|
+
fingerprint: requestFingerprint,
|
|
3479
|
+
id: dependencies.createId(),
|
|
3480
|
+
lease,
|
|
3481
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
3482
|
+
request,
|
|
3483
|
+
errorCode: void 0,
|
|
3484
|
+
expiresAt: void 0,
|
|
3485
|
+
phaseMessage: "Question queued.",
|
|
3486
|
+
result: void 0,
|
|
3487
|
+
runPromise: void 0,
|
|
3488
|
+
sequence: 0,
|
|
3489
|
+
snapshot: void 0,
|
|
3490
|
+
status: "queued",
|
|
3491
|
+
updatedAt: createdAt
|
|
3492
|
+
};
|
|
3493
|
+
jobs.set(job.id, job);
|
|
3494
|
+
requests.set(request.requestId, {
|
|
3495
|
+
fingerprint: requestFingerprint,
|
|
3496
|
+
jobId: job.id
|
|
3497
|
+
});
|
|
3498
|
+
leaseTransferred = true;
|
|
3499
|
+
emitSnapshot(job);
|
|
3500
|
+
emitPhase(job, "Question queued.");
|
|
3501
|
+
job.runPromise = run(job);
|
|
3502
|
+
return publicSnapshot(job);
|
|
3503
|
+
} catch (error) {
|
|
3504
|
+
throw error instanceof ContextualAskError ? error : new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", {
|
|
3505
|
+
cause: error
|
|
3506
|
+
});
|
|
3507
|
+
} finally {
|
|
3508
|
+
if (!leaseTransferred) lease.release();
|
|
3509
|
+
pendingRequests.delete(request.requestId);
|
|
3510
|
+
}
|
|
3511
|
+
})();
|
|
3512
|
+
pendingRequests.set(request.requestId, {
|
|
3513
|
+
controller,
|
|
3514
|
+
fingerprint: requestFingerprint,
|
|
3515
|
+
promise: creation
|
|
3516
|
+
});
|
|
3517
|
+
return await creation;
|
|
3518
|
+
},
|
|
3519
|
+
cancel(jobId) {
|
|
3520
|
+
const job = requireJob(jobId);
|
|
3521
|
+
if (TERMINAL_STATUSES.has(job.status)) return publicSnapshot(job);
|
|
3522
|
+
if (job.status !== "cancelling") {
|
|
3523
|
+
transition(job, "cancelling", "Cancelling question.");
|
|
3524
|
+
job.controller.abort(new ContextualAskError("ASK_CANCELLED"));
|
|
3525
|
+
job.snapshot?.dispose();
|
|
3526
|
+
job.snapshot = void 0;
|
|
3527
|
+
}
|
|
3528
|
+
return publicSnapshot(job);
|
|
3529
|
+
},
|
|
3530
|
+
events(jobId, afterSequence = 0) {
|
|
3531
|
+
if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) {
|
|
3532
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3533
|
+
}
|
|
3534
|
+
return Object.freeze(
|
|
3535
|
+
requireJob(jobId).events.filter((event) => event.sequence > afterSequence)
|
|
3536
|
+
);
|
|
3537
|
+
},
|
|
3538
|
+
async result(jobId) {
|
|
3539
|
+
const job = requireJob(jobId);
|
|
3540
|
+
await job.runPromise;
|
|
3541
|
+
const current = requireJob(jobId);
|
|
3542
|
+
return askJobResultResponseSchema.parse({
|
|
3543
|
+
snapshot: publicSnapshot(current),
|
|
3544
|
+
...current.result === void 0 ? {} : { result: current.result }
|
|
3545
|
+
});
|
|
3546
|
+
},
|
|
3547
|
+
snapshot(jobId) {
|
|
3548
|
+
return publicSnapshot(requireJob(jobId));
|
|
3549
|
+
},
|
|
3550
|
+
subscribe(jobId, listener) {
|
|
3551
|
+
const job = requireJob(jobId);
|
|
3552
|
+
if (job.listeners.size >= CONTEXTUAL_ASK_LIMITS3.maximumEventSubscribers) {
|
|
3553
|
+
throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
3554
|
+
}
|
|
3555
|
+
job.listeners.add(listener);
|
|
3556
|
+
return () => job.listeners.delete(listener);
|
|
3557
|
+
},
|
|
3558
|
+
async close() {
|
|
3559
|
+
if (closed) return;
|
|
3560
|
+
closed = true;
|
|
3561
|
+
for (const pending of pendingRequests.values()) {
|
|
3562
|
+
pending.controller.abort(new ContextualAskError("ASK_CANCELLED"));
|
|
3563
|
+
}
|
|
3564
|
+
await Promise.allSettled(
|
|
3565
|
+
[...pendingRequests.values()].map((pending) => pending.promise)
|
|
3566
|
+
);
|
|
3567
|
+
for (const job of jobs.values()) {
|
|
3568
|
+
if (ACTIVE_STATUSES.has(job.status)) {
|
|
3569
|
+
job.controller.abort(new ContextualAskError("ASK_CANCELLED"));
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
await Promise.allSettled(
|
|
3573
|
+
[...jobs.values()].map((job) => job.runPromise ?? Promise.resolve())
|
|
3574
|
+
);
|
|
3575
|
+
for (const job of jobs.values()) {
|
|
3576
|
+
job.snapshot?.dispose();
|
|
3577
|
+
job.listeners.clear();
|
|
3578
|
+
job.lease.release();
|
|
3579
|
+
}
|
|
3580
|
+
jobs.clear();
|
|
3581
|
+
requests.clear();
|
|
3582
|
+
pendingRequests.clear();
|
|
3583
|
+
await Promise.allSettled(
|
|
3584
|
+
[...executors.values()].map(async (executor) => executor.dispose?.())
|
|
3585
|
+
);
|
|
3586
|
+
}
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
// src/contextual-ask/configured-key-executors.ts
|
|
3591
|
+
import {
|
|
3592
|
+
createConfiguredKeyAskExecutor,
|
|
3593
|
+
resolveProviderCredential as resolveProviderCredential2
|
|
3594
|
+
} from "@spotpatch/agent";
|
|
3595
|
+
function createConfiguredKeyAskExecutors(options) {
|
|
3596
|
+
const preferred = options.defaultExecutor?.kind === "configured-key" ? options.defaultExecutor : Object.freeze({
|
|
3597
|
+
kind: "configured-key",
|
|
3598
|
+
providerProfileId: options.ai.defaultProvider,
|
|
3599
|
+
modelProfileId: options.ai.providers[options.ai.defaultProvider]?.defaultModel ?? ""
|
|
3600
|
+
});
|
|
3601
|
+
const providers = Object.values(options.ai.providers).sort(
|
|
3602
|
+
(left, right) => left.id === preferred.providerProfileId ? -1 : right.id === preferred.providerProfileId ? 1 : 0
|
|
3603
|
+
);
|
|
3604
|
+
return Object.freeze(
|
|
3605
|
+
providers.flatMap((provider) => {
|
|
3606
|
+
const credential = resolveProviderCredential2(
|
|
3607
|
+
provider.apiKeyEnv,
|
|
3608
|
+
options.environment
|
|
3609
|
+
);
|
|
3610
|
+
const models = Object.values(provider.models).sort(
|
|
3611
|
+
(left, right) => provider.id !== preferred.providerProfileId ? 0 : left.id === preferred.modelProfileId ? -1 : right.id === preferred.modelProfileId ? 1 : 0
|
|
3612
|
+
);
|
|
3613
|
+
return models.map(
|
|
3614
|
+
(model) => createConfiguredKeyAskExecutor({
|
|
3615
|
+
provider,
|
|
3616
|
+
model,
|
|
3617
|
+
credential,
|
|
3618
|
+
limits: options.ai.execution.limits,
|
|
3619
|
+
...options.fetch === void 0 ? {} : { fetch: options.fetch }
|
|
3620
|
+
})
|
|
3621
|
+
);
|
|
3622
|
+
})
|
|
3623
|
+
);
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
// src/contextual-ask/executors.ts
|
|
3627
|
+
function composeContextualAskExecutors(options) {
|
|
3628
|
+
return Object.freeze(
|
|
3629
|
+
options.defaultExecutor?.kind === "managed-codex" ? [options.managedCodex, ...options.configuredKey] : [...options.configuredKey, options.managedCodex]
|
|
3630
|
+
);
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
// src/workspace/activity-coordinator.ts
|
|
3634
|
+
function createWorkspaceActivityCoordinator() {
|
|
3635
|
+
let active;
|
|
3636
|
+
return Object.freeze({
|
|
3637
|
+
acquire(kind) {
|
|
3638
|
+
if (active !== void 0) return void 0;
|
|
3639
|
+
active = kind;
|
|
3640
|
+
let released = false;
|
|
3641
|
+
return Object.freeze({
|
|
3642
|
+
release() {
|
|
3643
|
+
if (released) return;
|
|
3644
|
+
released = true;
|
|
3645
|
+
if (active === kind) active = void 0;
|
|
3646
|
+
}
|
|
3647
|
+
});
|
|
3648
|
+
},
|
|
3649
|
+
current() {
|
|
3650
|
+
return active;
|
|
3651
|
+
}
|
|
3652
|
+
});
|
|
3653
|
+
}
|
|
3654
|
+
|
|
2497
3655
|
// src/server/middleware.ts
|
|
2498
3656
|
import {
|
|
2499
3657
|
ERROR_CODES as ERROR_CODES16,
|
|
2500
3658
|
SPOTPATCH_API_BASE,
|
|
2501
|
-
SPOTPATCH_ENDPOINTS as
|
|
3659
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS5,
|
|
2502
3660
|
SpotPatchError as SpotPatchError16,
|
|
2503
3661
|
openEditorRequestSchema,
|
|
2504
3662
|
sourceContextRequestSchema
|
|
2505
3663
|
} from "@spotpatch/shared";
|
|
2506
3664
|
|
|
3665
|
+
// src/contextual-ask/http.ts
|
|
3666
|
+
import {
|
|
3667
|
+
askJobActionRequestSchema,
|
|
3668
|
+
askJobCreateRequestSchema as askJobCreateRequestSchema2,
|
|
3669
|
+
askJobEventsRequestSchema,
|
|
3670
|
+
CONTEXTUAL_ASK_LIMITS as CONTEXTUAL_ASK_LIMITS4,
|
|
3671
|
+
SPOTPATCH_ENDPOINTS
|
|
3672
|
+
} from "@spotpatch/shared";
|
|
3673
|
+
var STATUS_BY_ERROR = Object.freeze({
|
|
3674
|
+
ASK_DISABLED: 404,
|
|
3675
|
+
ASK_SELECTION_REQUIRED: 422,
|
|
3676
|
+
ASK_SELECTION_STALE: 409,
|
|
3677
|
+
ASK_QUESTION_INVALID: 422,
|
|
3678
|
+
ASK_EXECUTOR_UNAVAILABLE: 503,
|
|
3679
|
+
ASK_TIMEOUT: 504,
|
|
3680
|
+
ASK_CONSENT_REQUIRED: 403,
|
|
3681
|
+
ASK_BUSY: 409,
|
|
3682
|
+
ASK_IDEMPOTENCY_CONFLICT: 409,
|
|
3683
|
+
ASK_SOURCE_SCOPE_DENIED: 403,
|
|
3684
|
+
ASK_LIMIT_EXCEEDED: 413,
|
|
3685
|
+
ASK_ANSWER_INVALID: 502,
|
|
3686
|
+
ASK_WRITE_ATTEMPTED: 403,
|
|
3687
|
+
ASK_CANCELLED: 409,
|
|
3688
|
+
ASK_RESULT_EXPIRED: 410,
|
|
3689
|
+
ASK_PROTOCOL_INCOMPATIBLE: 400
|
|
3690
|
+
});
|
|
3691
|
+
var PUBLIC_MESSAGES = Object.freeze({
|
|
3692
|
+
ASK_DISABLED: "Contextual Ask is not enabled.",
|
|
3693
|
+
ASK_SELECTION_REQUIRED: "Select at least one source-backed element.",
|
|
3694
|
+
ASK_SELECTION_STALE: "The selected source is stale.",
|
|
3695
|
+
ASK_QUESTION_INVALID: "The question is invalid.",
|
|
3696
|
+
ASK_EXECUTOR_UNAVAILABLE: "The selected Ask executor is unavailable.",
|
|
3697
|
+
ASK_TIMEOUT: "The Ask executor did not answer before the deadline.",
|
|
3698
|
+
ASK_CONSENT_REQUIRED: "Provider data consent is required.",
|
|
3699
|
+
ASK_BUSY: "Another workspace task is active.",
|
|
3700
|
+
ASK_IDEMPOTENCY_CONFLICT: "The request ID was reused with different content.",
|
|
3701
|
+
ASK_SOURCE_SCOPE_DENIED: "The requested source scope was denied.",
|
|
3702
|
+
ASK_LIMIT_EXCEEDED: "The Ask request exceeded a safety limit.",
|
|
3703
|
+
ASK_ANSWER_INVALID: "The executor returned an invalid answer.",
|
|
3704
|
+
ASK_WRITE_ATTEMPTED: "Ask executors cannot write project files.",
|
|
3705
|
+
ASK_CANCELLED: "The Ask job was cancelled.",
|
|
3706
|
+
ASK_RESULT_EXPIRED: "The Ask result has expired.",
|
|
3707
|
+
ASK_PROTOCOL_INCOMPATIBLE: "The Ask protocol request is incompatible."
|
|
3708
|
+
});
|
|
3709
|
+
function writeJson2(response, status, data) {
|
|
3710
|
+
response.statusCode = status;
|
|
3711
|
+
response.setHeader("Cache-Control", "no-store");
|
|
3712
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
3713
|
+
response.end(JSON.stringify({ ok: true, data }));
|
|
3714
|
+
}
|
|
3715
|
+
function writeError(response, error, logger) {
|
|
3716
|
+
const normalized = error instanceof ContextualAskError ? error : new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", { cause: error });
|
|
3717
|
+
if (normalized.code === "ASK_EXECUTOR_UNAVAILABLE" && error !== normalized) {
|
|
3718
|
+
logger?.warn("[spotpatch:ask] Internal contextual Ask request failure.");
|
|
3719
|
+
}
|
|
3720
|
+
response.statusCode = STATUS_BY_ERROR[normalized.code];
|
|
3721
|
+
response.setHeader("Cache-Control", "no-store");
|
|
3722
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
3723
|
+
response.end(
|
|
3724
|
+
JSON.stringify({
|
|
3725
|
+
ok: false,
|
|
3726
|
+
error: {
|
|
3727
|
+
code: normalized.code,
|
|
3728
|
+
message: PUBLIC_MESSAGES[normalized.code]
|
|
3729
|
+
}
|
|
3730
|
+
})
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3733
|
+
function matchContextualAskPath(pathname) {
|
|
3734
|
+
if (pathname === SPOTPATCH_ENDPOINTS.askCapability) {
|
|
3735
|
+
return Object.freeze({ kind: "capability" });
|
|
3736
|
+
}
|
|
3737
|
+
if (pathname === SPOTPATCH_ENDPOINTS.askJobs) {
|
|
3738
|
+
return Object.freeze({ kind: "create" });
|
|
3739
|
+
}
|
|
3740
|
+
const prefix = `${SPOTPATCH_ENDPOINTS.askJobs}/`;
|
|
3741
|
+
if (!pathname.startsWith(prefix)) return void 0;
|
|
3742
|
+
const segments = pathname.slice(prefix.length).split("/");
|
|
3743
|
+
if (segments.length !== 2) return void 0;
|
|
3744
|
+
let jobId;
|
|
3745
|
+
try {
|
|
3746
|
+
jobId = decodeURIComponent(segments[0] ?? "");
|
|
3747
|
+
} catch {
|
|
3748
|
+
return void 0;
|
|
3749
|
+
}
|
|
3750
|
+
const action = segments[1];
|
|
3751
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/u.test(jobId) || action !== "events" && action !== "result" && action !== "cancel") {
|
|
3752
|
+
return void 0;
|
|
3753
|
+
}
|
|
3754
|
+
return Object.freeze({ kind: "job", action, jobId });
|
|
3755
|
+
}
|
|
3756
|
+
function parseAfterSequence(request) {
|
|
3757
|
+
let url;
|
|
3758
|
+
try {
|
|
3759
|
+
url = new URL(request.url ?? "/", "http://spotpatch.invalid");
|
|
3760
|
+
} catch {
|
|
3761
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3762
|
+
}
|
|
3763
|
+
if ([...url.searchParams.keys()].some((key) => key !== "afterSequence")) {
|
|
3764
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3765
|
+
}
|
|
3766
|
+
const raw = url.searchParams.get("afterSequence");
|
|
3767
|
+
if (raw === null) return 0;
|
|
3768
|
+
if (!/^\d+$/u.test(raw)) throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3769
|
+
const parsed = askJobEventsRequestSchema.safeParse({ afterSequence: Number(raw) });
|
|
3770
|
+
if (!parsed.success || !Number.isSafeInteger(parsed.data.afterSequence)) {
|
|
3771
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3772
|
+
}
|
|
3773
|
+
return parsed.data.afterSequence ?? 0;
|
|
3774
|
+
}
|
|
3775
|
+
function streamEvents(request, response, manager, jobId) {
|
|
3776
|
+
const afterSequence = parseAfterSequence(request);
|
|
3777
|
+
response.statusCode = 200;
|
|
3778
|
+
response.setHeader("Cache-Control", "no-store");
|
|
3779
|
+
response.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
3780
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
3781
|
+
let unsubscribe = () => void 0;
|
|
3782
|
+
const heartbeat = setInterval(
|
|
3783
|
+
() => response.write("\n"),
|
|
3784
|
+
CONTEXTUAL_ASK_LIMITS4.eventHeartbeatMs
|
|
3785
|
+
);
|
|
3786
|
+
heartbeat.unref();
|
|
3787
|
+
const cleanup = () => {
|
|
3788
|
+
clearInterval(heartbeat);
|
|
3789
|
+
unsubscribe();
|
|
3790
|
+
};
|
|
3791
|
+
unsubscribe = manager.subscribe(jobId, (event) => {
|
|
3792
|
+
response.write(`${JSON.stringify(event)}
|
|
3793
|
+
`);
|
|
3794
|
+
if (["answered", "cancelled", "failed"].includes(event.status)) {
|
|
3795
|
+
cleanup();
|
|
3796
|
+
response.end();
|
|
3797
|
+
}
|
|
3798
|
+
});
|
|
3799
|
+
const replay = manager.events(jobId, afterSequence);
|
|
3800
|
+
for (const event of replay) response.write(`${JSON.stringify(event)}
|
|
3801
|
+
`);
|
|
3802
|
+
const latest = replay.at(-1);
|
|
3803
|
+
const currentStatus = latest?.status ?? manager.snapshot(jobId).status;
|
|
3804
|
+
if (["answered", "cancelled", "failed"].includes(currentStatus)) {
|
|
3805
|
+
cleanup();
|
|
3806
|
+
response.end();
|
|
3807
|
+
return;
|
|
3808
|
+
}
|
|
3809
|
+
request.once("close", cleanup);
|
|
3810
|
+
response.once("close", cleanup);
|
|
3811
|
+
}
|
|
3812
|
+
function hasEmptySelection(value) {
|
|
3813
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3814
|
+
const envelope = value.envelope;
|
|
3815
|
+
if (typeof envelope !== "object" || envelope === null) return false;
|
|
3816
|
+
const selection = envelope.selection;
|
|
3817
|
+
if (typeof selection !== "object" || selection === null) return false;
|
|
3818
|
+
return Array.isArray(selection.targets) && selection.targets.length === 0;
|
|
3819
|
+
}
|
|
3820
|
+
function lacksProviderDataConsent(value) {
|
|
3821
|
+
return typeof value === "object" && value !== null && value.providerDataConsent !== true;
|
|
3822
|
+
}
|
|
3823
|
+
async function handleContextualAskRequest(request, response, route, manager, logger) {
|
|
3824
|
+
try {
|
|
3825
|
+
if (manager === void 0) throw new ContextualAskError("ASK_DISABLED");
|
|
3826
|
+
if (route.kind === "capability") {
|
|
3827
|
+
if (request.method !== "GET")
|
|
3828
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3829
|
+
writeJson2(
|
|
3830
|
+
response,
|
|
3831
|
+
200,
|
|
3832
|
+
await manager.capability(
|
|
3833
|
+
AbortSignal.timeout(CONTEXTUAL_ASK_LIMITS4.capabilityTimeoutMs)
|
|
3834
|
+
)
|
|
3835
|
+
);
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
if (route.kind === "create") {
|
|
3839
|
+
if (request.method !== "POST")
|
|
3840
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3841
|
+
const raw = await readJsonRequestBody(
|
|
3842
|
+
request,
|
|
3843
|
+
CONTEXTUAL_ASK_LIMITS4.maximumRequestBodyBytes
|
|
3844
|
+
).catch((error) => {
|
|
3845
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE", { cause: error });
|
|
3846
|
+
});
|
|
3847
|
+
const parsed = askJobCreateRequestSchema2.safeParse(raw);
|
|
3848
|
+
if (!parsed.success) {
|
|
3849
|
+
throw new ContextualAskError(
|
|
3850
|
+
hasEmptySelection(raw) ? "ASK_SELECTION_REQUIRED" : lacksProviderDataConsent(raw) ? "ASK_CONSENT_REQUIRED" : "ASK_PROTOCOL_INCOMPATIBLE"
|
|
3851
|
+
);
|
|
3852
|
+
}
|
|
3853
|
+
writeJson2(response, 202, await manager.create(parsed.data));
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
if (route.action === "events") {
|
|
3857
|
+
if (request.method !== "GET")
|
|
3858
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3859
|
+
streamEvents(request, response, manager, route.jobId);
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
if (route.action === "result") {
|
|
3863
|
+
if (request.method !== "GET")
|
|
3864
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3865
|
+
writeJson2(response, 200, await manager.result(route.jobId));
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
if (request.method !== "POST")
|
|
3869
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3870
|
+
const action = askJobActionRequestSchema.safeParse(
|
|
3871
|
+
await readJsonRequestBody(request).catch((error) => {
|
|
3872
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE", { cause: error });
|
|
3873
|
+
})
|
|
3874
|
+
);
|
|
3875
|
+
if (!action.success) throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3876
|
+
writeJson2(response, 200, manager.cancel(route.jobId));
|
|
3877
|
+
} catch (error) {
|
|
3878
|
+
writeError(response, error, logger);
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
|
|
2507
3882
|
// src/external-handoff/browser-http.ts
|
|
2508
3883
|
import {
|
|
2509
3884
|
ERROR_CODES as ERROR_CODES10,
|
|
2510
3885
|
EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS5,
|
|
2511
|
-
SPOTPATCH_ENDPOINTS,
|
|
3886
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
|
|
2512
3887
|
SpotPatchError as SpotPatchError10,
|
|
2513
3888
|
externalHandoffCapabilityRequestSchema,
|
|
2514
3889
|
externalHandoffPublishRequestSchema,
|
|
@@ -2517,8 +3892,8 @@ import {
|
|
|
2517
3892
|
} from "@spotpatch/shared";
|
|
2518
3893
|
|
|
2519
3894
|
// src/server/annotation-authorizer.ts
|
|
2520
|
-
import { realpath as
|
|
2521
|
-
import
|
|
3895
|
+
import { realpath as realpath6 } from "fs/promises";
|
|
3896
|
+
import path8 from "path";
|
|
2522
3897
|
import {
|
|
2523
3898
|
ERROR_CODES as ERROR_CODES9,
|
|
2524
3899
|
SpotPatchError as SpotPatchError9,
|
|
@@ -2527,8 +3902,8 @@ import {
|
|
|
2527
3902
|
} from "@spotpatch/shared";
|
|
2528
3903
|
|
|
2529
3904
|
// src/server/source-context.ts
|
|
2530
|
-
import { readFile as readFile3, realpath as
|
|
2531
|
-
import
|
|
3905
|
+
import { readFile as readFile3, realpath as realpath5 } from "fs/promises";
|
|
3906
|
+
import path7 from "path";
|
|
2532
3907
|
import {
|
|
2533
3908
|
ERROR_CODES as ERROR_CODES8,
|
|
2534
3909
|
SpotPatchError as SpotPatchError8
|
|
@@ -2536,8 +3911,8 @@ import {
|
|
|
2536
3911
|
|
|
2537
3912
|
// src/server/extract-code-context.ts
|
|
2538
3913
|
import {
|
|
2539
|
-
parseSync,
|
|
2540
|
-
Visitor
|
|
3914
|
+
parseSync as parseSync2,
|
|
3915
|
+
Visitor as Visitor2
|
|
2541
3916
|
} from "oxc-parser";
|
|
2542
3917
|
function isComponentName(name) {
|
|
2543
3918
|
return /^[A-Z]/u.test(name);
|
|
@@ -2621,7 +3996,7 @@ function findComponentSpan(options) {
|
|
|
2621
3996
|
}
|
|
2622
3997
|
let parseResult;
|
|
2623
3998
|
try {
|
|
2624
|
-
parseResult =
|
|
3999
|
+
parseResult = parseSync2(options.sourcePath, options.source, {
|
|
2625
4000
|
sourceType: "module"
|
|
2626
4001
|
});
|
|
2627
4002
|
} catch {
|
|
@@ -2632,7 +4007,7 @@ function findComponentSpan(options) {
|
|
|
2632
4007
|
}
|
|
2633
4008
|
const jsxNodes = [];
|
|
2634
4009
|
const components = [];
|
|
2635
|
-
const visitor = new
|
|
4010
|
+
const visitor = new Visitor2({
|
|
2636
4011
|
JSXElement(node) {
|
|
2637
4012
|
jsxNodes.push(node);
|
|
2638
4013
|
},
|
|
@@ -2764,8 +4139,8 @@ function extractCodeContext(options) {
|
|
|
2764
4139
|
}
|
|
2765
4140
|
|
|
2766
4141
|
// src/server/source-file.ts
|
|
2767
|
-
import { realpath as
|
|
2768
|
-
import
|
|
4142
|
+
import { realpath as realpath4, stat as stat2 } from "fs/promises";
|
|
4143
|
+
import path6 from "path";
|
|
2769
4144
|
import { ERROR_CODES as ERROR_CODES7, SpotPatchError as SpotPatchError7 } from "@spotpatch/shared";
|
|
2770
4145
|
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
|
|
2771
4146
|
function isMissingFileError(error) {
|
|
@@ -2776,8 +4151,8 @@ async function assertInsideRoot(root, candidate) {
|
|
|
2776
4151
|
let realCandidate;
|
|
2777
4152
|
try {
|
|
2778
4153
|
[realRoot, realCandidate] = await Promise.all([
|
|
2779
|
-
|
|
2780
|
-
|
|
4154
|
+
realpath4(root),
|
|
4155
|
+
realpath4(candidate)
|
|
2781
4156
|
]);
|
|
2782
4157
|
} catch (error) {
|
|
2783
4158
|
if (isMissingFileError(error)) {
|
|
@@ -2787,8 +4162,8 @@ async function assertInsideRoot(root, candidate) {
|
|
|
2787
4162
|
}
|
|
2788
4163
|
throw error;
|
|
2789
4164
|
}
|
|
2790
|
-
const relative =
|
|
2791
|
-
const outside = relative.startsWith(`..${
|
|
4165
|
+
const relative = path6.relative(realRoot, realCandidate);
|
|
4166
|
+
const outside = relative.startsWith(`..${path6.sep}`) || relative === ".." || path6.isAbsolute(relative);
|
|
2792
4167
|
if (outside) {
|
|
2793
4168
|
throw new SpotPatchError7(ERROR_CODES7.SOURCE_OUTSIDE_ROOT);
|
|
2794
4169
|
}
|
|
@@ -2800,7 +4175,7 @@ async function resolveSourceFile(options) {
|
|
|
2800
4175
|
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
|
|
2801
4176
|
}
|
|
2802
4177
|
const sourcePath = await assertInsideRoot(options.root, registeredPath);
|
|
2803
|
-
if (!ALLOWED_EXTENSIONS.has(
|
|
4178
|
+
if (!ALLOWED_EXTENSIONS.has(path6.extname(sourcePath).toLowerCase())) {
|
|
2804
4179
|
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
|
|
2805
4180
|
}
|
|
2806
4181
|
let sourceStat;
|
|
@@ -2825,7 +4200,7 @@ async function resolveSourceFile(options) {
|
|
|
2825
4200
|
|
|
2826
4201
|
// src/server/source-context.ts
|
|
2827
4202
|
function toDisplayPath(root, sourcePath) {
|
|
2828
|
-
return
|
|
4203
|
+
return path7.relative(root, sourcePath).split(path7.sep).join("/");
|
|
2829
4204
|
}
|
|
2830
4205
|
async function readSourceContext(options) {
|
|
2831
4206
|
const sourcePath = await resolveSourceFile({
|
|
@@ -2848,11 +4223,11 @@ async function readSourceContext(options) {
|
|
|
2848
4223
|
if (options.request.line > lines.length) {
|
|
2849
4224
|
throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
|
|
2850
4225
|
}
|
|
2851
|
-
const extension =
|
|
4226
|
+
const extension = path7.extname(sourcePath).toLowerCase();
|
|
2852
4227
|
return extractCodeContext({
|
|
2853
4228
|
source,
|
|
2854
4229
|
sourcePath,
|
|
2855
|
-
relativePath: toDisplayPath(await
|
|
4230
|
+
relativePath: toDisplayPath(await realpath5(options.root), sourcePath),
|
|
2856
4231
|
language: extension === ".tsx" ? "tsx" : "jsx",
|
|
2857
4232
|
line: options.request.line,
|
|
2858
4233
|
column: options.request.column,
|
|
@@ -2894,7 +4269,7 @@ async function authorizeSourceRef(source, registry, root) {
|
|
|
2894
4269
|
return compactSourceRef(source);
|
|
2895
4270
|
}
|
|
2896
4271
|
const sourcePath = await resolveSourceFile({ fileId: source.fileId, registry, root });
|
|
2897
|
-
const relativePath =
|
|
4272
|
+
const relativePath = path8.relative(await realpath6(root), sourcePath).split(path8.sep).join("/");
|
|
2898
4273
|
if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
|
|
2899
4274
|
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
2900
4275
|
}
|
|
@@ -2997,11 +4372,11 @@ async function authorizeAnnotation(input) {
|
|
|
2997
4372
|
}
|
|
2998
4373
|
|
|
2999
4374
|
// src/external-handoff/browser-http.ts
|
|
3000
|
-
function matchExternalHandoffBrowserPath(
|
|
3001
|
-
if (
|
|
3002
|
-
if (
|
|
3003
|
-
if (
|
|
3004
|
-
if (
|
|
4375
|
+
function matchExternalHandoffBrowserPath(path10) {
|
|
4376
|
+
if (path10 === SPOTPATCH_ENDPOINTS2.externalHandoffCapability) return "capability";
|
|
4377
|
+
if (path10 === SPOTPATCH_ENDPOINTS2.externalHandoffPublish) return "publish";
|
|
4378
|
+
if (path10 === SPOTPATCH_ENDPOINTS2.externalHandoffStatus) return "status";
|
|
4379
|
+
if (path10 === SPOTPATCH_ENDPOINTS2.externalHandoffResolveDelivery) {
|
|
3005
4380
|
return "resolve-delivery";
|
|
3006
4381
|
}
|
|
3007
4382
|
return void 0;
|
|
@@ -3081,7 +4456,7 @@ async function handleExternalHandoffBrowserRequest(request, response, route, opt
|
|
|
3081
4456
|
import {
|
|
3082
4457
|
ERROR_CODES as ERROR_CODES11,
|
|
3083
4458
|
EXTERNAL_AGENT_CONTROL_LIMITS,
|
|
3084
|
-
SPOTPATCH_ENDPOINTS as
|
|
4459
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS3,
|
|
3085
4460
|
SpotPatchError as SpotPatchError11,
|
|
3086
4461
|
externalAgentControlCancelRequestSchema,
|
|
3087
4462
|
externalAgentControlConnectRequestSchema,
|
|
@@ -3093,15 +4468,15 @@ import {
|
|
|
3093
4468
|
externalAgentManagedResultSchema,
|
|
3094
4469
|
externalAgentResultRequestSchema
|
|
3095
4470
|
} from "@spotpatch/shared";
|
|
3096
|
-
function matchExternalAgentBrowserPath(
|
|
3097
|
-
if (
|
|
3098
|
-
if (
|
|
3099
|
-
if (
|
|
4471
|
+
function matchExternalAgentBrowserPath(path10) {
|
|
4472
|
+
if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentControlStatus) return "status";
|
|
4473
|
+
if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentControlConnect) return "connect";
|
|
4474
|
+
if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentControlDisconnect) {
|
|
3100
4475
|
return "disconnect";
|
|
3101
4476
|
}
|
|
3102
|
-
if (
|
|
3103
|
-
if (
|
|
3104
|
-
if (
|
|
4477
|
+
if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentControlCancel) return "cancel";
|
|
4478
|
+
if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentEvents) return "events";
|
|
4479
|
+
if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentResult) return "result";
|
|
3105
4480
|
return void 0;
|
|
3106
4481
|
}
|
|
3107
4482
|
function writeEvent(response, event) {
|
|
@@ -3240,7 +4615,7 @@ function createExternalAgentBrowserController(port) {
|
|
|
3240
4615
|
// src/server/agent-http.ts
|
|
3241
4616
|
import {
|
|
3242
4617
|
ERROR_CODES as ERROR_CODES12,
|
|
3243
|
-
SPOTPATCH_ENDPOINTS as
|
|
4618
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS4,
|
|
3244
4619
|
SpotPatchError as SpotPatchError12,
|
|
3245
4620
|
agentCapabilityRequestSchema,
|
|
3246
4621
|
agentJobActionRequestSchema,
|
|
@@ -3285,21 +4660,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
|
|
|
3285
4660
|
"reverted",
|
|
3286
4661
|
"failed"
|
|
3287
4662
|
]);
|
|
3288
|
-
function matchAgentRequestPath(
|
|
3289
|
-
if (
|
|
4663
|
+
function matchAgentRequestPath(path10) {
|
|
4664
|
+
if (path10 === SPOTPATCH_ENDPOINTS4.agentCapability) {
|
|
3290
4665
|
return Object.freeze({ kind: "capability" });
|
|
3291
4666
|
}
|
|
3292
|
-
if (
|
|
4667
|
+
if (path10 === SPOTPATCH_ENDPOINTS4.agentWorkspaceHealth) {
|
|
3293
4668
|
return Object.freeze({ kind: "workspace-health" });
|
|
3294
4669
|
}
|
|
3295
|
-
if (
|
|
4670
|
+
if (path10 === SPOTPATCH_ENDPOINTS4.agentJobs) {
|
|
3296
4671
|
return Object.freeze({ kind: "create-job" });
|
|
3297
4672
|
}
|
|
3298
|
-
const prefix = `${
|
|
3299
|
-
if (!
|
|
4673
|
+
const prefix = `${SPOTPATCH_ENDPOINTS4.agentJobs}/`;
|
|
4674
|
+
if (!path10.startsWith(prefix)) {
|
|
3300
4675
|
return void 0;
|
|
3301
4676
|
}
|
|
3302
|
-
const segments =
|
|
4677
|
+
const segments = path10.slice(prefix.length).split("/");
|
|
3303
4678
|
const jobId = segments[0];
|
|
3304
4679
|
const action = segments[1];
|
|
3305
4680
|
if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
|
|
@@ -3493,13 +4868,13 @@ function detectIntegratedEditor(environment) {
|
|
|
3493
4868
|
function editorCommand(editor) {
|
|
3494
4869
|
return editor === "cursor" ? "cursor" : "code";
|
|
3495
4870
|
}
|
|
3496
|
-
var
|
|
4871
|
+
var DEFAULT_DEPENDENCIES3 = Object.freeze({
|
|
3497
4872
|
environment: process.env,
|
|
3498
4873
|
fallbackLauncher: launchEditor,
|
|
3499
4874
|
processSpawner: (command, arguments_, options) => spawn(command, [...arguments_], options),
|
|
3500
4875
|
startupGraceMs: EDITOR_STARTUP_GRACE_MS
|
|
3501
4876
|
});
|
|
3502
|
-
function createEditorLauncher(dependencies =
|
|
4877
|
+
function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES3) {
|
|
3503
4878
|
return (target, configuredEditor) => {
|
|
3504
4879
|
const integratedEditor = detectIntegratedEditor(dependencies.environment);
|
|
3505
4880
|
const resolvedEditor = configuredEditor === "auto" ? integratedEditor : configuredEditor;
|
|
@@ -3554,7 +4929,7 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
3554
4929
|
var launchConfiguredEditor = createEditorLauncher();
|
|
3555
4930
|
|
|
3556
4931
|
// src/server/data-flow-http.ts
|
|
3557
|
-
import { createHash as
|
|
4932
|
+
import { createHash as createHash5 } from "crypto";
|
|
3558
4933
|
import {
|
|
3559
4934
|
createStaticDataFlowAnalyzer
|
|
3560
4935
|
} from "@spotpatch/analyzer";
|
|
@@ -3683,7 +5058,7 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
3683
5058
|
const analyzedVersions = new Set(
|
|
3684
5059
|
componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
|
|
3685
5060
|
);
|
|
3686
|
-
const reportId = `page_${
|
|
5061
|
+
const reportId = `page_${createHash5("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
|
|
3687
5062
|
const complete = componentReports.every((report2) => report2.completeness.complete);
|
|
3688
5063
|
const report = Object.freeze({
|
|
3689
5064
|
schemaVersion: DATA_FLOW_SCHEMA_VERSION,
|
|
@@ -3776,6 +5151,9 @@ function parseOrigin(value) {
|
|
|
3776
5151
|
return void 0;
|
|
3777
5152
|
}
|
|
3778
5153
|
}
|
|
5154
|
+
function isSameOriginBrowserGet(request) {
|
|
5155
|
+
return request.method === "GET" && getSingleHeader(request, "sec-fetch-site")?.toLowerCase() === "same-origin" && getSingleHeader(request, "sec-fetch-mode")?.toLowerCase() === "cors" && getSingleHeader(request, "sec-fetch-dest")?.toLowerCase() === "empty";
|
|
5156
|
+
}
|
|
3779
5157
|
function assertRequestAuthorized(request, options) {
|
|
3780
5158
|
const actualToken = getSingleHeader(request, SPOTPATCH_TOKEN_HEADER);
|
|
3781
5159
|
if (!tokensMatch2(actualToken, options.sessionToken)) {
|
|
@@ -3785,10 +5163,16 @@ function assertRequestAuthorized(request, options) {
|
|
|
3785
5163
|
const originHeader = getSingleHeader(request, "origin");
|
|
3786
5164
|
const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
|
|
3787
5165
|
const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
|
|
3788
|
-
if (host === void 0
|
|
5166
|
+
if (host === void 0) {
|
|
3789
5167
|
throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
|
|
3790
5168
|
}
|
|
3791
5169
|
const hostIsLoopback = isLoopbackHostname(host.hostname);
|
|
5170
|
+
if (origin === void 0) {
|
|
5171
|
+
if (!isSameOriginBrowserGet(request) || !options.allowLan && !hostIsLoopback) {
|
|
5172
|
+
throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
|
|
5173
|
+
}
|
|
5174
|
+
return;
|
|
5175
|
+
}
|
|
3792
5176
|
const originIsLoopback = isLoopbackHostname(origin.hostname);
|
|
3793
5177
|
if (!options.allowLan) {
|
|
3794
5178
|
if (!hostIsLoopback || !originIsLoopback) {
|
|
@@ -3861,7 +5245,7 @@ async function readRuntimeBootstrap(request, options) {
|
|
|
3861
5245
|
}
|
|
3862
5246
|
|
|
3863
5247
|
// src/server/middleware.ts
|
|
3864
|
-
var
|
|
5248
|
+
var STATUS_BY_ERROR2 = Object.freeze({
|
|
3865
5249
|
[ERROR_CODES16.INVALID_REQUEST]: 400,
|
|
3866
5250
|
[ERROR_CODES16.INVALID_TOKEN]: 401,
|
|
3867
5251
|
[ERROR_CODES16.ORIGIN_NOT_ALLOWED]: 403,
|
|
@@ -3917,7 +5301,7 @@ var STATUS_BY_ERROR = Object.freeze({
|
|
|
3917
5301
|
[ERROR_CODES16.APPLY_CONFLICT]: 409,
|
|
3918
5302
|
[ERROR_CODES16.INTERNAL_ERROR]: 500
|
|
3919
5303
|
});
|
|
3920
|
-
var
|
|
5304
|
+
var PUBLIC_MESSAGES2 = Object.freeze({
|
|
3921
5305
|
[ERROR_CODES16.INVALID_REQUEST]: "The request is invalid.",
|
|
3922
5306
|
[ERROR_CODES16.INVALID_TOKEN]: "The session token is invalid.",
|
|
3923
5307
|
[ERROR_CODES16.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
|
|
@@ -3973,7 +5357,7 @@ var PUBLIC_MESSAGES = Object.freeze({
|
|
|
3973
5357
|
[ERROR_CODES16.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
|
|
3974
5358
|
[ERROR_CODES16.INTERNAL_ERROR]: "The request could not be completed."
|
|
3975
5359
|
});
|
|
3976
|
-
function
|
|
5360
|
+
function writeJson3(response, status, payload) {
|
|
3977
5361
|
response.statusCode = status;
|
|
3978
5362
|
response.setHeader("Cache-Control", "no-store");
|
|
3979
5363
|
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
@@ -3982,16 +5366,16 @@ function writeJson2(response, status, payload) {
|
|
|
3982
5366
|
function asSpotPatchError(error) {
|
|
3983
5367
|
return error instanceof SpotPatchError16 ? error : new SpotPatchError16(ERROR_CODES16.INTERNAL_ERROR, void 0, { cause: error });
|
|
3984
5368
|
}
|
|
3985
|
-
function
|
|
5369
|
+
function writeError2(response, error, logger) {
|
|
3986
5370
|
const normalized = asSpotPatchError(error);
|
|
3987
5371
|
if (normalized.code === ERROR_CODES16.INTERNAL_ERROR) {
|
|
3988
5372
|
logger?.warn("[spotpatch:server] Internal request failure.");
|
|
3989
5373
|
}
|
|
3990
|
-
|
|
5374
|
+
writeJson3(response, STATUS_BY_ERROR2[normalized.code], {
|
|
3991
5375
|
ok: false,
|
|
3992
5376
|
error: {
|
|
3993
5377
|
code: normalized.code,
|
|
3994
|
-
message:
|
|
5378
|
+
message: PUBLIC_MESSAGES2[normalized.code]
|
|
3995
5379
|
}
|
|
3996
5380
|
});
|
|
3997
5381
|
}
|
|
@@ -4047,44 +5431,55 @@ function createSpotPatchMiddleware(options) {
|
|
|
4047
5431
|
const dataFlowAnalyzer = createDataFlowAnalyzer(options);
|
|
4048
5432
|
const externalAgentController = options.externalAgentControl === void 0 ? void 0 : createExternalAgentBrowserController(options.externalAgentControl);
|
|
4049
5433
|
const middleware = (request, response, next) => {
|
|
4050
|
-
const
|
|
4051
|
-
const agentRoute = matchAgentRequestPath(
|
|
4052
|
-
const
|
|
4053
|
-
const
|
|
4054
|
-
|
|
5434
|
+
const path10 = requestPath(request);
|
|
5435
|
+
const agentRoute = matchAgentRequestPath(path10);
|
|
5436
|
+
const askRoute = matchContextualAskPath(path10);
|
|
5437
|
+
const externalAgentRoute = matchExternalAgentBrowserPath(path10);
|
|
5438
|
+
const externalHandoffRoute = matchExternalHandoffBrowserPath(path10);
|
|
5439
|
+
if (path10 !== SPOTPATCH_ENDPOINTS5.sourceContext && path10 !== SPOTPATCH_ENDPOINTS5.openEditor && path10 !== SPOTPATCH_ENDPOINTS5.dataFlowComponentReport && path10 !== SPOTPATCH_ENDPOINTS5.dataFlowPageReport && agentRoute === void 0 && askRoute === void 0 && externalAgentRoute === void 0 && externalHandoffRoute === void 0 && !path10.startsWith(`${SPOTPATCH_API_BASE}/`)) {
|
|
4055
5440
|
next();
|
|
4056
5441
|
return;
|
|
4057
5442
|
}
|
|
4058
5443
|
const handle = async () => {
|
|
4059
|
-
if (
|
|
5444
|
+
if (path10 === SPOTPATCH_ENDPOINTS5.bootstrap && bootstrap !== void 0) {
|
|
4060
5445
|
const data = await readRuntimeBootstrap(
|
|
4061
5446
|
request,
|
|
4062
5447
|
bootstrap
|
|
4063
5448
|
);
|
|
4064
|
-
|
|
5449
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4065
5450
|
return;
|
|
4066
5451
|
}
|
|
4067
5452
|
assertRequestAuthorized(request, {
|
|
4068
5453
|
allowLan: options.options.allowLan,
|
|
4069
5454
|
sessionToken: options.session.token
|
|
4070
5455
|
});
|
|
4071
|
-
if (
|
|
5456
|
+
if (askRoute !== void 0) {
|
|
5457
|
+
await handleContextualAskRequest(
|
|
5458
|
+
request,
|
|
5459
|
+
response,
|
|
5460
|
+
askRoute,
|
|
5461
|
+
options.contextualAskManager,
|
|
5462
|
+
options.logger
|
|
5463
|
+
);
|
|
5464
|
+
return;
|
|
5465
|
+
}
|
|
5466
|
+
if (path10 === SPOTPATCH_ENDPOINTS5.sourceContext) {
|
|
4072
5467
|
if (request.method !== "POST") {
|
|
4073
5468
|
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
4074
5469
|
}
|
|
4075
5470
|
const data = await handleSourceContext(request, options);
|
|
4076
|
-
|
|
5471
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4077
5472
|
return;
|
|
4078
5473
|
}
|
|
4079
|
-
if (
|
|
5474
|
+
if (path10 === SPOTPATCH_ENDPOINTS5.openEditor) {
|
|
4080
5475
|
if (request.method !== "POST") {
|
|
4081
5476
|
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
4082
5477
|
}
|
|
4083
5478
|
const data = await handleOpenEditor(request, options);
|
|
4084
|
-
|
|
5479
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4085
5480
|
return;
|
|
4086
5481
|
}
|
|
4087
|
-
if (
|
|
5482
|
+
if (path10 === SPOTPATCH_ENDPOINTS5.dataFlowComponentReport) {
|
|
4088
5483
|
if (request.method !== "POST") {
|
|
4089
5484
|
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
4090
5485
|
}
|
|
@@ -4093,15 +5488,15 @@ function createSpotPatchMiddleware(options) {
|
|
|
4093
5488
|
dataFlowAnalyzer,
|
|
4094
5489
|
options
|
|
4095
5490
|
);
|
|
4096
|
-
|
|
5491
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4097
5492
|
return;
|
|
4098
5493
|
}
|
|
4099
|
-
if (
|
|
5494
|
+
if (path10 === SPOTPATCH_ENDPOINTS5.dataFlowPageReport) {
|
|
4100
5495
|
if (request.method !== "POST") {
|
|
4101
5496
|
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
4102
5497
|
}
|
|
4103
5498
|
const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
|
|
4104
|
-
|
|
5499
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4105
5500
|
return;
|
|
4106
5501
|
}
|
|
4107
5502
|
if (externalAgentRoute !== void 0) {
|
|
@@ -4113,7 +5508,7 @@ function createSpotPatchMiddleware(options) {
|
|
|
4113
5508
|
response,
|
|
4114
5509
|
externalAgentRoute,
|
|
4115
5510
|
(target, status, data) => {
|
|
4116
|
-
|
|
5511
|
+
writeJson3(target, status, { ok: true, data });
|
|
4117
5512
|
}
|
|
4118
5513
|
);
|
|
4119
5514
|
return;
|
|
@@ -4130,7 +5525,7 @@ function createSpotPatchMiddleware(options) {
|
|
|
4130
5525
|
...options.externalHandoffService === void 0 ? {} : { service: options.externalHandoffService }
|
|
4131
5526
|
},
|
|
4132
5527
|
(target, status, data) => {
|
|
4133
|
-
|
|
5528
|
+
writeJson3(target, status, { ok: true, data });
|
|
4134
5529
|
}
|
|
4135
5530
|
);
|
|
4136
5531
|
return;
|
|
@@ -4144,12 +5539,12 @@ function createSpotPatchMiddleware(options) {
|
|
|
4144
5539
|
options,
|
|
4145
5540
|
agentRoute,
|
|
4146
5541
|
(target, status, data) => {
|
|
4147
|
-
|
|
5542
|
+
writeJson3(target, status, { ok: true, data });
|
|
4148
5543
|
}
|
|
4149
5544
|
);
|
|
4150
5545
|
};
|
|
4151
5546
|
void handle().catch((error) => {
|
|
4152
|
-
|
|
5547
|
+
writeError2(response, error, options.logger);
|
|
4153
5548
|
});
|
|
4154
5549
|
};
|
|
4155
5550
|
return Object.assign(middleware, {
|
|
@@ -4161,17 +5556,33 @@ function createSpotPatchMiddleware(options) {
|
|
|
4161
5556
|
|
|
4162
5557
|
// src/server/source-registration.ts
|
|
4163
5558
|
import { timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
4164
|
-
import { lstat as
|
|
4165
|
-
import
|
|
4166
|
-
import {
|
|
5559
|
+
import { lstat as lstat5, readFile as readFile4, realpath as realpath7 } from "fs/promises";
|
|
5560
|
+
import path9 from "path";
|
|
5561
|
+
import {
|
|
5562
|
+
createDataFlowSourceFilter,
|
|
5563
|
+
createDataFlowSourceVersion,
|
|
5564
|
+
createSourceFilter
|
|
5565
|
+
} from "@spotpatch/compiler";
|
|
5566
|
+
import { DEFAULT_DATA_FLOW_LIMITS as DEFAULT_DATA_FLOW_LIMITS2 } from "@spotpatch/shared";
|
|
4167
5567
|
import { z as z2 } from "zod";
|
|
4168
|
-
var REGISTRATION_BODY_LIMIT_BYTES =
|
|
5568
|
+
var REGISTRATION_BODY_LIMIT_BYTES = DEFAULT_DATA_FLOW_LIMITS2.protocolRequestMaxBytes;
|
|
4169
5569
|
var REGISTRATION_IDENTITY_PATTERN = /^[A-Za-z0-9_-]{16,128}$/u;
|
|
5570
|
+
var DATA_FLOW_IDENTITY_PATTERN = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
4170
5571
|
var INTERNAL_SECRET_HEADER = "x-spotpatch-internal";
|
|
4171
5572
|
var FORBIDDEN_SOURCE_SEGMENTS = /* @__PURE__ */ new Set([".next", "node_modules"]);
|
|
4172
5573
|
var registrationRequestSchema = z2.strictObject({
|
|
4173
5574
|
epoch: z2.string().regex(REGISTRATION_IDENTITY_PATTERN),
|
|
4174
|
-
resourcePath: z2.string().min(1).max(3072)
|
|
5575
|
+
resourcePath: z2.string().min(1).max(3072),
|
|
5576
|
+
dataFlow: z2.strictObject({
|
|
5577
|
+
sourceVersion: z2.string().regex(DATA_FLOW_IDENTITY_PATTERN),
|
|
5578
|
+
components: z2.array(
|
|
5579
|
+
z2.strictObject({
|
|
5580
|
+
componentSourceId: z2.string().regex(DATA_FLOW_IDENTITY_PATTERN),
|
|
5581
|
+
line: z2.number().int().positive().max(1e7),
|
|
5582
|
+
column: z2.number().int().positive().max(1e7)
|
|
5583
|
+
})
|
|
5584
|
+
).max(DEFAULT_DATA_FLOW_LIMITS2.sourceMaxComponents)
|
|
5585
|
+
}).optional()
|
|
4175
5586
|
});
|
|
4176
5587
|
function getSingleHeader3(request, name) {
|
|
4177
5588
|
const value = request.headers[name.toLowerCase()];
|
|
@@ -4186,13 +5597,13 @@ function identitiesMatch(actual, expected) {
|
|
|
4186
5597
|
return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual3(actualBytes, expectedBytes);
|
|
4187
5598
|
}
|
|
4188
5599
|
function isWithinRoot(root, candidate) {
|
|
4189
|
-
const relative =
|
|
4190
|
-
return relative === "" || !relative.startsWith(`..${
|
|
5600
|
+
const relative = path9.relative(root, candidate);
|
|
5601
|
+
return relative === "" || !relative.startsWith(`..${path9.sep}`) && relative !== ".." && !path9.isAbsolute(relative);
|
|
4191
5602
|
}
|
|
4192
5603
|
function hasForbiddenSegment(root, candidate) {
|
|
4193
|
-
return
|
|
5604
|
+
return path9.relative(root, candidate).split(path9.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
|
|
4194
5605
|
}
|
|
4195
|
-
function
|
|
5606
|
+
function writeJson4(response, statusCode, payload) {
|
|
4196
5607
|
const body = JSON.stringify(payload);
|
|
4197
5608
|
response.statusCode = statusCode;
|
|
4198
5609
|
response.setHeader("Cache-Control", "no-store");
|
|
@@ -4212,15 +5623,15 @@ function requestComesFromLoopbackWorker(request) {
|
|
|
4212
5623
|
}
|
|
4213
5624
|
}
|
|
4214
5625
|
async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
|
|
4215
|
-
if (!
|
|
5626
|
+
if (!path9.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
|
|
4216
5627
|
return void 0;
|
|
4217
5628
|
}
|
|
4218
5629
|
try {
|
|
4219
|
-
const sourceStat = await
|
|
5630
|
+
const sourceStat = await lstat5(requestedPath);
|
|
4220
5631
|
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
4221
5632
|
return void 0;
|
|
4222
5633
|
}
|
|
4223
|
-
const resolvedPath = await
|
|
5634
|
+
const resolvedPath = await realpath7(requestedPath);
|
|
4224
5635
|
if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
|
|
4225
5636
|
return void 0;
|
|
4226
5637
|
}
|
|
@@ -4233,8 +5644,35 @@ async function createSourceRegistrationService(input) {
|
|
|
4233
5644
|
if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
|
|
4234
5645
|
throw new TypeError("The source registration identity is invalid.");
|
|
4235
5646
|
}
|
|
4236
|
-
const root = await
|
|
5647
|
+
const root = await realpath7(input.root);
|
|
4237
5648
|
const sourceFilter = createSourceFilter(root, input.options);
|
|
5649
|
+
const dataFlowFilter = createDataFlowSourceFilter(root, input.options);
|
|
5650
|
+
const registrationQueues = /* @__PURE__ */ new Map();
|
|
5651
|
+
async function registerCurrentSource(sourcePath, dataFlow) {
|
|
5652
|
+
if (dataFlow === void 0) {
|
|
5653
|
+
return input.registry.register(sourcePath);
|
|
5654
|
+
}
|
|
5655
|
+
let fileId;
|
|
5656
|
+
const previous = registrationQueues.get(sourcePath) ?? Promise.resolve();
|
|
5657
|
+
const current = previous.catch(() => void 0).then(async () => {
|
|
5658
|
+
const currentSource = await readFile4(sourcePath, "utf8");
|
|
5659
|
+
if (createDataFlowSourceVersion(currentSource) !== dataFlow.sourceVersion) {
|
|
5660
|
+
return;
|
|
5661
|
+
}
|
|
5662
|
+
fileId = input.registry.registerDataFlowComponents(
|
|
5663
|
+
sourcePath,
|
|
5664
|
+
dataFlow.sourceVersion,
|
|
5665
|
+
dataFlow.components
|
|
5666
|
+
);
|
|
5667
|
+
});
|
|
5668
|
+
registrationQueues.set(sourcePath, current);
|
|
5669
|
+
await current.finally(() => {
|
|
5670
|
+
if (registrationQueues.get(sourcePath) === current) {
|
|
5671
|
+
registrationQueues.delete(sourcePath);
|
|
5672
|
+
}
|
|
5673
|
+
});
|
|
5674
|
+
return fileId;
|
|
5675
|
+
}
|
|
4238
5676
|
const handler = (request, response) => {
|
|
4239
5677
|
const handle = async () => {
|
|
4240
5678
|
const contentType = getSingleHeader3(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
@@ -4242,33 +5680,35 @@ async function createSourceRegistrationService(input) {
|
|
|
4242
5680
|
getSingleHeader3(request, INTERNAL_SECRET_HEADER),
|
|
4243
5681
|
input.internalSecret
|
|
4244
5682
|
)) {
|
|
4245
|
-
|
|
5683
|
+
writeJson4(response, 403, { ok: false });
|
|
4246
5684
|
return;
|
|
4247
5685
|
}
|
|
4248
5686
|
const parsed = registrationRequestSchema.safeParse(
|
|
4249
5687
|
await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
|
|
4250
5688
|
);
|
|
4251
5689
|
if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
|
|
4252
|
-
|
|
5690
|
+
writeJson4(response, 400, { ok: false });
|
|
4253
5691
|
return;
|
|
4254
5692
|
}
|
|
4255
5693
|
const sourcePath = await resolveAuthorizedSource(
|
|
4256
5694
|
root,
|
|
4257
5695
|
parsed.data.resourcePath,
|
|
4258
|
-
(absolutePath) => sourceFilter.shouldTransform(absolutePath, "<")
|
|
5696
|
+
(absolutePath) => parsed.data.dataFlow === void 0 ? sourceFilter.shouldTransform(absolutePath, "<") : dataFlowFilter.shouldTransform(absolutePath, "")
|
|
4259
5697
|
);
|
|
4260
5698
|
if (sourcePath === void 0) {
|
|
4261
|
-
|
|
5699
|
+
writeJson4(response, 403, { ok: false });
|
|
4262
5700
|
return;
|
|
4263
5701
|
}
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
5702
|
+
const fileId = await registerCurrentSource(sourcePath, parsed.data.dataFlow);
|
|
5703
|
+
if (fileId === void 0) {
|
|
5704
|
+
writeJson4(response, 409, { ok: false });
|
|
5705
|
+
return;
|
|
5706
|
+
}
|
|
5707
|
+
writeJson4(response, 200, { epoch: input.registryEpoch, fileId });
|
|
4268
5708
|
};
|
|
4269
5709
|
void handle().catch(() => {
|
|
4270
5710
|
if (!response.headersSent) {
|
|
4271
|
-
|
|
5711
|
+
writeJson4(response, 400, { ok: false });
|
|
4272
5712
|
} else {
|
|
4273
5713
|
response.destroy();
|
|
4274
5714
|
}
|
|
@@ -4278,11 +5718,11 @@ async function createSourceRegistrationService(input) {
|
|
|
4278
5718
|
}
|
|
4279
5719
|
|
|
4280
5720
|
// src/session/session.ts
|
|
4281
|
-
import { randomBytes as
|
|
5721
|
+
import { randomBytes as randomBytes10 } from "crypto";
|
|
4282
5722
|
function createSession() {
|
|
4283
5723
|
return Object.freeze({
|
|
4284
|
-
id:
|
|
4285
|
-
token:
|
|
5724
|
+
id: randomBytes10(16).toString("base64url"),
|
|
5725
|
+
token: randomBytes10(16).toString("base64url")
|
|
4286
5726
|
});
|
|
4287
5727
|
}
|
|
4288
5728
|
|
|
@@ -4293,6 +5733,7 @@ var OPTION_KEYS = Object.freeze([
|
|
|
4293
5733
|
"budget",
|
|
4294
5734
|
"debug",
|
|
4295
5735
|
"dataFlow",
|
|
5736
|
+
"contextualAsk",
|
|
4296
5737
|
"editor",
|
|
4297
5738
|
"enabled",
|
|
4298
5739
|
"externalAgent",
|
|
@@ -4384,6 +5825,9 @@ function serializeResolvedSpotPatchOptions(options) {
|
|
|
4384
5825
|
dataFlow: options.dataFlow.enabled ? Object.freeze({
|
|
4385
5826
|
runtime: options.dataFlow.runtime
|
|
4386
5827
|
}) : false,
|
|
5828
|
+
contextualAsk: options.contextualAsk.enabled ? Object.freeze({
|
|
5829
|
+
...options.contextualAsk.defaultExecutor === void 0 ? {} : { defaultExecutor: options.contextualAsk.defaultExecutor }
|
|
5830
|
+
}) : false,
|
|
4387
5831
|
editor: options.editor,
|
|
4388
5832
|
enabled: options.enabled,
|
|
4389
5833
|
externalAgent: options.externalAgent.enabled,
|
|
@@ -4436,6 +5880,33 @@ function parseDataFlow(value) {
|
|
|
4436
5880
|
runtime: value.runtime
|
|
4437
5881
|
});
|
|
4438
5882
|
}
|
|
5883
|
+
function parseContextualAsk(value) {
|
|
5884
|
+
if (value === false || value === true) return value;
|
|
5885
|
+
if (!isRecord2(value)) {
|
|
5886
|
+
throw new TypeError("The SpotPatch contextual Ask transport is invalid.");
|
|
5887
|
+
}
|
|
5888
|
+
const keys = Object.keys(value);
|
|
5889
|
+
if (keys.length === 0) return Object.freeze({});
|
|
5890
|
+
if (!hasExactKeys(value, ["defaultExecutor"]) || !isRecord2(value.defaultExecutor)) {
|
|
5891
|
+
throw new TypeError("The SpotPatch contextual Ask transport is invalid.");
|
|
5892
|
+
}
|
|
5893
|
+
const preference = value.defaultExecutor;
|
|
5894
|
+
if (preference.kind === "managed-codex" && hasExactKeys(preference, ["kind"])) {
|
|
5895
|
+
return Object.freeze({
|
|
5896
|
+
defaultExecutor: Object.freeze({ kind: "managed-codex" })
|
|
5897
|
+
});
|
|
5898
|
+
}
|
|
5899
|
+
if (preference.kind === "configured-key" && hasExactKeys(preference, ["kind", "modelProfileId", "providerProfileId"]) && typeof preference.providerProfileId === "string" && typeof preference.modelProfileId === "string") {
|
|
5900
|
+
return Object.freeze({
|
|
5901
|
+
defaultExecutor: Object.freeze({
|
|
5902
|
+
kind: "configured-key",
|
|
5903
|
+
providerProfileId: preference.providerProfileId,
|
|
5904
|
+
modelProfileId: preference.modelProfileId
|
|
5905
|
+
})
|
|
5906
|
+
});
|
|
5907
|
+
}
|
|
5908
|
+
throw new TypeError("The SpotPatch contextual Ask transport is invalid.");
|
|
5909
|
+
}
|
|
4439
5910
|
function parseSerializedSpotPatchOptions(value) {
|
|
4440
5911
|
if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
|
|
4441
5912
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
@@ -4450,6 +5921,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
4450
5921
|
budget: parseBudget(value.budget),
|
|
4451
5922
|
debug: value.debug,
|
|
4452
5923
|
dataFlow: parseDataFlow(value.dataFlow),
|
|
5924
|
+
contextualAsk: parseContextualAsk(value.contextualAsk),
|
|
4453
5925
|
editor: value.editor,
|
|
4454
5926
|
enabled: value.enabled,
|
|
4455
5927
|
externalAgent: value.externalAgent,
|
|
@@ -4467,10 +5939,15 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
4467
5939
|
}
|
|
4468
5940
|
}
|
|
4469
5941
|
export {
|
|
5942
|
+
ContextualAskError,
|
|
4470
5943
|
DEFAULT_EXCLUDE,
|
|
4471
5944
|
DEFAULT_OPTIONS,
|
|
4472
5945
|
applyIntegrationPlan,
|
|
5946
|
+
captureAskReadSnapshot,
|
|
5947
|
+
composeContextualAskExecutors,
|
|
4473
5948
|
createAgentJobManager,
|
|
5949
|
+
createConfiguredKeyAskExecutors,
|
|
5950
|
+
createContextualAskManager,
|
|
4474
5951
|
createExternalHandoffService,
|
|
4475
5952
|
createIntegrationFileChange,
|
|
4476
5953
|
createRuntimeAiConfig,
|
|
@@ -4479,9 +5956,12 @@ export {
|
|
|
4479
5956
|
createSourceRegistrationService,
|
|
4480
5957
|
createSourceRegistry,
|
|
4481
5958
|
createSpotPatchMiddleware,
|
|
5959
|
+
createWorkspaceActivityCoordinator,
|
|
4482
5960
|
discoverProjectValidationCheck,
|
|
5961
|
+
handleContextualAskRequest,
|
|
4483
5962
|
integrationPathExists,
|
|
4484
5963
|
isLoopbackHostname,
|
|
5964
|
+
matchContextualAskPath,
|
|
4485
5965
|
parseSerializedSpotPatchOptions,
|
|
4486
5966
|
readIntegrationFile,
|
|
4487
5967
|
readJsonRequestBody,
|