@clubnet/seedclub 0.2.27 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,9 +30,10 @@ seedclub
30
30
 
31
31
  On a fresh install, the normal setup flow is:
32
32
 
33
- 1. `/login` to sign in to a model provider such as Anthropic, OpenAI, or Gemini
34
- 2. `/model` to choose the model you want to use
35
- 3. `/connect` to connect your Seed Club account
33
+ 1. Run `seedclub`
34
+ 2. Seed Club auth opens automatically
35
+ 3. `/login` to sign in to a model provider such as Anthropic, OpenAI, or Gemini
36
+ 4. `/model` to choose the model you want to use
36
37
 
37
38
  After that, run `/seedclub` to open the main Seed Club menu.
38
39
 
@@ -42,24 +43,19 @@ After that, run `/seedclub` to open the main Seed Club menu.
42
43
  curl -fsSL https://raw.githubusercontent.com/seedclub/seedclub-agent/main/install.sh | bash
43
44
  ```
44
45
 
45
- ### Internal install auth
46
+ ### Package access
46
47
 
47
- `@clubnet/seedclub` is currently a private npm package. This auth is only for installing or updating the package from npm. It is separate from `/login` and `/connect` inside the app.
48
-
49
- ```bash
50
- npm login
51
- ```
52
-
53
- Then `npm install -g @clubnet/seedclub` and `seedclub update` work.
48
+ `@clubnet/seedclub` is a public npm package. Install access is open; runtime access is enforced by Seed Club auth inside the app.
54
49
 
55
50
  ## Core workflow
56
51
 
57
52
  The normal interactive flow is:
58
53
 
59
54
  1. Start the app with `seedclub`
60
- 2. Complete `/login`, `/model`, and `/connect` if this is your first run
61
- 3. Open `/seedclub`
62
- 4. Choose the CRM, meetings, media, recordings, or transcript workflow you need
55
+ 2. Complete Seed Club auth when the browser opens
56
+ 3. Complete `/login` and `/model` if this is your first run
57
+ 4. Open `/seedclub`
58
+ 5. Choose the CRM, meetings, media, recordings, or transcript workflow you need
63
59
 
64
60
  ## Commands
65
61
 
@@ -68,6 +64,7 @@ The normal interactive flow is:
68
64
  | `/login` | Sign in to a model provider for the underlying agent |
69
65
  | `/model` | Choose which model to use |
70
66
  | `/connect` | Connect your Seed Club account |
67
+ | `/connect-calendar` | Connect a personal Google Calendar to your Seed Club account |
71
68
  | `/seedclub` | Main menu — connect, inspect access, and jump into CRM/meetings/media/headlines workflows |
72
69
  | `/transcripts` | Export transcript VTT files with filters (date, person, time, output dir) |
73
70
 
@@ -77,13 +74,21 @@ Natural-language transcript retrieval is also supported (no slash command requir
77
74
 
78
75
  There are two separate auth layers in the product:
79
76
 
80
- 1. Model auth: `/login`
81
- This signs you into the LLM provider you want the agent to use.
82
- 2. Seed Club auth: `/connect`
77
+ 1. Seed Club auth: auto-start on launch or `/connect`
83
78
  This connects the CLI to your Seed Club account so Seed Club tools and commands can read and write account data.
79
+ 2. Model auth: `/login`
80
+ This signs you into the LLM provider you want the agent to use.
81
+ 3. Personal calendar connect: `/connect-calendar`
82
+ This connects a Google Calendar to your Seed Club account for booking and availability workflows. It is only needed if you want the agent to schedule using your personal calendar.
83
+
84
+ Seed Club auth is the first gate. Until it succeeds, normal harness usage is blocked and only the setup commands are available.
84
85
 
85
86
  `/seedclub` is the main entry point for Seed Club actions. If you are not connected yet, it will start the Seed Club connect flow automatically.
86
87
 
88
+ CLI auth now prefers an authorization-code exchange:
89
+ - browser callback: `http://127.0.0.1:<port>/callback?code=...&state=...`
90
+ - CLI exchange: `POST ${SEEDCLUB_API_URL}/auth/cli/exchange` with `{ code, state }`
91
+
87
92
  Power-user env overrides:
88
93
 
89
94
  ```bash
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Token storage for Seed Club.
3
3
  *
4
- * Priority: SEEDCLUB_ACCESS_TOKEN / SEEDCLUB_TOKEN env var > stored token file.
4
+ * Priority: SEEDCLUB_ACCESS_TOKEN env var > stored token file.
5
5
  * Use /seedclub to connect.
6
6
  */
7
7
 
@@ -76,12 +76,7 @@ function tryReadStoredBasesSync(): StoredBases | null {
76
76
  }
77
77
 
78
78
  export function getApiBase(): string {
79
- if (
80
- process.env.SEEDCLUB_API_URL ||
81
- process.env.SEEDCLUB_API ||
82
- process.env.SEED_NETWORK_API
83
- )
84
- return process.env.SEEDCLUB_API_URL || process.env.SEEDCLUB_API || process.env.SEED_NETWORK_API!;
79
+ if (process.env.SEEDCLUB_API_URL) return process.env.SEEDCLUB_API_URL;
85
80
  if (shouldPreferLocalBases()) return LOCAL_API_BASE;
86
81
  const storedBases = tryReadStoredBasesSync();
87
82
  if (storedBases?.apiBase) return storedBases.apiBase;
@@ -90,12 +85,7 @@ export function getApiBase(): string {
90
85
  }
91
86
 
92
87
  export function getAuthBase(): string {
93
- if (
94
- process.env.SEEDCLUB_AUTH_URL ||
95
- process.env.SEEDCLUB_AUTH ||
96
- process.env.SEED_NETWORK_AUTH
97
- )
98
- return process.env.SEEDCLUB_AUTH_URL || process.env.SEEDCLUB_AUTH || process.env.SEED_NETWORK_AUTH!;
88
+ if (process.env.SEEDCLUB_AUTH_URL) return process.env.SEEDCLUB_AUTH_URL;
99
89
  if (shouldPreferLocalBases()) return LOCAL_AUTH_BASE;
100
90
  const storedBases = tryReadStoredBasesSync();
101
91
  if (storedBases?.authBase) return storedBases.authBase;
@@ -141,18 +131,14 @@ async function tryReadTokenFile(path: string): Promise<StoredToken | null> {
141
131
  if (
142
132
  stored.apiBase &&
143
133
  !shouldPreferLocalBases() &&
144
- !process.env.SEEDCLUB_API_URL &&
145
- !process.env.SEEDCLUB_API &&
146
- !process.env.SEED_NETWORK_API
134
+ !process.env.SEEDCLUB_API_URL
147
135
  ) {
148
136
  _cachedApiBase = stored.apiBase;
149
137
  }
150
138
  if (
151
139
  stored.authBase &&
152
140
  !shouldPreferLocalBases() &&
153
- !process.env.SEEDCLUB_AUTH_URL &&
154
- !process.env.SEEDCLUB_AUTH &&
155
- !process.env.SEED_NETWORK_AUTH
141
+ !process.env.SEEDCLUB_AUTH_URL
156
142
  ) {
157
143
  _cachedAuthBase = stored.authBase;
158
144
  }
@@ -189,8 +175,7 @@ export async function getStoredBases(): Promise<StoredBases | null> {
189
175
  }
190
176
 
191
177
  export async function getToken(): Promise<string | null> {
192
- if (process.env.SEEDCLUB_ACCESS_TOKEN || process.env.SEEDCLUB_TOKEN || process.env.SEED_NETWORK_TOKEN)
193
- return (process.env.SEEDCLUB_ACCESS_TOKEN || process.env.SEEDCLUB_TOKEN || process.env.SEED_NETWORK_TOKEN)!;
178
+ if (process.env.SEEDCLUB_ACCESS_TOKEN) return process.env.SEEDCLUB_ACCESS_TOKEN;
194
179
  const stored = await getStoredToken();
195
180
  return stored?.token ?? null;
196
181
  }
@@ -15,7 +15,8 @@ import {
15
15
  import { getCurrentUser, getSessionContext } from "../tools/utility.js";
16
16
 
17
17
  interface SeedclubDeps {
18
- connect: (args: string | undefined, ctx: any) => Promise<void>;
18
+ connect: (args: string | undefined, ctx: any) => Promise<boolean>;
19
+ connectCalendar: (ctx: any) => Promise<void>;
19
20
  disconnect: (ctx: any) => Promise<void>;
20
21
  }
21
22
 
@@ -64,6 +65,13 @@ export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
64
65
  },
65
66
  });
66
67
 
68
+ pi.registerCommand("connect-calendar", {
69
+ description: "Connect a personal Google Calendar to your Seed Club account",
70
+ handler: async (_args, ctx) => {
71
+ await deps.connectCalendar(ctx);
72
+ },
73
+ });
74
+
67
75
  pi.registerCommand("clearcontext", {
68
76
  description: "Compact the current conversation to free context while keeping Seed Club state",
69
77
  handler: async (_args, ctx) => {
@@ -101,10 +109,7 @@ export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
101
109
  description: "Seed Club",
102
110
  handler: async (args, ctx) => {
103
111
  const stored = await getStoredToken();
104
- const hasEnvToken =
105
- !!process.env.SEEDCLUB_ACCESS_TOKEN ||
106
- !!process.env.SEEDCLUB_TOKEN ||
107
- !!process.env.SEED_NETWORK_TOKEN;
112
+ const hasEnvToken = !!process.env.SEEDCLUB_ACCESS_TOKEN;
108
113
  const isConnected = !!stored || hasEnvToken;
109
114
 
110
115
  if (!isConnected) {
@@ -129,6 +134,7 @@ export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
129
134
  "Show API/Auth environment",
130
135
  "Use local API/Auth",
131
136
  "Use prod API/Auth",
137
+ "Connect personal calendar",
132
138
  "Open CRM prompt",
133
139
  "Open meetings prompt",
134
140
  "Open transcripts prompt",
@@ -168,6 +174,9 @@ export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
168
174
  case "Use prod API/Auth":
169
175
  await setSeedEnvironment("prod", ctx);
170
176
  break;
177
+ case "Connect personal calendar":
178
+ await deps.connectCalendar(ctx);
179
+ break;
171
180
  case "Open CRM prompt":
172
181
  await new Promise((r) => setTimeout(r, 300));
173
182
  ctx.ui.setEditorText("List CRM records for my workspace.");
@@ -0,0 +1,85 @@
1
+ export type SeedclubAuthGateStatus = "auth_required" | "auth_in_progress" | "auth_complete";
2
+
3
+ export interface SeedclubAuthGateState {
4
+ status: SeedclubAuthGateStatus;
5
+ authUrl: string | null;
6
+ message: string | null;
7
+ error: string | null;
8
+ }
9
+
10
+ export const AUTH_GATE_ALLOWED_COMMANDS = new Set([
11
+ "connect",
12
+ "seedclub",
13
+ "seedenv",
14
+ "commands",
15
+ "extensions",
16
+ ]);
17
+
18
+ const state: SeedclubAuthGateState = {
19
+ status: "auth_required",
20
+ authUrl: null,
21
+ message: "Seed Club sign-in is required before /login or /model.",
22
+ error: null,
23
+ };
24
+
25
+ const listeners = new Set<() => void>();
26
+
27
+ function emit() {
28
+ for (const listener of listeners) listener();
29
+ }
30
+
31
+ function setState(next: Partial<SeedclubAuthGateState>) {
32
+ Object.assign(state, next);
33
+ emit();
34
+ }
35
+
36
+ export function getAuthGateState(): SeedclubAuthGateState {
37
+ return { ...state };
38
+ }
39
+
40
+ export function subscribeToAuthGate(listener: () => void): () => void {
41
+ listeners.add(listener);
42
+ return () => listeners.delete(listener);
43
+ }
44
+
45
+ export function markAuthRequired(options?: { authUrl?: string | null; message?: string | null; error?: string | null }) {
46
+ setState({
47
+ status: "auth_required",
48
+ authUrl: options?.authUrl ?? state.authUrl,
49
+ message: options?.message ?? "Seed Club sign-in is required before /login or /model.",
50
+ error: options?.error ?? null,
51
+ });
52
+ }
53
+
54
+ export function markAuthInProgress(options?: { authUrl?: string | null; message?: string | null; error?: string | null }) {
55
+ setState({
56
+ status: "auth_in_progress",
57
+ authUrl: options?.authUrl ?? state.authUrl,
58
+ message: options?.message ?? "Opening your browser for Seed Club sign-in.",
59
+ error: options?.error ?? null,
60
+ });
61
+ }
62
+
63
+ export function markAuthComplete(message?: string | null) {
64
+ setState({
65
+ status: "auth_complete",
66
+ authUrl: null,
67
+ message: message ?? null,
68
+ error: null,
69
+ });
70
+ }
71
+
72
+ export function isAuthGateBlocking(): boolean {
73
+ return state.status !== "auth_complete";
74
+ }
75
+
76
+ export function getCommandName(text: string): string | null {
77
+ const trimmed = text.trim();
78
+ if (!trimmed.startsWith("/")) return null;
79
+ const token = trimmed.slice(1).split(/\s+/).find(Boolean);
80
+ return token ? token.toLowerCase() : null;
81
+ }
82
+
83
+ export function isAllowedDuringAuthGate(commandName: string | null): boolean {
84
+ return !!commandName && AUTH_GATE_ALLOWED_COMMANDS.has(commandName);
85
+ }