@clubnet/seedclub 0.2.18 → 0.2.20
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 +32 -23
- 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 +295 -94
- package/assets/theme/dark.json +47 -59
- package/assets/theme/light.json +49 -61
- package/bin/cli.js +386 -138
- 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
|
|
|
@@ -46,21 +46,11 @@ curl -fsSL https://raw.githubusercontent.com/seedclub/seedclub-agent/main/instal
|
|
|
46
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
48
|
|
|
49
|
-
Fast path:
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
SEEDCLUB_NPM_TOKEN=YOUR_NPM_TOKEN curl -fsSL https://raw.githubusercontent.com/seedclub/seedclub-agent/main/install.sh | bash
|
|
53
|
-
seedclub setup-auth
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Manual one-time `.npmrc` setup:
|
|
57
|
-
|
|
58
49
|
```bash
|
|
59
|
-
|
|
60
|
-
echo "//registry.npmjs.org/:_authToken=YOUR_NPM_TOKEN" >> ~/.npmrc
|
|
50
|
+
npm login
|
|
61
51
|
```
|
|
62
52
|
|
|
63
|
-
Then `npm install -g @clubnet/seedclub`
|
|
53
|
+
Then `npm install -g @clubnet/seedclub` and `seedclub update` work.
|
|
64
54
|
|
|
65
55
|
## Core workflow
|
|
66
56
|
|
|
@@ -69,7 +59,7 @@ The normal interactive flow is:
|
|
|
69
59
|
1. Start the app with `seedclub`
|
|
70
60
|
2. Complete `/login`, `/model`, and `/connect` if this is your first run
|
|
71
61
|
3. Open `/seedclub`
|
|
72
|
-
4. Choose
|
|
62
|
+
4. Choose the CRM, meetings, media, recordings, or transcript workflow you need
|
|
73
63
|
|
|
74
64
|
## Commands
|
|
75
65
|
|
|
@@ -78,10 +68,10 @@ The normal interactive flow is:
|
|
|
78
68
|
| `/login` | Sign in to a model provider for the underlying agent |
|
|
79
69
|
| `/model` | Choose which model to use |
|
|
80
70
|
| `/connect` | Connect your Seed Club account |
|
|
81
|
-
| `/seedclub` |
|
|
82
|
-
| `/
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
| `/seedclub` | Main menu — connect, inspect access, and jump into CRM/meetings/media/headlines workflows |
|
|
72
|
+
| `/transcripts` | Export transcript VTT files with filters (date, person, time, output dir) |
|
|
73
|
+
|
|
74
|
+
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.
|
|
85
75
|
|
|
86
76
|
## Auth
|
|
87
77
|
|
|
@@ -94,6 +84,23 @@ There are two separate auth layers in the product:
|
|
|
94
84
|
|
|
95
85
|
`/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
86
|
|
|
87
|
+
Power-user env overrides:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
export SEEDCLUB_API_URL=http://localhost:3001
|
|
91
|
+
export SEEDCLUB_AUTH_URL=http://localhost:3000
|
|
92
|
+
export SEEDCLUB_ACCESS_TOKEN=<bearer-token>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Production defaults are already built into the auth extension:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
SEEDCLUB_API_URL=https://api.seedclub.com
|
|
99
|
+
SEEDCLUB_AUTH_URL=https://auth.seedclub.com
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
You do not need to export those for normal use. Only set env vars when you want to override them for local/dev.
|
|
103
|
+
|
|
97
104
|
## How it works
|
|
98
105
|
|
|
99
106
|
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 +136,9 @@ seedclub pins versions in `package.json`:
|
|
|
129
136
|
|
|
130
137
|
```json
|
|
131
138
|
{
|
|
132
|
-
"version": "0.2.
|
|
139
|
+
"version": "0.2.19",
|
|
133
140
|
"dependencies": {
|
|
134
|
-
"@mariozechner/pi-coding-agent": "0.
|
|
141
|
+
"@mariozechner/pi-coding-agent": "0.65.2"
|
|
135
142
|
}
|
|
136
143
|
}
|
|
137
144
|
```
|
|
@@ -155,6 +162,8 @@ It's a standard pi theme with 51 color tokens. Edit it to change any visual aspe
|
|
|
155
162
|
|
|
156
163
|
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
164
|
|
|
165
|
+
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.
|
|
166
|
+
|
|
158
167
|
**Hot reload:** Edit the theme file while seedclub is running and it reloads instantly.
|
|
159
168
|
|
|
160
169
|
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 +258,8 @@ seedclub # test
|
|
|
249
258
|
# When ready, bump package version and publish
|
|
250
259
|
git add -A
|
|
251
260
|
git commit -m "update extensions"
|
|
252
|
-
|
|
253
|
-
|
|
261
|
+
npm version patch|minor|major
|
|
262
|
+
git push --follow-tags
|
|
254
263
|
```
|
|
255
264
|
|
|
256
265
|
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
|
+
}
|