@clubnet/seedclub 0.2.19 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,21 +46,11 @@ curl -fsSL https://raw.githubusercontent.com/seedclub/seedclub-agent/main/instal
46
46
 
47
47
  `@clubnet/seedclub` is currently a private npm package. This auth is only for installing or updating the package from npm. It is separate from `/login` and `/connect` inside the app.
48
48
 
49
- Fast path:
50
-
51
- ```bash
52
- SEEDCLUB_NPM_TOKEN=YOUR_NPM_TOKEN curl -fsSL https://raw.githubusercontent.com/seedclub/seedclub-agent/main/install.sh | bash
53
- seedclub setup-auth
54
- ```
55
-
56
- Manual one-time `.npmrc` setup:
57
-
58
49
  ```bash
59
- echo "@clubnet:registry=https://registry.npmjs.org/" >> ~/.npmrc
60
- echo "//registry.npmjs.org/:_authToken=YOUR_NPM_TOKEN" >> ~/.npmrc
50
+ npm login
61
51
  ```
62
52
 
63
- Then `npm install -g @clubnet/seedclub` works.
53
+ Then `npm install -g @clubnet/seedclub` and `seedclub update` work.
64
54
 
65
55
  ## Core workflow
66
56
 
@@ -80,7 +70,6 @@ The normal interactive flow is:
80
70
  | `/connect` | Connect your Seed Club account |
81
71
  | `/seedclub` | Main menu — connect, inspect access, and jump into CRM/meetings/media/headlines workflows |
82
72
  | `/transcripts` | Export transcript VTT files with filters (date, person, time, output dir) |
83
- | `seedclub setup-auth` | Configure npm auth for npmjs private package access in `~/.npmrc` |
84
73
 
85
74
  Natural-language transcript retrieval is also supported (no slash command required). Examples: `download vibhu transcripts from 11am`, `i need transcripts for all guests on 11am last week`. Seed Club will run metadata-first export confirmation and then write VTT files.
86
75
 
@@ -147,7 +136,7 @@ seedclub pins versions in `package.json`:
147
136
 
148
137
  ```json
149
138
  {
150
- "version": "0.2.18",
139
+ "version": "0.2.19",
151
140
  "dependencies": {
152
141
  "@mariozechner/pi-coding-agent": "0.65.2"
153
142
  }
@@ -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",
@@ -451,32 +451,44 @@ export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean })
451
451
  loaderTimer.unref?.();
452
452
  tuiRef?.requestRender();
453
453
 
454
+ const todayPromise = fetchTodayOn11am();
454
455
  void Promise.all([
455
456
  getData(),
456
- withTimeout(fetchTodayOn11am(), TODAY_PREFETCH_TIMEOUT_MS, null),
457
+ withTimeout(todayPromise, TODAY_PREFETCH_TIMEOUT_MS, null),
457
458
  ]).then(([{ weather, market }, todayOn11am]) => {
458
459
  clearInterval(loaderTimer);
459
- const theme = ctx.ui.theme;
460
- const weatherLine = ` ${weather.icon} ${theme.fg("text", weather.temp)} ${theme.fg("dim", weather.condition)} ${theme.fg("dim", "·")} ${theme.fg("dim", weather.location)}`;
461
- const marketLine = ` ${market.map((quote) => formatQuote(quote, theme)).join(` ${theme.fg("dim", "·")} `)}`;
462
- const todayLines = renderTodayOn11amLines(todayOn11am, theme);
463
- uiState.todayOn11am = todayOn11am;
460
+ const renderReadyHeader = (today: TodayOn11am | null) => {
461
+ const theme = ctx.ui.theme;
462
+ const weatherLine = ` ${weather.icon} ${theme.fg("text", weather.temp)} ${theme.fg("dim", weather.condition)} ${theme.fg("dim", "·")} ${theme.fg("dim", weather.location)}`;
463
+ const marketLine = ` ${market.map((quote) => formatQuote(quote, theme)).join(` ${theme.fg("dim", "·")} `)}`;
464
+ const todayLines = renderTodayOn11amLines(today, theme);
465
+ uiState.todayOn11am = today;
466
+ headerLines = [
467
+ "",
468
+ renderTitle(theme),
469
+ "",
470
+ weatherLine,
471
+ marketLine,
472
+ "",
473
+ ...todayLines,
474
+ ...(todayLines.length ? [""] : []),
475
+ ...setupLines,
476
+ "",
477
+ ];
478
+ };
464
479
 
465
- headerLines = [
466
- "",
467
- renderTitle(theme),
468
- "",
469
- weatherLine,
470
- marketLine,
471
- "",
472
- ...todayLines,
473
- ...(todayLines.length ? [""] : []),
474
- ...setupLines,
475
- "",
476
- ];
480
+ renderReadyHeader(todayOn11am);
477
481
  uiState.ready = true;
478
482
  ctx.ui.setEditorText("");
479
483
  tuiRef?.requestRender();
484
+
485
+ if (!todayOn11am?.guests.length) {
486
+ void todayPromise.then((freshToday) => {
487
+ if (!freshToday?.guests.length) return;
488
+ renderReadyHeader(freshToday);
489
+ tuiRef?.requestRender();
490
+ }).catch(() => {});
491
+ }
480
492
  }).catch(() => {
481
493
  clearInterval(loaderTimer);
482
494
  headerLines = [
package/bin/cli.js CHANGED
@@ -2,120 +2,22 @@
2
2
  "use strict";
3
3
 
4
4
  const { execFileSync, spawn } = require("child_process");
5
- const { readFileSync, existsSync, writeFileSync, unlinkSync, mkdirSync } = require("fs");
5
+ const { readFileSync, existsSync, writeFileSync, mkdirSync } = require("fs");
6
6
  const { join, dirname, basename } = require("path");
7
7
  const { homedir } = require("os");
8
- const readline = require("readline");
9
8
 
10
9
  const SC_DIR = join(homedir(), ".seedclub", "agent");
11
10
  const VERSION_FILE = join(SC_DIR, ".seedclub-version");
12
11
  const SETTINGS_FILE = join(SC_DIR, "settings.json");
13
12
  const PI_MAIN_LAUNCHER = join(__dirname, "pi-main-launcher.js");
14
13
  process.title = "seedclub";
15
- const SEEDCLUB_ENV_EXCLUDE = new Set(["SEEDCLUB_NPM_TOKEN", "SEEDCLUB_PI_MAIN"]);
14
+ const SEEDCLUB_ENV_EXCLUDE = new Set(["SEEDCLUB_PI_MAIN"]);
16
15
 
17
16
  function printPrivateRegistryHint() {
18
17
  console.error("seedclub: install/update failed.");
19
- console.error("This package is private on npmjs.");
20
- console.error("Configure npm auth, then retry:");
21
- console.error(" seedclub setup-auth");
22
- console.error("Or run with an ephemeral token:");
23
- console.error(" SEEDCLUB_NPM_TOKEN=YOUR_NPM_TOKEN seedclub update");
24
- }
25
-
26
- function upsertLine(lines, prefix, value) {
27
- const idx = lines.findIndex((line) => line.trim().startsWith(prefix));
28
- if (idx >= 0) {
29
- lines[idx] = value;
30
- } else {
31
- lines.push(value);
32
- }
33
- }
34
-
35
- function readNpmrcLines(path) {
36
- if (!existsSync(path)) return [];
37
- const raw = readFileSync(path, "utf-8");
38
- if (!raw.trim()) return [];
39
- return raw.split(/\r?\n/).filter((line) => line.length > 0);
40
- }
41
-
42
- function askHidden(prompt) {
43
- return new Promise((resolve, reject) => {
44
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
45
- return reject(new Error("Interactive prompt requires a TTY"));
46
- }
47
- const rl = readline.createInterface({
48
- input: process.stdin,
49
- output: process.stdout,
50
- terminal: true,
51
- });
52
- const onData = (char) => {
53
- const ch = String(char);
54
- if (ch === "\n" || ch === "\r" || ch === "\u0004") {
55
- process.stdout.write("\n");
56
- } else if (ch === "\u0003") {
57
- process.stdout.write("^C\n");
58
- } else {
59
- process.stdout.write("\x1b[2K\x1b[200D" + prompt + "*".repeat(rl.line.length));
60
- }
61
- };
62
- process.stdin.on("data", onData);
63
- rl.question(prompt, (value) => {
64
- process.stdin.removeListener("data", onData);
65
- rl.close();
66
- resolve(value.trim());
67
- });
68
- rl.on("SIGINT", () => {
69
- process.stdin.removeListener("data", onData);
70
- rl.close();
71
- reject(new Error("Cancelled"));
72
- });
73
- });
74
- }
75
-
76
- async function setupAuth() {
77
- const npmrcPath = join(homedir(), ".npmrc");
78
- let token = process.env.SEEDCLUB_NPM_TOKEN || process.env.NPM_TOKEN || "";
79
- if (!token) {
80
- try {
81
- token = await askHidden("npm token: ");
82
- } catch (err) {
83
- console.error(`seedclub: ${err.message}`);
84
- process.exit(1);
85
- }
86
- }
87
- if (!token) {
88
- console.error("seedclub: no token provided.");
89
- process.exit(1);
90
- }
91
-
92
- const lines = readNpmrcLines(npmrcPath);
93
- upsertLine(lines, "@clubnet:registry=", "@clubnet:registry=https://registry.npmjs.org/");
94
- upsertLine(lines, "//registry.npmjs.org/:_authToken=", `//registry.npmjs.org/:_authToken=${token}`);
95
- writeFileSync(npmrcPath, lines.join("\n") + "\n", { mode: 0o600 });
96
-
97
- console.log(`Wrote npm auth config to ${npmrcPath}`);
98
- console.log("You can now run:");
99
- console.log(" npm install -g @clubnet/seedclub");
100
- }
101
-
102
- function withOptionalEphemeralNpmrc(run) {
103
- const token = process.env.SEEDCLUB_NPM_TOKEN || process.env.NPM_TOKEN;
104
- if (!token) return run([]);
105
-
106
- const npmrcPath = join(SC_DIR, ".npmrc.tmp");
107
- try {
108
- writeFileSync(
109
- npmrcPath,
110
- `@clubnet:registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=${token}\n`,
111
- { mode: 0o600 },
112
- );
113
- return run(["--userconfig", npmrcPath]);
114
- } finally {
115
- try {
116
- unlinkSync(npmrcPath);
117
- } catch {}
118
- }
18
+ console.error("If npm reports a private package or permission error, run:");
19
+ console.error(" npm login");
20
+ console.error(" seedclub update");
119
21
  }
120
22
 
121
23
  function findPackageRoot(fromFile, expectedName) {
@@ -533,19 +435,10 @@ if (cmd === "theme") {
533
435
  process.exit(0);
534
436
  }
535
437
 
536
- if (cmd === "setup-auth") {
537
- setupAuth().catch((err) => {
538
- console.error(`seedclub: ${err instanceof Error ? err.message : String(err)}`);
539
- process.exit(1);
540
- });
541
- }
542
-
543
438
  if (cmd === "update") {
544
439
  try {
545
- withOptionalEphemeralNpmrc((extraArgs) => {
546
- execFileSync("npm", ["install", "-g", "@clubnet/seedclub@latest", ...extraArgs], {
547
- stdio: "inherit",
548
- });
440
+ execFileSync("npm", ["install", "-g", "@clubnet/seedclub@latest"], {
441
+ stdio: "inherit",
549
442
  });
550
443
  } catch {
551
444
  printPrivateRegistryHint();
@@ -554,8 +447,7 @@ if (cmd === "update") {
554
447
  process.exit(0);
555
448
  }
556
449
 
557
- if (cmd !== "setup-auth") {
558
- // ── Resolve pi binary ───────────────────────────────────────────────────
450
+ // ── Resolve pi binary ───────────────────────────────────────────────────
559
451
 
560
452
  let piBin;
561
453
  let piEntry;
@@ -618,4 +510,3 @@ if (cmd !== "setup-auth") {
618
510
  }
619
511
  });
620
512
  });
621
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubnet/seedclub",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "description": "A branded command-line agent wrapper around pi, with integrated Seed Club commands, tools, and app actions",
5
5
  "license": "MIT",
6
6
  "repository": {