@clubnet/seedclub 0.2.18 → 0.2.19
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 +30 -10
- package/assets/extensions/seedclub/api-client.ts +24 -6
- package/assets/extensions/seedclub/auth.ts +181 -10
- package/assets/extensions/seedclub/branding.ts +13 -0
- package/assets/extensions/seedclub/commands/clip-status.ts +187 -0
- package/assets/extensions/seedclub/commands/seedclub.ts +169 -25
- package/assets/extensions/seedclub/commands/transcript-intent.ts +998 -0
- package/assets/extensions/seedclub/commands/transcripts.ts +383 -0
- package/assets/extensions/seedclub/index.ts +218 -27
- package/assets/extensions/seedclub/package-lock.json +8 -1
- package/assets/extensions/seedclub/package.json +5 -2
- package/assets/extensions/seedclub/tool-utils.ts +45 -3
- package/assets/extensions/seedclub/tools/crm.ts +183 -0
- package/assets/extensions/seedclub/tools/media.ts +217 -0
- package/assets/extensions/seedclub/tools/meetings.ts +1053 -0
- package/assets/extensions/seedclub/tools/utility.ts +106 -9
- package/assets/extensions/seedclub-ui/editor.ts +16 -29
- package/assets/extensions/seedclub-ui/footer.ts +1 -3
- package/assets/extensions/seedclub-ui/state.ts +10 -1
- package/assets/extensions/seedclub-ui/welcome.ts +274 -85
- package/assets/theme/dark.json +47 -59
- package/assets/theme/light.json +49 -61
- package/bin/cli.js +380 -23
- package/bin/pi-main-launcher.js +29 -0
- package/package.json +6 -2
- package/postinstall.js +1 -1
- package/assets/extensions/seedclub/commands/add.ts +0 -601
- package/assets/extensions/seedclub/commands/extract.ts +0 -123
- package/assets/extensions/seedclub/commands/signals.ts +0 -86
- package/assets/extensions/seedclub/commands/sort.ts +0 -91
- package/assets/extensions/seedclub/dia-cookies.ts +0 -126
- package/assets/extensions/seedclub/extraction/capture.ts +0 -47
- package/assets/extensions/seedclub/extraction/schema.ts +0 -117
- package/assets/extensions/seedclub/tools/extractions.ts +0 -517
- package/assets/extensions/seedclub/tools/signals.ts +0 -275
- package/assets/extensions/seedclub/twitter-client.ts +0 -277
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# seedclub
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Seed Club's pi-based agent for authenticated access to Seed Club programs, CRM, meetings, media, and headlines.
|
|
4
4
|
|
|
5
5
|
Requirements: Node.js 22+
|
|
6
6
|
|
|
7
7
|
## What it is
|
|
8
8
|
|
|
9
|
-
`seedclub` is the Seed Club distribution of pi. This repo provides the `seedclub` launcher, installs the Seed Club theme and extensions, and adds Seed Club-specific workflows for connecting your account,
|
|
9
|
+
`seedclub` is the Seed Club distribution of pi. This repo provides the `seedclub` launcher, installs the Seed Club theme and extensions, and adds Seed Club-specific workflows for connecting your account, using CRM/meeting/media tools, retrieving transcripts, and working with Seed Club-backed app surfaces.
|
|
10
10
|
|
|
11
11
|
This repo is the source of truth for the published package:
|
|
12
12
|
|
|
@@ -69,7 +69,7 @@ The normal interactive flow is:
|
|
|
69
69
|
1. Start the app with `seedclub`
|
|
70
70
|
2. Complete `/login`, `/model`, and `/connect` if this is your first run
|
|
71
71
|
3. Open `/seedclub`
|
|
72
|
-
4. Choose
|
|
72
|
+
4. Choose the CRM, meetings, media, recordings, or transcript workflow you need
|
|
73
73
|
|
|
74
74
|
## Commands
|
|
75
75
|
|
|
@@ -78,11 +78,12 @@ The normal interactive flow is:
|
|
|
78
78
|
| `/login` | Sign in to a model provider for the underlying agent |
|
|
79
79
|
| `/model` | Choose which model to use |
|
|
80
80
|
| `/connect` | Connect your Seed Club account |
|
|
81
|
-
| `/seedclub` |
|
|
82
|
-
| `/
|
|
83
|
-
| `/sort` | Review unsorted signals and sort them automatically, in the browser, or delete them |
|
|
81
|
+
| `/seedclub` | Main menu — connect, inspect access, and jump into CRM/meetings/media/headlines workflows |
|
|
82
|
+
| `/transcripts` | Export transcript VTT files with filters (date, person, time, output dir) |
|
|
84
83
|
| `seedclub setup-auth` | Configure npm auth for npmjs private package access in `~/.npmrc` |
|
|
85
84
|
|
|
85
|
+
Natural-language transcript retrieval is also supported (no slash command required). Examples: `download vibhu transcripts from 11am`, `i need transcripts for all guests on 11am last week`. Seed Club will run metadata-first export confirmation and then write VTT files.
|
|
86
|
+
|
|
86
87
|
## Auth
|
|
87
88
|
|
|
88
89
|
There are two separate auth layers in the product:
|
|
@@ -94,6 +95,23 @@ There are two separate auth layers in the product:
|
|
|
94
95
|
|
|
95
96
|
`/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.
|
|
96
97
|
|
|
98
|
+
Power-user env overrides:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
export SEEDCLUB_API_URL=http://localhost:3001
|
|
102
|
+
export SEEDCLUB_AUTH_URL=http://localhost:3000
|
|
103
|
+
export SEEDCLUB_ACCESS_TOKEN=<bearer-token>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Production defaults are already built into the auth extension:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
SEEDCLUB_API_URL=https://api.seedclub.com
|
|
110
|
+
SEEDCLUB_AUTH_URL=https://auth.seedclub.com
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
You do not need to export those for normal use. Only set env vars when you want to override them for local/dev.
|
|
114
|
+
|
|
97
115
|
## How it works
|
|
98
116
|
|
|
99
117
|
seedclub is an npm package (`@clubnet/seedclub`) that wraps [pi](https://github.com/badlogic/pi-mono) as a dependency. Installing the package globally gives you the `seedclub` command.
|
|
@@ -129,9 +147,9 @@ seedclub pins versions in `package.json`:
|
|
|
129
147
|
|
|
130
148
|
```json
|
|
131
149
|
{
|
|
132
|
-
"version": "0.2.
|
|
150
|
+
"version": "0.2.18",
|
|
133
151
|
"dependencies": {
|
|
134
|
-
"@mariozechner/pi-coding-agent": "0.
|
|
152
|
+
"@mariozechner/pi-coding-agent": "0.65.2"
|
|
135
153
|
}
|
|
136
154
|
}
|
|
137
155
|
```
|
|
@@ -155,6 +173,8 @@ It's a standard pi theme with 51 color tokens. Edit it to change any visual aspe
|
|
|
155
173
|
|
|
156
174
|
The `vars` block at the top defines reusable colors (e.g. `brand: "#00C853"`) that are referenced throughout `colors`. To change the brand color, just update `vars.brand`.
|
|
157
175
|
|
|
176
|
+
Seed Club keeps product-level names in `vars` (`editorBg`, `messageBg`, `successBg`, `errorBg`) and maps pi's required tokens to those names (`selectedBg`, `userMessageBg`, `toolPendingBg`, etc.). The CLI probes the terminal background with OSC 11 and sets `COLORFGBG` before pi starts when no explicit theme is configured, so light/dark defaults track the actual terminal window instead of pi's fallback.
|
|
177
|
+
|
|
158
178
|
**Hot reload:** Edit the theme file while seedclub is running and it reloads instantly.
|
|
159
179
|
|
|
160
180
|
Colors can be hex (`"#00C853"`), 256-color palette index (`242`), a reference to a `vars` entry (`"brand"`), or empty string (`""`) for the terminal default.
|
|
@@ -249,8 +269,8 @@ seedclub # test
|
|
|
249
269
|
# When ready, bump package version and publish
|
|
250
270
|
git add -A
|
|
251
271
|
git commit -m "update extensions"
|
|
252
|
-
|
|
253
|
-
|
|
272
|
+
npm version patch|minor|major
|
|
273
|
+
git push --follow-tags
|
|
254
274
|
```
|
|
255
275
|
|
|
256
276
|
For reproducible extension dependency installs, commit `assets/extensions/seedclub/package-lock.json` and keep it in sync when changing extension deps.
|
|
@@ -52,12 +52,26 @@ interface RequestOptions {
|
|
|
52
52
|
params?: Record<string, string | number | undefined>;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
async function shouldClearCredentialsAfterUnauthorized(apiBase: string, token: string): Promise<boolean> {
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL("/session/context", apiBase.endsWith("/") ? apiBase : `${apiBase}/`);
|
|
58
|
+
const response = await fetch(url.toString(), {
|
|
59
|
+
method: "GET",
|
|
60
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
61
|
+
});
|
|
62
|
+
return response.status === 401;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
async function apiRequest<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
|
|
56
69
|
const { method = "GET", body, params } = options;
|
|
57
70
|
const token = await getAuthToken();
|
|
58
71
|
const apiBase = cachedApiBase || getApiBase();
|
|
59
72
|
|
|
60
|
-
const
|
|
73
|
+
const path = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
74
|
+
const url = new URL(path, apiBase.endsWith("/") ? apiBase : `${apiBase}/`);
|
|
61
75
|
if (params) {
|
|
62
76
|
for (const [key, value] of Object.entries(params)) {
|
|
63
77
|
if (value !== undefined) url.searchParams.set(key, String(value));
|
|
@@ -70,11 +84,6 @@ async function apiRequest<T>(endpoint: string, options: RequestOptions = {}): Pr
|
|
|
70
84
|
body: body ? JSON.stringify(body) : undefined,
|
|
71
85
|
});
|
|
72
86
|
|
|
73
|
-
if (response.status === 401) {
|
|
74
|
-
await clearCredentials();
|
|
75
|
-
throw new ApiError(401, "Token expired or revoked. Run /seedclub to reconnect.");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
87
|
const text = await response.text();
|
|
79
88
|
let data: any;
|
|
80
89
|
try {
|
|
@@ -86,6 +95,15 @@ async function apiRequest<T>(endpoint: string, options: RequestOptions = {}): Pr
|
|
|
86
95
|
throw new ApiError(response.status, `Invalid JSON response from server: ${text.slice(0, 200)}`);
|
|
87
96
|
}
|
|
88
97
|
|
|
98
|
+
if (response.status === 401) {
|
|
99
|
+
const shouldClear = await shouldClearCredentialsAfterUnauthorized(apiBase, token);
|
|
100
|
+
if (shouldClear) {
|
|
101
|
+
await clearCredentials();
|
|
102
|
+
throw new ApiError(401, "Token expired or revoked. Run /seedclub to reconnect.");
|
|
103
|
+
}
|
|
104
|
+
throw new ApiError(401, data?.error || "Request failed (401)", data?.details);
|
|
105
|
+
}
|
|
106
|
+
|
|
89
107
|
if (!response.ok) {
|
|
90
108
|
throw new ApiError(response.status, data.error || `Request failed (${response.status})`, data.details);
|
|
91
109
|
}
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Token storage for Seed Club.
|
|
3
3
|
*
|
|
4
|
-
* Priority: SEEDCLUB_TOKEN env var > stored token file.
|
|
4
|
+
* Priority: SEEDCLUB_ACCESS_TOKEN / SEEDCLUB_TOKEN env var > stored token file.
|
|
5
5
|
* Use /seedclub to connect.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
9
10
|
import { homedir } from "node:os";
|
|
10
11
|
import { join } from "node:path";
|
|
11
12
|
|
|
12
13
|
const CONFIG_DIR = join(homedir(), ".config", "seedclub");
|
|
13
14
|
const TOKEN_FILE = join(CONFIG_DIR, "token");
|
|
15
|
+
const BASES_FILE = join(CONFIG_DIR, "bases.json");
|
|
14
16
|
const LEGACY_CONFIG_DIR = join(homedir(), ".config", "looseleaf");
|
|
15
17
|
const LEGACY_TOKEN_FILE = join(LEGACY_CONFIG_DIR, "token");
|
|
16
|
-
const DEFAULT_API_BASE = "https://
|
|
18
|
+
const DEFAULT_API_BASE = "https://api.seedclub.com";
|
|
19
|
+
const DEFAULT_AUTH_BASE = "https://auth.seedclub.com";
|
|
20
|
+
const LOCAL_API_BASE = "http://localhost:3001";
|
|
21
|
+
const LOCAL_AUTH_BASE = "http://localhost:3000";
|
|
22
|
+
|
|
23
|
+
export type SeedclubEnvironmentMode = "local" | "prod" | "custom";
|
|
17
24
|
|
|
18
25
|
export interface StoredToken {
|
|
19
26
|
token: string;
|
|
@@ -21,33 +28,134 @@ export interface StoredToken {
|
|
|
21
28
|
name?: string;
|
|
22
29
|
createdAt: string;
|
|
23
30
|
apiBase: string;
|
|
31
|
+
authBase?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StoredBases {
|
|
35
|
+
apiBase: string;
|
|
36
|
+
authBase: string;
|
|
37
|
+
mode: SeedclubEnvironmentMode;
|
|
38
|
+
updatedAt: string;
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
let _cachedApiBase: string | null = null;
|
|
42
|
+
let _cachedAuthBase: string | null = null;
|
|
43
|
+
let _cachedBases: StoredBases | null | undefined = undefined;
|
|
44
|
+
|
|
45
|
+
function shouldPreferLocalBases(): boolean {
|
|
46
|
+
return (
|
|
47
|
+
process.env.SEEDCLUB_LOCAL === "1" ||
|
|
48
|
+
process.env.SEEDCLUB_LOCAL === "true" ||
|
|
49
|
+
process.env.SEEDCLUB_ENV === "local"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function tryReadStoredBasesSync(): StoredBases | null {
|
|
54
|
+
if (_cachedBases !== undefined) {
|
|
55
|
+
return _cachedBases;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
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
|
+
) {
|
|
67
|
+
_cachedBases = null;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
_cachedBases = stored;
|
|
71
|
+
return stored;
|
|
72
|
+
} catch {
|
|
73
|
+
_cachedBases = null;
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
27
77
|
|
|
28
78
|
export function getApiBase(): string {
|
|
29
|
-
if (
|
|
30
|
-
|
|
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!;
|
|
85
|
+
if (shouldPreferLocalBases()) return LOCAL_API_BASE;
|
|
86
|
+
const storedBases = tryReadStoredBasesSync();
|
|
87
|
+
if (storedBases?.apiBase) return storedBases.apiBase;
|
|
31
88
|
if (_cachedApiBase) return _cachedApiBase;
|
|
32
89
|
return DEFAULT_API_BASE;
|
|
33
90
|
}
|
|
34
91
|
|
|
92
|
+
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!;
|
|
99
|
+
if (shouldPreferLocalBases()) return LOCAL_AUTH_BASE;
|
|
100
|
+
const storedBases = tryReadStoredBasesSync();
|
|
101
|
+
if (storedBases?.authBase) return storedBases.authBase;
|
|
102
|
+
if (_cachedAuthBase) return _cachedAuthBase;
|
|
103
|
+
return DEFAULT_AUTH_BASE;
|
|
104
|
+
}
|
|
105
|
+
|
|
35
106
|
export function setCachedApiBase(apiBase: string): void {
|
|
36
107
|
_cachedApiBase = apiBase;
|
|
37
108
|
}
|
|
38
109
|
|
|
110
|
+
export function setCachedAuthBase(authBase: string): void {
|
|
111
|
+
_cachedAuthBase = authBase;
|
|
112
|
+
}
|
|
113
|
+
|
|
39
114
|
export function clearCachedApiBase(): void {
|
|
40
115
|
_cachedApiBase = null;
|
|
41
116
|
}
|
|
42
117
|
|
|
118
|
+
export function clearCachedAuthBase(): void {
|
|
119
|
+
_cachedAuthBase = null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getDefaultBases(mode: "local" | "prod"): { apiBase: string; authBase: string } {
|
|
123
|
+
if (mode === "local") {
|
|
124
|
+
return {
|
|
125
|
+
apiBase: LOCAL_API_BASE,
|
|
126
|
+
authBase: LOCAL_AUTH_BASE,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
apiBase: DEFAULT_API_BASE,
|
|
132
|
+
authBase: DEFAULT_AUTH_BASE,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
43
136
|
async function tryReadTokenFile(path: string): Promise<StoredToken | null> {
|
|
44
137
|
try {
|
|
45
138
|
const content = await readFile(path, "utf-8");
|
|
46
139
|
const stored = JSON.parse(content) as StoredToken;
|
|
47
|
-
if (!stored.token || !stored.token.
|
|
48
|
-
if (
|
|
140
|
+
if (!stored.token || typeof stored.token !== "string" || !stored.token.trim()) return null;
|
|
141
|
+
if (
|
|
142
|
+
stored.apiBase &&
|
|
143
|
+
!shouldPreferLocalBases() &&
|
|
144
|
+
!process.env.SEEDCLUB_API_URL &&
|
|
145
|
+
!process.env.SEEDCLUB_API &&
|
|
146
|
+
!process.env.SEED_NETWORK_API
|
|
147
|
+
) {
|
|
49
148
|
_cachedApiBase = stored.apiBase;
|
|
50
149
|
}
|
|
150
|
+
if (
|
|
151
|
+
stored.authBase &&
|
|
152
|
+
!shouldPreferLocalBases() &&
|
|
153
|
+
!process.env.SEEDCLUB_AUTH_URL &&
|
|
154
|
+
!process.env.SEEDCLUB_AUTH &&
|
|
155
|
+
!process.env.SEED_NETWORK_AUTH
|
|
156
|
+
) {
|
|
157
|
+
_cachedAuthBase = stored.authBase;
|
|
158
|
+
}
|
|
51
159
|
return stored;
|
|
52
160
|
} catch {
|
|
53
161
|
return null;
|
|
@@ -61,24 +169,85 @@ export async function getStoredToken(): Promise<StoredToken | null> {
|
|
|
61
169
|
return await tryReadTokenFile(LEGACY_TOKEN_FILE);
|
|
62
170
|
}
|
|
63
171
|
|
|
172
|
+
export async function getStoredBases(): Promise<StoredBases | null> {
|
|
173
|
+
try {
|
|
174
|
+
const content = await readFile(BASES_FILE, "utf-8");
|
|
175
|
+
const stored = JSON.parse(content) as StoredBases;
|
|
176
|
+
if (
|
|
177
|
+
!stored ||
|
|
178
|
+
typeof stored.apiBase !== "string" ||
|
|
179
|
+
typeof stored.authBase !== "string" ||
|
|
180
|
+
typeof stored.mode !== "string"
|
|
181
|
+
) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
_cachedBases = stored;
|
|
185
|
+
return stored;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
64
191
|
export async function getToken(): Promise<string | null> {
|
|
65
|
-
if (process.env.SEEDCLUB_TOKEN || process.env.SEED_NETWORK_TOKEN)
|
|
66
|
-
return (process.env.SEEDCLUB_TOKEN || process.env.SEED_NETWORK_TOKEN)!;
|
|
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)!;
|
|
67
194
|
const stored = await getStoredToken();
|
|
68
195
|
return stored?.token ?? null;
|
|
69
196
|
}
|
|
70
197
|
|
|
198
|
+
export async function storeBases(
|
|
199
|
+
apiBase: string,
|
|
200
|
+
authBase: string,
|
|
201
|
+
mode: SeedclubEnvironmentMode = "custom",
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
const stored: StoredBases = {
|
|
204
|
+
apiBase,
|
|
205
|
+
authBase,
|
|
206
|
+
mode,
|
|
207
|
+
updatedAt: new Date().toISOString(),
|
|
208
|
+
};
|
|
209
|
+
_cachedApiBase = apiBase;
|
|
210
|
+
_cachedAuthBase = authBase;
|
|
211
|
+
_cachedBases = stored;
|
|
212
|
+
await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
213
|
+
await writeFile(BASES_FILE, JSON.stringify(stored, null, 2), { mode: 0o600 });
|
|
214
|
+
try {
|
|
215
|
+
await chmod(BASES_FILE, 0o600);
|
|
216
|
+
} catch {}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function clearStoredBases(): Promise<boolean> {
|
|
220
|
+
_cachedBases = null;
|
|
221
|
+
try {
|
|
222
|
+
await unlink(BASES_FILE);
|
|
223
|
+
return true;
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
71
229
|
export async function storeToken(
|
|
72
230
|
token: string,
|
|
73
231
|
email: string,
|
|
74
232
|
apiBase: string,
|
|
75
|
-
options?: { name?: string },
|
|
233
|
+
options?: { authBase?: string; name?: string },
|
|
76
234
|
): Promise<void> {
|
|
235
|
+
_cachedApiBase = apiBase;
|
|
236
|
+
if (options?.authBase) {
|
|
237
|
+
_cachedAuthBase = options.authBase;
|
|
238
|
+
}
|
|
77
239
|
await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
78
240
|
await writeFile(
|
|
79
241
|
TOKEN_FILE,
|
|
80
242
|
JSON.stringify(
|
|
81
|
-
{
|
|
243
|
+
{
|
|
244
|
+
token,
|
|
245
|
+
email,
|
|
246
|
+
name: options?.name,
|
|
247
|
+
createdAt: new Date().toISOString(),
|
|
248
|
+
apiBase,
|
|
249
|
+
authBase: options?.authBase,
|
|
250
|
+
},
|
|
82
251
|
null,
|
|
83
252
|
2,
|
|
84
253
|
),
|
|
@@ -90,6 +259,8 @@ export async function storeToken(
|
|
|
90
259
|
}
|
|
91
260
|
|
|
92
261
|
export async function clearStoredToken(): Promise<boolean> {
|
|
262
|
+
_cachedApiBase = null;
|
|
263
|
+
_cachedAuthBase = null;
|
|
93
264
|
try {
|
|
94
265
|
await unlink(TOKEN_FILE);
|
|
95
266
|
return true;
|
|
@@ -6,6 +6,19 @@ IMPORTANT BRANDING RULES:
|
|
|
6
6
|
- Never mention the name \"pi\" in any response.
|
|
7
7
|
- If the user mentions \"pi\", interpret it as Seed Club and respond using \"Seed Club\".
|
|
8
8
|
- When summarizing intent or describing the system, use Seed Club wording only.
|
|
9
|
+
|
|
10
|
+
IMPORTANT SEED CLUB API ROUTING:
|
|
11
|
+
- For transcript-first discovery/review asks (not file retrieval), call seedclub_list_meeting_transcripts first with the program slug and limit 20.
|
|
12
|
+
- For transcript inventory questions like "what was the last day we have transcripts for 11am?", keep the user prompt in the normal agent flow and answer with the date plus the full transcript_for names from returned rows.
|
|
13
|
+
- Treat conversation-level transcripts as valid 11am interview/show transcripts. For "latest/last transcript" questions, use seedclub_list_meeting_transcripts.latest.overall first; only use latest.meeting when the user explicitly asks for meeting-level records.
|
|
14
|
+
- Treat "on 11am" or "for 11am" as the 11am program slug when that program is accessible, not as an 11am time filter.
|
|
15
|
+
- If the user asks for "all transcripts" and the tool returns a limited page, summarize the returned transcript rows and say you can fetch full text for selected rows. Do not claim exhaustive coverage unless the API result proves it.
|
|
16
|
+
- Use includeFullText only after narrowing to rows the user wants to inspect.
|
|
17
|
+
- Use seedclub_list_program_media_assets(assetKind=full_conversation) only when the user asks for full conversations or media assets. Do not use it as the default source for transcript inventory.
|
|
18
|
+
- For transcript retrieval asks (e.g., "I need transcripts...", "get transcripts...", guest/date transcript pulls), first use chat and metadata tools to establish exact constraints when needed, then call seedclub_export_transcripts with the user's original request. This preserves the user prompt in context, confirms destination, then writes VTT files.
|
|
19
|
+
- Treat short transcript artifact prompts like "today's transcripts", "2pm transcripts", "Tuesday VTTs", or "recent captions" as local transcript export tool requests, not inline transcript display requests.
|
|
20
|
+
- Do not paste full transcript JSON or raw session context into the reply unless the user explicitly asks for raw data.
|
|
21
|
+
- Use seedclub_list_show_recordings for show recording URLs, then follow transcript event_date only when needed.
|
|
9
22
|
`;
|
|
10
23
|
|
|
11
24
|
const BRAND_REGEX = /\bpi\b/gi;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { api } from "../api-client.js";
|
|
3
|
+
import { getSessionContext } from "../tools/utility.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_LIMIT = 25;
|
|
6
|
+
const MAX_LIMIT = 25;
|
|
7
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
interface ClipStatusOptions {
|
|
10
|
+
programSlug?: string;
|
|
11
|
+
from?: string;
|
|
12
|
+
to?: string;
|
|
13
|
+
limit: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isoDate(value: Date) {
|
|
17
|
+
return value.toISOString().slice(0, 10);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function startOfDayUTC(value: Date) {
|
|
21
|
+
return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate(), 0, 0, 0, 0));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function addDays(value: Date, days: number) {
|
|
25
|
+
return new Date(value.getTime() + days * MS_PER_DAY);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeLimit(value?: number) {
|
|
29
|
+
if (!Number.isFinite(value)) return DEFAULT_LIMIT;
|
|
30
|
+
return Math.max(1, Math.min(MAX_LIMIT, Math.trunc(value!)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseArgs(raw?: string): ClipStatusOptions {
|
|
34
|
+
const text = (raw ?? "").trim();
|
|
35
|
+
const now = new Date();
|
|
36
|
+
const weekStart = startOfDayUTC(addDays(now, -6));
|
|
37
|
+
const tomorrow = startOfDayUTC(addDays(now, 1));
|
|
38
|
+
|
|
39
|
+
const options: ClipStatusOptions = {
|
|
40
|
+
from: isoDate(weekStart),
|
|
41
|
+
to: isoDate(tomorrow),
|
|
42
|
+
limit: DEFAULT_LIMIT,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const from = text.match(/(?:--from|from)\s+(\d{4}-\d{2}-\d{2})/i);
|
|
46
|
+
if (from?.[1]) options.from = from[1];
|
|
47
|
+
|
|
48
|
+
const to = text.match(/(?:--to|to)\s+(\d{4}-\d{2}-\d{2})/i);
|
|
49
|
+
if (to?.[1]) options.to = to[1];
|
|
50
|
+
|
|
51
|
+
const date = text.match(/(?:--date|date|on)\s+(\d{4}-\d{2}-\d{2})/i);
|
|
52
|
+
if (date?.[1]) {
|
|
53
|
+
options.from = date[1];
|
|
54
|
+
const next = addDays(new Date(`${date[1]}T00:00:00.000Z`), 1);
|
|
55
|
+
options.to = isoDate(next);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const limit = text.match(/(?:--limit|limit)\s+(\d{1,2})/i);
|
|
59
|
+
if (limit?.[1]) options.limit = normalizeLimit(Number.parseInt(limit[1], 10));
|
|
60
|
+
|
|
61
|
+
const lastDays = text.match(/(?:last)\s+(\d{1,2})\s*(?:d|day|days)/i);
|
|
62
|
+
if (lastDays?.[1]) {
|
|
63
|
+
const days = Math.max(1, Number.parseInt(lastDays[1], 10));
|
|
64
|
+
const rangeStart = startOfDayUTC(addDays(now, -(days - 1)));
|
|
65
|
+
options.from = isoDate(rangeStart);
|
|
66
|
+
options.to = isoDate(tomorrow);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const program = text.match(/(?:--program|program)\s+([a-z0-9-]+)/i);
|
|
70
|
+
if (program?.[1]) options.programSlug = program[1];
|
|
71
|
+
|
|
72
|
+
return options;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseLeadingProgramSlug(raw: string, accessibleSlugs: string[]): string | undefined {
|
|
76
|
+
const firstToken = raw.trim().split(/\s+/).find(Boolean);
|
|
77
|
+
if (!firstToken) return undefined;
|
|
78
|
+
const token = firstToken.toLowerCase();
|
|
79
|
+
return accessibleSlugs.find((slug) => slug.toLowerCase() === token);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function resolveProgramSlug(ctx: any, session: any, options: ClipStatusOptions, rawArgs: string): Promise<string | null> {
|
|
83
|
+
const slugs: string[] = (session.program_access ?? []).map((entry: any) => entry?.program?.slug).filter(Boolean);
|
|
84
|
+
if (!slugs.length) return null;
|
|
85
|
+
if (options.programSlug && slugs.includes(options.programSlug)) return options.programSlug;
|
|
86
|
+
|
|
87
|
+
const fromLeadingToken = parseLeadingProgramSlug(rawArgs, slugs);
|
|
88
|
+
if (fromLeadingToken) return fromLeadingToken;
|
|
89
|
+
|
|
90
|
+
if (slugs.length === 1) return slugs[0];
|
|
91
|
+
const selected = await ctx.ui.select("Choose a program for clip status", [...slugs, "Cancel"]);
|
|
92
|
+
if (!selected || selected === "Cancel") return null;
|
|
93
|
+
return selected;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function titleForMeeting(row: any) {
|
|
97
|
+
return row?.meeting?.title ?? "Untitled meeting";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function startsAtForMeeting(row: any) {
|
|
101
|
+
return row?.meeting?.starts_at ?? null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatRow(prefix: string, row: any, reason?: string) {
|
|
105
|
+
const startsAt = startsAtForMeeting(row);
|
|
106
|
+
const date = typeof startsAt === "string" ? startsAt.slice(0, 10) : "unknown-date";
|
|
107
|
+
const title = titleForMeeting(row);
|
|
108
|
+
return reason ? `${prefix} ${date} — ${title} (${reason})` : `${prefix} ${date} — ${title}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function registerClipStatusCommand(pi: ExtensionAPI) {
|
|
112
|
+
pi.registerCommand("clip-status", {
|
|
113
|
+
description:
|
|
114
|
+
"Show clip readiness for a program/date range. Defaults to last 7 days. Example: /clip-status 11am last 10 days",
|
|
115
|
+
handler: async (args, ctx) => {
|
|
116
|
+
try {
|
|
117
|
+
const rawArgs = (args ?? "").trim();
|
|
118
|
+
const options = parseArgs(rawArgs);
|
|
119
|
+
|
|
120
|
+
const session = await getSessionContext();
|
|
121
|
+
if ("error" in session) {
|
|
122
|
+
ctx.ui.notify(`Unable to load session context: ${session.error}`, "error");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const programSlug = await resolveProgramSlug(ctx, session, options, rawArgs);
|
|
127
|
+
if (!programSlug) {
|
|
128
|
+
ctx.ui.notify("No program selected. Try /clip-status <program-slug>", "warning");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const meetingsResponse = await api.get<any>("/meetings", {
|
|
133
|
+
program_slug: programSlug,
|
|
134
|
+
from: options.from ? `${options.from}T00:00:00Z` : undefined,
|
|
135
|
+
to: options.to ? `${options.to}T00:00:00Z` : undefined,
|
|
136
|
+
limit: normalizeLimit(options.limit),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const rows = Array.isArray(meetingsResponse?.data) ? meetingsResponse.data : [];
|
|
140
|
+
if (!rows.length) {
|
|
141
|
+
ctx.ui.notify(
|
|
142
|
+
`No meetings found for ${programSlug} in range ${options.from ?? "?"} → ${options.to ?? "?"}.`,
|
|
143
|
+
"info",
|
|
144
|
+
);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const transcriptResults = await Promise.all(
|
|
149
|
+
rows.map(async (row: any) => {
|
|
150
|
+
const meetingId = row?.meeting?.id;
|
|
151
|
+
if (!meetingId) return { row, transcriptAvailable: false, reason: "missing_meeting_id" };
|
|
152
|
+
try {
|
|
153
|
+
const transcriptResponse = await api.get<any>(`/meetings/${meetingId}/transcript`);
|
|
154
|
+
const transcriptAvailable = !!transcriptResponse?.transcript;
|
|
155
|
+
return { row, transcriptAvailable, reason: transcriptAvailable ? null : "no_transcript" };
|
|
156
|
+
} catch {
|
|
157
|
+
return { row, transcriptAvailable: false, reason: "transcript_lookup_failed" };
|
|
158
|
+
}
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const ready = transcriptResults.filter((item) => item.transcriptAvailable);
|
|
163
|
+
const blocked = transcriptResults.filter((item) => !item.transcriptAvailable);
|
|
164
|
+
|
|
165
|
+
ctx.ui.setStatus("clips", `${ready.length}/${rows.length} ready`);
|
|
166
|
+
|
|
167
|
+
const readyLines = ready.slice(0, 12).map((item) => formatRow("✓", item.row));
|
|
168
|
+
const blockedLines = blocked.slice(0, 12).map((item) => formatRow("•", item.row, item.reason ?? undefined));
|
|
169
|
+
|
|
170
|
+
const summary = [
|
|
171
|
+
`Clip status for ${programSlug}`,
|
|
172
|
+
`range: ${options.from ?? "?"} → ${options.to ?? "?"}`,
|
|
173
|
+
`ready: ${ready.length}`,
|
|
174
|
+
`blocked: ${blocked.length}`,
|
|
175
|
+
readyLines.length ? `\nReady\n${readyLines.join("\n")}` : "",
|
|
176
|
+
blockedLines.length ? `\nBlocked\n${blockedLines.join("\n")}` : "",
|
|
177
|
+
rows.length > 12 ? `\nShowing first 12 of ${rows.length} meetings.` : "",
|
|
178
|
+
].filter(Boolean);
|
|
179
|
+
|
|
180
|
+
ctx.ui.notify(summary.join("\n"), "info");
|
|
181
|
+
} catch (error) {
|
|
182
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
183
|
+
ctx.ui.notify(`Clip status failed: ${message}`, "error");
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|