agentweaver 0.1.18 → 0.1.20
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/README.md +54 -6
- package/dist/artifacts.js +9 -0
- package/dist/executors/git-commit-executor.js +24 -6
- package/dist/flow-state.js +3 -8
- package/dist/git/git-diff-parser.js +223 -0
- package/dist/git/git-service.js +562 -0
- package/dist/git/git-stage-selection.js +24 -0
- package/dist/git/git-status-parser.js +171 -0
- package/dist/git/git-types.js +1 -0
- package/dist/index.js +454 -108
- package/dist/interactive/auto-flow.js +644 -0
- package/dist/interactive/controller.js +489 -7
- package/dist/interactive/progress.js +194 -1
- package/dist/interactive/state.js +34 -0
- package/dist/interactive/web/index.js +237 -5
- package/dist/interactive/web/protocol.js +222 -1
- package/dist/interactive/web/server.js +497 -3
- package/dist/interactive/web/static/app.js +2462 -37
- package/dist/interactive/web/static/index.html +113 -11
- package/dist/interactive/web/static/styles.css +1 -1
- package/dist/interactive/web/static/styles.input.css +1383 -149
- package/dist/pipeline/auto-flow-blocks.js +307 -0
- package/dist/pipeline/auto-flow-config.js +273 -0
- package/dist/pipeline/auto-flow-identity.js +49 -0
- package/dist/pipeline/auto-flow-presets.js +52 -0
- package/dist/pipeline/auto-flow-resolver.js +830 -0
- package/dist/pipeline/auto-flow-types.js +17 -0
- package/dist/pipeline/context.js +1 -0
- package/dist/pipeline/declarative-flows.js +27 -1
- package/dist/pipeline/flow-specs/auto-common-guided.json +11 -0
- package/dist/pipeline/flow-specs/auto-golang.json +12 -1
- package/dist/pipeline/flow-specs/bugz/bug-analyze.json +54 -1
- package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +19 -1
- package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +33 -1
- package/dist/pipeline/flow-specs/review/review-project.json +19 -1
- package/dist/pipeline/flow-specs/task-source/manual-jira-input.json +70 -0
- package/dist/pipeline/node-registry.js +9 -0
- package/dist/pipeline/nodes/codex-prompt-node.js +8 -1
- package/dist/pipeline/nodes/flow-run-node.js +5 -3
- package/dist/pipeline/nodes/git-status-node.js +2 -168
- package/dist/pipeline/nodes/manual-jira-task-input-node.js +146 -0
- package/dist/pipeline/nodes/opencode-prompt-node.js +8 -1
- package/dist/pipeline/nodes/plan-codex-node.js +8 -1
- package/dist/pipeline/spec-loader.js +14 -4
- package/dist/runtime/artifact-catalog.js +403 -0
- package/dist/runtime/settings.js +114 -0
- package/dist/scope.js +14 -4
- package/package.json +1 -1
- package/dist/pipeline/flow-specs/auto-common.json +0 -179
- package/dist/pipeline/flow-specs/auto-simple.json +0 -141
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
3
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, realpathSync, statSync } from "node:fs";
|
|
4
4
|
import http from "node:http";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { parseArtifactReference } from "../../artifact-manifest.js";
|
|
9
|
+
import { scopeArtifactsDir, scopeWorkspaceDir } from "../../artifacts.js";
|
|
10
|
+
import { GitDiffError } from "../../git/git-service.js";
|
|
11
|
+
import { groupArtifactCatalog, inferArtifactRenderKind, inferArtifactRole, inferArtifactTitle, } from "../../runtime/artifact-catalog.js";
|
|
12
|
+
import { createArtifactRegistry } from "../../runtime/artifact-registry.js";
|
|
8
13
|
import { parseClientAction } from "./protocol.js";
|
|
9
14
|
const STATIC_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
15
|
+
const MAX_PREVIEW_SIZE = 512 * 1024;
|
|
16
|
+
const MAX_JSON_PARSE_SIZE = 2 * 1024 * 1024;
|
|
10
17
|
const CONTENT_TYPES = new Map([
|
|
11
18
|
[".html", "text/html; charset=utf-8"],
|
|
12
19
|
[".css", "text/css; charset=utf-8"],
|
|
@@ -15,6 +22,27 @@ const CONTENT_TYPES = new Map([
|
|
|
15
22
|
[".svg", "image/svg+xml; charset=utf-8"],
|
|
16
23
|
]);
|
|
17
24
|
const BASIC_AUTH_REALM = "AgentWeaver Web UI";
|
|
25
|
+
const ARTIFACT_API_PREFIX = "/__agentweaver/api/artifacts";
|
|
26
|
+
const GIT_DIFF_API_PATH = "/__agentweaver/api/git/diff";
|
|
27
|
+
const GIT_DIFF_ERROR_STATUSES = {
|
|
28
|
+
missing_path: 400,
|
|
29
|
+
invalid_path: 403,
|
|
30
|
+
invalid_mode: 400,
|
|
31
|
+
missing_provider: 503,
|
|
32
|
+
repository_unavailable: 503,
|
|
33
|
+
forbidden_path: 403,
|
|
34
|
+
read_failed: 500,
|
|
35
|
+
git_failed: 500,
|
|
36
|
+
};
|
|
37
|
+
const ARTIFACT_ERROR_STATUSES = {
|
|
38
|
+
invalid_id: 400,
|
|
39
|
+
missing_scope: 400,
|
|
40
|
+
scope_mismatch: 403,
|
|
41
|
+
forbidden_path: 403,
|
|
42
|
+
not_found: 404,
|
|
43
|
+
unsupported_preview: 415,
|
|
44
|
+
read_failed: 500,
|
|
45
|
+
};
|
|
18
46
|
function hashCredential(value) {
|
|
19
47
|
return createHash("sha256").update(value, "utf8").digest();
|
|
20
48
|
}
|
|
@@ -106,6 +134,448 @@ function serveStaticAsset(request, response) {
|
|
|
106
134
|
response.end(readFileSync(assetPath));
|
|
107
135
|
return true;
|
|
108
136
|
}
|
|
137
|
+
function writeJson(response, statusCode, value) {
|
|
138
|
+
response.writeHead(statusCode, {
|
|
139
|
+
"content-type": "application/json; charset=utf-8",
|
|
140
|
+
"cache-control": "no-store",
|
|
141
|
+
});
|
|
142
|
+
response.end(JSON.stringify(value));
|
|
143
|
+
}
|
|
144
|
+
function writeArtifactApiError(response, code, message, artifact) {
|
|
145
|
+
writeJson(response, ARTIFACT_ERROR_STATUSES[code], {
|
|
146
|
+
code,
|
|
147
|
+
message,
|
|
148
|
+
...(artifact ? { artifact } : {}),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function writeGitDiffApiError(response, code, message) {
|
|
152
|
+
writeJson(response, GIT_DIFF_ERROR_STATUSES[code], { code, message });
|
|
153
|
+
}
|
|
154
|
+
function safeArtifactMetadata(item) {
|
|
155
|
+
return {
|
|
156
|
+
id: item.id,
|
|
157
|
+
scopeKey: item.scopeKey,
|
|
158
|
+
runId: item.runId,
|
|
159
|
+
logicalKey: item.logicalKey,
|
|
160
|
+
title: item.title,
|
|
161
|
+
relativePath: item.relativePath,
|
|
162
|
+
kind: item.kind,
|
|
163
|
+
role: item.role,
|
|
164
|
+
phaseId: item.phaseId,
|
|
165
|
+
stepId: item.stepId,
|
|
166
|
+
schemaId: item.schemaId,
|
|
167
|
+
sizeBytes: item.sizeBytes,
|
|
168
|
+
updatedAt: item.updatedAt,
|
|
169
|
+
isLatest: item.isLatest,
|
|
170
|
+
source: item.source,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function normalizePathSeparators(value) {
|
|
174
|
+
return value.replace(/\\/g, "/");
|
|
175
|
+
}
|
|
176
|
+
function isInsideDirectory(parent, candidate) {
|
|
177
|
+
const relative = path.relative(parent, candidate);
|
|
178
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
179
|
+
}
|
|
180
|
+
function safeDecodeURIComponent(value) {
|
|
181
|
+
try {
|
|
182
|
+
return decodeURIComponent(value);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function parseArtifactApiRoute(requestUrl) {
|
|
189
|
+
const parsed = new URL(requestUrl ?? "/", "http://agentweaver.local");
|
|
190
|
+
const scopeKey = parsed.searchParams.get("scope");
|
|
191
|
+
const runIds = parsed.searchParams.getAll("runId").filter((value, index, values) => (value.length > 0 && values.indexOf(value) === index));
|
|
192
|
+
if (parsed.pathname === ARTIFACT_API_PREFIX) {
|
|
193
|
+
return { kind: "list", scopeKey, runIds };
|
|
194
|
+
}
|
|
195
|
+
if (!parsed.pathname.startsWith(`${ARTIFACT_API_PREFIX}/`)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const suffix = parsed.pathname.slice(`${ARTIFACT_API_PREFIX}/`.length);
|
|
199
|
+
for (const action of ["preview", "raw", "download"]) {
|
|
200
|
+
const actionSuffix = `/${action}`;
|
|
201
|
+
if (!suffix.endsWith(actionSuffix)) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const encodedId = suffix.slice(0, -actionSuffix.length);
|
|
205
|
+
const artifactId = safeDecodeURIComponent(encodedId);
|
|
206
|
+
if (!artifactId || artifactId.includes("\0")) {
|
|
207
|
+
return { kind: "content", artifactId: "", action, scopeKey, runIds };
|
|
208
|
+
}
|
|
209
|
+
return { kind: "content", artifactId, action, scopeKey, runIds };
|
|
210
|
+
}
|
|
211
|
+
return { kind: "content", artifactId: "", action: "preview", scopeKey, runIds };
|
|
212
|
+
}
|
|
213
|
+
function filterCatalog(catalog, scopeKey) {
|
|
214
|
+
const markdownItems = catalog.items.filter((item) => item.scopeKey === scopeKey && item.kind === "markdown");
|
|
215
|
+
const sortedItems = markdownItems.slice();
|
|
216
|
+
return {
|
|
217
|
+
scopeKey,
|
|
218
|
+
items: sortedItems,
|
|
219
|
+
groups: groupArtifactCatalog(sortedItems),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
async function loadArtifactCatalog(options, input) {
|
|
223
|
+
if (!options.getArtifactCatalog) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return options.getArtifactCatalog(input);
|
|
227
|
+
}
|
|
228
|
+
function scopeRelativePath(scopeKey, payloadPath) {
|
|
229
|
+
const workspaceDir = path.resolve(scopeWorkspaceDir(scopeKey));
|
|
230
|
+
const normalizedPayloadPath = path.resolve(payloadPath);
|
|
231
|
+
if (isInsideDirectory(workspaceDir, normalizedPayloadPath)) {
|
|
232
|
+
return normalizePathSeparators(path.relative(workspaceDir, normalizedPayloadPath));
|
|
233
|
+
}
|
|
234
|
+
return normalizePathSeparators(path.basename(normalizedPayloadPath));
|
|
235
|
+
}
|
|
236
|
+
function itemFromManifest(scopeKey, manifest) {
|
|
237
|
+
const relativePath = scopeRelativePath(scopeKey, manifest.payload_path);
|
|
238
|
+
const kind = inferArtifactRenderKind({
|
|
239
|
+
payloadFamily: manifest.payload_family,
|
|
240
|
+
schemaId: manifest.schema_id,
|
|
241
|
+
filePath: manifest.payload_path,
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
id: manifest.artifact_id,
|
|
245
|
+
scopeKey,
|
|
246
|
+
runId: manifest.run_id,
|
|
247
|
+
logicalKey: manifest.logical_key,
|
|
248
|
+
title: inferArtifactTitle(scopeKey, manifest.logical_key, relativePath),
|
|
249
|
+
relativePath,
|
|
250
|
+
kind,
|
|
251
|
+
role: inferArtifactRole(manifest.logical_key, relativePath),
|
|
252
|
+
phaseId: manifest.phase_id || null,
|
|
253
|
+
stepId: manifest.step_id || null,
|
|
254
|
+
schemaId: manifest.schema_id || null,
|
|
255
|
+
sizeBytes: 0,
|
|
256
|
+
updatedAt: manifest.created_at,
|
|
257
|
+
isLatest: manifest.status === "ready",
|
|
258
|
+
source: "manifest",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function realExistingRoots(scopeKey) {
|
|
262
|
+
const roots = [scopeWorkspaceDir(scopeKey), scopeArtifactsDir(scopeKey)];
|
|
263
|
+
const realRoots = [];
|
|
264
|
+
for (const root of roots) {
|
|
265
|
+
try {
|
|
266
|
+
realRoots.push(realpathSync(root));
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// A missing .artifacts directory should not block regular scope files.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return [...new Set(realRoots)];
|
|
273
|
+
}
|
|
274
|
+
function assertContainedRegularFile(scopeKey, filePath) {
|
|
275
|
+
let realPath;
|
|
276
|
+
try {
|
|
277
|
+
realPath = realpathSync(filePath);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
throw new ArtifactApiResolutionError("not_found");
|
|
281
|
+
}
|
|
282
|
+
const allowedRoots = realExistingRoots(scopeKey);
|
|
283
|
+
if (allowedRoots.length === 0 || !allowedRoots.some((root) => isInsideDirectory(root, realPath))) {
|
|
284
|
+
throw new ArtifactApiResolutionError("forbidden_path");
|
|
285
|
+
}
|
|
286
|
+
let stats;
|
|
287
|
+
try {
|
|
288
|
+
stats = statSync(realPath);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
throw new ArtifactApiResolutionError("not_found");
|
|
292
|
+
}
|
|
293
|
+
if (!stats.isFile()) {
|
|
294
|
+
throw new ArtifactApiResolutionError("forbidden_path");
|
|
295
|
+
}
|
|
296
|
+
return { filePath, realPath, stats };
|
|
297
|
+
}
|
|
298
|
+
class ArtifactApiResolutionError extends Error {
|
|
299
|
+
code;
|
|
300
|
+
constructor(code) {
|
|
301
|
+
super(code);
|
|
302
|
+
this.code = code;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function catalogFilePath(scopeKey, item) {
|
|
306
|
+
if (path.isAbsolute(item.relativePath) || item.relativePath.includes("\0")) {
|
|
307
|
+
throw new ArtifactApiResolutionError("forbidden_path");
|
|
308
|
+
}
|
|
309
|
+
return path.join(scopeWorkspaceDir(scopeKey), item.relativePath);
|
|
310
|
+
}
|
|
311
|
+
function findCatalogItemForReference(catalog, reference) {
|
|
312
|
+
return catalog.items.find((item) => item.id === reference) ?? null;
|
|
313
|
+
}
|
|
314
|
+
function resolveArtifactRequest(catalog, artifactId) {
|
|
315
|
+
if (!artifactId.trim() || artifactId.includes("\0")) {
|
|
316
|
+
throw new ArtifactApiResolutionError("invalid_id");
|
|
317
|
+
}
|
|
318
|
+
const parsedReference = parseArtifactReference(artifactId);
|
|
319
|
+
if (parsedReference) {
|
|
320
|
+
if (parsedReference.kind === "artifact-id" && parsedReference.parsedId.scopeKey !== catalog.scopeKey) {
|
|
321
|
+
throw new ArtifactApiResolutionError("scope_mismatch");
|
|
322
|
+
}
|
|
323
|
+
let manifest;
|
|
324
|
+
try {
|
|
325
|
+
manifest = createArtifactRegistry().resolveArtifact(catalog.scopeKey, artifactId);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
throw new ArtifactApiResolutionError("not_found");
|
|
329
|
+
}
|
|
330
|
+
const item = findCatalogItemForReference(catalog, manifest.artifact_id) ?? itemFromManifest(catalog.scopeKey, manifest);
|
|
331
|
+
const contained = assertContainedRegularFile(catalog.scopeKey, manifest.payload_path);
|
|
332
|
+
return { item: { ...item, sizeBytes: contained.stats.size }, ...contained };
|
|
333
|
+
}
|
|
334
|
+
const item = findCatalogItemForReference(catalog, artifactId);
|
|
335
|
+
if (!item) {
|
|
336
|
+
throw new ArtifactApiResolutionError("invalid_id");
|
|
337
|
+
}
|
|
338
|
+
if (item.scopeKey !== catalog.scopeKey) {
|
|
339
|
+
throw new ArtifactApiResolutionError("scope_mismatch");
|
|
340
|
+
}
|
|
341
|
+
const contained = assertContainedRegularFile(catalog.scopeKey, catalogFilePath(catalog.scopeKey, item));
|
|
342
|
+
return { item: { ...item, sizeBytes: contained.stats.size }, ...contained };
|
|
343
|
+
}
|
|
344
|
+
function readLeadingBytes(filePath, byteLimit) {
|
|
345
|
+
const fd = openSync(filePath, "r");
|
|
346
|
+
try {
|
|
347
|
+
const buffer = Buffer.alloc(byteLimit);
|
|
348
|
+
const loadedBytes = readSync(fd, buffer, 0, byteLimit, 0);
|
|
349
|
+
return { buffer: buffer.subarray(0, loadedBytes), loadedBytes };
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
closeSync(fd);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function contentTypeForArtifactKind(kind) {
|
|
356
|
+
switch (kind) {
|
|
357
|
+
case "markdown":
|
|
358
|
+
return "text/markdown; charset=utf-8";
|
|
359
|
+
case "json":
|
|
360
|
+
return "application/json; charset=utf-8";
|
|
361
|
+
case "text":
|
|
362
|
+
case "diff":
|
|
363
|
+
return "text/plain; charset=utf-8";
|
|
364
|
+
case "binary":
|
|
365
|
+
case "unknown":
|
|
366
|
+
return "application/octet-stream";
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function sanitizeDownloadFilename(value) {
|
|
370
|
+
const normalizedSeparators = normalizePathSeparators(value);
|
|
371
|
+
const basename = path.posix.basename(normalizedSeparators);
|
|
372
|
+
const sanitized = basename
|
|
373
|
+
.replace(/["\r\n\\/]/g, "_")
|
|
374
|
+
.replace(/[\x00-\x1f\x7f-\x9f]/g, "_")
|
|
375
|
+
.replace(/[^\x20-\x7e]/g, "_")
|
|
376
|
+
.replace(/\s+/g, " ")
|
|
377
|
+
.trim()
|
|
378
|
+
.slice(0, 120);
|
|
379
|
+
return sanitized && sanitized !== "." && sanitized !== ".." ? sanitized : "artifact";
|
|
380
|
+
}
|
|
381
|
+
function writeArtifactBytes(response, resolved, attachment) {
|
|
382
|
+
const headers = {
|
|
383
|
+
"content-type": contentTypeForArtifactKind(resolved.item.kind),
|
|
384
|
+
"cache-control": "no-store",
|
|
385
|
+
"x-content-type-options": "nosniff",
|
|
386
|
+
};
|
|
387
|
+
if (attachment) {
|
|
388
|
+
headers["content-disposition"] = `attachment; filename="${sanitizeDownloadFilename(resolved.item.relativePath || path.basename(resolved.realPath))}"`;
|
|
389
|
+
}
|
|
390
|
+
let bytes;
|
|
391
|
+
try {
|
|
392
|
+
bytes = readFileSync(resolved.realPath);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
writeArtifactApiError(response, "read_failed", "Artifact content could not be read.", safeArtifactMetadata(resolved.item));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
response.writeHead(200, headers);
|
|
399
|
+
response.end(bytes);
|
|
400
|
+
}
|
|
401
|
+
function previewJsonArtifact(resolved) {
|
|
402
|
+
if (resolved.stats.size <= MAX_JSON_PARSE_SIZE) {
|
|
403
|
+
const loadedBytes = Number(resolved.stats.size);
|
|
404
|
+
const content = readFileSync(resolved.realPath).toString("utf8");
|
|
405
|
+
const contentBytes = Buffer.from(content, "utf8");
|
|
406
|
+
if (contentBytes.length > MAX_PREVIEW_SIZE) {
|
|
407
|
+
return {
|
|
408
|
+
content: contentBytes.subarray(0, MAX_PREVIEW_SIZE).toString("utf8"),
|
|
409
|
+
loadedBytes,
|
|
410
|
+
truncated: true,
|
|
411
|
+
jsonParseSafe: false,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return { content, loadedBytes, truncated: false, jsonParseSafe: true };
|
|
415
|
+
}
|
|
416
|
+
const { buffer, loadedBytes } = readLeadingBytes(resolved.realPath, MAX_PREVIEW_SIZE);
|
|
417
|
+
return {
|
|
418
|
+
content: buffer.toString("utf8"),
|
|
419
|
+
loadedBytes,
|
|
420
|
+
truncated: true,
|
|
421
|
+
jsonParseSafe: false,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function writeArtifactPreview(response, resolved) {
|
|
425
|
+
if (resolved.item.kind === "binary" || resolved.item.kind === "unknown") {
|
|
426
|
+
writeArtifactApiError(response, "unsupported_preview", "Artifact preview is not supported for this content type.", safeArtifactMetadata(resolved.item));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
const preview = resolved.item.kind === "json"
|
|
431
|
+
? previewJsonArtifact(resolved)
|
|
432
|
+
: (() => {
|
|
433
|
+
const byteLimit = Math.min(Number(resolved.stats.size), MAX_PREVIEW_SIZE);
|
|
434
|
+
const { buffer, loadedBytes } = readLeadingBytes(resolved.realPath, byteLimit);
|
|
435
|
+
return {
|
|
436
|
+
content: buffer.toString("utf8"),
|
|
437
|
+
loadedBytes,
|
|
438
|
+
truncated: resolved.stats.size > loadedBytes,
|
|
439
|
+
};
|
|
440
|
+
})();
|
|
441
|
+
writeJson(response, 200, {
|
|
442
|
+
artifact: safeArtifactMetadata(resolved.item),
|
|
443
|
+
renderKind: resolved.item.kind,
|
|
444
|
+
kind: resolved.item.kind,
|
|
445
|
+
encoding: "utf-8",
|
|
446
|
+
content: preview.content,
|
|
447
|
+
truncated: preview.truncated,
|
|
448
|
+
sizeBytes: resolved.stats.size,
|
|
449
|
+
loadedBytes: preview.loadedBytes,
|
|
450
|
+
...(resolved.item.kind === "json" ? { jsonParseSafe: preview.jsonParseSafe } : {}),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
writeArtifactApiError(response, "read_failed", "Artifact preview could not be read.", safeArtifactMetadata(resolved.item));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function handleArtifactApiRequest(request, response, options, auth) {
|
|
458
|
+
if (request.method !== "GET") {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
const route = parseArtifactApiRoute(request.url);
|
|
462
|
+
if (!route) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
if (!isAuthorized(request, auth)) {
|
|
466
|
+
writeAuthRequired(response);
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
if (route.kind === "list") {
|
|
470
|
+
if (!route.scopeKey) {
|
|
471
|
+
writeArtifactApiError(response, "missing_scope", "A scope query parameter is required.");
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
void loadArtifactCatalog(options, {
|
|
475
|
+
scopeKey: route.scopeKey,
|
|
476
|
+
})
|
|
477
|
+
.then((catalog) => {
|
|
478
|
+
if (!catalog) {
|
|
479
|
+
writeArtifactApiError(response, "not_found", "Artifact catalog provider is not configured.");
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (catalog.scopeKey !== route.scopeKey) {
|
|
483
|
+
writeArtifactApiError(response, "scope_mismatch", "Requested scope does not match the active Web UI scope.");
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
writeJson(response, 200, filterCatalog(catalog, route.scopeKey));
|
|
487
|
+
})
|
|
488
|
+
.catch(() => {
|
|
489
|
+
writeArtifactApiError(response, "read_failed", "Artifact catalog could not be loaded.");
|
|
490
|
+
});
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
if (!route.artifactId) {
|
|
494
|
+
writeArtifactApiError(response, "invalid_id", "Artifact identifier is invalid.");
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
void loadArtifactCatalog(options, {
|
|
498
|
+
...(route.scopeKey ? { scopeKey: route.scopeKey } : {}),
|
|
499
|
+
})
|
|
500
|
+
.then((catalog) => {
|
|
501
|
+
if (!catalog) {
|
|
502
|
+
writeArtifactApiError(response, "not_found", "Artifact catalog provider is not configured.");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (route.scopeKey && catalog.scopeKey !== route.scopeKey) {
|
|
506
|
+
writeArtifactApiError(response, "scope_mismatch", "Requested scope does not match the active Web UI scope.");
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
let resolved;
|
|
510
|
+
try {
|
|
511
|
+
resolved = resolveArtifactRequest(catalog, route.artifactId);
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
const code = error instanceof ArtifactApiResolutionError ? error.code : "read_failed";
|
|
515
|
+
writeArtifactApiError(response, code, code === "invalid_id" ? "Artifact identifier is invalid." : "Artifact could not be resolved.");
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (route.action === "preview") {
|
|
519
|
+
writeArtifactPreview(response, resolved);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
writeArtifactBytes(response, resolved, route.action === "download");
|
|
523
|
+
})
|
|
524
|
+
.catch(() => {
|
|
525
|
+
writeArtifactApiError(response, "read_failed", "Artifact could not be loaded.");
|
|
526
|
+
});
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
function isGitDiffMode(value) {
|
|
530
|
+
return value === "head" || value === "staged" || value === "worktree";
|
|
531
|
+
}
|
|
532
|
+
function gitDiffErrorCode(error) {
|
|
533
|
+
if (error instanceof GitDiffError && error.code in GIT_DIFF_ERROR_STATUSES) {
|
|
534
|
+
return error.code;
|
|
535
|
+
}
|
|
536
|
+
return "git_failed";
|
|
537
|
+
}
|
|
538
|
+
function handleGitDiffApiRequest(request, response, options, auth) {
|
|
539
|
+
const parsed = new URL(request.url ?? "/", "http://agentweaver.local");
|
|
540
|
+
if (parsed.pathname !== GIT_DIFF_API_PATH) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
if (request.method !== "GET") {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
if (!isAuthorized(request, auth)) {
|
|
547
|
+
writeAuthRequired(response);
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
if (!options.gitService || !options.getGitWorkspaceSnapshot) {
|
|
551
|
+
writeGitDiffApiError(response, "missing_provider", "Git diff provider is not configured.");
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
const filePath = parsed.searchParams.get("path");
|
|
555
|
+
const mode = parsed.searchParams.get("mode");
|
|
556
|
+
if (!filePath) {
|
|
557
|
+
writeGitDiffApiError(response, "missing_path", "A path query parameter is required.");
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
if (!isGitDiffMode(mode)) {
|
|
561
|
+
writeGitDiffApiError(response, "invalid_mode", "Mode must be head, staged, or worktree.");
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
const snapshot = options.getGitWorkspaceSnapshot();
|
|
565
|
+
if (!snapshot || !snapshot.available || !snapshot.repositoryRoot) {
|
|
566
|
+
writeGitDiffApiError(response, "repository_unavailable", snapshot?.error ?? "Git repository is not available.");
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
void options.gitService.diffFile(filePath, mode, snapshot)
|
|
570
|
+
.then((diff) => {
|
|
571
|
+
writeJson(response, 200, diff);
|
|
572
|
+
})
|
|
573
|
+
.catch((error) => {
|
|
574
|
+
const code = gitDiffErrorCode(error);
|
|
575
|
+
writeGitDiffApiError(response, code, error.message || "Git diff request failed.");
|
|
576
|
+
});
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
109
579
|
function htmlShell() {
|
|
110
580
|
return `<!doctype html>
|
|
111
581
|
<html lang="en">
|
|
@@ -425,8 +895,32 @@ export async function startWebServer(options) {
|
|
|
425
895
|
let closed = false;
|
|
426
896
|
const server = http.createServer((request, response) => {
|
|
427
897
|
if (request.method === "GET" && request.url === "/__agentweaver/health") {
|
|
428
|
-
response
|
|
429
|
-
|
|
898
|
+
writeJson(response, 200, { ok: true });
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (handleGitDiffApiRequest(request, response, options, auth)) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (handleArtifactApiRequest(request, response, options, auth)) {
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (request.method === "GET" && request.url === "/__agentweaver/artifacts") {
|
|
908
|
+
if (!isAuthorized(request, auth)) {
|
|
909
|
+
writeAuthRequired(response);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (!options.getArtifactCatalog) {
|
|
913
|
+
writeJson(response, 404, { error: "Artifact catalog provider is not configured." });
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
void Promise.resolve()
|
|
917
|
+
.then(() => options.getArtifactCatalog?.())
|
|
918
|
+
.then((catalog) => {
|
|
919
|
+
writeJson(response, 200, catalog);
|
|
920
|
+
})
|
|
921
|
+
.catch((error) => {
|
|
922
|
+
writeJson(response, 500, { error: error.message });
|
|
923
|
+
});
|
|
430
924
|
return;
|
|
431
925
|
}
|
|
432
926
|
if (request.method === "GET" && staticAssetPath(request.url)) {
|