@tonyclaw/agent-inspector 2.0.31 → 2.0.33
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-ClTJxdoU.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-BxRaXhKR.js → ProxyViewerContainer--Pes01q2.js} +28 -28
- package/.output/public/assets/{ReplayDialog-D6v1xcuC.js → ReplayDialog-iuzfh85b.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-D6PniTlx.js → RequestAnatomy-FZnnP9Gx.js} +1 -1
- package/.output/public/assets/{ResponseView-Sx8PjuvU.js → ResponseView-MZ6ZWvbL.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-D-Rbfnxh.js → StreamingChunkSequence-B8mZMdzs.js} +1 -1
- package/.output/public/assets/_sessionId-Bc26Rfxs.js +1 -0
- package/.output/public/assets/index-C6AiRPsJ.js +1 -0
- package/.output/public/assets/index-C_8PFAm2.css +1 -0
- package/.output/public/assets/{main-B1kdhY98.js → main-B797Hnge.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +9 -9
- package/.output/server/{_sessionId-BceHFKf4.mjs → _sessionId-DD05qd8d.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-uSxK5Ei5.mjs → CompareDrawer-oGOG5-kQ.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-_npQ_f2i.mjs → ProxyViewerContainer-Bbl5tqT1.mjs} +205 -25
- package/.output/server/_ssr/{ReplayDialog-BHAzSLCT.mjs → ReplayDialog-BegfwWIy.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-DjlZLXBd.mjs → RequestAnatomy-BW_X2DyZ.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-BuDEJ3fl.mjs → ResponseView-Bx_uJ4RS.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-DFcu0bD8.mjs → StreamingChunkSequence-CQp5QQnB.mjs} +3 -3
- package/.output/server/_ssr/{index-Bw6Fw33c.mjs → index-ODSKeDD8.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-D90tMCb3.mjs → router-QcI4GOvo.mjs} +138 -39
- package/.output/server/{_tanstack-start-manifest_v-BVM4AUlx.mjs → _tanstack-start-manifest_v-dEujKJ3-.mjs} +1 -1
- package/.output/server/index.mjs +63 -63
- package/package.json +1 -1
- package/src/components/groups/GroupsDialog.tsx +241 -17
- 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-Byk60-jA.css +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,6 +299,61 @@ export function GroupsDialog(): JSX.Element {
|
|
|
280
299
|
[mutate, mutateEvidence],
|
|
281
300
|
);
|
|
282
301
|
|
|
302
|
+
const handleRequestDelete = useCallback((group: InspectorGroup) => {
|
|
303
|
+
setDeleteTarget(group);
|
|
304
|
+
setDeleteEvidence(false);
|
|
305
|
+
setDeleteError(null);
|
|
306
|
+
}, []);
|
|
307
|
+
|
|
308
|
+
const handleCancelDelete = useCallback(() => {
|
|
309
|
+
if (deletingGroupId !== null) return;
|
|
310
|
+
setDeleteTarget(null);
|
|
311
|
+
setDeleteEvidence(false);
|
|
312
|
+
setDeleteError(null);
|
|
313
|
+
}, [deletingGroupId]);
|
|
314
|
+
|
|
315
|
+
const handleSelectGroup = useCallback((groupId: string) => {
|
|
316
|
+
setSelectedGroupId(groupId);
|
|
317
|
+
setDeleteTarget(null);
|
|
318
|
+
setDeleteEvidence(false);
|
|
319
|
+
setDeleteError(null);
|
|
320
|
+
}, []);
|
|
321
|
+
|
|
322
|
+
const handleDelete = useCallback(async () => {
|
|
323
|
+
const group = deleteTarget;
|
|
324
|
+
if (group === null) return;
|
|
325
|
+
|
|
326
|
+
setDeletingGroupId(group.id);
|
|
327
|
+
setDeleteError(null);
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const response = await fetch(groupDeletePath(group.id, deleteEvidence), {
|
|
331
|
+
method: "DELETE",
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (!response.ok) {
|
|
335
|
+
setDeleteError(
|
|
336
|
+
await readApiError(response, `Delete failed with status ${String(response.status)}`),
|
|
337
|
+
);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
await parseJsonResponse(response, DeleteInspectorGroupResponseSchema);
|
|
342
|
+
setExportResult((current) =>
|
|
343
|
+
current !== null && current.group.id === group.id ? null : current,
|
|
344
|
+
);
|
|
345
|
+
setSelectedGroupId((current) => (current === group.id ? null : current));
|
|
346
|
+
setDeleteTarget(null);
|
|
347
|
+
setDeleteEvidence(false);
|
|
348
|
+
await mutate();
|
|
349
|
+
await mutateEvidence();
|
|
350
|
+
} catch (err) {
|
|
351
|
+
setDeleteError(err instanceof Error ? err.message : String(err));
|
|
352
|
+
} finally {
|
|
353
|
+
setDeletingGroupId(null);
|
|
354
|
+
}
|
|
355
|
+
}, [deleteEvidence, deleteTarget, mutate, mutateEvidence]);
|
|
356
|
+
|
|
283
357
|
return (
|
|
284
358
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
285
359
|
<DialogTrigger asChild>
|
|
@@ -333,17 +407,25 @@ export function GroupsDialog(): JSX.Element {
|
|
|
333
407
|
selectedGroupId={selectedGroup?.id ?? null}
|
|
334
408
|
isLoading={isLoading}
|
|
335
409
|
error={error}
|
|
336
|
-
onSelect={
|
|
410
|
+
onSelect={handleSelectGroup}
|
|
337
411
|
/>
|
|
338
412
|
<GroupDetails
|
|
339
413
|
group={selectedGroup}
|
|
414
|
+
deleteTarget={deleteTarget}
|
|
415
|
+
deleteEvidence={deleteEvidence}
|
|
340
416
|
exportResult={selectedExportResult}
|
|
341
417
|
evidenceResponse={evidenceResponse}
|
|
342
418
|
evidenceLoading={evidenceLoading}
|
|
343
419
|
evidenceError={evidenceError}
|
|
344
420
|
exportError={exportError}
|
|
421
|
+
deleteError={deleteError}
|
|
345
422
|
exportingGroupId={exportingGroupId}
|
|
423
|
+
deletingGroupId={deletingGroupId}
|
|
424
|
+
onDeleteEvidenceChange={setDeleteEvidence}
|
|
346
425
|
onExport={handleExport}
|
|
426
|
+
onRequestDelete={handleRequestDelete}
|
|
427
|
+
onCancelDelete={handleCancelDelete}
|
|
428
|
+
onConfirmDelete={() => void handleDelete()}
|
|
347
429
|
/>
|
|
348
430
|
</div>
|
|
349
431
|
</DialogContent>
|
|
@@ -426,22 +508,38 @@ function GroupList({
|
|
|
426
508
|
|
|
427
509
|
function GroupDetails({
|
|
428
510
|
group,
|
|
511
|
+
deleteTarget,
|
|
512
|
+
deleteEvidence,
|
|
429
513
|
exportResult,
|
|
430
514
|
evidenceResponse,
|
|
431
515
|
evidenceLoading,
|
|
432
516
|
evidenceError,
|
|
433
517
|
exportError,
|
|
518
|
+
deleteError,
|
|
434
519
|
exportingGroupId,
|
|
520
|
+
deletingGroupId,
|
|
521
|
+
onDeleteEvidenceChange,
|
|
435
522
|
onExport,
|
|
523
|
+
onRequestDelete,
|
|
524
|
+
onCancelDelete,
|
|
525
|
+
onConfirmDelete,
|
|
436
526
|
}: {
|
|
437
527
|
group: InspectorGroup | null;
|
|
528
|
+
deleteTarget: InspectorGroup | null;
|
|
529
|
+
deleteEvidence: boolean;
|
|
438
530
|
exportResult: GroupEvidenceExportResult | null;
|
|
439
531
|
evidenceResponse: GroupEvidenceReadResponse | null;
|
|
440
532
|
evidenceLoading: boolean;
|
|
441
533
|
evidenceError: Error | undefined;
|
|
442
534
|
exportError: string | null;
|
|
535
|
+
deleteError: string | null;
|
|
443
536
|
exportingGroupId: string | null;
|
|
537
|
+
deletingGroupId: string | null;
|
|
538
|
+
onDeleteEvidenceChange: (deleteEvidence: boolean) => void;
|
|
444
539
|
onExport: (group: InspectorGroup) => Promise<void>;
|
|
540
|
+
onRequestDelete: (group: InspectorGroup) => void;
|
|
541
|
+
onCancelDelete: () => void;
|
|
542
|
+
onConfirmDelete: () => void;
|
|
445
543
|
}): JSX.Element {
|
|
446
544
|
if (group === null) {
|
|
447
545
|
return (
|
|
@@ -457,6 +555,8 @@ function GroupDetails({
|
|
|
457
555
|
const evidence = evidenceFor(group, exportResult);
|
|
458
556
|
const rows = buildGroupMemberRows(group, exportResult);
|
|
459
557
|
const isExporting = exportingGroupId === group.id;
|
|
558
|
+
const isDeleting = deletingGroupId === group.id;
|
|
559
|
+
const isConfirmingDelete = deleteTarget?.id === group.id;
|
|
460
560
|
|
|
461
561
|
return (
|
|
462
562
|
<div className="min-h-0 overflow-y-auto pr-1">
|
|
@@ -475,21 +575,39 @@ function GroupDetails({
|
|
|
475
575
|
<span>updated {formatTimestamp(group.updatedAt)}</span>
|
|
476
576
|
</div>
|
|
477
577
|
</div>
|
|
478
|
-
<
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
578
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
579
|
+
<Button
|
|
580
|
+
type="button"
|
|
581
|
+
variant="outline"
|
|
582
|
+
size="icon"
|
|
583
|
+
className="size-8 text-red-600 hover:text-red-700"
|
|
584
|
+
aria-label="Delete group"
|
|
585
|
+
title="Delete group"
|
|
586
|
+
disabled={isDeleting}
|
|
587
|
+
onClick={() => onRequestDelete(group)}
|
|
588
|
+
>
|
|
589
|
+
{isDeleting ? (
|
|
590
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
591
|
+
) : (
|
|
592
|
+
<Trash2 className="size-3.5" />
|
|
593
|
+
)}
|
|
594
|
+
</Button>
|
|
595
|
+
<Button
|
|
596
|
+
type="button"
|
|
597
|
+
variant="default"
|
|
598
|
+
size="sm"
|
|
599
|
+
className="shrink-0"
|
|
600
|
+
disabled={isExporting || isDeleting}
|
|
601
|
+
onClick={() => void onExport(group)}
|
|
602
|
+
>
|
|
603
|
+
{isExporting ? (
|
|
604
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
605
|
+
) : (
|
|
606
|
+
<Download className="size-3.5" />
|
|
607
|
+
)}
|
|
608
|
+
Export evidence
|
|
609
|
+
</Button>
|
|
610
|
+
</div>
|
|
493
611
|
</div>
|
|
494
612
|
|
|
495
613
|
{group.task !== null && group.task.trim().length > 0 && (
|
|
@@ -498,6 +616,18 @@ function GroupDetails({
|
|
|
498
616
|
</p>
|
|
499
617
|
)}
|
|
500
618
|
|
|
619
|
+
{isConfirmingDelete && (
|
|
620
|
+
<DeleteGroupPanel
|
|
621
|
+
group={group}
|
|
622
|
+
deleteEvidence={deleteEvidence}
|
|
623
|
+
isDeleting={isDeleting}
|
|
624
|
+
error={deleteError}
|
|
625
|
+
onDeleteEvidenceChange={onDeleteEvidenceChange}
|
|
626
|
+
onCancel={onCancelDelete}
|
|
627
|
+
onConfirm={onConfirmDelete}
|
|
628
|
+
/>
|
|
629
|
+
)}
|
|
630
|
+
|
|
501
631
|
<div className="grid gap-2 sm:grid-cols-4">
|
|
502
632
|
<SummaryStat label="Members" value={summary.memberCount} />
|
|
503
633
|
<SummaryStat label="Done" value={summary.completedMembers} />
|
|
@@ -529,12 +659,106 @@ function GroupDetails({
|
|
|
529
659
|
</div>
|
|
530
660
|
)}
|
|
531
661
|
|
|
662
|
+
{deleteError !== null && !isConfirmingDelete && (
|
|
663
|
+
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700">
|
|
664
|
+
{deleteError}
|
|
665
|
+
</div>
|
|
666
|
+
)}
|
|
667
|
+
|
|
532
668
|
<MemberTable rows={rows} />
|
|
533
669
|
</div>
|
|
534
670
|
</div>
|
|
535
671
|
);
|
|
536
672
|
}
|
|
537
673
|
|
|
674
|
+
function DeleteGroupPanel({
|
|
675
|
+
group,
|
|
676
|
+
deleteEvidence,
|
|
677
|
+
isDeleting,
|
|
678
|
+
error,
|
|
679
|
+
onDeleteEvidenceChange,
|
|
680
|
+
onCancel,
|
|
681
|
+
onConfirm,
|
|
682
|
+
}: {
|
|
683
|
+
group: InspectorGroup;
|
|
684
|
+
deleteEvidence: boolean;
|
|
685
|
+
isDeleting: boolean;
|
|
686
|
+
error: string | null;
|
|
687
|
+
onDeleteEvidenceChange: (deleteEvidence: boolean) => void;
|
|
688
|
+
onCancel: () => void;
|
|
689
|
+
onConfirm: () => void;
|
|
690
|
+
}): JSX.Element {
|
|
691
|
+
const hasEvidence = group.evidence !== null;
|
|
692
|
+
const evidencePath = group.evidence?.markdownPath ?? "No evidence pack exported.";
|
|
693
|
+
|
|
694
|
+
return (
|
|
695
|
+
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-3 text-red-900">
|
|
696
|
+
<div className="flex min-w-0 flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
697
|
+
<div className="min-w-0 space-y-2">
|
|
698
|
+
<div className="flex items-center gap-2 text-sm font-semibold">
|
|
699
|
+
<Trash2 className="size-4 text-red-600" />
|
|
700
|
+
<span>Delete group?</span>
|
|
701
|
+
</div>
|
|
702
|
+
<p className="text-xs text-red-800">
|
|
703
|
+
Delete <span className="font-medium">{group.title}</span>. Session logs and run records
|
|
704
|
+
stay intact.
|
|
705
|
+
</p>
|
|
706
|
+
<label
|
|
707
|
+
className={cn(
|
|
708
|
+
"flex min-w-0 items-start gap-2 text-xs",
|
|
709
|
+
hasEvidence ? "cursor-pointer" : "opacity-60",
|
|
710
|
+
)}
|
|
711
|
+
>
|
|
712
|
+
<input
|
|
713
|
+
type="checkbox"
|
|
714
|
+
className="mt-0.5 size-4 shrink-0"
|
|
715
|
+
checked={deleteEvidence}
|
|
716
|
+
disabled={!hasEvidence || isDeleting}
|
|
717
|
+
onChange={(event) => onDeleteEvidenceChange(event.currentTarget.checked)}
|
|
718
|
+
/>
|
|
719
|
+
<span className="min-w-0">
|
|
720
|
+
<span className="block font-medium">Also delete exported evidence files</span>
|
|
721
|
+
<code className="mt-1 block truncate font-mono text-[11px] text-red-700">
|
|
722
|
+
{evidencePath}
|
|
723
|
+
</code>
|
|
724
|
+
</span>
|
|
725
|
+
</label>
|
|
726
|
+
{error !== null && (
|
|
727
|
+
<div className="rounded-md border border-red-200 bg-white px-2 py-1.5 text-xs text-red-700">
|
|
728
|
+
{error}
|
|
729
|
+
</div>
|
|
730
|
+
)}
|
|
731
|
+
</div>
|
|
732
|
+
<div className="flex shrink-0 justify-end gap-2">
|
|
733
|
+
<Button
|
|
734
|
+
type="button"
|
|
735
|
+
variant="outline"
|
|
736
|
+
size="sm"
|
|
737
|
+
disabled={isDeleting}
|
|
738
|
+
onClick={onCancel}
|
|
739
|
+
>
|
|
740
|
+
Cancel
|
|
741
|
+
</Button>
|
|
742
|
+
<Button
|
|
743
|
+
type="button"
|
|
744
|
+
variant="destructive"
|
|
745
|
+
size="sm"
|
|
746
|
+
disabled={isDeleting}
|
|
747
|
+
onClick={onConfirm}
|
|
748
|
+
>
|
|
749
|
+
{isDeleting ? (
|
|
750
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
751
|
+
) : (
|
|
752
|
+
<Trash2 className="size-3.5" />
|
|
753
|
+
)}
|
|
754
|
+
Delete
|
|
755
|
+
</Button>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
</div>
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
|
|
538
762
|
function SummaryStat({ label, value }: { label: string; value: number }): JSX.Element {
|
|
539
763
|
return (
|
|
540
764
|
<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,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
2
|
import { UpdateInspectorGroupInputSchema } from "../../lib/groupContract";
|
|
3
|
-
import { getGroup, updateGroup } from "../../proxy/groupStore";
|
|
3
|
+
import { deleteGroup, getGroup, updateGroup } from "../../proxy/groupStore";
|
|
4
4
|
|
|
5
5
|
type JsonBodyResult = { ok: true; value: unknown } | { ok: false };
|
|
6
|
+
type DeleteEvidenceFlagResult = { ok: true; value: boolean | undefined } | { ok: false };
|
|
6
7
|
|
|
7
8
|
async function readJsonBody(request: Request): Promise<JsonBodyResult> {
|
|
8
9
|
try {
|
|
@@ -14,6 +15,26 @@ async function readJsonBody(request: Request): Promise<JsonBodyResult> {
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
function readDeleteEvidenceFlag(request: Request): DeleteEvidenceFlagResult {
|
|
19
|
+
try {
|
|
20
|
+
const value = new URL(request.url).searchParams.get("deleteEvidence");
|
|
21
|
+
switch (value) {
|
|
22
|
+
case null:
|
|
23
|
+
return { ok: true, value: undefined };
|
|
24
|
+
case "1":
|
|
25
|
+
case "true":
|
|
26
|
+
return { ok: true, value: true };
|
|
27
|
+
case "0":
|
|
28
|
+
case "false":
|
|
29
|
+
return { ok: true, value: false };
|
|
30
|
+
default:
|
|
31
|
+
return { ok: false };
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
return { ok: false };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
export const Route = createFileRoute("/api/groups/$groupId")({
|
|
18
39
|
server: {
|
|
19
40
|
handlers: {
|
|
@@ -45,6 +66,22 @@ export const Route = createFileRoute("/api/groups/$groupId")({
|
|
|
45
66
|
|
|
46
67
|
return Response.json(updated);
|
|
47
68
|
},
|
|
69
|
+
DELETE: ({ params, request }: { params: { groupId: string }; request: Request }) => {
|
|
70
|
+
const deleteEvidence = readDeleteEvidenceFlag(request);
|
|
71
|
+
if (!deleteEvidence.ok) {
|
|
72
|
+
return Response.json(
|
|
73
|
+
{ error: "deleteEvidence must be one of true, false, 1, or 0" },
|
|
74
|
+
{ status: 400 },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const deleted = deleteGroup(params.groupId, { deleteEvidence: deleteEvidence.value });
|
|
79
|
+
if (deleted === null) {
|
|
80
|
+
return Response.json({ error: "Group not found" }, { status: 404 });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return Response.json(deleted);
|
|
84
|
+
},
|
|
48
85
|
},
|
|
49
86
|
},
|
|
50
87
|
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{R as s,j as e}from"./main-B1kdhY98.js";import{P as i}from"./ProxyViewerContainer-BxRaXhKR.js";function t(){const{sessionId:o}=s.useParams();return e.jsx(i,{initialSessionId:o},o)}export{t as component};
|