@clubnet/seedclub 0.2.28 → 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 +19 -17
- package/assets/extensions/seedclub/auth.ts +6 -21
- package/assets/extensions/seedclub/commands/seedclub.ts +2 -5
- package/assets/extensions/seedclub/gate-state.ts +85 -0
- package/assets/extensions/seedclub/index.ts +348 -67
- package/assets/extensions/seedclub-ui/welcome.ts +133 -82
- package/bin/cli.js +2 -3
- package/package.json +2 -2
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.
|
|
34
|
-
2.
|
|
35
|
-
3. `/
|
|
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
|
-
###
|
|
46
|
+
### Package access
|
|
46
47
|
|
|
47
|
-
`@clubnet/seedclub` is
|
|
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
|
|
61
|
-
3.
|
|
62
|
-
4.
|
|
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
|
|
|
@@ -78,15 +74,21 @@ Natural-language transcript retrieval is also supported (no slash command requir
|
|
|
78
74
|
|
|
79
75
|
There are two separate auth layers in the product:
|
|
80
76
|
|
|
81
|
-
1.
|
|
82
|
-
This signs you into the LLM provider you want the agent to use.
|
|
83
|
-
2. Seed Club auth: `/connect`
|
|
77
|
+
1. Seed Club auth: auto-start on launch or `/connect`
|
|
84
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.
|
|
85
81
|
3. Personal calendar connect: `/connect-calendar`
|
|
86
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.
|
|
87
83
|
|
|
84
|
+
Seed Club auth is the first gate. Until it succeeds, normal harness usage is blocked and only the setup commands are available.
|
|
85
|
+
|
|
88
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.
|
|
89
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
|
+
|
|
90
92
|
Power-user env overrides:
|
|
91
93
|
|
|
92
94
|
```bash
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Token storage for Seed Club.
|
|
3
3
|
*
|
|
4
|
-
* Priority: SEEDCLUB_ACCESS_TOKEN
|
|
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
|
|
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,7 @@ import {
|
|
|
15
15
|
import { getCurrentUser, getSessionContext } from "../tools/utility.js";
|
|
16
16
|
|
|
17
17
|
interface SeedclubDeps {
|
|
18
|
-
connect: (args: string | undefined, ctx: any) => Promise<
|
|
18
|
+
connect: (args: string | undefined, ctx: any) => Promise<boolean>;
|
|
19
19
|
connectCalendar: (ctx: any) => Promise<void>;
|
|
20
20
|
disconnect: (ctx: any) => Promise<void>;
|
|
21
21
|
}
|
|
@@ -109,10 +109,7 @@ export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
|
|
|
109
109
|
description: "Seed Club",
|
|
110
110
|
handler: async (args, ctx) => {
|
|
111
111
|
const stored = await getStoredToken();
|
|
112
|
-
const hasEnvToken =
|
|
113
|
-
!!process.env.SEEDCLUB_ACCESS_TOKEN ||
|
|
114
|
-
!!process.env.SEEDCLUB_TOKEN ||
|
|
115
|
-
!!process.env.SEED_NETWORK_TOKEN;
|
|
112
|
+
const hasEnvToken = !!process.env.SEEDCLUB_ACCESS_TOKEN;
|
|
116
113
|
const isConnected = !!stored || hasEnvToken;
|
|
117
114
|
|
|
118
115
|
if (!isConnected) {
|
|
@@ -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
|
+
}
|
|
@@ -19,8 +19,19 @@ import { registerCrmTools } from "./tools/crm.js";
|
|
|
19
19
|
import { registerMeetingTools } from "./tools/meetings.js";
|
|
20
20
|
import { registerMediaTools } from "./tools/media.js";
|
|
21
21
|
import registerBrandingGuard from "./branding.js";
|
|
22
|
+
import {
|
|
23
|
+
getCommandName,
|
|
24
|
+
isAllowedDuringAuthGate,
|
|
25
|
+
isAuthGateBlocking,
|
|
26
|
+
markAuthComplete,
|
|
27
|
+
markAuthInProgress,
|
|
28
|
+
markAuthRequired,
|
|
29
|
+
} from "./gate-state.js";
|
|
22
30
|
|
|
23
31
|
export default function (pi: ExtensionAPI) {
|
|
32
|
+
const ENV_TOKEN_KEYS = ["SEEDCLUB_ACCESS_TOKEN"] as const;
|
|
33
|
+
let connectInFlight: Promise<boolean> | null = null;
|
|
34
|
+
|
|
24
35
|
const formatSeedLabel = (name?: string, email?: string) => {
|
|
25
36
|
const label = (name?.trim() || email?.trim() || "connected").replace(/\s+/g, " ");
|
|
26
37
|
return label;
|
|
@@ -65,60 +76,176 @@ export default function (pi: ExtensionAPI) {
|
|
|
65
76
|
registerTranscriptIntentInterceptor(pi);
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
79
|
+
function getPostAuthInstruction(ctx: any): string | null {
|
|
80
|
+
const hasProviderAuth = ctx.modelRegistry.getAvailable().length > 0;
|
|
81
|
+
const hasSelectedModel = !!ctx.model;
|
|
82
|
+
if (!hasProviderAuth) return "Next: /login, then /model.";
|
|
83
|
+
if (!hasSelectedModel) return "Next: /model.";
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function clearSeedStatuses(ctx: any) {
|
|
88
|
+
ctx.ui.setStatus("seed", undefined);
|
|
89
|
+
ctx.ui.setStatus("seed-env", undefined);
|
|
90
|
+
ctx.ui.setStatus("seed-api", undefined);
|
|
91
|
+
ctx.ui.setStatus("seed-auth", undefined);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function applyConnectedStatus(ctx: any, user: { name?: string | null; email?: string | null }) {
|
|
71
95
|
const storedBases = await getStoredBases();
|
|
72
96
|
const effectiveApiBase = getApiBase();
|
|
73
97
|
const effectiveAuthBase = getAuthBase();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
const isDev = effectiveApiBase.includes("localhost") || effectiveApiBase.includes("127.0.0.1");
|
|
99
|
+
const hasSeparateDevAuthBase =
|
|
100
|
+
effectiveAuthBase !== effectiveApiBase &&
|
|
101
|
+
(effectiveAuthBase.includes("localhost") || effectiveAuthBase.includes("127.0.0.1"));
|
|
102
|
+
|
|
103
|
+
ctx.ui.setStatus("seed", formatSeedLabel(user.name ?? undefined, user.email ?? undefined));
|
|
104
|
+
if (storedBases?.mode) ctx.ui.setStatus("seed-env", `env: ${storedBases.mode}`);
|
|
105
|
+
else ctx.ui.setStatus("seed-env", undefined);
|
|
106
|
+
if (isDev) ctx.ui.setStatus("seed-api", `dev: ${effectiveApiBase}`);
|
|
107
|
+
else ctx.ui.setStatus("seed-api", undefined);
|
|
108
|
+
if (hasSeparateDevAuthBase) ctx.ui.setStatus("seed-auth", `auth: ${effectiveAuthBase}`);
|
|
109
|
+
else ctx.ui.setStatus("seed-auth", undefined);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getRuntimeEnvToken(): string | null {
|
|
113
|
+
for (const key of ENV_TOKEN_KEYS) {
|
|
114
|
+
const value = process.env[key];
|
|
115
|
+
if (value?.trim()) return value;
|
|
85
116
|
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function clearRuntimeEnvTokens() {
|
|
121
|
+
for (const key of ENV_TOKEN_KEYS) delete process.env[key];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function validateCurrentCredential(ctx: any): Promise<{ name?: string | null; email?: string | null } | null> {
|
|
125
|
+
const envToken = getRuntimeEnvToken();
|
|
126
|
+
const stored = envToken ? null : await getStoredToken();
|
|
127
|
+
const token = envToken || stored?.token;
|
|
128
|
+
if (!token) return null;
|
|
129
|
+
|
|
130
|
+
markAuthInProgress({ message: "Checking Seed Club access..." });
|
|
131
|
+
setCachedToken(token, getApiBase());
|
|
132
|
+
const user = await getCurrentUser();
|
|
133
|
+
if ("error" in user) {
|
|
134
|
+
await clearCredentials();
|
|
135
|
+
if (envToken) clearRuntimeEnvTokens();
|
|
136
|
+
clearSeedStatuses(ctx);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await applyConnectedStatus(ctx, user);
|
|
141
|
+
return user;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function ensureSeedclubAuthenticated(ctx: any): Promise<boolean> {
|
|
145
|
+
const existing = await validateCurrentCredential(ctx);
|
|
146
|
+
if (existing) {
|
|
147
|
+
markAuthComplete(getPostAuthInstruction(ctx));
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return connect(undefined, ctx, { autoStart: true });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
pi.on("session_start", (_event, ctx) => {
|
|
154
|
+
if (getRuntimeEnvToken()) {
|
|
155
|
+
markAuthInProgress({ message: "Checking Seed Club access..." });
|
|
156
|
+
}
|
|
157
|
+
void ensureSeedclubAuthenticated(ctx);
|
|
86
158
|
});
|
|
87
159
|
|
|
88
160
|
// --- Auth handlers ---
|
|
89
161
|
|
|
90
|
-
async function connect(args: string | undefined, ctx: any) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
ctx.ui.notify("Invalid token.", "error");
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
await verifyAndStore(token, ctx);
|
|
98
|
-
return;
|
|
162
|
+
async function connect(args: string | undefined, ctx: any, options?: { autoStart?: boolean }) {
|
|
163
|
+
if (connectInFlight) {
|
|
164
|
+
ctx.ui.notify("Seed Club sign-in is already in progress.", "info");
|
|
165
|
+
return connectInFlight;
|
|
99
166
|
}
|
|
100
167
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
168
|
+
const run = async () => {
|
|
169
|
+
const trimmedArgs = args?.trim();
|
|
170
|
+
const isReset = trimmedArgs?.toLowerCase() === "reset";
|
|
171
|
+
const token = isReset ? undefined : trimmedArgs;
|
|
172
|
+
if (token) return verifyAndStore(token, ctx, { notifyOnSuccess: true });
|
|
173
|
+
|
|
174
|
+
const apiBase = getApiBase();
|
|
175
|
+
const authBase = getAuthBase();
|
|
176
|
+
const port = await findAvailablePort();
|
|
177
|
+
const state = randomBytes(16).toString("hex");
|
|
178
|
+
const authorizePath = `/auth/cli/authorize?port=${port}&state=${state}`;
|
|
179
|
+
const authUrl = isReset
|
|
180
|
+
? new URL(`/auth/sign-out?redirect=${encodeURIComponent(authorizePath)}`, authBase.endsWith("/") ? authBase : `${authBase}/`).toString()
|
|
181
|
+
: new URL(authorizePath, authBase.endsWith("/") ? authBase : `${authBase}/`).toString();
|
|
182
|
+
|
|
183
|
+
if (isReset) {
|
|
184
|
+
await clearCredentials();
|
|
185
|
+
clearRuntimeEnvTokens();
|
|
186
|
+
clearSeedStatuses(ctx);
|
|
187
|
+
markAuthRequired({
|
|
188
|
+
authUrl: null,
|
|
189
|
+
message: "Seed Club sign-in is required before /login or /model.",
|
|
190
|
+
error: null,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
106
193
|
|
|
107
|
-
|
|
194
|
+
markAuthInProgress({
|
|
195
|
+
authUrl,
|
|
196
|
+
message: isReset
|
|
197
|
+
? "Resetting your Seed Club sign-in and opening the browser to switch accounts."
|
|
198
|
+
: options?.autoStart
|
|
199
|
+
? "Seed Club sign-in is required before /login or /model. Opening your browser now."
|
|
200
|
+
: "Opening your browser for Seed Club sign-in.",
|
|
201
|
+
error: null,
|
|
202
|
+
});
|
|
203
|
+
ctx.ui.notify(isReset ? "Opening browser to switch Seed Club accounts..." : "Opening browser to sign in...", "info");
|
|
204
|
+
|
|
205
|
+
const opened = await openExternalUrl(pi, authUrl, ctx);
|
|
206
|
+
if (!opened) {
|
|
207
|
+
markAuthInProgress({
|
|
208
|
+
authUrl,
|
|
209
|
+
message: "Open the Seed Club auth link below to continue.",
|
|
210
|
+
error: "Browser launch failed. Open the auth URL manually.",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
108
213
|
|
|
109
|
-
|
|
214
|
+
try {
|
|
215
|
+
const result = await waitForCallback(port, state, apiBase);
|
|
216
|
+
return verifyAndStore(result.token, ctx, {
|
|
217
|
+
emailHint: result.email,
|
|
218
|
+
notifyOnSuccess: true,
|
|
219
|
+
});
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const message = error instanceof Error ? error.message : "Auth failed";
|
|
222
|
+
markAuthRequired({
|
|
223
|
+
authUrl,
|
|
224
|
+
message: "Seed Club sign-in is still required. Run /connect to retry.",
|
|
225
|
+
error: message,
|
|
226
|
+
});
|
|
227
|
+
ctx.ui.notify(message, "error");
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
110
231
|
|
|
232
|
+
connectInFlight = run();
|
|
111
233
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
ctx.ui.notify(error instanceof Error ? error.message : "Auth failed", "error");
|
|
234
|
+
return await connectInFlight;
|
|
235
|
+
} finally {
|
|
236
|
+
connectInFlight = null;
|
|
116
237
|
}
|
|
117
238
|
}
|
|
118
239
|
|
|
119
240
|
async function disconnect(ctx: any) {
|
|
120
241
|
await clearCredentials();
|
|
121
|
-
|
|
242
|
+
clearRuntimeEnvTokens();
|
|
243
|
+
clearSeedStatuses(ctx);
|
|
244
|
+
markAuthRequired({
|
|
245
|
+
authUrl: null,
|
|
246
|
+
message: "Seed Club sign-in is required before /login or /model.",
|
|
247
|
+
error: null,
|
|
248
|
+
});
|
|
122
249
|
ctx.ui.notify("Logged out", "info");
|
|
123
250
|
}
|
|
124
251
|
|
|
@@ -192,23 +319,61 @@ export default function (pi: ExtensionAPI) {
|
|
|
192
319
|
}
|
|
193
320
|
}
|
|
194
321
|
|
|
195
|
-
async function verifyAndStore(
|
|
322
|
+
async function verifyAndStore(
|
|
323
|
+
token: string,
|
|
324
|
+
ctx: any,
|
|
325
|
+
options?: { emailHint?: string; notifyOnSuccess?: boolean },
|
|
326
|
+
): Promise<boolean> {
|
|
196
327
|
const apiBase = getApiBase();
|
|
197
328
|
const authBase = getAuthBase();
|
|
198
|
-
|
|
329
|
+
markAuthInProgress({ message: "Verifying Seed Club access...", error: null });
|
|
330
|
+
await storeToken(token, options?.emailHint || "pending", apiBase, { authBase });
|
|
199
331
|
setCachedToken(token, apiBase);
|
|
200
332
|
|
|
201
333
|
const result = await getCurrentUser();
|
|
202
334
|
if ("error" in result) {
|
|
203
335
|
await clearCredentials();
|
|
336
|
+
clearSeedStatuses(ctx);
|
|
337
|
+
markAuthRequired({
|
|
338
|
+
authUrl: null,
|
|
339
|
+
message: "Seed Club sign-in is still required. Run /connect to retry.",
|
|
340
|
+
error: `Token verification failed: ${result.error}`,
|
|
341
|
+
});
|
|
204
342
|
ctx.ui.notify(`Token verification failed: ${result.error}`, "error");
|
|
205
|
-
return;
|
|
343
|
+
return false;
|
|
206
344
|
}
|
|
207
345
|
|
|
208
346
|
await storeToken(token, result.email, apiBase, { authBase, name: result.name });
|
|
209
|
-
ctx
|
|
210
|
-
|
|
347
|
+
await applyConnectedStatus(ctx, result);
|
|
348
|
+
const nextStep = getPostAuthInstruction(ctx);
|
|
349
|
+
markAuthComplete(nextStep);
|
|
350
|
+
if (options?.notifyOnSuccess) {
|
|
351
|
+
const suffix = nextStep ? ` ${nextStep}` : "";
|
|
352
|
+
ctx.ui.notify(`Connected as ${result.name || result.email}.${suffix}`.trim(), "info");
|
|
353
|
+
}
|
|
354
|
+
return true;
|
|
211
355
|
}
|
|
356
|
+
|
|
357
|
+
pi.on("input", async (event, ctx) => {
|
|
358
|
+
if (event.source !== "interactive" || !isAuthGateBlocking()) return;
|
|
359
|
+
|
|
360
|
+
const text = event.text.trim();
|
|
361
|
+
if (!text) return { action: "handled" as const };
|
|
362
|
+
|
|
363
|
+
const commandName = getCommandName(text);
|
|
364
|
+
if (commandName && isAllowedDuringAuthGate(commandName)) return;
|
|
365
|
+
|
|
366
|
+
if (commandName === "login" || commandName === "model") {
|
|
367
|
+
ctx.ui.notify("Connect to Seed Club first.", "info");
|
|
368
|
+
return { action: "handled" as const };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
ctx.ui.notify(
|
|
372
|
+
"Connect to Seed Club first. Allowed now: /connect, /seedclub, /seedenv, /commands, /extensions.",
|
|
373
|
+
"info",
|
|
374
|
+
);
|
|
375
|
+
return { action: "handled" as const };
|
|
376
|
+
});
|
|
212
377
|
}
|
|
213
378
|
|
|
214
379
|
// --- Helpers ---
|
|
@@ -228,7 +393,84 @@ function findAvailablePort(): Promise<number> {
|
|
|
228
393
|
});
|
|
229
394
|
}
|
|
230
395
|
|
|
231
|
-
function
|
|
396
|
+
async function exchangeCliCode(
|
|
397
|
+
apiBase: string,
|
|
398
|
+
code: string,
|
|
399
|
+
state: string,
|
|
400
|
+
): Promise<{ token: string; email: string; name?: string | null }> {
|
|
401
|
+
const url = new URL("/auth/cli/exchange", apiBase.endsWith("/") ? apiBase : `${apiBase}/`);
|
|
402
|
+
const response = await fetch(url.toString(), {
|
|
403
|
+
method: "POST",
|
|
404
|
+
headers: {
|
|
405
|
+
"Content-Type": "application/json",
|
|
406
|
+
Accept: "application/json",
|
|
407
|
+
},
|
|
408
|
+
body: JSON.stringify({ code, state }),
|
|
409
|
+
signal: AbortSignal.timeout(10_000),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const text = await response.text();
|
|
413
|
+
let data: any = {};
|
|
414
|
+
try {
|
|
415
|
+
data = text ? JSON.parse(text) : {};
|
|
416
|
+
} catch {
|
|
417
|
+
if (!response.ok) {
|
|
418
|
+
throw new Error(`CLI code exchange failed (${response.status}).`);
|
|
419
|
+
}
|
|
420
|
+
throw new Error("CLI code exchange returned invalid JSON.");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!response.ok) {
|
|
424
|
+
const message =
|
|
425
|
+
typeof data?.error === "string" && data.error.trim()
|
|
426
|
+
? data.error.trim()
|
|
427
|
+
: `CLI code exchange failed (${response.status}).`;
|
|
428
|
+
throw new Error(message);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (typeof data?.token !== "string" || !data.token.trim()) {
|
|
432
|
+
throw new Error("CLI code exchange returned no token.");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
token: data.token,
|
|
437
|
+
email: typeof data?.email === "string" && data.email.trim() ? data.email : "unknown",
|
|
438
|
+
name: typeof data?.name === "string" && data.name.trim() ? data.name : null,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function verifyCliSessionContext(apiBase: string, token: string): Promise<void> {
|
|
443
|
+
const url = new URL("/session/context", apiBase.endsWith("/") ? apiBase : `${apiBase}/`);
|
|
444
|
+
const response = await fetch(url.toString(), {
|
|
445
|
+
method: "GET",
|
|
446
|
+
headers: {
|
|
447
|
+
Authorization: `Bearer ${token}`,
|
|
448
|
+
Accept: "application/json",
|
|
449
|
+
},
|
|
450
|
+
signal: AbortSignal.timeout(10_000),
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const text = await response.text();
|
|
454
|
+
let data: any = {};
|
|
455
|
+
try {
|
|
456
|
+
data = text ? JSON.parse(text) : {};
|
|
457
|
+
} catch {
|
|
458
|
+
if (!response.ok) {
|
|
459
|
+
throw new Error(`Seed Club access verification failed (${response.status}).`);
|
|
460
|
+
}
|
|
461
|
+
throw new Error("Seed Club access verification returned invalid JSON.");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (!response.ok) {
|
|
465
|
+
const message =
|
|
466
|
+
typeof data?.error === "string" && data.error.trim()
|
|
467
|
+
? data.error.trim()
|
|
468
|
+
: `Seed Club access verification failed (${response.status}).`;
|
|
469
|
+
throw new Error(message);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function waitForCallback(port: number, state: string, apiBase: string): Promise<{ token: string; email: string }> {
|
|
232
474
|
return new Promise((resolve, reject) => {
|
|
233
475
|
const timeout = setTimeout(() => {
|
|
234
476
|
server.close();
|
|
@@ -244,7 +486,12 @@ function waitForCallback(port: number, state: string): Promise<{ token: string;
|
|
|
244
486
|
}
|
|
245
487
|
|
|
246
488
|
const done = (status: number, body: string) => {
|
|
247
|
-
res.writeHead(status, {
|
|
489
|
+
res.writeHead(status, {
|
|
490
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
491
|
+
"Cache-Control": "no-store, max-age=0",
|
|
492
|
+
Pragma: "no-cache",
|
|
493
|
+
Expires: "0",
|
|
494
|
+
});
|
|
248
495
|
res.end(body);
|
|
249
496
|
clearTimeout(timeout);
|
|
250
497
|
server.close();
|
|
@@ -271,26 +518,44 @@ function waitForCallback(port: number, state: string): Promise<{ token: string;
|
|
|
271
518
|
reject(new Error(error));
|
|
272
519
|
return;
|
|
273
520
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
521
|
+
void (async () => {
|
|
522
|
+
try {
|
|
523
|
+
const code = url.searchParams.get("code");
|
|
524
|
+
if (!code?.trim()) {
|
|
525
|
+
done(400, renderCallbackPage({
|
|
526
|
+
eyebrow: "Seed Club Auth",
|
|
527
|
+
title: "No code was returned.",
|
|
528
|
+
message: "Seed Club completed sign-in, but the local callback did not receive a usable authorization code. Try /connect again.",
|
|
529
|
+
status: "error",
|
|
530
|
+
cleanUrlPath: "/callback",
|
|
531
|
+
}));
|
|
532
|
+
reject(new Error("Invalid authorization code"));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const exchange = await exchangeCliCode(apiBase, code, state);
|
|
537
|
+
await verifyCliSessionContext(apiBase, exchange.token);
|
|
538
|
+
done(200, renderCallbackPage({
|
|
539
|
+
eyebrow: "Seed Club Auth",
|
|
540
|
+
title: "You're connected.",
|
|
541
|
+
message: `Signed in as ${escapeHtml(exchange.email)}. Seed Club access is verified and your CLI session is ready.`,
|
|
542
|
+
status: "success",
|
|
543
|
+
cleanUrlPath: "/callback",
|
|
544
|
+
}));
|
|
545
|
+
resolve({ token: exchange.token, email: exchange.email });
|
|
546
|
+
} catch (exchangeError) {
|
|
547
|
+
const message =
|
|
548
|
+
exchangeError instanceof Error ? exchangeError.message : "CLI code exchange failed.";
|
|
549
|
+
done(400, renderCallbackPage({
|
|
550
|
+
eyebrow: "Seed Club Auth",
|
|
551
|
+
title: "CLI token exchange failed.",
|
|
552
|
+
message,
|
|
553
|
+
status: "error",
|
|
554
|
+
cleanUrlPath: "/callback",
|
|
555
|
+
}));
|
|
556
|
+
reject(new Error(message));
|
|
557
|
+
}
|
|
558
|
+
})();
|
|
294
559
|
});
|
|
295
560
|
|
|
296
561
|
server.listen(port, "127.0.0.1");
|
|
@@ -301,13 +566,17 @@ function waitForCallback(port: number, state: string): Promise<{ token: string;
|
|
|
301
566
|
});
|
|
302
567
|
}
|
|
303
568
|
|
|
304
|
-
function openExternalUrl(pi: ExtensionAPI, url: string, ctx: any) {
|
|
569
|
+
async function openExternalUrl(pi: ExtensionAPI, url: string, ctx: any): Promise<boolean> {
|
|
305
570
|
const openCmd =
|
|
306
571
|
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
307
572
|
|
|
308
|
-
|
|
573
|
+
try {
|
|
574
|
+
await pi.exec(openCmd, [url]);
|
|
575
|
+
return true;
|
|
576
|
+
} catch {
|
|
309
577
|
ctx.ui.notify(`Open this link:\n${url}`, "info");
|
|
310
|
-
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
311
580
|
}
|
|
312
581
|
|
|
313
582
|
function waitForCalendarCallback(
|
|
@@ -329,7 +598,12 @@ function waitForCalendarCallback(
|
|
|
329
598
|
}
|
|
330
599
|
|
|
331
600
|
const done = (status: number, body: string) => {
|
|
332
|
-
res.writeHead(status, {
|
|
601
|
+
res.writeHead(status, {
|
|
602
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
603
|
+
"Cache-Control": "no-store, max-age=0",
|
|
604
|
+
Pragma: "no-cache",
|
|
605
|
+
Expires: "0",
|
|
606
|
+
});
|
|
333
607
|
res.end(body);
|
|
334
608
|
clearTimeout(timeout);
|
|
335
609
|
server.close();
|
|
@@ -368,6 +642,7 @@ function waitForCalendarCallback(
|
|
|
368
642
|
title: "Your calendar is connected.",
|
|
369
643
|
message: `Connected ${escapeHtml(accountLabel || accountUsername || "your Google Calendar")} to your Seed Club account.`,
|
|
370
644
|
status: "success",
|
|
645
|
+
cleanUrlPath: "/callback",
|
|
371
646
|
}));
|
|
372
647
|
resolve({
|
|
373
648
|
accountId: url.searchParams.get("accountId"),
|
|
@@ -398,6 +673,7 @@ function renderCallbackPage(input: {
|
|
|
398
673
|
title: string;
|
|
399
674
|
message: string;
|
|
400
675
|
status: "success" | "error";
|
|
676
|
+
cleanUrlPath?: string;
|
|
401
677
|
}) {
|
|
402
678
|
const palette =
|
|
403
679
|
input.status === "success"
|
|
@@ -519,6 +795,11 @@ function renderCallbackPage(input: {
|
|
|
519
795
|
}
|
|
520
796
|
}
|
|
521
797
|
</style>
|
|
798
|
+
<script>
|
|
799
|
+
try {
|
|
800
|
+
${input.cleanUrlPath ? `window.history.replaceState(null, "", ${JSON.stringify(input.cleanUrlPath)});` : ""}
|
|
801
|
+
} catch {}
|
|
802
|
+
</script>
|
|
522
803
|
</head>
|
|
523
804
|
<body>
|
|
524
805
|
<main class="shell">
|
|
@@ -526,7 +807,7 @@ function renderCallbackPage(input: {
|
|
|
526
807
|
<p class="eyebrow">${escapeHtml(input.eyebrow)}</p>
|
|
527
808
|
<h1>${escapeHtml(input.title)}</h1>
|
|
528
809
|
<p>${escapeHtml(input.message)}</p>
|
|
529
|
-
<div class="note">You can close this
|
|
810
|
+
<div class="note">You can close this page and return to your terminal.</div>
|
|
530
811
|
</main>
|
|
531
812
|
</body>
|
|
532
813
|
</html>`;
|
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
|
-
import {
|
|
8
|
-
import { homedir } from "node:os";
|
|
9
|
-
import { basename, join } from "node:path";
|
|
7
|
+
import { basename } from "node:path";
|
|
10
8
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
11
9
|
import { ApiError, NotConnectedError, api } from "../seedclub/api-client.js";
|
|
10
|
+
import { getAuthGateState, isAuthGateBlocking, subscribeToAuthGate } from "../seedclub/gate-state.js";
|
|
12
11
|
import { uiState } from "./state.js";
|
|
13
12
|
|
|
14
13
|
const BOLD = "\x1b[1m";
|
|
@@ -278,12 +277,6 @@ async function withTimeout<T>(promise: Promise<T>, ms: number, fallback: T): Pro
|
|
|
278
277
|
}
|
|
279
278
|
}
|
|
280
279
|
|
|
281
|
-
function hasSeedConnection(): boolean {
|
|
282
|
-
if (process.env.SEEDCLUB_ACCESS_TOKEN || process.env.SEEDCLUB_TOKEN || process.env.SEED_NETWORK_TOKEN) return true;
|
|
283
|
-
const tokenPath = join(homedir(), ".config", "seedclub", "token");
|
|
284
|
-
return existsSync(tokenPath);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
280
|
function getTerminalTitle(): string {
|
|
288
281
|
const cwd = basename(process.cwd());
|
|
289
282
|
return cwd ? `⦿ - ${cwd}` : "⦿";
|
|
@@ -305,19 +298,47 @@ function formatQuote(q: MarketQuote, theme: ThemeLike): string {
|
|
|
305
298
|
|
|
306
299
|
function renderSetupLines(setupHints: string[], theme: ThemeLike): string[] {
|
|
307
300
|
return setupHints.flatMap((hint) => {
|
|
301
|
+
if (hint === "/connect") {
|
|
302
|
+
return [` ${theme.fg("text", "/connect")} ${theme.fg("dim", "sign in to Seed Club")}`];
|
|
303
|
+
}
|
|
308
304
|
if (hint === "/login") {
|
|
309
305
|
return [` ${theme.fg("text", "/login")} ${theme.fg("dim", "sign in with Anthropic, OpenAI, Gemini, or others")}`];
|
|
310
306
|
}
|
|
311
307
|
if (hint === "/model") {
|
|
312
308
|
return [` ${theme.fg("text", "/model")} ${theme.fg("dim", "choose your model")}`];
|
|
313
309
|
}
|
|
314
|
-
if (hint === "/connect") {
|
|
315
|
-
return [` ${theme.fg("text", "/connect")} ${theme.fg("dim", "to seeclub.com")}`];
|
|
316
|
-
}
|
|
317
310
|
return [` ${theme.fg("text", hint)}`];
|
|
318
311
|
});
|
|
319
312
|
}
|
|
320
313
|
|
|
314
|
+
function renderAuthGateLines(theme: ThemeLike): string[] {
|
|
315
|
+
const gate = getAuthGateState();
|
|
316
|
+
const lines = [
|
|
317
|
+
"",
|
|
318
|
+
renderTitle(theme),
|
|
319
|
+
"",
|
|
320
|
+
` ${theme.fg("accent", "Secure access required")}`,
|
|
321
|
+
` ${theme.fg("dim", gate.message || "Seed Club sign-in is required before /login or /model.")}`,
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
if (gate.error) {
|
|
325
|
+
lines.push("");
|
|
326
|
+
lines.push(` ${theme.fg("error", gate.error)}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (gate.authUrl) {
|
|
330
|
+
lines.push("");
|
|
331
|
+
lines.push(` ${theme.fg("text", "Auth URL")}`);
|
|
332
|
+
lines.push(` ${theme.fg("mdLink", gate.authUrl)}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
lines.push("");
|
|
336
|
+
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
|
+
lines.push("");
|
|
339
|
+
return lines;
|
|
340
|
+
}
|
|
341
|
+
|
|
321
342
|
function renderTodayOn11amLines(today: TodayOn11am | null, theme: ThemeLike): string[] {
|
|
322
343
|
if (!today || !today.guests.length) return [];
|
|
323
344
|
const lines = [` ${theme.fg("text", "Today on 11AM")}`];
|
|
@@ -392,6 +413,10 @@ function renderCoinLoaderLines(frame: number, theme: ThemeLike): string[] {
|
|
|
392
413
|
return [...lines, "", `${loadingIndent}${theme.fg("dim", loadingLabel)}`];
|
|
393
414
|
}
|
|
394
415
|
|
|
416
|
+
function shouldShowCommandInList(name: string): boolean {
|
|
417
|
+
return !name.startsWith("skill:");
|
|
418
|
+
}
|
|
419
|
+
|
|
395
420
|
export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean }) {
|
|
396
421
|
let headerLines: string[] = [
|
|
397
422
|
"",
|
|
@@ -400,18 +425,13 @@ export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean })
|
|
|
400
425
|
...renderCoinLoaderLines(0, PLAIN_THEME),
|
|
401
426
|
"",
|
|
402
427
|
];
|
|
428
|
+
let unsubscribeAuthGate: (() => void) | undefined;
|
|
403
429
|
|
|
404
430
|
pi.on("session_start", async (_event, ctx) => {
|
|
405
431
|
if (!ctx.hasUI) return;
|
|
406
432
|
applyTerminalTitle(ctx);
|
|
407
433
|
uiState.ready = false;
|
|
408
|
-
|
|
409
|
-
const hasSelectedModel = !!ctx.model;
|
|
410
|
-
const connectedToSeed = hasSeedConnection();
|
|
411
|
-
const setupHints: string[] = [];
|
|
412
|
-
if (!hasAnyAuth) setupHints.push("/login");
|
|
413
|
-
if (!hasSelectedModel) setupHints.push("/model");
|
|
414
|
-
if (!connectedToSeed) setupHints.push("/connect");
|
|
434
|
+
unsubscribeAuthGate?.();
|
|
415
435
|
|
|
416
436
|
let tuiRef: any = null;
|
|
417
437
|
ctx.ui.setHeader((tui, theme) => {
|
|
@@ -436,77 +456,107 @@ export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean })
|
|
|
436
456
|
};
|
|
437
457
|
});
|
|
438
458
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
459
|
+
let loadStarted = false;
|
|
460
|
+
|
|
461
|
+
const startReadyHeader = () => {
|
|
462
|
+
if (loadStarted) return;
|
|
463
|
+
loadStarted = true;
|
|
464
|
+
uiState.ready = false;
|
|
465
|
+
|
|
466
|
+
const setupHints: string[] = [];
|
|
467
|
+
const hasAnyAuth = ctx.modelRegistry.getAvailable().length > 0;
|
|
468
|
+
const hasSelectedModel = !!ctx.model;
|
|
469
|
+
if (!hasAnyAuth) setupHints.push("/login");
|
|
470
|
+
if (!hasSelectedModel) setupHints.push("/model");
|
|
471
|
+
const setupLines = renderSetupLines(setupHints, ctx.ui.theme);
|
|
472
|
+
|
|
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
|
+
};
|
|
453
483
|
renderLoadingHeader();
|
|
484
|
+
const loaderTimer = setInterval(() => {
|
|
485
|
+
loaderFrame += 1;
|
|
486
|
+
renderLoadingHeader();
|
|
487
|
+
tuiRef?.requestRender();
|
|
488
|
+
}, 120);
|
|
489
|
+
loaderTimer.unref?.();
|
|
454
490
|
tuiRef?.requestRender();
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
491
|
+
|
|
492
|
+
const todayPromise = fetchTodayOn11am();
|
|
493
|
+
void Promise.all([
|
|
494
|
+
getData(),
|
|
495
|
+
withTimeout(todayPromise, TODAY_PREFETCH_TIMEOUT_MS, null),
|
|
496
|
+
]).then(([{ weather, market }, todayOn11am]) => {
|
|
497
|
+
clearInterval(loaderTimer);
|
|
498
|
+
const renderReadyHeader = (today: TodayOn11am | null) => {
|
|
499
|
+
const theme = ctx.ui.theme;
|
|
500
|
+
const weatherLine = ` ${weather.icon} ${theme.fg("text", weather.temp)} ${theme.fg("dim", weather.condition)} ${theme.fg("dim", "·")} ${theme.fg("dim", weather.location)}`;
|
|
501
|
+
const marketLine = ` ${market.map((quote) => formatQuote(quote, theme)).join(` ${theme.fg("dim", "·")} `)}`;
|
|
502
|
+
const todayLines = renderTodayOn11amLines(today, theme);
|
|
503
|
+
uiState.todayOn11am = today;
|
|
504
|
+
headerLines = [
|
|
505
|
+
"",
|
|
506
|
+
renderTitle(theme),
|
|
507
|
+
"",
|
|
508
|
+
weatherLine,
|
|
509
|
+
marketLine,
|
|
510
|
+
"",
|
|
511
|
+
...todayLines,
|
|
512
|
+
...(todayLines.length ? [""] : []),
|
|
513
|
+
...setupLines,
|
|
514
|
+
"",
|
|
515
|
+
];
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
renderReadyHeader(todayOn11am);
|
|
519
|
+
uiState.ready = true;
|
|
520
|
+
ctx.ui.setEditorText("");
|
|
521
|
+
tuiRef?.requestRender();
|
|
522
|
+
|
|
523
|
+
if (!todayOn11am?.guests.length) {
|
|
524
|
+
void todayPromise.then((freshToday) => {
|
|
525
|
+
if (!freshToday?.guests.length) return;
|
|
526
|
+
renderReadyHeader(freshToday);
|
|
527
|
+
tuiRef?.requestRender();
|
|
528
|
+
}).catch(() => {});
|
|
529
|
+
}
|
|
530
|
+
}).catch(() => {
|
|
531
|
+
clearInterval(loaderTimer);
|
|
471
532
|
headerLines = [
|
|
472
533
|
"",
|
|
473
|
-
renderTitle(theme),
|
|
474
|
-
"",
|
|
475
|
-
weatherLine,
|
|
476
|
-
marketLine,
|
|
534
|
+
renderTitle(ctx.ui.theme),
|
|
477
535
|
"",
|
|
478
|
-
...todayLines,
|
|
479
|
-
...(todayLines.length ? [""] : []),
|
|
480
536
|
...setupLines,
|
|
481
537
|
"",
|
|
482
538
|
];
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
tuiRef?.requestRender();
|
|
539
|
+
uiState.ready = true;
|
|
540
|
+
ctx.ui.setEditorText("");
|
|
541
|
+
tuiRef?.requestRender();
|
|
542
|
+
});
|
|
543
|
+
};
|
|
489
544
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
545
|
+
const renderCurrentHeader = () => {
|
|
546
|
+
if (isAuthGateBlocking()) {
|
|
547
|
+
loadStarted = false;
|
|
548
|
+
uiState.todayOn11am = null;
|
|
549
|
+
headerLines = renderAuthGateLines(ctx.ui.theme);
|
|
550
|
+
uiState.ready = true;
|
|
551
|
+
ctx.ui.setEditorText("");
|
|
552
|
+
tuiRef?.requestRender();
|
|
553
|
+
return;
|
|
496
554
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
"",
|
|
503
|
-
...setupLines,
|
|
504
|
-
"",
|
|
505
|
-
];
|
|
506
|
-
uiState.ready = true;
|
|
507
|
-
ctx.ui.setEditorText("");
|
|
508
|
-
tuiRef?.requestRender();
|
|
509
|
-
});
|
|
555
|
+
startReadyHeader();
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
unsubscribeAuthGate = subscribeToAuthGate(renderCurrentHeader);
|
|
559
|
+
renderCurrentHeader();
|
|
510
560
|
});
|
|
511
561
|
|
|
512
562
|
pi.on("turn_start", (_event, ctx) => {
|
|
@@ -546,9 +596,10 @@ ${rows.join("\n")}`,
|
|
|
546
596
|
pi.on("input", async (event, ctx) => {
|
|
547
597
|
if (event.source !== "interactive") return;
|
|
548
598
|
if (!uiState.ready) return { action: "handled" };
|
|
599
|
+
if (isAuthGateBlocking()) return;
|
|
549
600
|
if (event.text.trim().startsWith("/")) return;
|
|
550
601
|
if (ctx.model) return;
|
|
551
|
-
ctx.ui.notify("Set up
|
|
602
|
+
ctx.ui.notify("Set up next: /login, then /model.", "info");
|
|
552
603
|
return { action: "handled" };
|
|
553
604
|
});
|
|
554
605
|
|
|
@@ -556,7 +607,7 @@ ${rows.join("\n")}`,
|
|
|
556
607
|
description: "List all available commands",
|
|
557
608
|
handler: async (_args, ctx) => {
|
|
558
609
|
const theme = ctx.ui.theme;
|
|
559
|
-
const commands = pi.getCommands();
|
|
610
|
+
const commands = pi.getCommands().filter((cmd) => shouldShowCommandInList(cmd.name));
|
|
560
611
|
const lines = commands
|
|
561
612
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
562
613
|
.map((cmd) => ` ${theme.fg("accent", `/${cmd.name}`)} ${theme.fg("dim", cmd.description || "")}`)
|
package/bin/cli.js
CHANGED
|
@@ -15,9 +15,8 @@ const SEEDCLUB_ENV_EXCLUDE = new Set(["SEEDCLUB_PI_MAIN"]);
|
|
|
15
15
|
|
|
16
16
|
function printPrivateRegistryHint() {
|
|
17
17
|
console.error("seedclub: install/update failed.");
|
|
18
|
-
console.error("
|
|
19
|
-
console.error(" npm
|
|
20
|
-
console.error(" seedclub update");
|
|
18
|
+
console.error("Retry with:");
|
|
19
|
+
console.error(" npm install -g @clubnet/seedclub@latest");
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
function findPackageRoot(fromFile, expectedName) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubnet/seedclub",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.29",
|
|
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": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"url": "git+https://github.com/seedclub/seedclub-agent.git"
|
|
9
9
|
},
|
|
10
10
|
"publishConfig": {
|
|
11
|
-
"access": "
|
|
11
|
+
"access": "public"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
14
|
"seedclub": "bin/cli.js"
|