@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.
- package/assets/extensions/seedclub/api-client.ts +4 -2
- package/assets/extensions/seedclub/gate-state.ts +2 -2
- package/assets/extensions/seedclub/index.ts +2 -4
- package/assets/extensions/seedclub/tools/crm.ts +11 -2
- package/assets/extensions/seedclub/tools/media.ts +25 -4
- package/assets/extensions/seedclub/tools/meetings.ts +130 -12
- package/assets/extensions/seedclub/ui-copy.ts +40 -0
- package/assets/extensions/seedclub-ui/footer.ts +0 -6
- package/assets/extensions/seedclub-ui/state.ts +5 -0
- package/assets/extensions/seedclub-ui/update.ts +155 -16
- package/assets/extensions/seedclub-ui/welcome.ts +131 -38
- package/package.json +1 -1
|
@@ -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:
|
|
84
|
-
|
|
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: "
|
|
19
|
+
status: "auth_in_progress",
|
|
20
20
|
authUrl: null,
|
|
21
|
-
message: "Seed Club
|
|
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)
|
|
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 =
|
|
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)
|
|
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 =
|
|
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)
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
1803
|
-
"muted",
|
|
1804
|
-
|
|
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)
|
|
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 =
|
|
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: "
|
|
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(
|
|
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",
|
|
@@ -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
|
|
3
|
-
* To update, run `seedclub update` in your terminal.
|
|
2
|
+
* Update prompt — checks 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 {
|
|
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:
|
|
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",
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
202
|
+
const prefs = getUpdatePrefs();
|
|
203
|
+
if (prefs.skipVersion !== latest) {
|
|
204
|
+
setAvailableUpdate(installed.seedclubVersion, latest);
|
|
205
|
+
}
|
|
65
206
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
* /
|
|
3
|
+
* /extensions is available as a command.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
|
-
import {
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
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
|
-
|
|
631
|
-
|
|
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