@clubnet/seedclub 0.2.31 → 0.2.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.
@@ -80,8 +80,10 @@ async function apiRequest<T>(endpoint: string, options: RequestOptions = {}): Pr
80
80
 
81
81
  const response = await fetch(url.toString(), {
82
82
  method,
83
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
84
- body: body ? JSON.stringify(body) : undefined,
83
+ headers: body === undefined
84
+ ? { Authorization: `Bearer ${token}` }
85
+ : { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
86
+ body: body === undefined ? undefined : JSON.stringify(body),
85
87
  });
86
88
 
87
89
  const text = await response.text();
@@ -16,9 +16,9 @@ export const AUTH_GATE_ALLOWED_COMMANDS = new Set([
16
16
  ]);
17
17
 
18
18
  const state: SeedclubAuthGateState = {
19
- status: "auth_required",
19
+ status: "auth_in_progress",
20
20
  authUrl: null,
21
- message: "Seed Club sign-in is required before /login or /model.",
21
+ message: "Checking Seed Club access...",
22
22
  error: null,
23
23
  };
24
24
 
@@ -139,15 +139,13 @@ export default function (pi: ExtensionAPI) {
139
139
  }
140
140
 
141
141
  await applyConnectedStatus(ctx, user);
142
+ markAuthComplete(getPostAuthInstruction(ctx));
142
143
  return user;
143
144
  }
144
145
 
145
146
  async function ensureSeedclubAuthenticated(ctx: any): Promise<boolean> {
146
147
  const existing = await validateCurrentCredential(ctx);
147
- if (existing) {
148
- markAuthComplete(getPostAuthInstruction(ctx));
149
- return true;
150
- }
148
+ if (existing) return true;
151
149
  return connect(undefined, ctx, { autoStart: true });
152
150
  }
153
151
 
@@ -195,13 +195,22 @@ export function registerCrmTools(pi: ExtensionAPI) {
195
195
  renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_list_program_contacts"), (args) => args?.programSlug || undefined),
196
196
  renderResult(result: any, _args: any, theme: any) {
197
197
  if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
198
+ const successLabel = getToolSuccessLabel("seedclub_list_program_contacts");
198
199
  const details = result.details ?? {};
199
200
  const rows = Array.isArray(details?.data) ? details.data : [];
200
201
  const program = details?.program?.slug ?? details?.program?.name ?? "program";
201
- if (!rows.length) return new Text(theme.fg("dim", `No contacts found for ${program}`), 0, 0);
202
+ if (!rows.length) {
203
+ return new Text(
204
+ `${theme.fg("success", "✓")} ${theme.fg("muted", successLabel)}${theme.fg("dim", ` · No contacts found for ${program}`)}`,
205
+ 0,
206
+ 0,
207
+ );
208
+ }
202
209
 
203
210
  const shown = rows.slice(0, 8);
204
- let text = theme.fg("muted", `${rows.length} contact${rows.length === 1 ? "" : "s"} loaded for ${program}`);
211
+ let text =
212
+ `${theme.fg("success", "✓")} ${theme.fg("muted", successLabel)}` +
213
+ theme.fg("dim", ` · ${rows.length} contact${rows.length === 1 ? "" : "s"} loaded for ${program}`);
205
214
  for (const row of shown) {
206
215
  const name = row?.party?.display_name ?? row?.person?.full_name ?? "Unknown";
207
216
  const org = row?.roles?.[0]?.organization_name ?? row?.organization?.name ?? null;
@@ -222,11 +222,20 @@ export function registerMediaTools(pi: ExtensionAPI) {
222
222
  renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_list_program_media_assets"), (args) => args?.programSlug || undefined),
223
223
  renderResult(result: any, { expanded }: any, theme: any) {
224
224
  if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
225
+ const successLabel = getToolSuccessLabel("seedclub_list_program_media_assets");
225
226
  const rows = Array.isArray(result.details?.data) ? result.details.data : [];
226
227
  const program = result.details?.program?.slug ?? result.details?.program?.name ?? "program";
227
- if (!rows.length) return new Text(theme.fg("dim", `No media assets for ${program}`), 0, 0);
228
+ if (!rows.length) {
229
+ return new Text(
230
+ `${theme.fg("success", "✓")} ${theme.fg("muted", successLabel)}${theme.fg("dim", ` · No media assets for ${program}`)}`,
231
+ 0,
232
+ 0,
233
+ );
234
+ }
228
235
 
229
- let text = theme.fg("muted", `${rows.length} media asset${rows.length === 1 ? "" : "s"} for ${program}`);
236
+ let text =
237
+ `${theme.fg("success", "✓")} ${theme.fg("muted", successLabel)}` +
238
+ theme.fg("dim", ` · ${rows.length} media asset${rows.length === 1 ? "" : "s"} for ${program}`);
230
239
  for (const row of rows.slice(0, expanded ? 12 : 6)) {
231
240
  const asset = row?.asset ?? row;
232
241
  const date = asset?.event_date ?? "unknown date";
@@ -259,14 +268,26 @@ export function registerMediaTools(pi: ExtensionAPI) {
259
268
  renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_get_program_media_asset"), (args) => args?.assetId || undefined),
260
269
  renderResult(result: any, _args: any, theme: any) {
261
270
  if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
271
+ const successLabel = getToolSuccessLabel("seedclub_get_program_media_asset");
262
272
  const asset = result.details?.asset ?? null;
263
- if (!asset) return new Text(theme.fg("dim", "No media asset found"), 0, 0);
273
+ if (!asset) {
274
+ return new Text(
275
+ `${theme.fg("success", "✓")} ${theme.fg("muted", successLabel)}${theme.fg("dim", " · No media asset found")}`,
276
+ 0,
277
+ 0,
278
+ );
279
+ }
264
280
  const date = asset?.event_date ?? "unknown date";
265
281
  const title = asset?.title ?? asset?.file_name ?? "untitled";
266
282
  const kind = asset?.asset_kind ?? "asset";
267
283
  const hasText = asset?.has_transcript_text ? "text" : "no-text";
268
284
  const hasVtt = asset?.has_transcript_vtt ? "vtt" : "no-vtt";
269
- return new Text(theme.fg("muted", `${date} - ${title} [${kind}; ${hasText}; ${hasVtt}]`), 0, 0);
285
+ return new Text(
286
+ `${theme.fg("success", "✓")} ${theme.fg("muted", successLabel)}` +
287
+ theme.fg("dim", ` · ${date} - ${title} [${kind}; ${hasText}; ${hasVtt}]`),
288
+ 0,
289
+ 0,
290
+ );
270
291
  },
271
292
  });
272
293
 
@@ -39,6 +39,26 @@ const GUEST_ROSTER_DEFAULT_FIELDS = [
39
39
  "organizationRole",
40
40
  ];
41
41
 
42
+ function summarizeCount(count: number | undefined, noun: string) {
43
+ return typeof count === "number" ? `${count} ${noun}${count === 1 ? "" : "s"}` : undefined;
44
+ }
45
+
46
+ function countFromArrays(...values: any[]) {
47
+ for (const value of values) {
48
+ if (Array.isArray(value)) return value.length;
49
+ }
50
+ return undefined;
51
+ }
52
+
53
+ function firstNonEmptyString(...values: unknown[]) {
54
+ for (const value of values) {
55
+ if (typeof value === "string" && value.trim()) {
56
+ return value.trim();
57
+ }
58
+ }
59
+ return undefined;
60
+ }
61
+
42
62
  function normalizeLimit(limit: number | undefined, defaults: { fallback: number; max: number }) {
43
63
  if (!Number.isFinite(limit)) return defaults.fallback;
44
64
  return Math.max(1, Math.min(defaults.max, Math.trunc(limit!)));
@@ -1423,7 +1443,7 @@ async function findCommonMeetingAvailability(args: {
1423
1443
  }
1424
1444
  }
1425
1445
 
1426
- async function bookMeetingFull(args: {
1446
+ async function bookMeeting(args: {
1427
1447
  startsAt: string;
1428
1448
  endsAt?: string | null;
1429
1449
  durationMinutes?: number | null;
@@ -1440,7 +1460,7 @@ async function bookMeetingFull(args: {
1440
1460
  }) {
1441
1461
  try {
1442
1462
  const resolvedEndsAt = resolveBookingEndsAt(args.startsAt, args.endsAt, args.durationMinutes);
1443
- return await api.post<any>("/meetings/book-full", {
1463
+ return await api.post<any>("/meetings/book", {
1444
1464
  calendar_account_id: args.calendarAccountId,
1445
1465
  program_slug: args.programSlug,
1446
1466
  starts_at: args.startsAt,
@@ -1463,6 +1483,44 @@ async function bookMeetingFull(args: {
1463
1483
  }
1464
1484
  }
1465
1485
 
1486
+ async function rescheduleMeeting(args: {
1487
+ meetingId: string;
1488
+ startsAt: string;
1489
+ endsAt?: string | null;
1490
+ durationMinutes?: number | null;
1491
+ timeZone?: string;
1492
+ title?: string | null;
1493
+ description?: string | null;
1494
+ calendarAccountId?: string | null;
1495
+ }) {
1496
+ try {
1497
+ const resolvedEndsAt = resolveBookingEndsAt(args.startsAt, args.endsAt, args.durationMinutes);
1498
+ return await api.post<any>(`/meetings/${args.meetingId}/reschedule`, {
1499
+ calendar_account_id: args.calendarAccountId,
1500
+ starts_at: args.startsAt,
1501
+ ends_at: resolvedEndsAt,
1502
+ duration_minutes: args.durationMinutes,
1503
+ time_zone: args.timeZone,
1504
+ title: args.title,
1505
+ description: args.description,
1506
+ });
1507
+ } catch (error) {
1508
+ if (error instanceof ApiError) return { error: error.message, status: error.status };
1509
+ throw error;
1510
+ }
1511
+ }
1512
+
1513
+ async function cancelMeeting(args: { meetingId: string; calendarAccountId?: string | null }) {
1514
+ try {
1515
+ return await api.post<any>(`/meetings/${args.meetingId}/cancel`, {
1516
+ calendar_account_id: args.calendarAccountId,
1517
+ });
1518
+ } catch (error) {
1519
+ if (error instanceof ApiError) return { error: error.message, status: error.status };
1520
+ throw error;
1521
+ }
1522
+ }
1523
+
1466
1524
  async function updateMeeting(args: {
1467
1525
  meetingId: string;
1468
1526
  startsAt?: string | null;
@@ -1512,6 +1570,10 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1512
1570
  ),
1513
1571
  }),
1514
1572
  execute: wrapExecute(searchPeopleForBooking),
1573
+ renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_search_people"), (args) => args?.query || undefined),
1574
+ renderResult: makeProgressResultRenderer(getToolSuccessLabel("seedclub_search_people"), (details) =>
1575
+ summarizeCount(countFromArrays(details?.data), "match"),
1576
+ ),
1515
1577
  });
1516
1578
 
1517
1579
  pi.registerTool({
@@ -1527,6 +1589,10 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1527
1589
  timeZone: Type.Optional(Type.String({ description: "Optional timezone used to interpret the requested date. Defaults to America/New_York." })),
1528
1590
  }),
1529
1591
  execute: wrapExecute(listMeetingAvailabilitySlots),
1592
+ renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_list_meeting_availability"), (args) => args?.date || undefined),
1593
+ renderResult: makeProgressResultRenderer(getToolSuccessLabel("seedclub_list_meeting_availability"), (details) =>
1594
+ summarizeCount(countFromArrays(details?.slots), "slot"),
1595
+ ),
1530
1596
  });
1531
1597
 
1532
1598
  pi.registerTool({
@@ -1538,6 +1604,10 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1538
1604
  programSlug: Type.Optional(Type.String({ description: "Program slug. Defaults to 11am on the server." })),
1539
1605
  }),
1540
1606
  execute: wrapExecute(listMeetingCalendars),
1607
+ renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_list_meeting_calendars"), (args) => args?.programSlug || undefined),
1608
+ renderResult: makeProgressResultRenderer(getToolSuccessLabel("seedclub_list_meeting_calendars"), (details) =>
1609
+ summarizeCount(countFromArrays(details?.data, details?.calendars), "calendar"),
1610
+ ),
1541
1611
  });
1542
1612
 
1543
1613
  pi.registerTool({
@@ -1554,6 +1624,10 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1554
1624
  query: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Optional Google Calendar search query." })),
1555
1625
  }),
1556
1626
  execute: wrapExecute(listPersonalCalendarEvents),
1627
+ renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_list_personal_calendar_events"), (args) => args?.accountId || args?.query || undefined),
1628
+ renderResult: makeProgressResultRenderer(getToolSuccessLabel("seedclub_list_personal_calendar_events"), (details) =>
1629
+ summarizeCount(countFromArrays(details?.data, details?.events, details?.items), "event"),
1630
+ ),
1557
1631
  });
1558
1632
 
1559
1633
  pi.registerTool({
@@ -1571,6 +1645,10 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1571
1645
  attendeeEmails: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Null()])),
1572
1646
  }),
1573
1647
  execute: wrapExecute(createPersonalCalendarEvent),
1648
+ renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_create_personal_calendar_event"), (args) => args?.summary || undefined),
1649
+ renderResult: makeProgressResultRenderer(getToolSuccessLabel("seedclub_create_personal_calendar_event"), (details) =>
1650
+ firstNonEmptyString(details?.event?.id, details?.id, details?.event_id, details?.eventId, details?.summary),
1651
+ ),
1574
1652
  });
1575
1653
 
1576
1654
  pi.registerTool({
@@ -1589,6 +1667,10 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1589
1667
  attendeeEmails: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Null()])),
1590
1668
  }),
1591
1669
  execute: wrapExecute(updatePersonalCalendarEvent),
1670
+ renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_update_personal_calendar_event"), (args) => args?.eventId || undefined),
1671
+ renderResult: makeProgressResultRenderer(getToolSuccessLabel("seedclub_update_personal_calendar_event"), (details, args) =>
1672
+ firstNonEmptyString(details?.event?.id, details?.id, details?.event_id, details?.eventId, args?.eventId),
1673
+ ),
1592
1674
  });
1593
1675
 
1594
1676
  pi.registerTool({
@@ -1601,6 +1683,10 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1601
1683
  eventId: Type.String({ description: "Google Calendar event id." }),
1602
1684
  }),
1603
1685
  execute: wrapExecute(deletePersonalCalendarEvent),
1686
+ renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_delete_personal_calendar_event"), (args) => args?.eventId || undefined),
1687
+ renderResult: makeProgressResultRenderer(getToolSuccessLabel("seedclub_delete_personal_calendar_event"), (_details, args) =>
1688
+ firstNonEmptyString(args?.eventId, "deleted"),
1689
+ ),
1604
1690
  });
1605
1691
 
1606
1692
  pi.registerTool({
@@ -1628,6 +1714,12 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1628
1714
  ),
1629
1715
  }),
1630
1716
  execute: wrapExecute(findCommonMeetingAvailability),
1717
+ renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_find_common_meeting_availability"), (args) =>
1718
+ summarizeCount(Array.isArray(args?.calendarAccountIds) ? args.calendarAccountIds.length : undefined, "calendar"),
1719
+ ),
1720
+ renderResult: makeProgressResultRenderer(getToolSuccessLabel("seedclub_find_common_meeting_availability"), (details) =>
1721
+ summarizeCount(countFromArrays(details?.slots, details?.data, details?.availability, details?.matches), "slot"),
1722
+ ),
1631
1723
  });
1632
1724
 
1633
1725
  pi.registerTool({
@@ -1755,13 +1847,23 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1755
1847
  renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_find_latest_guest_transcript"), (args) => args?.guest || undefined),
1756
1848
  renderResult(result: any, _args: any, theme: any) {
1757
1849
  if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
1850
+ const successLabel = getToolSuccessLabel("seedclub_find_latest_guest_transcript");
1758
1851
  const found = result.details?.found === true;
1759
1852
  if (!found) {
1760
- return new Text(theme.fg("dim", result.details?.reason || "No matching transcript found"), 0, 0);
1853
+ return new Text(
1854
+ `${theme.fg("success", "✓")} ${theme.fg("muted", successLabel)}` +
1855
+ theme.fg("dim", ` · ${result.details?.reason || "No matching transcript found"}`),
1856
+ 0,
1857
+ 0,
1858
+ );
1761
1859
  }
1762
1860
  const row = result.details?.result ?? {};
1763
1861
  const status = `${row?.eventDate ?? "unknown date"} · ${row?.guest ?? "guest"} · text:${row?.hasText ? "yes" : "no"} · vtt:${row?.hasVtt ? "yes" : "no"}`;
1764
- return new Text(theme.fg("muted", status), 0, 0);
1862
+ return new Text(
1863
+ `${theme.fg("success", "✓")} ${theme.fg("muted", successLabel)}` + theme.fg("dim", ` · ${status}`),
1864
+ 0,
1865
+ 0,
1866
+ );
1765
1867
  },
1766
1868
  });
1767
1869
 
@@ -1783,6 +1885,7 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1783
1885
  renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_prepare_clip_packet"), (args) => args?.meetingId || undefined),
1784
1886
  renderResult(result: any, _args: any, theme: any) {
1785
1887
  if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
1888
+ const successLabel = getToolSuccessLabel("seedclub_prepare_clip_packet");
1786
1889
 
1787
1890
  const details = result.details ?? {};
1788
1891
  const meeting = details.meeting ?? {};
@@ -1799,10 +1902,12 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1799
1902
  ? details.clipReadiness.reasons.filter((reason: any) => typeof reason === "string")
1800
1903
  : [];
1801
1904
 
1802
- let text = theme.fg(
1803
- "muted",
1804
- `${eventDate} · ${guestName} · transcript:${hasTranscript ? "yes" : "no"} · recording:${hasRecording ? "yes" : "no"} · ready:${ready ? "yes" : "no"}`,
1805
- );
1905
+ let text =
1906
+ `${theme.fg("success", "✓")} ${theme.fg("muted", successLabel)}` +
1907
+ theme.fg(
1908
+ "dim",
1909
+ ` · ${eventDate} · ${guestName} · transcript:${hasTranscript ? "yes" : "no"} · recording:${hasRecording ? "yes" : "no"} · ready:${ready ? "yes" : "no"}`,
1910
+ );
1806
1911
  if (!ready && reasons.length) {
1807
1912
  text += theme.fg("dim", `\n blockers: ${reasons.join(", ")}`);
1808
1913
  }
@@ -1864,15 +1969,24 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1864
1969
  renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_list_meeting_transcripts"), (args) => args?.programSlug || undefined),
1865
1970
  renderResult(result: any, renderArgs: any, theme: any) {
1866
1971
  if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
1972
+ const successLabel = getToolSuccessLabel("seedclub_list_meeting_transcripts");
1867
1973
  const expanded = renderArgs?.expanded === true;
1868
1974
  const requestedLimit = Number.isFinite(renderArgs?.limit) ? Math.trunc(renderArgs.limit) : DEFAULT_TRANSCRIPT_LIMIT;
1869
1975
  const rows = Array.isArray(result.details?.data) ? result.details.data : [];
1870
1976
  const program = result.details?.program?.slug ?? result.details?.program?.name ?? "program";
1871
- if (!rows.length) return new Text(theme.fg("dim", `No transcripts found for ${program}`), 0, 0);
1977
+ if (!rows.length) {
1978
+ return new Text(
1979
+ `${theme.fg("success", "✓")} ${theme.fg("muted", successLabel)}${theme.fg("dim", ` · No transcripts found for ${program}`)}`,
1980
+ 0,
1981
+ 0,
1982
+ );
1983
+ }
1872
1984
 
1873
1985
  const visibleCount = expanded ? 20 : 8;
1874
1986
  const shown = Math.min(visibleCount, rows.length);
1875
- let text = theme.fg("muted", `${rows.length} transcript${rows.length === 1 ? "" : "s"} loaded for ${program}`);
1987
+ let text =
1988
+ `${theme.fg("success", "✓")} ${theme.fg("muted", successLabel)}` +
1989
+ theme.fg("dim", ` · ${rows.length} transcript${rows.length === 1 ? "" : "s"} loaded for ${program}`);
1876
1990
  for (const row of rows.slice(0, shown)) {
1877
1991
  const date = row?.transcript?.event_date ?? "unknown date";
1878
1992
  const name = row?.transcript_for ?? "Unknown";
@@ -1928,7 +2042,7 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1928
2042
  });
1929
2043
 
1930
2044
  pi.registerTool({
1931
- name: "seedclub_book_meeting_full",
2045
+ name: "seedclub_book_meeting",
1932
2046
  label: "Book Meeting",
1933
2047
  description:
1934
2048
  "Book one guest meeting end-to-end: create the meeting, create the studio link, and create the Google Calendar invite. Always fetch availability first with seedclub_list_meeting_availability and use one of the returned startsAt/endsAt pairs. For common meetings, use seedclub_list_meeting_calendars and seedclub_find_common_meeting_availability first, then pass the chosen calendarAccountId. Only use this after you know guestName, guestEmail, and startsAt. Prefer passing endsAt when using an availability slot. If they only give a slot or duration, pass durationMinutes instead; if neither is available, this tool defaults to a 20-minute meeting, but the backend will still reject times that do not exactly match a configured slot. Search for an existing guest first and pass partyId when you have a confident match; otherwise proceed without partyId and the backend will find or create a contact using guestEmail before booking.",
@@ -1959,7 +2073,11 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1959
2073
  profileImageUrl: Type.Optional(Type.Union([Type.String(), Type.Null()])),
1960
2074
  programSlug: Type.Optional(Type.String({ description: "Program slug. Defaults to 11am on the server." })),
1961
2075
  }),
1962
- execute: wrapExecute(bookMeetingFull),
2076
+ execute: wrapExecute(bookMeeting),
2077
+ renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_book_meeting_full"), (args) => args?.guestName || undefined),
2078
+ renderResult: makeProgressResultRenderer(getToolSuccessLabel("seedclub_book_meeting_full"), (details, args) =>
2079
+ firstNonEmptyString(details?.meeting?.id, details?.meeting_id, details?.meetingId, details?.meeting?.title, args?.guestName),
2080
+ ),
1963
2081
  });
1964
2082
 
1965
2083
  pi.registerTool({
@@ -8,7 +8,16 @@ export const TOOL_CALL_LABELS: Record<string, string> = {
8
8
  seedclub_create_crm_note: "Saving CRM note",
9
9
  seedclub_create_crm_task: "Creating CRM task",
10
10
  seedclub_list_program_contacts: "Loading program contacts",
11
+ seedclub_search_people: "Searching people",
12
+ seedclub_list_meeting_availability: "Checking availability slots",
13
+ seedclub_list_meeting_calendars: "Loading booking calendars",
14
+ seedclub_list_personal_calendar_events: "Loading personal calendar events",
15
+ seedclub_create_personal_calendar_event: "Creating personal calendar event",
16
+ seedclub_update_personal_calendar_event: "Updating personal calendar event",
17
+ seedclub_delete_personal_calendar_event: "Deleting personal calendar event",
18
+ seedclub_find_common_meeting_availability: "Checking common availability",
11
19
  seedclub_list_meetings: "Checking meeting schedule",
20
+ seedclub_book_meeting_full: "Booking meeting",
12
21
  seedclub_list_show_guests: "Looking up show guests",
13
22
  seedclub_list_guest_roster: "Building guest roster",
14
23
  seedclub_get_guest_profile: "Resolving guest profile",
@@ -18,6 +27,17 @@ export const TOOL_CALL_LABELS: Record<string, string> = {
18
27
  seedclub_list_meeting_transcripts: "Checking transcript availability",
19
28
  seedclub_get_meeting_transcript: "Loading meeting transcript",
20
29
  seedclub_get_meeting: "Loading meeting details",
30
+ seedclub_search_people: "Searching people",
31
+ seedclub_list_meeting_availability: "Loading availability slots",
32
+ seedclub_list_meeting_calendars: "Loading booking calendars",
33
+ seedclub_list_personal_calendar_events: "Loading personal calendar events",
34
+ seedclub_create_personal_calendar_event: "Creating personal calendar event",
35
+ seedclub_update_personal_calendar_event: "Updating personal calendar event",
36
+ seedclub_delete_personal_calendar_event: "Deleting personal calendar event",
37
+ seedclub_find_common_meeting_availability: "Checking common availability",
38
+ seedclub_book_meeting: "Booking meeting",
39
+ seedclub_reschedule_meeting: "Rescheduling meeting",
40
+ seedclub_cancel_meeting: "Cancelling meeting",
21
41
  seedclub_update_meeting: "Updating meeting",
22
42
  seedclub_assign_meeting: "Assigning meeting",
23
43
  seedclub_list_program_media_assets: "Checking media assets",
@@ -38,7 +58,16 @@ export const TOOL_SUCCESS_LABELS: Record<string, string> = {
38
58
  seedclub_create_crm_note: "CRM note saved",
39
59
  seedclub_create_crm_task: "CRM task created",
40
60
  seedclub_list_program_contacts: "Program contacts loaded",
61
+ seedclub_search_people: "People search complete",
62
+ seedclub_list_meeting_availability: "Availability slots loaded",
63
+ seedclub_list_meeting_calendars: "Booking calendars loaded",
64
+ seedclub_list_personal_calendar_events: "Personal calendar events loaded",
65
+ seedclub_create_personal_calendar_event: "Personal calendar event created",
66
+ seedclub_update_personal_calendar_event: "Personal calendar event updated",
67
+ seedclub_delete_personal_calendar_event: "Personal calendar event deleted",
68
+ seedclub_find_common_meeting_availability: "Common availability loaded",
41
69
  seedclub_list_meetings: "Meeting schedule loaded",
70
+ seedclub_book_meeting_full: "Meeting booked",
42
71
  seedclub_list_show_guests: "Show guests loaded",
43
72
  seedclub_list_guest_roster: "Guest roster ready",
44
73
  seedclub_get_guest_profile: "Guest profile loaded",
@@ -48,6 +77,17 @@ export const TOOL_SUCCESS_LABELS: Record<string, string> = {
48
77
  seedclub_list_meeting_transcripts: "Transcript availability checked",
49
78
  seedclub_get_meeting_transcript: "Meeting transcript loaded",
50
79
  seedclub_get_meeting: "Meeting details loaded",
80
+ seedclub_search_people: "People loaded",
81
+ seedclub_list_meeting_availability: "Availability loaded",
82
+ seedclub_list_meeting_calendars: "Booking calendars loaded",
83
+ seedclub_list_personal_calendar_events: "Personal calendar events loaded",
84
+ seedclub_create_personal_calendar_event: "Personal calendar event created",
85
+ seedclub_update_personal_calendar_event: "Personal calendar event updated",
86
+ seedclub_delete_personal_calendar_event: "Personal calendar event deleted",
87
+ seedclub_find_common_meeting_availability: "Common availability loaded",
88
+ seedclub_book_meeting: "Meeting booked",
89
+ seedclub_reschedule_meeting: "Meeting rescheduled",
90
+ seedclub_cancel_meeting: "Meeting cancelled",
51
91
  seedclub_update_meeting: "Meeting updated",
52
92
  seedclub_assign_meeting: "Meeting assigned",
53
93
  seedclub_list_program_media_assets: "Media assets checked",
@@ -123,10 +123,4 @@ let lastCtx: ExtensionContext | undefined;
123
123
  lastCtx = ctx;
124
124
  refresh(ctx);
125
125
  });
126
-
127
- pi.on("session_switch", (_event, ctx) => {
128
- if (!ctx.hasUI) return;
129
- lastCtx = ctx;
130
- refresh(ctx);
131
- });
132
126
  }
@@ -1,5 +1,6 @@
1
1
  export const uiState = {
2
2
  ready: false,
3
+ requestRender: undefined as undefined | (() => void),
3
4
  todayOn11am: null as null | {
4
5
  date: string;
5
6
  guests: Array<{
@@ -10,4 +11,8 @@ export const uiState = {
10
11
  transcriptAvailable: boolean;
11
12
  }>;
12
13
  },
14
+ update: null as null | {
15
+ installedVersion: string;
16
+ latestVersion: string;
17
+ },
13
18
  };
@@ -1,19 +1,28 @@
1
1
  /**
2
- * Update checkruns on session start, notifies if a new version is available.
3
- * To update, run `seedclub update` in your terminal.
2
+ * Update promptchecks npm for a newer package version and offers actions.
4
3
  */
5
4
 
6
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
- import { readFileSync, existsSync } from "node:fs";
8
5
  import { execFile } from "node:child_process";
9
- import { join } from "node:path";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { mkdir, writeFile } from "node:fs/promises";
10
8
  import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
+ import { isAuthGateBlocking } from "../seedclub/gate-state.js";
12
+ import { uiState } from "./state.js";
11
13
 
12
14
  interface VersionInfo {
13
15
  seedclubVersion: string;
14
16
  piVersion: string;
15
17
  }
16
18
 
19
+ interface UpdatePrefs {
20
+ skipVersion?: string | null;
21
+ }
22
+
23
+ const UPDATE_PREFS_FILE = join(homedir(), ".seedclub", "agent", ".seedclub-update-prefs.json");
24
+ const UPDATE_COMMAND = "npm install -g @clubnet/seedclub@latest";
25
+
17
26
  function compareSemver(a: string, b: string): number {
18
27
  const pa = a.trim().split(".").map((x) => Number.parseInt(x, 10));
19
28
  const pb = b.trim().split(".").map((x) => Number.parseInt(x, 10));
@@ -38,9 +47,23 @@ function getInstalledVersion(): VersionInfo | null {
38
47
  }
39
48
  }
40
49
 
50
+ function getUpdatePrefs(): UpdatePrefs {
51
+ if (!existsSync(UPDATE_PREFS_FILE)) return {};
52
+ try {
53
+ return JSON.parse(readFileSync(UPDATE_PREFS_FILE, "utf-8"));
54
+ } catch {
55
+ return {};
56
+ }
57
+ }
58
+
59
+ async function storeUpdatePrefs(prefs: UpdatePrefs): Promise<void> {
60
+ await mkdir(join(homedir(), ".seedclub", "agent"), { recursive: true });
61
+ await writeFile(UPDATE_PREFS_FILE, JSON.stringify(prefs, null, 2) + "\n");
62
+ }
63
+
41
64
  function getLatestVersion(): Promise<string | null> {
42
65
  return new Promise((resolve) => {
43
- execFile("npm", ["view", "@clubnet/seedclub", "version"], { timeout: 10000 }, (err, stdout) => {
66
+ execFile("npm", ["view", "@clubnet/seedclub", "version"], { timeout: 10_000 }, (err, stdout) => {
44
67
  if (err) return resolve(null);
45
68
  const version = stdout.trim();
46
69
  resolve(version || null);
@@ -48,24 +71,140 @@ function getLatestVersion(): Promise<string | null> {
48
71
  });
49
72
  }
50
73
 
74
+ async function waitForReadyWindow(): Promise<boolean> {
75
+ const startedAt = Date.now();
76
+ while (Date.now() - startedAt < 30_000) {
77
+ if (uiState.ready && !isAuthGateBlocking()) return true;
78
+ await new Promise((resolve) => setTimeout(resolve, 250));
79
+ }
80
+ return false;
81
+ }
82
+
83
+ async function runUpdateCommand(pi: ExtensionAPI): Promise<void> {
84
+ await new Promise<void>((resolve, reject) => {
85
+ execFile("npm", ["install", "-g", "@clubnet/seedclub@latest"], { timeout: 120_000 }, (error) => {
86
+ if (error) {
87
+ reject(error);
88
+ return;
89
+ }
90
+ resolve();
91
+ });
92
+ });
93
+
94
+ // Trigger a pi-side version refresh if available in the current runtime.
95
+ try {
96
+ await pi.exec("seedclub", ["version"]);
97
+ } catch {}
98
+ }
99
+
100
+ function setAvailableUpdate(installedVersion: string | null, latestVersion: string | null) {
101
+ if (installedVersion && latestVersion) {
102
+ uiState.update = { installedVersion, latestVersion };
103
+ } else {
104
+ uiState.update = null;
105
+ }
106
+ uiState.requestRender?.();
107
+ }
108
+
109
+ function notifyAvailableUpdate(ctx: any, latestVersion: string) {
110
+ ctx.ui.notify(`New update available (v${latestVersion}). Use /update to update seedclub.`, "info");
111
+ }
112
+
113
+ async function openUpdatePrompt(
114
+ pi: ExtensionAPI,
115
+ ctx: any,
116
+ installedVersion: string,
117
+ latestVersion: string,
118
+ ): Promise<void> {
119
+ const choice = await ctx.ui.select(`Update available (${installedVersion} -> ${latestVersion})`, [
120
+ `Update now (${UPDATE_COMMAND})`,
121
+ "Skip",
122
+ "Skip until next version",
123
+ ]);
124
+
125
+ switch (choice) {
126
+ case `Update now (${UPDATE_COMMAND})`:
127
+ ctx.ui.notify(`Updating seedclub...\nRunning: ${UPDATE_COMMAND}`, "info");
128
+ try {
129
+ await runUpdateCommand(pi);
130
+ await storeUpdatePrefs({ skipVersion: null });
131
+ setAvailableUpdate(null, null);
132
+ ctx.ui.notify("seedclub was updated. Restart the app to load the new version.", "info");
133
+ } catch (error) {
134
+ const message = error instanceof Error ? error.message : "Update failed.";
135
+ ctx.ui.notify(`Update failed: ${message}`, "error");
136
+ }
137
+ break;
138
+ case "Skip until next version":
139
+ await storeUpdatePrefs({ skipVersion: latestVersion });
140
+ setAvailableUpdate(null, null);
141
+ break;
142
+ default:
143
+ break;
144
+ }
145
+ }
146
+
51
147
  export default function (pi: ExtensionAPI) {
52
148
  let checked = false;
149
+ let announced = false;
53
150
 
54
- pi.on("session_start", async (_event, ctx) => {
151
+ pi.on("session_start", (_event, ctx) => {
55
152
  if (!ctx.hasUI || checked) return;
56
153
  checked = true;
154
+ announced = false;
155
+ setAvailableUpdate(null, null);
156
+
157
+ void (async () => {
158
+ const installed = getInstalledVersion();
159
+ if (!installed?.seedclubVersion) return;
160
+
161
+ const latest = await getLatestVersion();
162
+ if (!latest) return;
163
+ if (compareSemver(latest, installed.seedclubVersion) <= 0) return;
164
+
165
+ const prefs = getUpdatePrefs();
166
+ if (prefs.skipVersion === latest) return;
167
+
168
+ if (isAuthGateBlocking()) {
169
+ const ready = await waitForReadyWindow();
170
+ if (!ready) return;
171
+ }
172
+ setAvailableUpdate(installed.seedclubVersion, latest);
173
+ if (!announced) {
174
+ notifyAvailableUpdate(ctx, latest);
175
+ announced = true;
176
+ }
177
+ })().catch(() => {});
178
+ });
179
+
180
+ pi.registerCommand("update", {
181
+ description: "Show the seedclub package update menu",
182
+ handler: async (_args, ctx) => {
183
+ const installed = getInstalledVersion();
184
+ if (!installed?.seedclubVersion) {
185
+ ctx.ui.notify("Unable to determine the installed seedclub version.", "error");
186
+ return;
187
+ }
57
188
 
58
- const installed = getInstalledVersion();
59
- if (!installed?.seedclubVersion) return;
189
+ const latest = await getLatestVersion();
190
+ if (!latest) {
191
+ ctx.ui.notify("Unable to check npm for the latest seedclub version.", "error");
192
+ return;
193
+ }
60
194
 
61
- const latest = await getLatestVersion();
62
- if (!latest) return;
195
+ if (compareSemver(latest, installed.seedclubVersion) <= 0) {
196
+ setAvailableUpdate(null, null);
197
+ await storeUpdatePrefs({ skipVersion: null });
198
+ ctx.ui.notify(`seedclub is up to date (v${installed.seedclubVersion}).`, "info");
199
+ return;
200
+ }
63
201
 
64
- if (compareSemver(latest, installed.seedclubVersion) <= 0) return;
202
+ const prefs = getUpdatePrefs();
203
+ if (prefs.skipVersion !== latest) {
204
+ setAvailableUpdate(installed.seedclubVersion, latest);
205
+ }
65
206
 
66
- ctx.ui.notify(
67
- `Update available: seedclub v${latest} is out (installed: v${installed.seedclubVersion}). Run \`seedclub update\`.`,
68
- "info",
69
- );
207
+ await openUpdatePrompt(pi, ctx, installed.seedclubVersion, latest);
208
+ },
70
209
  });
71
210
  }
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Welcome header with live weather + market data.
3
- * /commands and /extensions are available as commands.
3
+ * /extensions is available as a command.
4
4
  */
5
5
 
6
6
  import { execFileSync } from "node:child_process";
7
- import { basename } from "node:path";
7
+ import { existsSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { basename, join } from "node:path";
8
10
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
11
  import { ApiError, NotConnectedError, api } from "../seedclub/api-client.js";
10
12
  import { getAuthGateState, isAuthGateBlocking, subscribeToAuthGate } from "../seedclub/gate-state.js";
@@ -311,6 +313,15 @@ function renderSetupLines(setupHints: string[], theme: ThemeLike): string[] {
311
313
  });
312
314
  }
313
315
 
316
+ function renderUpdateLines(theme: ThemeLike): string[] {
317
+ const update = uiState.update;
318
+ if (!update) return [];
319
+ return [
320
+ ` ${theme.fg("accent", "New update available")}`,
321
+ ` ${theme.fg("text", "/update")} ${theme.fg("dim", "open the package update menu")}`,
322
+ ];
323
+ }
324
+
314
325
  function renderAuthGateLines(theme: ThemeLike): string[] {
315
326
  const gate = getAuthGateState();
316
327
  const lines = [
@@ -334,11 +345,42 @@ function renderAuthGateLines(theme: ThemeLike): string[] {
334
345
 
335
346
  lines.push("");
336
347
  lines.push(` ${theme.fg("text", "/connect")} ${theme.fg("dim", "retry sign-in")}`);
337
- lines.push(` ${theme.fg("text", "/commands")} ${theme.fg("dim", "list commands available during setup")}`);
338
348
  lines.push("");
339
349
  return lines;
340
350
  }
341
351
 
352
+ function renderAuthCheckingLines(theme: ThemeLike, frame: number): string[] {
353
+ const gate = getAuthGateState();
354
+ const lines = [
355
+ "",
356
+ renderTitle(theme),
357
+ "",
358
+ ...renderCoinLoaderLines(frame, theme, { showLabel: false }),
359
+ "",
360
+ ` ${theme.fg("accent", "Checking access")}`,
361
+ ` ${theme.fg("dim", gate.message || "Checking Seed Club access...")}`,
362
+ "",
363
+ ];
364
+
365
+ if (gate.error) {
366
+ lines.push(` ${theme.fg("error", gate.error)}`);
367
+ lines.push("");
368
+ }
369
+
370
+ if (gate.authUrl) {
371
+ lines.push(` ${theme.fg("text", "Auth URL")}`);
372
+ lines.push(` ${theme.fg("mdLink", gate.authUrl)}`);
373
+ lines.push("");
374
+ }
375
+
376
+ return lines;
377
+ }
378
+
379
+ function hasSeedclubCredential(): boolean {
380
+ if (process.env.SEEDCLUB_ACCESS_TOKEN?.trim()) return true;
381
+ return existsSync(join(homedir(), ".config", "seedclub", "token"));
382
+ }
383
+
342
384
  function renderTodayOn11amLines(today: TodayOn11am | null, theme: ThemeLike): string[] {
343
385
  if (!today || !today.guests.length) return [];
344
386
  const lines = [` ${theme.fg("text", "Today on 11AM")}`];
@@ -354,7 +396,7 @@ function renderTodayOn11amLines(today: TodayOn11am | null, theme: ThemeLike): st
354
396
  return lines;
355
397
  }
356
398
 
357
- function renderCoinLoaderLines(frame: number, theme: ThemeLike): string[] {
399
+ function renderCoinLoaderLines(frame: number, theme: ThemeLike, options?: { showLabel?: boolean }): string[] {
358
400
  const rawShape = [
359
401
  " XXXXXX XXXXXX",
360
402
  " XXXXXX XXXXXX",
@@ -410,6 +452,9 @@ function renderCoinLoaderLines(frame: number, theme: ThemeLike): string[] {
410
452
 
411
453
  const loadingLabel = "loading...";
412
454
  const loadingIndent = " ".repeat(2 + Math.max(0, Math.floor((cols - loadingLabel.length) / 2)));
455
+ if (options?.showLabel === false) {
456
+ return lines;
457
+ }
413
458
  return [...lines, "", `${loadingIndent}${theme.fg("dim", loadingLabel)}`];
414
459
  }
415
460
 
@@ -434,8 +479,15 @@ export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean })
434
479
  unsubscribeAuthGate?.();
435
480
 
436
481
  let tuiRef: any = null;
482
+ let animatedHeaderTimer: ReturnType<typeof setInterval> | undefined;
483
+ let animatedHeaderFrame = 0;
484
+ let repaintHeader: (() => void) | undefined;
437
485
  ctx.ui.setHeader((tui, theme) => {
438
486
  tuiRef = tui;
487
+ uiState.requestRender = () => {
488
+ repaintHeader?.();
489
+ tui.requestRender();
490
+ };
439
491
  tui.setClearOnShrink(true);
440
492
 
441
493
  // Enable window frame around entire TUI (footer excluded)
@@ -456,6 +508,31 @@ export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean })
456
508
  };
457
509
  });
458
510
 
511
+ const stopAnimatedHeader = () => {
512
+ if (!animatedHeaderTimer) return;
513
+ clearInterval(animatedHeaderTimer);
514
+ animatedHeaderTimer = undefined;
515
+ };
516
+
517
+ const startAnimatedHeader = (renderFrame: (frame: number) => string[]) => {
518
+ stopAnimatedHeader();
519
+ animatedHeaderFrame = 0;
520
+ repaintHeader = () => {
521
+ headerLines = renderFrame(animatedHeaderFrame);
522
+ };
523
+ const render = () => {
524
+ headerLines = renderFrame(animatedHeaderFrame);
525
+ };
526
+ render();
527
+ tuiRef?.requestRender();
528
+ animatedHeaderTimer = setInterval(() => {
529
+ animatedHeaderFrame += 1;
530
+ render();
531
+ tuiRef?.requestRender();
532
+ }, 120);
533
+ animatedHeaderTimer.unref?.();
534
+ };
535
+
459
536
  let loadStarted = false;
460
537
 
461
538
  const startReadyHeader = () => {
@@ -470,33 +547,23 @@ export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean })
470
547
  if (!hasSelectedModel) setupHints.push("/model");
471
548
  const setupLines = renderSetupLines(setupHints, ctx.ui.theme);
472
549
 
473
- let loaderFrame = 0;
474
- const renderLoadingHeader = () => {
475
- headerLines = [
476
- "",
477
- renderTitle(ctx.ui.theme),
478
- "",
479
- ...renderCoinLoaderLines(loaderFrame, ctx.ui.theme),
480
- "",
481
- ];
482
- };
483
- renderLoadingHeader();
484
- const loaderTimer = setInterval(() => {
485
- loaderFrame += 1;
486
- renderLoadingHeader();
487
- tuiRef?.requestRender();
488
- }, 120);
489
- loaderTimer.unref?.();
490
- tuiRef?.requestRender();
550
+ startAnimatedHeader((frame) => [
551
+ "",
552
+ renderTitle(ctx.ui.theme),
553
+ "",
554
+ ...renderCoinLoaderLines(frame, ctx.ui.theme),
555
+ "",
556
+ ]);
491
557
 
492
558
  const todayPromise = fetchTodayOn11am();
493
559
  void Promise.all([
494
560
  getData(),
495
561
  withTimeout(todayPromise, TODAY_PREFETCH_TIMEOUT_MS, null),
496
562
  ]).then(([{ weather, market }, todayOn11am]) => {
497
- clearInterval(loaderTimer);
563
+ stopAnimatedHeader();
498
564
  const renderReadyHeader = (today: TodayOn11am | null) => {
499
565
  const theme = ctx.ui.theme;
566
+ const updateLines = renderUpdateLines(theme);
500
567
  const weatherLine = ` ${weather.icon} ${theme.fg("text", weather.temp)} ${theme.fg("dim", weather.condition)} ${theme.fg("dim", "·")} ${theme.fg("dim", weather.location)}`;
501
568
  const marketLine = ` ${market.map((quote) => formatQuote(quote, theme)).join(` ${theme.fg("dim", "·")} `)}`;
502
569
  const todayLines = renderTodayOn11amLines(today, theme);
@@ -508,12 +575,16 @@ export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean })
508
575
  weatherLine,
509
576
  marketLine,
510
577
  "",
578
+ ...updateLines,
579
+ ...(updateLines.length ? [""] : []),
511
580
  ...todayLines,
512
581
  ...(todayLines.length ? [""] : []),
513
582
  ...setupLines,
583
+ ...(setupLines.length ? [""] : []),
514
584
  "",
515
585
  ];
516
586
  };
587
+ repaintHeader = () => renderReadyHeader(uiState.todayOn11am);
517
588
 
518
589
  renderReadyHeader(todayOn11am);
519
590
  uiState.ready = true;
@@ -528,14 +599,21 @@ export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean })
528
599
  }).catch(() => {});
529
600
  }
530
601
  }).catch(() => {
531
- clearInterval(loaderTimer);
532
- headerLines = [
533
- "",
534
- renderTitle(ctx.ui.theme),
535
- "",
536
- ...setupLines,
537
- "",
538
- ];
602
+ stopAnimatedHeader();
603
+ repaintHeader = () => {
604
+ const updateLines = renderUpdateLines(ctx.ui.theme);
605
+ headerLines = [
606
+ "",
607
+ renderTitle(ctx.ui.theme),
608
+ "",
609
+ ...updateLines,
610
+ ...(updateLines.length ? [""] : []),
611
+ ...setupLines,
612
+ ...(setupLines.length ? [""] : []),
613
+ "",
614
+ ];
615
+ };
616
+ repaintHeader();
539
617
  uiState.ready = true;
540
618
  ctx.ui.setEditorText("");
541
619
  tuiRef?.requestRender();
@@ -543,10 +621,28 @@ export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean })
543
621
  };
544
622
 
545
623
  const renderCurrentHeader = () => {
624
+ const gate = getAuthGateState();
625
+ if (gate.status === "auth_in_progress") {
626
+ if (hasSeedclubCredential()) {
627
+ startReadyHeader();
628
+ return;
629
+ }
630
+ loadStarted = false;
631
+ uiState.todayOn11am = null;
632
+ startAnimatedHeader((frame) => renderAuthCheckingLines(ctx.ui.theme, frame));
633
+ uiState.ready = true;
634
+ ctx.ui.setEditorText("");
635
+ return;
636
+ }
637
+
546
638
  if (isAuthGateBlocking()) {
639
+ stopAnimatedHeader();
547
640
  loadStarted = false;
548
641
  uiState.todayOn11am = null;
549
- headerLines = renderAuthGateLines(ctx.ui.theme);
642
+ repaintHeader = () => {
643
+ headerLines = renderAuthGateLines(ctx.ui.theme);
644
+ };
645
+ repaintHeader();
550
646
  uiState.ready = true;
551
647
  ctx.ui.setEditorText("");
552
648
  tuiRef?.requestRender();
@@ -571,10 +667,6 @@ export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean })
571
667
  applyTerminalTitle(ctx);
572
668
  });
573
669
 
574
- pi.on("session_switch", (_event, ctx) => {
575
- applyTerminalTitle(ctx);
576
- });
577
-
578
670
  pi.on("before_agent_start", async (event) => {
579
671
  const today = uiState.todayOn11am;
580
672
  if (!today?.guests.length) return;
@@ -627,8 +719,9 @@ ${rows.join("\n")}`,
627
719
  const commands = pi.getCommands();
628
720
  const extPaths = new Map<string, string[]>();
629
721
  for (const cmd of commands) {
630
- if (cmd.source === "extension" && cmd.path) {
631
- const name = cmd.path.replace(/.*\/extensions\//, "").replace(/\.ts$/, "");
722
+ const cmdPath = cmd.sourceInfo?.path;
723
+ if (cmd.source === "extension" && cmdPath) {
724
+ const name = cmdPath.replace(/.*\/extensions\//, "").replace(/\.ts$/, "");
632
725
  if (!extPaths.has(name)) extPaths.set(name, []);
633
726
  extPaths.get(name)!.push(cmd.name);
634
727
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubnet/seedclub",
3
- "version": "0.2.31",
3
+ "version": "0.2.32",
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": {