@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.
Files changed (34) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-tIUf2EJm.js → CompareDrawer-ClTJxdoU.js} +1 -1
  3. package/.output/public/assets/{ProxyViewerContainer-BxRaXhKR.js → ProxyViewerContainer--Pes01q2.js} +28 -28
  4. package/.output/public/assets/{ReplayDialog-D6v1xcuC.js → ReplayDialog-iuzfh85b.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-D6PniTlx.js → RequestAnatomy-FZnnP9Gx.js} +1 -1
  6. package/.output/public/assets/{ResponseView-Sx8PjuvU.js → ResponseView-MZ6ZWvbL.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-D-Rbfnxh.js → StreamingChunkSequence-B8mZMdzs.js} +1 -1
  8. package/.output/public/assets/_sessionId-Bc26Rfxs.js +1 -0
  9. package/.output/public/assets/index-C6AiRPsJ.js +1 -0
  10. package/.output/public/assets/index-C_8PFAm2.css +1 -0
  11. package/.output/public/assets/{main-B1kdhY98.js → main-B797Hnge.js} +2 -2
  12. package/.output/server/_libs/lucide-react.mjs +9 -9
  13. package/.output/server/{_sessionId-BceHFKf4.mjs → _sessionId-DD05qd8d.mjs} +2 -2
  14. package/.output/server/_ssr/{CompareDrawer-uSxK5Ei5.mjs → CompareDrawer-oGOG5-kQ.mjs} +3 -3
  15. package/.output/server/_ssr/{ProxyViewerContainer-_npQ_f2i.mjs → ProxyViewerContainer-Bbl5tqT1.mjs} +205 -25
  16. package/.output/server/_ssr/{ReplayDialog-BHAzSLCT.mjs → ReplayDialog-BegfwWIy.mjs} +3 -3
  17. package/.output/server/_ssr/{RequestAnatomy-DjlZLXBd.mjs → RequestAnatomy-BW_X2DyZ.mjs} +3 -3
  18. package/.output/server/_ssr/{ResponseView-BuDEJ3fl.mjs → ResponseView-Bx_uJ4RS.mjs} +3 -3
  19. package/.output/server/_ssr/{StreamingChunkSequence-DFcu0bD8.mjs → StreamingChunkSequence-CQp5QQnB.mjs} +3 -3
  20. package/.output/server/_ssr/{index-Bw6Fw33c.mjs → index-ODSKeDD8.mjs} +2 -2
  21. package/.output/server/_ssr/index.mjs +2 -2
  22. package/.output/server/_ssr/{router-D90tMCb3.mjs → router-QcI4GOvo.mjs} +138 -39
  23. package/.output/server/{_tanstack-start-manifest_v-BVM4AUlx.mjs → _tanstack-start-manifest_v-dEujKJ3-.mjs} +1 -1
  24. package/.output/server/index.mjs +63 -63
  25. package/package.json +1 -1
  26. package/src/components/groups/GroupsDialog.tsx +241 -17
  27. package/src/lib/groupContract.ts +13 -0
  28. package/src/mcp/server.ts +16 -0
  29. package/src/mcp/toolHandlers.ts +22 -0
  30. package/src/proxy/groupStore.ts +52 -2
  31. package/src/routes/api/groups.$groupId.ts +38 -1
  32. package/.output/public/assets/_sessionId-BhNTuZ31.js +0 -1
  33. package/.output/public/assets/index-Byk60-jA.css +0 -1
  34. 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 { Check, Copy, Download, ExternalLink, Layers, Loader2, RefreshCw } from "lucide-react";
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={setSelectedGroupId}
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
- <Button
479
- type="button"
480
- variant="default"
481
- size="sm"
482
- className="shrink-0"
483
- disabled={isExporting}
484
- onClick={() => void onExport(group)}
485
- >
486
- {isExporting ? (
487
- <Loader2 className="size-3.5 animate-spin" />
488
- ) : (
489
- <Download className="size-3.5" />
490
- )}
491
- Export evidence
492
- </Button>
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">
@@ -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",
@@ -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;
@@ -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};