@tonyclaw/agent-inspector 2.0.31 → 2.0.32
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/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-tIUf2EJm.js → CompareDrawer-Dgi0hhg5.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-BxRaXhKR.js → ProxyViewerContainer-4dG0nLRp.js} +28 -28
- package/.output/public/assets/{ReplayDialog-D6v1xcuC.js → ReplayDialog-BL6BwDuI.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-D6PniTlx.js → RequestAnatomy-DZMmiIiD.js} +1 -1
- package/.output/public/assets/{ResponseView-Sx8PjuvU.js → ResponseView-DJLOiTYq.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-D-Rbfnxh.js → StreamingChunkSequence--SxZ5cxc.js} +1 -1
- package/.output/public/assets/_sessionId-C4z6gu9i.js +1 -0
- package/.output/public/assets/index-Drpb2qR5.js +1 -0
- package/.output/public/assets/{index-Byk60-jA.css → index-fUUi8iQF.css} +1 -1
- package/.output/public/assets/{main-B1kdhY98.js → main-CbRIHhJ5.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +9 -9
- package/.output/server/{_sessionId-BceHFKf4.mjs → _sessionId-DNfumR8F.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-uSxK5Ei5.mjs → CompareDrawer-CzgjhnUa.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-_npQ_f2i.mjs → ProxyViewerContainer-BqOcZ7en.mjs} +258 -87
- package/.output/server/_ssr/{ReplayDialog-BHAzSLCT.mjs → ReplayDialog-DmEknIL8.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-DjlZLXBd.mjs → RequestAnatomy-DNoiMudY.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-BuDEJ3fl.mjs → ResponseView-C_cRrleT.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-DFcu0bD8.mjs → StreamingChunkSequence-DHZ9Q_dL.mjs} +3 -3
- package/.output/server/_ssr/{index-Bw6Fw33c.mjs → index-coYS9XyS.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-D90tMCb3.mjs → router-DUb5lKw1.mjs} +138 -39
- package/.output/server/{_tanstack-start-manifest_v-BVM4AUlx.mjs → _tanstack-start-manifest_v-xcG1o8Vt.mjs} +1 -1
- package/.output/server/index.mjs +67 -67
- package/package.json +1 -1
- package/src/components/groups/GroupsDialog.tsx +291 -78
- package/src/lib/groupContract.ts +13 -0
- package/src/mcp/server.ts +16 -0
- package/src/mcp/toolHandlers.ts +22 -0
- package/src/proxy/groupStore.ts +52 -2
- package/src/routes/api/groups.$groupId.ts +38 -1
- package/.output/public/assets/_sessionId-BhNTuZ31.js +0 -1
- package/.output/public/assets/index-Kxptlkpg.js +0 -1
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Check,
|
|
4
|
+
Copy,
|
|
5
|
+
Download,
|
|
6
|
+
ExternalLink,
|
|
7
|
+
Layers,
|
|
8
|
+
Loader2,
|
|
9
|
+
RefreshCw,
|
|
10
|
+
Trash2,
|
|
11
|
+
} from "lucide-react";
|
|
3
12
|
import { Button } from "../ui/button";
|
|
4
13
|
import { Badge } from "../ui/badge";
|
|
5
14
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
|
|
6
15
|
import { cn } from "../../lib/utils";
|
|
7
16
|
import { parseJsonResponse, readApiError } from "../../lib/apiClient";
|
|
8
17
|
import {
|
|
18
|
+
DeleteInspectorGroupResponseSchema,
|
|
9
19
|
GroupEvidenceExportResultSchema,
|
|
10
20
|
type GroupEvidenceReadResponse,
|
|
11
21
|
type GroupEvidenceExportResult,
|
|
@@ -36,6 +46,11 @@ type GroupMemberRow = {
|
|
|
36
46
|
exportMember: InspectorGroupExportMember | null;
|
|
37
47
|
};
|
|
38
48
|
|
|
49
|
+
export function groupDeletePath(groupId: string, deleteEvidence: boolean): string {
|
|
50
|
+
const path = `/api/groups/${encodeURIComponent(groupId)}`;
|
|
51
|
+
return deleteEvidence ? `${path}?deleteEvidence=1` : path;
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
export function summarizeGroup(group: InspectorGroup): GroupSummary {
|
|
40
55
|
let completedMembers = 0;
|
|
41
56
|
let failedMembers = 0;
|
|
@@ -218,6 +233,10 @@ export function GroupsDialog(): JSX.Element {
|
|
|
218
233
|
const [exportingGroupId, setExportingGroupId] = useState<string | null>(null);
|
|
219
234
|
const [exportError, setExportError] = useState<string | null>(null);
|
|
220
235
|
const [exportResult, setExportResult] = useState<GroupEvidenceExportResult | null>(null);
|
|
236
|
+
const [deleteTarget, setDeleteTarget] = useState<InspectorGroup | null>(null);
|
|
237
|
+
const [deleteEvidence, setDeleteEvidence] = useState(false);
|
|
238
|
+
const [deletingGroupId, setDeletingGroupId] = useState<string | null>(null);
|
|
239
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
221
240
|
const { groups, isLoading, error, mutate } = useGroups();
|
|
222
241
|
|
|
223
242
|
const selectedGroup = useMemo(
|
|
@@ -280,74 +299,140 @@ export function GroupsDialog(): JSX.Element {
|
|
|
280
299
|
[mutate, mutateEvidence],
|
|
281
300
|
);
|
|
282
301
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
<span className="absolute -right-1 -top-1 min-w-4 rounded-full bg-primary px-1 text-[10px] leading-4 text-primary-foreground">
|
|
299
|
-
{groups.length}
|
|
300
|
-
</span>
|
|
301
|
-
) : null}
|
|
302
|
-
<span className="sr-only">Evaluation groups</span>
|
|
303
|
-
</Button>
|
|
304
|
-
</DialogTrigger>
|
|
305
|
-
<DialogContent className="flex max-h-[85vh] max-w-5xl flex-col overflow-hidden">
|
|
306
|
-
<DialogHeader>
|
|
307
|
-
<DialogTitle className="flex items-center gap-2 pr-8">
|
|
308
|
-
<Layers className="size-5 text-muted-foreground" />
|
|
309
|
-
<span>Groups</span>
|
|
310
|
-
<Badge variant="outline">{formatNumber(groups.length)}</Badge>
|
|
311
|
-
</DialogTitle>
|
|
312
|
-
</DialogHeader>
|
|
302
|
+
const handleRequestDelete = useCallback((group: InspectorGroup) => {
|
|
303
|
+
setDeleteTarget(group);
|
|
304
|
+
setDeleteEvidence(false);
|
|
305
|
+
setDeleteError(null);
|
|
306
|
+
}, []);
|
|
307
|
+
|
|
308
|
+
const handleDeleteDialogOpenChange = useCallback(
|
|
309
|
+
(nextOpen: boolean) => {
|
|
310
|
+
if (nextOpen || deletingGroupId !== null) return;
|
|
311
|
+
setDeleteTarget(null);
|
|
312
|
+
setDeleteEvidence(false);
|
|
313
|
+
setDeleteError(null);
|
|
314
|
+
},
|
|
315
|
+
[deletingGroupId],
|
|
316
|
+
);
|
|
313
317
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
+
const handleDelete = useCallback(async () => {
|
|
319
|
+
const group = deleteTarget;
|
|
320
|
+
if (group === null) return;
|
|
321
|
+
|
|
322
|
+
setDeletingGroupId(group.id);
|
|
323
|
+
setDeleteError(null);
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const response = await fetch(groupDeletePath(group.id, deleteEvidence), {
|
|
327
|
+
method: "DELETE",
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
setDeleteError(
|
|
332
|
+
await readApiError(response, `Delete failed with status ${String(response.status)}`),
|
|
333
|
+
);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await parseJsonResponse(response, DeleteInspectorGroupResponseSchema);
|
|
338
|
+
setExportResult((current) =>
|
|
339
|
+
current !== null && current.group.id === group.id ? null : current,
|
|
340
|
+
);
|
|
341
|
+
setSelectedGroupId((current) => (current === group.id ? null : current));
|
|
342
|
+
setDeleteTarget(null);
|
|
343
|
+
setDeleteEvidence(false);
|
|
344
|
+
await mutate();
|
|
345
|
+
await mutateEvidence();
|
|
346
|
+
} catch (err) {
|
|
347
|
+
setDeleteError(err instanceof Error ? err.message : String(err));
|
|
348
|
+
} finally {
|
|
349
|
+
setDeletingGroupId(null);
|
|
350
|
+
}
|
|
351
|
+
}, [deleteEvidence, deleteTarget, mutate, mutateEvidence]);
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<>
|
|
355
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
356
|
+
<DialogTrigger asChild>
|
|
318
357
|
<Button
|
|
319
358
|
type="button"
|
|
320
|
-
variant="
|
|
321
|
-
size="
|
|
322
|
-
className="
|
|
323
|
-
|
|
359
|
+
variant="ghost"
|
|
360
|
+
size="icon"
|
|
361
|
+
className="relative size-8"
|
|
362
|
+
aria-label="Evaluation groups"
|
|
363
|
+
title="Evaluation groups"
|
|
324
364
|
>
|
|
325
|
-
<
|
|
326
|
-
|
|
365
|
+
<Layers className="size-4" />
|
|
366
|
+
{isLoading ? (
|
|
367
|
+
<Loader2 className="absolute -right-0.5 -top-0.5 size-3 animate-spin text-muted-foreground" />
|
|
368
|
+
) : groups.length > 0 ? (
|
|
369
|
+
<span className="absolute -right-1 -top-1 min-w-4 rounded-full bg-primary px-1 text-[10px] leading-4 text-primary-foreground">
|
|
370
|
+
{groups.length}
|
|
371
|
+
</span>
|
|
372
|
+
) : null}
|
|
373
|
+
<span className="sr-only">Evaluation groups</span>
|
|
327
374
|
</Button>
|
|
328
|
-
</
|
|
375
|
+
</DialogTrigger>
|
|
376
|
+
<DialogContent className="flex max-h-[85vh] max-w-5xl flex-col overflow-hidden">
|
|
377
|
+
<DialogHeader>
|
|
378
|
+
<DialogTitle className="flex items-center gap-2 pr-8">
|
|
379
|
+
<Layers className="size-5 text-muted-foreground" />
|
|
380
|
+
<span>Groups</span>
|
|
381
|
+
<Badge variant="outline">{formatNumber(groups.length)}</Badge>
|
|
382
|
+
</DialogTitle>
|
|
383
|
+
</DialogHeader>
|
|
384
|
+
|
|
385
|
+
<div className="flex min-h-0 items-center justify-between gap-3 border-b border-border pb-3">
|
|
386
|
+
<p className="text-xs text-muted-foreground">
|
|
387
|
+
Evaluation batches, model runs, and exported evidence packs.
|
|
388
|
+
</p>
|
|
389
|
+
<Button
|
|
390
|
+
type="button"
|
|
391
|
+
variant="outline"
|
|
392
|
+
size="sm"
|
|
393
|
+
className="shrink-0"
|
|
394
|
+
onClick={handleRefresh}
|
|
395
|
+
>
|
|
396
|
+
<RefreshCw className="size-3.5" />
|
|
397
|
+
Refresh
|
|
398
|
+
</Button>
|
|
399
|
+
</div>
|
|
329
400
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
401
|
+
<div className="grid min-h-0 flex-1 gap-4 md:grid-cols-[260px_minmax(0,1fr)]">
|
|
402
|
+
<GroupList
|
|
403
|
+
groups={groups}
|
|
404
|
+
selectedGroupId={selectedGroup?.id ?? null}
|
|
405
|
+
isLoading={isLoading}
|
|
406
|
+
error={error}
|
|
407
|
+
onSelect={setSelectedGroupId}
|
|
408
|
+
/>
|
|
409
|
+
<GroupDetails
|
|
410
|
+
group={selectedGroup}
|
|
411
|
+
exportResult={selectedExportResult}
|
|
412
|
+
evidenceResponse={evidenceResponse}
|
|
413
|
+
evidenceLoading={evidenceLoading}
|
|
414
|
+
evidenceError={evidenceError}
|
|
415
|
+
exportError={exportError}
|
|
416
|
+
deleteError={deleteError}
|
|
417
|
+
exportingGroupId={exportingGroupId}
|
|
418
|
+
deletingGroupId={deletingGroupId}
|
|
419
|
+
onExport={handleExport}
|
|
420
|
+
onRequestDelete={handleRequestDelete}
|
|
421
|
+
/>
|
|
422
|
+
</div>
|
|
423
|
+
</DialogContent>
|
|
424
|
+
</Dialog>
|
|
425
|
+
<DeleteGroupDialog
|
|
426
|
+
group={deleteTarget}
|
|
427
|
+
open={deleteTarget !== null}
|
|
428
|
+
deleteEvidence={deleteEvidence}
|
|
429
|
+
isDeleting={deleteTarget !== null && deletingGroupId === deleteTarget.id}
|
|
430
|
+
error={deleteError}
|
|
431
|
+
onDeleteEvidenceChange={setDeleteEvidence}
|
|
432
|
+
onOpenChange={handleDeleteDialogOpenChange}
|
|
433
|
+
onConfirm={() => void handleDelete()}
|
|
434
|
+
/>
|
|
435
|
+
</>
|
|
351
436
|
);
|
|
352
437
|
}
|
|
353
438
|
|
|
@@ -431,8 +516,11 @@ function GroupDetails({
|
|
|
431
516
|
evidenceLoading,
|
|
432
517
|
evidenceError,
|
|
433
518
|
exportError,
|
|
519
|
+
deleteError,
|
|
434
520
|
exportingGroupId,
|
|
521
|
+
deletingGroupId,
|
|
435
522
|
onExport,
|
|
523
|
+
onRequestDelete,
|
|
436
524
|
}: {
|
|
437
525
|
group: InspectorGroup | null;
|
|
438
526
|
exportResult: GroupEvidenceExportResult | null;
|
|
@@ -440,8 +528,11 @@ function GroupDetails({
|
|
|
440
528
|
evidenceLoading: boolean;
|
|
441
529
|
evidenceError: Error | undefined;
|
|
442
530
|
exportError: string | null;
|
|
531
|
+
deleteError: string | null;
|
|
443
532
|
exportingGroupId: string | null;
|
|
533
|
+
deletingGroupId: string | null;
|
|
444
534
|
onExport: (group: InspectorGroup) => Promise<void>;
|
|
535
|
+
onRequestDelete: (group: InspectorGroup) => void;
|
|
445
536
|
}): JSX.Element {
|
|
446
537
|
if (group === null) {
|
|
447
538
|
return (
|
|
@@ -457,6 +548,7 @@ function GroupDetails({
|
|
|
457
548
|
const evidence = evidenceFor(group, exportResult);
|
|
458
549
|
const rows = buildGroupMemberRows(group, exportResult);
|
|
459
550
|
const isExporting = exportingGroupId === group.id;
|
|
551
|
+
const isDeleting = deletingGroupId === group.id;
|
|
460
552
|
|
|
461
553
|
return (
|
|
462
554
|
<div className="min-h-0 overflow-y-auto pr-1">
|
|
@@ -475,21 +567,39 @@ function GroupDetails({
|
|
|
475
567
|
<span>updated {formatTimestamp(group.updatedAt)}</span>
|
|
476
568
|
</div>
|
|
477
569
|
</div>
|
|
478
|
-
<
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
570
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
571
|
+
<Button
|
|
572
|
+
type="button"
|
|
573
|
+
variant="outline"
|
|
574
|
+
size="icon"
|
|
575
|
+
className="size-8 text-red-600 hover:text-red-700"
|
|
576
|
+
aria-label="Delete group"
|
|
577
|
+
title="Delete group"
|
|
578
|
+
disabled={isDeleting}
|
|
579
|
+
onClick={() => onRequestDelete(group)}
|
|
580
|
+
>
|
|
581
|
+
{isDeleting ? (
|
|
582
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
583
|
+
) : (
|
|
584
|
+
<Trash2 className="size-3.5" />
|
|
585
|
+
)}
|
|
586
|
+
</Button>
|
|
587
|
+
<Button
|
|
588
|
+
type="button"
|
|
589
|
+
variant="default"
|
|
590
|
+
size="sm"
|
|
591
|
+
className="shrink-0"
|
|
592
|
+
disabled={isExporting || isDeleting}
|
|
593
|
+
onClick={() => void onExport(group)}
|
|
594
|
+
>
|
|
595
|
+
{isExporting ? (
|
|
596
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
597
|
+
) : (
|
|
598
|
+
<Download className="size-3.5" />
|
|
599
|
+
)}
|
|
600
|
+
Export evidence
|
|
601
|
+
</Button>
|
|
602
|
+
</div>
|
|
493
603
|
</div>
|
|
494
604
|
|
|
495
605
|
{group.task !== null && group.task.trim().length > 0 && (
|
|
@@ -529,12 +639,115 @@ function GroupDetails({
|
|
|
529
639
|
</div>
|
|
530
640
|
)}
|
|
531
641
|
|
|
642
|
+
{deleteError !== null && (
|
|
643
|
+
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700">
|
|
644
|
+
{deleteError}
|
|
645
|
+
</div>
|
|
646
|
+
)}
|
|
647
|
+
|
|
532
648
|
<MemberTable rows={rows} />
|
|
533
649
|
</div>
|
|
534
650
|
</div>
|
|
535
651
|
);
|
|
536
652
|
}
|
|
537
653
|
|
|
654
|
+
function DeleteGroupDialog({
|
|
655
|
+
group,
|
|
656
|
+
open,
|
|
657
|
+
deleteEvidence,
|
|
658
|
+
isDeleting,
|
|
659
|
+
error,
|
|
660
|
+
onDeleteEvidenceChange,
|
|
661
|
+
onOpenChange,
|
|
662
|
+
onConfirm,
|
|
663
|
+
}: {
|
|
664
|
+
group: InspectorGroup | null;
|
|
665
|
+
open: boolean;
|
|
666
|
+
deleteEvidence: boolean;
|
|
667
|
+
isDeleting: boolean;
|
|
668
|
+
error: string | null;
|
|
669
|
+
onDeleteEvidenceChange: (deleteEvidence: boolean) => void;
|
|
670
|
+
onOpenChange: (open: boolean) => void;
|
|
671
|
+
onConfirm: () => void;
|
|
672
|
+
}): JSX.Element {
|
|
673
|
+
const title = group?.title ?? "this group";
|
|
674
|
+
const hasEvidence = group?.evidence !== null && group?.evidence !== undefined;
|
|
675
|
+
const evidencePath = group?.evidence?.markdownPath ?? "No evidence pack exported.";
|
|
676
|
+
|
|
677
|
+
return (
|
|
678
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
679
|
+
<DialogContent className="max-w-md">
|
|
680
|
+
<DialogHeader>
|
|
681
|
+
<DialogTitle className="flex items-center gap-2 pr-8">
|
|
682
|
+
<Trash2 className="size-5 text-red-600" />
|
|
683
|
+
<span>Delete group</span>
|
|
684
|
+
</DialogTitle>
|
|
685
|
+
</DialogHeader>
|
|
686
|
+
|
|
687
|
+
<div className="space-y-3">
|
|
688
|
+
<p className="text-sm text-muted-foreground">
|
|
689
|
+
Delete <span className="font-medium text-foreground">{title}</span>? Session logs and
|
|
690
|
+
run records stay intact.
|
|
691
|
+
</p>
|
|
692
|
+
|
|
693
|
+
<label
|
|
694
|
+
className={cn(
|
|
695
|
+
"flex items-start gap-3 rounded-md border border-border bg-muted/20 px-3 py-2 text-sm",
|
|
696
|
+
hasEvidence ? "cursor-pointer" : "opacity-60",
|
|
697
|
+
)}
|
|
698
|
+
>
|
|
699
|
+
<input
|
|
700
|
+
type="checkbox"
|
|
701
|
+
className="mt-0.5 size-4"
|
|
702
|
+
checked={deleteEvidence}
|
|
703
|
+
disabled={!hasEvidence || isDeleting}
|
|
704
|
+
onChange={(event) => onDeleteEvidenceChange(event.currentTarget.checked)}
|
|
705
|
+
/>
|
|
706
|
+
<span className="min-w-0">
|
|
707
|
+
<span className="block font-medium">Also delete exported evidence files</span>
|
|
708
|
+
<code className="mt-1 block truncate font-mono text-[11px] text-muted-foreground">
|
|
709
|
+
{evidencePath}
|
|
710
|
+
</code>
|
|
711
|
+
</span>
|
|
712
|
+
</label>
|
|
713
|
+
|
|
714
|
+
{error !== null && (
|
|
715
|
+
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700">
|
|
716
|
+
{error}
|
|
717
|
+
</div>
|
|
718
|
+
)}
|
|
719
|
+
</div>
|
|
720
|
+
|
|
721
|
+
<div className="flex justify-end gap-2 pt-1">
|
|
722
|
+
<Button
|
|
723
|
+
type="button"
|
|
724
|
+
variant="outline"
|
|
725
|
+
size="sm"
|
|
726
|
+
disabled={isDeleting}
|
|
727
|
+
onClick={() => onOpenChange(false)}
|
|
728
|
+
>
|
|
729
|
+
Cancel
|
|
730
|
+
</Button>
|
|
731
|
+
<Button
|
|
732
|
+
type="button"
|
|
733
|
+
variant="destructive"
|
|
734
|
+
size="sm"
|
|
735
|
+
disabled={isDeleting || group === null}
|
|
736
|
+
onClick={onConfirm}
|
|
737
|
+
>
|
|
738
|
+
{isDeleting ? (
|
|
739
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
740
|
+
) : (
|
|
741
|
+
<Trash2 className="size-3.5" />
|
|
742
|
+
)}
|
|
743
|
+
Delete
|
|
744
|
+
</Button>
|
|
745
|
+
</div>
|
|
746
|
+
</DialogContent>
|
|
747
|
+
</Dialog>
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
538
751
|
function SummaryStat({ label, value }: { label: string; value: number }): JSX.Element {
|
|
539
752
|
return (
|
|
540
753
|
<div className="rounded-md border border-border bg-muted/20 px-3 py-2">
|
package/src/lib/groupContract.ts
CHANGED
|
@@ -92,6 +92,17 @@ export const GroupEvidenceExportOptionsSchema = z
|
|
|
92
92
|
})
|
|
93
93
|
.optional();
|
|
94
94
|
|
|
95
|
+
export const DeleteInspectorGroupOptionsSchema = z.object({
|
|
96
|
+
deleteEvidence: z.boolean().optional(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export const DeleteInspectorGroupResponseSchema = z.object({
|
|
100
|
+
groupId: z.string(),
|
|
101
|
+
deleted: z.boolean(),
|
|
102
|
+
evidenceDeleted: z.boolean(),
|
|
103
|
+
evidencePath: z.string().nullable(),
|
|
104
|
+
});
|
|
105
|
+
|
|
95
106
|
export const InspectorGroupExportMemberSchema = z.object({
|
|
96
107
|
member: InspectorGroupMemberSchema,
|
|
97
108
|
run: InspectorRunSchema.nullable(),
|
|
@@ -141,6 +152,8 @@ export type CreateInspectorGroupInput = z.infer<typeof CreateInspectorGroupInput
|
|
|
141
152
|
export type UpdateInspectorGroupInput = z.infer<typeof UpdateInspectorGroupInputSchema>;
|
|
142
153
|
export type AddInspectorGroupSessionInput = z.infer<typeof AddInspectorGroupSessionInputSchema>;
|
|
143
154
|
export type GroupEvidenceExportOptions = z.infer<typeof GroupEvidenceExportOptionsSchema>;
|
|
155
|
+
export type DeleteInspectorGroupOptions = z.infer<typeof DeleteInspectorGroupOptionsSchema>;
|
|
156
|
+
export type DeleteInspectorGroupResponse = z.infer<typeof DeleteInspectorGroupResponseSchema>;
|
|
144
157
|
export type InspectorGroupExportMember = z.infer<typeof InspectorGroupExportMemberSchema>;
|
|
145
158
|
export type InspectorGroupEvidenceSummary = z.infer<typeof InspectorGroupEvidenceSummarySchema>;
|
|
146
159
|
export type GroupEvidenceReadResponse = z.infer<typeof GroupEvidenceReadResponseSchema>;
|
package/src/mcp/server.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "@modelcontextprotocol/server";
|
|
29
29
|
import {
|
|
30
30
|
AddInspectorGroupSessionInputSchema,
|
|
31
|
+
DeleteInspectorGroupOptionsSchema,
|
|
31
32
|
GroupEvidenceExportOptionsSchema,
|
|
32
33
|
InspectorGroupSchema,
|
|
33
34
|
InspectorGroupStatusSchema,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
createGroupImpl,
|
|
49
50
|
createRunImpl,
|
|
50
51
|
createSessionKnowledgeImpl,
|
|
52
|
+
deleteGroupImpl,
|
|
51
53
|
exportEvidenceImpl,
|
|
52
54
|
exportGroupEvidenceImpl,
|
|
53
55
|
getGroupImpl,
|
|
@@ -661,6 +663,19 @@ function registerTools(server: McpServer): void {
|
|
|
661
663
|
(args) => safeCall(() => updateGroupImpl(callApi, args)),
|
|
662
664
|
);
|
|
663
665
|
|
|
666
|
+
server.registerTool(
|
|
667
|
+
"inspector_delete_group",
|
|
668
|
+
{
|
|
669
|
+
title: "Delete an Inspector group",
|
|
670
|
+
description:
|
|
671
|
+
"Deletes one declared Inspector group from <dataDir>/groups.json. By default this keeps exported evidence files; set deleteEvidence=true only when the local evidence pack should also be removed.",
|
|
672
|
+
inputSchema: DeleteInspectorGroupOptionsSchema.extend({
|
|
673
|
+
groupId: z.string().min(1).describe("The Inspector group id."),
|
|
674
|
+
}),
|
|
675
|
+
},
|
|
676
|
+
(args) => safeCall(() => deleteGroupImpl(callApi, args)),
|
|
677
|
+
);
|
|
678
|
+
|
|
664
679
|
server.registerTool(
|
|
665
680
|
"inspector_add_group_session",
|
|
666
681
|
{
|
|
@@ -1224,6 +1239,7 @@ export const TOOL_NAMES = [
|
|
|
1224
1239
|
"inspector_create_group",
|
|
1225
1240
|
"inspector_get_group",
|
|
1226
1241
|
"inspector_update_group",
|
|
1242
|
+
"inspector_delete_group",
|
|
1227
1243
|
"inspector_add_group_session",
|
|
1228
1244
|
"inspector_export_group_evidence",
|
|
1229
1245
|
"inspector_list_models",
|
package/src/mcp/toolHandlers.ts
CHANGED
|
@@ -18,11 +18,13 @@ import { z } from "zod";
|
|
|
18
18
|
import { LoopbackTimeoutError, type CallApiOptions } from "./loopback";
|
|
19
19
|
import { extractLastUserMessagePreview, extractResponsePreview } from "./previewExtractor";
|
|
20
20
|
import {
|
|
21
|
+
DeleteInspectorGroupResponseSchema,
|
|
21
22
|
GroupEvidenceExportResultSchema,
|
|
22
23
|
InspectorGroupSchema,
|
|
23
24
|
InspectorGroupsListResponseSchema,
|
|
24
25
|
type AddInspectorGroupSessionInput,
|
|
25
26
|
type CreateInspectorGroupInput,
|
|
27
|
+
type DeleteInspectorGroupOptions,
|
|
26
28
|
type UpdateInspectorGroupInput,
|
|
27
29
|
} from "../lib/groupContract";
|
|
28
30
|
import {
|
|
@@ -446,6 +448,26 @@ export async function updateGroupImpl(
|
|
|
446
448
|
return textJson(parsed.data);
|
|
447
449
|
}
|
|
448
450
|
|
|
451
|
+
export type DeleteGroupArgs = {
|
|
452
|
+
groupId: string;
|
|
453
|
+
} & DeleteInspectorGroupOptions;
|
|
454
|
+
|
|
455
|
+
export async function deleteGroupImpl(
|
|
456
|
+
callApi: CallApiFn,
|
|
457
|
+
args: DeleteGroupArgs,
|
|
458
|
+
): Promise<ToolResult> {
|
|
459
|
+
const query = args.deleteEvidence === true ? "?deleteEvidence=1" : "";
|
|
460
|
+
const path = `/api/groups/${encodeURIComponent(args.groupId)}${query}`;
|
|
461
|
+
const res = await callApi(path, { method: "DELETE" });
|
|
462
|
+
if (!res.ok) return toolError(`DELETE ${path} returned ${res.status}`);
|
|
463
|
+
const rawBody: unknown = await res.json();
|
|
464
|
+
const parsed = DeleteInspectorGroupResponseSchema.safeParse(rawBody);
|
|
465
|
+
if (!parsed.success) {
|
|
466
|
+
return toolError("DELETE /api/groups/{groupId} returned an unparseable delete result");
|
|
467
|
+
}
|
|
468
|
+
return textJson(parsed.data);
|
|
469
|
+
}
|
|
470
|
+
|
|
449
471
|
export type AddGroupSessionArgs = {
|
|
450
472
|
groupId: string;
|
|
451
473
|
} & AddInspectorGroupSessionInput;
|
package/src/proxy/groupStore.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import {
|
|
6
6
|
AddInspectorGroupSessionInputSchema,
|
|
7
7
|
CreateInspectorGroupInputSchema,
|
|
8
|
+
DeleteInspectorGroupOptionsSchema,
|
|
8
9
|
InspectorGroupSchema,
|
|
9
10
|
UpdateInspectorGroupInputSchema,
|
|
10
11
|
type AddInspectorGroupSessionInput,
|
|
11
12
|
type CreateInspectorGroupInput,
|
|
13
|
+
type DeleteInspectorGroupOptions,
|
|
14
|
+
type DeleteInspectorGroupResponse,
|
|
12
15
|
type InspectorGroup,
|
|
13
16
|
type InspectorGroupEvidence,
|
|
14
17
|
type InspectorGroupMember,
|
|
@@ -88,6 +91,33 @@ function sortedGroups(groups: readonly InspectorGroup[]): InspectorGroup[] {
|
|
|
88
91
|
return [...groups].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
function isPathInside(parentPath: string, childPath: string): boolean {
|
|
95
|
+
const relativePath = relative(parentPath, childPath);
|
|
96
|
+
return (
|
|
97
|
+
relativePath === "" ||
|
|
98
|
+
(relativePath.length > 0 && !relativePath.startsWith("..") && !isAbsolute(relativePath))
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function evidenceDirectoryFor(group: InspectorGroup): string | null {
|
|
103
|
+
if (group.evidence === null) return null;
|
|
104
|
+
|
|
105
|
+
const root = resolve(getDataDir(), "evidence", "groups");
|
|
106
|
+
const evidenceDir = resolve(dirname(group.evidence.jsonPath));
|
|
107
|
+
return isPathInside(root, evidenceDir) ? evidenceDir : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function removeEvidenceDirectory(evidencePath: string | null, deleteEvidence: boolean): boolean {
|
|
111
|
+
if (!deleteEvidence || evidencePath === null || !existsSync(evidencePath)) return false;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
rmSync(evidencePath, { recursive: true, force: true });
|
|
115
|
+
return !existsSync(evidencePath);
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
91
121
|
export function listGroups(): InspectorGroup[] {
|
|
92
122
|
return sortedGroups(readGroups());
|
|
93
123
|
}
|
|
@@ -153,6 +183,26 @@ export function updateGroup(
|
|
|
153
183
|
return updatedGroup;
|
|
154
184
|
}
|
|
155
185
|
|
|
186
|
+
export function deleteGroup(
|
|
187
|
+
groupId: string,
|
|
188
|
+
options: DeleteInspectorGroupOptions = {},
|
|
189
|
+
): DeleteInspectorGroupResponse | null {
|
|
190
|
+
const parsed = DeleteInspectorGroupOptionsSchema.parse(options);
|
|
191
|
+
const groups = readGroups();
|
|
192
|
+
const group = groups.find((candidate) => candidate.id === groupId);
|
|
193
|
+
if (group === undefined) return null;
|
|
194
|
+
|
|
195
|
+
const evidencePath = evidenceDirectoryFor(group);
|
|
196
|
+
writeGroups(groups.filter((candidate) => candidate.id !== groupId));
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
groupId: group.id,
|
|
200
|
+
deleted: true,
|
|
201
|
+
evidenceDeleted: removeEvidenceDirectory(evidencePath, parsed.deleteEvidence === true),
|
|
202
|
+
evidencePath,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
156
206
|
function matchesExistingMember(
|
|
157
207
|
member: InspectorGroupMember,
|
|
158
208
|
memberId: string,
|