@clubnet/seedclub 0.2.20 → 0.2.22
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.
|
@@ -701,6 +701,231 @@ async function listShowRecordings(args: {
|
|
|
701
701
|
}
|
|
702
702
|
}
|
|
703
703
|
|
|
704
|
+
function todayUtcDate() {
|
|
705
|
+
return new Date().toISOString().slice(0, 10);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function normalizeDateCutoff(value?: string) {
|
|
709
|
+
if (!value || typeof value !== "string") return todayUtcDate();
|
|
710
|
+
const trimmed = value.trim();
|
|
711
|
+
if (!trimmed) return todayUtcDate();
|
|
712
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) return trimmed;
|
|
713
|
+
const parsed = new Date(trimmed);
|
|
714
|
+
if (!Number.isFinite(parsed.getTime())) return todayUtcDate();
|
|
715
|
+
return parsed.toISOString().slice(0, 10);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function normalizeGuestNeedle(value?: string | null) {
|
|
719
|
+
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function includesNeedle(haystack: unknown, needle: string) {
|
|
723
|
+
if (!needle) return false;
|
|
724
|
+
if (typeof haystack !== "string") return false;
|
|
725
|
+
return haystack.trim().toLowerCase().includes(needle);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function isOnOrBeforeDate(value: string | null | undefined, beforeDate: string) {
|
|
729
|
+
if (!value || typeof value !== "string") return false;
|
|
730
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
731
|
+
return value <= beforeDate;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function transcriptMeetingPartyId(row: any) {
|
|
735
|
+
return (
|
|
736
|
+
row?.meeting?.primary_contact?.party?.id ??
|
|
737
|
+
row?.meeting?.primary_party?.id ??
|
|
738
|
+
row?.meeting?.primary_party_id ??
|
|
739
|
+
null
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function transcriptMeetingGuestLabel(row: any) {
|
|
744
|
+
return (
|
|
745
|
+
row?.transcript_for ??
|
|
746
|
+
row?.meeting?.primary_contact?.party?.display_name ??
|
|
747
|
+
row?.meeting?.primary_contact?.person?.full_name ??
|
|
748
|
+
personFromMeetingTitle(row?.meeting?.title) ??
|
|
749
|
+
null
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function sortByEventDateDesc<T>(rows: T[], eventDatePicker: (row: T) => string | null | undefined) {
|
|
754
|
+
return [...rows].sort((a, b) => {
|
|
755
|
+
const aDate = eventDatePicker(a) ?? "";
|
|
756
|
+
const bDate = eventDatePicker(b) ?? "";
|
|
757
|
+
if (aDate === bDate) return 0;
|
|
758
|
+
return aDate < bDate ? 1 : -1;
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function findLatestGuestTranscript(args: {
|
|
763
|
+
programSlug: string;
|
|
764
|
+
guest: string;
|
|
765
|
+
beforeDate?: string;
|
|
766
|
+
needVtt?: boolean;
|
|
767
|
+
mode?: "compact" | "detail";
|
|
768
|
+
}) {
|
|
769
|
+
try {
|
|
770
|
+
const beforeDate = normalizeDateCutoff(args.beforeDate);
|
|
771
|
+
const needVtt = args.needVtt === true;
|
|
772
|
+
const mode = args.mode === "detail" ? "detail" : "compact";
|
|
773
|
+
const guestNeedle = normalizeGuestNeedle(args.guest);
|
|
774
|
+
if (!guestNeedle) {
|
|
775
|
+
return {
|
|
776
|
+
query: {
|
|
777
|
+
programSlug: args.programSlug,
|
|
778
|
+
guest: args.guest,
|
|
779
|
+
beforeDate,
|
|
780
|
+
needVtt,
|
|
781
|
+
mode,
|
|
782
|
+
},
|
|
783
|
+
found: false,
|
|
784
|
+
result: null,
|
|
785
|
+
reason: "Guest is required.",
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const guestProfile = await getGuestProfile({
|
|
790
|
+
programSlug: args.programSlug,
|
|
791
|
+
search: args.guest,
|
|
792
|
+
includeCrm: false,
|
|
793
|
+
});
|
|
794
|
+
const resolvedPartyId = guestProfile?.contact?.partyId ?? null;
|
|
795
|
+
const resolvedGuestName = guestProfile?.contact?.displayName ?? args.guest;
|
|
796
|
+
const resolvedGuestEmail = guestProfile?.contact?.email ?? null;
|
|
797
|
+
|
|
798
|
+
let mediaCandidate: any = null;
|
|
799
|
+
if (resolvedPartyId) {
|
|
800
|
+
const mediaResponse = await api.get<any>(`/programs/${args.programSlug}/media/assets`, {
|
|
801
|
+
asset_kind: "full_conversation",
|
|
802
|
+
party_id: resolvedPartyId,
|
|
803
|
+
});
|
|
804
|
+
const mediaRows = Array.isArray(mediaResponse?.data) ? mediaResponse.data : [];
|
|
805
|
+
const mediaMatches = sortByEventDateDesc(mediaRows, (row: any) => row?.asset?.event_date).filter((row: any) =>
|
|
806
|
+
isOnOrBeforeDate(row?.asset?.event_date, beforeDate),
|
|
807
|
+
);
|
|
808
|
+
mediaCandidate = mediaMatches[0] ?? null;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (mediaCandidate) {
|
|
812
|
+
const hasText = !!(mediaCandidate?.asset?.transcript_raw && String(mediaCandidate.asset.transcript_raw).trim());
|
|
813
|
+
const hasVtt = !!(mediaCandidate?.asset?.transcript_vtt && String(mediaCandidate.asset.transcript_vtt).trim());
|
|
814
|
+
const compactResult = {
|
|
815
|
+
query: {
|
|
816
|
+
programSlug: args.programSlug,
|
|
817
|
+
guest: args.guest,
|
|
818
|
+
beforeDate,
|
|
819
|
+
needVtt,
|
|
820
|
+
mode,
|
|
821
|
+
},
|
|
822
|
+
found: needVtt ? hasVtt : hasText || hasVtt,
|
|
823
|
+
result: {
|
|
824
|
+
guest: resolvedGuestName,
|
|
825
|
+
partyId: resolvedPartyId,
|
|
826
|
+
eventDate: mediaCandidate?.asset?.event_date ?? null,
|
|
827
|
+
meetingId: mediaCandidate?.asset?.meeting_id ?? null,
|
|
828
|
+
transcriptType: "full_conversation_asset",
|
|
829
|
+
hasText,
|
|
830
|
+
hasVtt,
|
|
831
|
+
exportReady: hasVtt,
|
|
832
|
+
source: "media_asset",
|
|
833
|
+
},
|
|
834
|
+
reason: needVtt && !hasVtt ? "Latest matching appearance has transcript text but no VTT file." : null,
|
|
835
|
+
};
|
|
836
|
+
if (mode === "detail") {
|
|
837
|
+
return {
|
|
838
|
+
...compactResult,
|
|
839
|
+
detail: {
|
|
840
|
+
resolvedGuestEmail,
|
|
841
|
+
mediaAssetId: mediaCandidate?.asset?.id ?? null,
|
|
842
|
+
mediaWorkflowStatus: mediaCandidate?.asset?.workflow_status ?? null,
|
|
843
|
+
mediaVisibility: mediaCandidate?.asset?.visibility ?? null,
|
|
844
|
+
},
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return compactResult;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const transcriptResponse = await api.get<any>("/meetings/transcripts", {
|
|
851
|
+
program_slug: args.programSlug,
|
|
852
|
+
limit: MAX_TRANSCRIPT_LIMIT,
|
|
853
|
+
});
|
|
854
|
+
const transcriptRows = shapeTranscriptList(transcriptResponse, false, false)?.data ?? [];
|
|
855
|
+
const matchingTranscriptRows = sortByEventDateDesc(transcriptRows, (row: any) => row?.transcript?.event_date).filter((row: any) => {
|
|
856
|
+
const rowEventDate = row?.transcript?.event_date;
|
|
857
|
+
if (!isOnOrBeforeDate(rowEventDate, beforeDate)) return false;
|
|
858
|
+
if (resolvedPartyId && transcriptMeetingPartyId(row) === resolvedPartyId) return true;
|
|
859
|
+
return includesNeedle(transcriptMeetingGuestLabel(row), guestNeedle);
|
|
860
|
+
});
|
|
861
|
+
const transcriptCandidate = matchingTranscriptRows[0] ?? null;
|
|
862
|
+
|
|
863
|
+
if (transcriptCandidate) {
|
|
864
|
+
const hasText = transcriptCandidate?.transcript?.has_text === true;
|
|
865
|
+
const hasVtt = transcriptCandidate?.transcript?.has_vtt === true;
|
|
866
|
+
const compactResult = {
|
|
867
|
+
query: {
|
|
868
|
+
programSlug: args.programSlug,
|
|
869
|
+
guest: args.guest,
|
|
870
|
+
beforeDate,
|
|
871
|
+
needVtt,
|
|
872
|
+
mode,
|
|
873
|
+
},
|
|
874
|
+
found: needVtt ? hasVtt : hasText || hasVtt,
|
|
875
|
+
result: {
|
|
876
|
+
guest: transcriptMeetingGuestLabel(transcriptCandidate) ?? resolvedGuestName,
|
|
877
|
+
partyId: transcriptMeetingPartyId(transcriptCandidate) ?? resolvedPartyId,
|
|
878
|
+
eventDate: transcriptCandidate?.transcript?.event_date ?? null,
|
|
879
|
+
meetingId: transcriptCandidate?.transcript?.meeting_id ?? null,
|
|
880
|
+
transcriptType: transcriptCandidate?.transcript_type ?? (transcriptCandidate?.transcript?.meeting_id ? "meeting" : "conversation"),
|
|
881
|
+
hasText,
|
|
882
|
+
hasVtt,
|
|
883
|
+
exportReady: hasVtt,
|
|
884
|
+
source: "meetings_transcripts",
|
|
885
|
+
},
|
|
886
|
+
reason: needVtt && !hasVtt ? "Latest matching transcript row has text but no VTT file." : null,
|
|
887
|
+
};
|
|
888
|
+
if (mode === "detail") {
|
|
889
|
+
return {
|
|
890
|
+
...compactResult,
|
|
891
|
+
detail: {
|
|
892
|
+
resolvedGuestEmail,
|
|
893
|
+
transcriptId: transcriptCandidate?.transcript?.id ?? null,
|
|
894
|
+
transcriptStatus: transcriptCandidate?.transcript?.status ?? null,
|
|
895
|
+
scannedRows: transcriptRows.length,
|
|
896
|
+
matchedRows: matchingTranscriptRows.length,
|
|
897
|
+
},
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
return compactResult;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return {
|
|
904
|
+
query: {
|
|
905
|
+
programSlug: args.programSlug,
|
|
906
|
+
guest: args.guest,
|
|
907
|
+
beforeDate,
|
|
908
|
+
needVtt,
|
|
909
|
+
mode,
|
|
910
|
+
},
|
|
911
|
+
found: false,
|
|
912
|
+
result: null,
|
|
913
|
+
reason: "No transcript rows found for this guest before the requested date.",
|
|
914
|
+
...(mode === "detail"
|
|
915
|
+
? {
|
|
916
|
+
detail: {
|
|
917
|
+
resolvedPartyId,
|
|
918
|
+
resolvedGuestEmail,
|
|
919
|
+
},
|
|
920
|
+
}
|
|
921
|
+
: {}),
|
|
922
|
+
};
|
|
923
|
+
} catch (error) {
|
|
924
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
925
|
+
throw error;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
704
929
|
function dateFromTimestamp(value: string | null | undefined) {
|
|
705
930
|
if (!value || typeof value !== "string") return null;
|
|
706
931
|
const match = value.match(/\d{4}-\d{2}-\d{2}/);
|
|
@@ -915,6 +1140,43 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
915
1140
|
execute: wrapExecute(getGuestProfile),
|
|
916
1141
|
});
|
|
917
1142
|
|
|
1143
|
+
pi.registerTool({
|
|
1144
|
+
name: "seedclub_find_latest_guest_transcript",
|
|
1145
|
+
label: "Find Latest Guest Transcript",
|
|
1146
|
+
description:
|
|
1147
|
+
"Find the latest transcript context for a guest before a cutoff date with compact output. This orchestrates guest resolution, full-conversation assets, and transcript metadata so callers avoid multi-step JSON-heavy discovery.",
|
|
1148
|
+
parameters: Type.Object({
|
|
1149
|
+
programSlug: Type.String({ description: "Program slug" }),
|
|
1150
|
+
guest: Type.String({ description: "Guest name/email search string" }),
|
|
1151
|
+
beforeDate: Type.Optional(
|
|
1152
|
+
Type.String({
|
|
1153
|
+
description: "Optional inclusive YYYY-MM-DD cutoff. Defaults to today.",
|
|
1154
|
+
}),
|
|
1155
|
+
),
|
|
1156
|
+
needVtt: Type.Optional(
|
|
1157
|
+
Type.Boolean({
|
|
1158
|
+
description: "Set true when caller specifically needs VTT availability.",
|
|
1159
|
+
}),
|
|
1160
|
+
),
|
|
1161
|
+
mode: Type.Optional(
|
|
1162
|
+
Type.Union([Type.Literal("compact"), Type.Literal("detail")], {
|
|
1163
|
+
description: "Response mode. Defaults to compact.",
|
|
1164
|
+
}),
|
|
1165
|
+
),
|
|
1166
|
+
}),
|
|
1167
|
+
execute: wrapExecute(findLatestGuestTranscript),
|
|
1168
|
+
renderResult(result: any, _args: any, theme: any) {
|
|
1169
|
+
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
1170
|
+
const found = result.details?.found === true;
|
|
1171
|
+
if (!found) {
|
|
1172
|
+
return new Text(theme.fg("dim", result.details?.reason || "No matching transcript found"), 0, 0);
|
|
1173
|
+
}
|
|
1174
|
+
const row = result.details?.result ?? {};
|
|
1175
|
+
const status = `${row?.eventDate ?? "unknown date"} · ${row?.guest ?? "guest"} · text:${row?.hasText ? "yes" : "no"} · vtt:${row?.hasVtt ? "yes" : "no"}`;
|
|
1176
|
+
return new Text(theme.fg("muted", status), 0, 0);
|
|
1177
|
+
},
|
|
1178
|
+
});
|
|
1179
|
+
|
|
918
1180
|
pi.registerTool({
|
|
919
1181
|
name: "seedclub_prepare_clip_packet",
|
|
920
1182
|
label: "Prepare Clip Packet",
|
|
@@ -14,12 +14,13 @@ class SurfaceEditor extends CustomEditor {
|
|
|
14
14
|
private readonly getPalette: () => Palette;
|
|
15
15
|
private static readonly RESET = "\x1b[0m";
|
|
16
16
|
private static readonly INSET = "";
|
|
17
|
+
private static readonly CONTENT_INSET = 2;
|
|
17
18
|
private static readonly PLACEHOLDER = "/ for menu";
|
|
18
19
|
|
|
19
20
|
constructor(tui: TUI, theme: EditorTheme, kb: KeybindingsManager, getPalette: () => Palette) {
|
|
20
21
|
super(tui, theme, kb);
|
|
21
22
|
this.getPalette = getPalette;
|
|
22
|
-
|
|
23
|
+
super.setPaddingX(SurfaceEditor.CONTENT_INSET);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
render(width: number): string[] {
|
|
@@ -47,6 +48,10 @@ class SurfaceEditor extends CustomEditor {
|
|
|
47
48
|
super.invalidate();
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
override setPaddingX(_padding: number): void {
|
|
52
|
+
super.setPaddingX(SurfaceEditor.CONTENT_INSET);
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
override handleInput(data: string): void {
|
|
51
56
|
if (!uiState.ready) {
|
|
52
57
|
// Keep app-level interrupts available while startup UI is loading.
|
package/package.json
CHANGED