@clubnet/seedclub 0.2.32 → 0.2.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,8 +20,6 @@ const DEFAULT_AUTH_BASE = "https://auth.seedclub.com";
20
20
  const LOCAL_API_BASE = "http://localhost:3001";
21
21
  const LOCAL_AUTH_BASE = "http://localhost:3000";
22
22
 
23
- export type SeedclubEnvironmentMode = "local" | "prod" | "custom";
24
-
25
23
  export interface StoredToken {
26
24
  token: string;
27
25
  email: string;
@@ -34,8 +32,6 @@ export interface StoredToken {
34
32
  export interface StoredBases {
35
33
  apiBase: string;
36
34
  authBase: string;
37
- mode: SeedclubEnvironmentMode;
38
- updatedAt: string;
39
35
  }
40
36
 
41
37
  let _cachedApiBase: string | null = null;
@@ -50,6 +46,23 @@ function shouldPreferLocalBases(): boolean {
50
46
  );
51
47
  }
52
48
 
49
+ function normalizeStoredBases(value: unknown): StoredBases | null {
50
+ if (!value || typeof value !== "object") {
51
+ return null;
52
+ }
53
+
54
+ const stored = value as Partial<StoredBases>;
55
+
56
+ if (typeof stored.apiBase !== "string" || typeof stored.authBase !== "string") {
57
+ return null;
58
+ }
59
+
60
+ return {
61
+ apiBase: stored.apiBase,
62
+ authBase: stored.authBase,
63
+ };
64
+ }
65
+
53
66
  function tryReadStoredBasesSync(): StoredBases | null {
54
67
  if (_cachedBases !== undefined) {
55
68
  return _cachedBases;
@@ -57,13 +70,8 @@ function tryReadStoredBasesSync(): StoredBases | null {
57
70
 
58
71
  try {
59
72
  const content = readFileSync(BASES_FILE, "utf-8");
60
- const stored = JSON.parse(content) as StoredBases;
61
- if (
62
- !stored ||
63
- typeof stored.apiBase !== "string" ||
64
- typeof stored.authBase !== "string" ||
65
- typeof stored.mode !== "string"
66
- ) {
73
+ const stored = normalizeStoredBases(JSON.parse(content));
74
+ if (!stored) {
67
75
  _cachedBases = null;
68
76
  return null;
69
77
  }
@@ -158,13 +166,8 @@ export async function getStoredToken(): Promise<StoredToken | null> {
158
166
  export async function getStoredBases(): Promise<StoredBases | null> {
159
167
  try {
160
168
  const content = await readFile(BASES_FILE, "utf-8");
161
- const stored = JSON.parse(content) as StoredBases;
162
- if (
163
- !stored ||
164
- typeof stored.apiBase !== "string" ||
165
- typeof stored.authBase !== "string" ||
166
- typeof stored.mode !== "string"
167
- ) {
169
+ const stored = normalizeStoredBases(JSON.parse(content));
170
+ if (!stored) {
168
171
  return null;
169
172
  }
170
173
  _cachedBases = stored;
@@ -183,13 +186,10 @@ export async function getToken(): Promise<string | null> {
183
186
  export async function storeBases(
184
187
  apiBase: string,
185
188
  authBase: string,
186
- mode: SeedclubEnvironmentMode = "custom",
187
189
  ): Promise<void> {
188
190
  const stored: StoredBases = {
189
191
  apiBase,
190
192
  authBase,
191
- mode,
192
- updatedAt: new Date().toISOString(),
193
193
  };
194
194
  _cachedApiBase = apiBase;
195
195
  _cachedAuthBase = authBase;
@@ -41,7 +41,6 @@ async function showSeedEnvironment(ctx: any) {
41
41
  const lines = [
42
42
  `effective api: ${getApiBase()}`,
43
43
  `effective auth: ${getAuthBase()}`,
44
- `stored mode: ${storedBases?.mode ?? "none"}`,
45
44
  `stored api: ${storedBases?.apiBase ?? "none"}`,
46
45
  `stored auth: ${storedBases?.authBase ?? "none"}`,
47
46
  ];
@@ -50,7 +49,7 @@ async function showSeedEnvironment(ctx: any) {
50
49
 
51
50
  async function setSeedEnvironment(mode: "local" | "prod", ctx: any) {
52
51
  const bases = getDefaultBases(mode);
53
- await storeBases(bases.apiBase, bases.authBase, mode);
52
+ await storeBases(bases.apiBase, bases.authBase);
54
53
  ctx.ui.notify(
55
54
  `Seed Club environment set to ${mode}.\napi: ${bases.apiBase}\nauth: ${bases.authBase}\nReconnect if your current token belongs to a different environment.`,
56
55
  "info",
@@ -113,13 +112,15 @@ export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
113
112
  const isConnected = !!stored || hasEnvToken;
114
113
 
115
114
  if (!isConnected) {
116
- return await deps.connect(args, ctx);
115
+ await deps.connect(args, ctx);
116
+ return;
117
117
  }
118
118
 
119
119
  const user = await getCurrentUser();
120
120
  if ("error" in user) {
121
121
  ctx.ui.notify("Session expired. Reconnecting...", "info");
122
- return await deps.connect(args, ctx);
122
+ await deps.connect(args, ctx);
123
+ return;
123
124
  }
124
125
 
125
126
  const session = await getSessionContext();
@@ -9,7 +9,7 @@ import { randomBytes } from "node:crypto";
9
9
  import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
10
10
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
11
  import { api, clearCredentials, setCachedToken } from "./api-client.js";
12
- import { getApiBase, getAuthBase, getStoredBases, getStoredToken, storeToken } from "./auth.js";
12
+ import { getApiBase, getAuthBase, getStoredToken, storeToken } from "./auth.js";
13
13
  import { renderCallbackPage } from "./browser-pages.js";
14
14
  import { registerSeedclubCommand } from "./commands/seedclub.js";
15
15
  import { registerTranscriptsCommand } from "./commands/transcripts.js";
@@ -93,7 +93,6 @@ export default function (pi: ExtensionAPI) {
93
93
  }
94
94
 
95
95
  async function applyConnectedStatus(ctx: any, user: { name?: string | null; email?: string | null }) {
96
- const storedBases = await getStoredBases();
97
96
  const effectiveApiBase = getApiBase();
98
97
  const effectiveAuthBase = getAuthBase();
99
98
  const isDev = effectiveApiBase.includes("localhost") || effectiveApiBase.includes("127.0.0.1");
@@ -102,8 +101,7 @@ export default function (pi: ExtensionAPI) {
102
101
  (effectiveAuthBase.includes("localhost") || effectiveAuthBase.includes("127.0.0.1"));
103
102
 
104
103
  ctx.ui.setStatus("seed", formatSeedLabel(user.name ?? undefined, user.email ?? undefined));
105
- if (storedBases?.mode) ctx.ui.setStatus("seed-env", `env: ${storedBases.mode}`);
106
- else ctx.ui.setStatus("seed-env", undefined);
104
+ ctx.ui.setStatus("seed-env", undefined);
107
105
  if (isDev) ctx.ui.setStatus("seed-api", `dev: ${effectiveApiBase}`);
108
106
  else ctx.ui.setStatus("seed-api", undefined);
109
107
  if (hasSeparateDevAuthBase) ctx.ui.setStatus("seed-auth", `auth: ${effectiveAuthBase}`);
@@ -111,9 +111,9 @@ export function makeProgressCallRenderer(
111
111
  label: string,
112
112
  detail?: (args: any) => string | undefined,
113
113
  ) {
114
- return (args: any, theme: any, context: any) => {
115
- const state = (context.state ??= {});
116
- if (context.isPartial) {
114
+ return (args: any, theme: any, context?: any) => {
115
+ const state = context?.state;
116
+ if (context?.isPartial && state) {
117
117
  if (!state.spinnerTimer) {
118
118
  state.spinnerFrame = 0;
119
119
  state.spinnerTimer = setInterval(() => {
@@ -122,13 +122,13 @@ export function makeProgressCallRenderer(
122
122
  }, 90);
123
123
  state.spinnerTimer.unref?.();
124
124
  }
125
- } else if (state.spinnerTimer) {
125
+ } else if (state?.spinnerTimer) {
126
126
  clearInterval(state.spinnerTimer);
127
127
  state.spinnerTimer = undefined;
128
128
  }
129
129
 
130
- const frame = context.isPartial ? SPINNER_FRAMES[state.spinnerFrame ?? 0] : "";
131
- const tone = context.isPartial ? "accent" : context.isError ? "error" : "success";
130
+ const frame = context?.isPartial && state ? SPINNER_FRAMES[state.spinnerFrame ?? 0] : "";
131
+ const tone = context?.isPartial ? "accent" : context?.isError ? "error" : "muted";
132
132
  let text = `${theme.fg(tone, frame)} ${theme.fg("toolTitle", label)}`;
133
133
  const detailText = detail?.(args);
134
134
  if (detailText) text += theme.fg("dim", ` · ${detailText}`);
@@ -158,9 +158,9 @@ export function makeProgressResultRenderer(
158
158
  successLabel = "Completed",
159
159
  summary?: (details: any, args: any) => string | undefined,
160
160
  ) {
161
- return (result: any, options: any, theme: any, context: any) => {
162
- const state = context.state ?? {};
163
- if (state.spinnerTimer) {
161
+ return (result: any, options: any, theme: any, context?: any) => {
162
+ const state = context?.state;
163
+ if (state?.spinnerTimer) {
164
164
  clearInterval(state.spinnerTimer);
165
165
  state.spinnerTimer = undefined;
166
166
  }
@@ -170,14 +170,18 @@ export function makeProgressResultRenderer(
170
170
  if (result?.isError) {
171
171
  const line = firstTextLine(result?.content) ?? "Request failed";
172
172
  let text = `${theme.fg("error", "✕")} ${theme.fg("error", line)}`;
173
- const debugText = getCachedDebugText(state, result, options?.expanded === true);
173
+ const debugText = state
174
+ ? getCachedDebugText(state, result, options?.expanded === true)
175
+ : getDebugText(result, options?.expanded === true);
174
176
  if (debugText) text += `\n${theme.fg("dim", debugText)}`;
175
177
  return new Text(text, 0, 0);
176
178
  }
177
179
  const detailText = summary?.(result?.details, context?.args) ?? summarizeDetails(result?.details);
178
180
  let text = `${theme.fg("success", "✓")} ${theme.fg("muted", successLabel)}`;
179
181
  if (detailText) text += theme.fg("dim", ` · ${detailText}`);
180
- const debugText = getCachedDebugText(state, result, options?.expanded === true);
182
+ const debugText = state
183
+ ? getCachedDebugText(state, result, options?.expanded === true)
184
+ : getDebugText(result, options?.expanded === true);
181
185
  if (debugText) text += `\n${theme.fg("dim", debugText)}`;
182
186
  return new Text(text, 0, 0);
183
187
  };
@@ -173,6 +173,49 @@ function formatSlotRange(startMinutes: number, endMinutes: number, timeZone: str
173
173
  return `${formatter.format(start)} - ${formatter.format(end)}`;
174
174
  }
175
175
 
176
+ function formatMeetingDateTimeRange(startsAt: string | null | undefined, endsAt: string | null | undefined) {
177
+ if (typeof startsAt !== "string" || !startsAt.trim()) return undefined;
178
+ const start = new Date(startsAt);
179
+ if (Number.isNaN(start.getTime())) return undefined;
180
+
181
+ const end =
182
+ typeof endsAt === "string" && endsAt.trim()
183
+ ? new Date(endsAt)
184
+ : null;
185
+
186
+ const dateFormatter = new Intl.DateTimeFormat("en-US", {
187
+ weekday: "short",
188
+ month: "short",
189
+ day: "numeric",
190
+ });
191
+ const timeFormatter = new Intl.DateTimeFormat("en-US", {
192
+ hour: "numeric",
193
+ minute: "2-digit",
194
+ hour12: true,
195
+ });
196
+
197
+ const startDateLabel = dateFormatter.format(start);
198
+ const startTimeLabel = timeFormatter.format(start);
199
+
200
+ if (!end || Number.isNaN(end.getTime())) {
201
+ return `${startDateLabel} · ${startTimeLabel}`;
202
+ }
203
+
204
+ const sameDay =
205
+ start.getFullYear() === end.getFullYear() &&
206
+ start.getMonth() === end.getMonth() &&
207
+ start.getDate() === end.getDate();
208
+
209
+ const endDateLabel = dateFormatter.format(end);
210
+ const endTimeLabel = timeFormatter.format(end);
211
+
212
+ if (sameDay) {
213
+ return `${startDateLabel} · ${startTimeLabel}-${endTimeLabel}`;
214
+ }
215
+
216
+ return `${startDateLabel} ${startTimeLabel} - ${endDateLabel} ${endTimeLabel}`;
217
+ }
218
+
176
219
  function shapeTranscriptSource(source: any, includeFullText: boolean, includePreview = true) {
177
220
  if (!source) {
178
221
  return null;
@@ -2092,7 +2135,13 @@ export function registerMeetingTools(pi: ExtensionAPI) {
2092
2135
  producerFraming: Type.Optional(Type.Union([Type.String(), Type.Null()])),
2093
2136
  }),
2094
2137
  execute: wrapExecute(updateMeeting),
2095
- renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_update_meeting"), (args) => args?.meetingId || undefined),
2138
+ renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_update_meeting"), (args) =>
2139
+ firstNonEmptyString(
2140
+ formatMeetingDateTimeRange(args?.startsAt, args?.endsAt),
2141
+ args?.title,
2142
+ args?.meetingId,
2143
+ ),
2144
+ ),
2096
2145
  renderResult: makeProgressResultRenderer(getToolSuccessLabel("seedclub_update_meeting")),
2097
2146
  });
2098
2147
 
@@ -9,7 +9,7 @@ export const TOOL_CALL_LABELS: Record<string, string> = {
9
9
  seedclub_create_crm_task: "Creating CRM task",
10
10
  seedclub_list_program_contacts: "Loading program contacts",
11
11
  seedclub_search_people: "Searching people",
12
- seedclub_list_meeting_availability: "Checking availability slots",
12
+ seedclub_list_meeting_availability: "Loading availability slots",
13
13
  seedclub_list_meeting_calendars: "Loading booking calendars",
14
14
  seedclub_list_personal_calendar_events: "Loading personal calendar events",
15
15
  seedclub_create_personal_calendar_event: "Creating personal calendar event",
@@ -17,7 +17,6 @@ export const TOOL_CALL_LABELS: Record<string, string> = {
17
17
  seedclub_delete_personal_calendar_event: "Deleting personal calendar event",
18
18
  seedclub_find_common_meeting_availability: "Checking common availability",
19
19
  seedclub_list_meetings: "Checking meeting schedule",
20
- seedclub_book_meeting_full: "Booking meeting",
21
20
  seedclub_list_show_guests: "Looking up show guests",
22
21
  seedclub_list_guest_roster: "Building guest roster",
23
22
  seedclub_get_guest_profile: "Resolving guest profile",
@@ -27,14 +26,6 @@ export const TOOL_CALL_LABELS: Record<string, string> = {
27
26
  seedclub_list_meeting_transcripts: "Checking transcript availability",
28
27
  seedclub_get_meeting_transcript: "Loading meeting transcript",
29
28
  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
29
  seedclub_book_meeting: "Booking meeting",
39
30
  seedclub_reschedule_meeting: "Rescheduling meeting",
40
31
  seedclub_cancel_meeting: "Cancelling meeting",
@@ -58,8 +49,8 @@ export const TOOL_SUCCESS_LABELS: Record<string, string> = {
58
49
  seedclub_create_crm_note: "CRM note saved",
59
50
  seedclub_create_crm_task: "CRM task created",
60
51
  seedclub_list_program_contacts: "Program contacts loaded",
61
- seedclub_search_people: "People search complete",
62
- seedclub_list_meeting_availability: "Availability slots loaded",
52
+ seedclub_search_people: "People loaded",
53
+ seedclub_list_meeting_availability: "Availability loaded",
63
54
  seedclub_list_meeting_calendars: "Booking calendars loaded",
64
55
  seedclub_list_personal_calendar_events: "Personal calendar events loaded",
65
56
  seedclub_create_personal_calendar_event: "Personal calendar event created",
@@ -67,7 +58,6 @@ export const TOOL_SUCCESS_LABELS: Record<string, string> = {
67
58
  seedclub_delete_personal_calendar_event: "Personal calendar event deleted",
68
59
  seedclub_find_common_meeting_availability: "Common availability loaded",
69
60
  seedclub_list_meetings: "Meeting schedule loaded",
70
- seedclub_book_meeting_full: "Meeting booked",
71
61
  seedclub_list_show_guests: "Show guests loaded",
72
62
  seedclub_list_guest_roster: "Guest roster ready",
73
63
  seedclub_get_guest_profile: "Guest profile loaded",
@@ -77,14 +67,6 @@ export const TOOL_SUCCESS_LABELS: Record<string, string> = {
77
67
  seedclub_list_meeting_transcripts: "Transcript availability checked",
78
68
  seedclub_get_meeting_transcript: "Meeting transcript loaded",
79
69
  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
70
  seedclub_book_meeting: "Meeting booked",
89
71
  seedclub_reschedule_meeting: "Meeting rescheduled",
90
72
  seedclub_cancel_meeting: "Meeting cancelled",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubnet/seedclub",
3
- "version": "0.2.32",
3
+ "version": "0.2.33",
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": {