@clubnet/seedclub 0.2.36 → 0.2.38
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 +18 -29
- package/assets/SYSTEM.md +45 -0
- package/assets/extensions/seedclub/commands/seedclub.ts +6 -0
- package/assets/extensions/seedclub/commands/transcript-export.ts +191 -168
- package/assets/extensions/seedclub/commands/transcript-intent.ts +5 -2
- package/assets/extensions/seedclub/gate-state.ts +31 -14
- package/assets/extensions/seedclub/index.ts +326 -14
- package/assets/extensions/seedclub/memory-client.js +57 -0
- package/assets/extensions/seedclub/memory-helpers.js +78 -0
- package/assets/extensions/seedclub/memory.ts +364 -0
- package/assets/extensions/seedclub/platform-routing.js +351 -0
- package/assets/extensions/seedclub/recent-entities.js +786 -0
- package/assets/extensions/seedclub/tool-utils.ts +37 -6
- package/assets/extensions/seedclub/tools/media.ts +45 -237
- package/assets/extensions/seedclub/tools/meetings.ts +22 -12
- package/assets/extensions/seedclub/tools/web.ts +475 -0
- package/assets/extensions/seedclub/ui-copy.ts +4 -2
- package/assets/theme/dark.json +9 -7
- package/assets/theme/light.json +9 -7
- package/bin/cli.js +38 -110
- package/package.json +9 -2
- package/packages/seedclub-runtime/README.md +13 -0
- package/packages/seedclub-runtime/package.json +5 -0
- package/packages/seedclub-runtime/pi-contract-baseline.json +62 -0
- package/packages/seedclub-runtime/src/contract.test.mjs +90 -0
- package/packages/seedclub-runtime/src/index.mjs +259 -0
- package/packages/seedclub-runtime/src/index.test.mjs +49 -0
- package/packages/seedclub-runtime/src/workspace-deps.mjs +66 -0
- package/packages/seedclub-tui/README.md +19 -0
- package/packages/seedclub-tui/package.json +5 -0
- package/packages/seedclub-tui/src/app/interactive-mode.mjs +2469 -0
- package/packages/seedclub-tui/src/cli.mjs +14 -0
- package/packages/seedclub-tui/src/index.mjs +48 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/README.md +11 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/config.js +76 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/core/footer-data-provider.js +143 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/core/keybindings.js +204 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/core/tools/edit-diff.js +243 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/core/tools/path-utils.js +81 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/core/tools/truncate.js +205 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/assistant-message.js +96 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/bordered-loader.js +51 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/countdown-timer.js +34 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/custom-editor.js +70 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/diff.js +133 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/dynamic-border.js +21 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/extension-editor.js +95 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/extension-input.js +69 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/extension-selector.js +92 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/footer.js +213 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/keybinding-hints.js +61 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/login-dialog.js +132 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/oauth-selector.js +80 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/tool-execution.js +712 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/user-message.js +31 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/visual-truncate.js +33 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/theme/dark.json +85 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/theme/light.json +84 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/theme/theme-schema.json +335 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/theme/theme.js +944 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/utils/image-convert.js +35 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/utils/photon.js +121 -0
- package/packages/seedclub-tui/src/vendor/coding-agent/utils/shell.js +38 -0
- package/postinstall.js +18 -73
- package/assets/extensions/seedclub/branding.ts +0 -52
- package/assets/extensions/seedclub/package-lock.json +0 -72
- package/assets/extensions/seedclub/package.json +0 -14
- package/assets/extensions/seedclub-ui/editor.ts +0 -110
- package/assets/extensions/seedclub-ui/footer.ts +0 -126
- package/assets/extensions/seedclub-ui/index.ts +0 -19
- package/assets/extensions/seedclub-ui/state.ts +0 -18
- package/assets/extensions/seedclub-ui/tool-progress.ts +0 -24
- package/assets/extensions/seedclub-ui/update.ts +0 -210
- package/assets/extensions/seedclub-ui/welcome.ts +0 -748
- package/bin/pi-main-launcher.js +0 -94
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ 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, using CRM
|
|
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 and meeting 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
|
|
|
@@ -18,15 +18,16 @@ This repo is the source of truth for the published package:
|
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
npm install -g @clubnet/seedclub
|
|
21
|
+
seedclub
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
Alternative:
|
|
24
25
|
|
|
25
26
|
```bash
|
|
26
|
-
seedclub
|
|
27
|
+
curl -fsSL https://raw.githubusercontent.com/seedclub/seedclub-agent/main/install.sh | bash
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
## First run
|
|
30
31
|
|
|
31
32
|
1. Run `seedclub`
|
|
32
33
|
2. Complete Seed Club sign-in when the browser opens
|
|
@@ -34,20 +35,10 @@ seedclub
|
|
|
34
35
|
4. Run `/model`
|
|
35
36
|
5. Open `/seedclub`
|
|
36
37
|
|
|
37
|
-
### Alternative: curl | bash
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
curl -fsSL https://raw.githubusercontent.com/seedclub/seedclub-agent/main/install.sh | bash
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### Package access
|
|
44
|
-
|
|
45
|
-
`@clubnet/seedclub` is a public npm package. Install access is open; runtime access is enforced inside the app.
|
|
46
|
-
|
|
47
38
|
## Core workflow
|
|
48
39
|
|
|
49
40
|
1. Start the app with `seedclub`
|
|
50
|
-
2. Complete Seed Club sign-in when
|
|
41
|
+
2. Complete Seed Club sign-in when prompted
|
|
51
42
|
3. Complete `/login` and `/model` if this is your first run
|
|
52
43
|
4. Open `/seedclub`
|
|
53
44
|
5. Choose the workflow you need
|
|
@@ -55,15 +46,23 @@ curl -fsSL https://raw.githubusercontent.com/seedclub/seedclub-agent/main/instal
|
|
|
55
46
|
## Commands
|
|
56
47
|
|
|
57
48
|
| Command | What it does |
|
|
58
|
-
|
|
49
|
+
| --- | --- |
|
|
59
50
|
| `/login` | Sign in to a model provider for the underlying agent |
|
|
60
51
|
| `/model` | Choose which model to use |
|
|
61
52
|
| `/connect` | Connect your Seed Club account |
|
|
62
53
|
| `/connect-calendar` | Connect a personal Google Calendar to your Seed Club account |
|
|
63
|
-
| `/seedclub` | Main menu
|
|
64
|
-
| `/transcripts` | Export transcript VTT files with filters
|
|
54
|
+
| `/seedclub` | Main menu for Seed Club workflows |
|
|
55
|
+
| `/transcripts` | Export transcript VTT files with filters |
|
|
56
|
+
|
|
57
|
+
Natural-language transcript retrieval is also supported. Examples: `download vibhu transcripts from 11am`, `i need transcripts for all guests on 11am last week`.
|
|
58
|
+
|
|
59
|
+
## Repo docs
|
|
65
60
|
|
|
66
|
-
|
|
61
|
+
- `README.md` is the public package and repo landing page.
|
|
62
|
+
- `CONTRIBUTING.md` covers local development, architecture boundaries, and release workflow.
|
|
63
|
+
- `AGENTS.md` contains repo instructions for coding agents working in this codebase.
|
|
64
|
+
- `WORKFLOWS.md` contains Seed Club API route-selection rules.
|
|
65
|
+
- `packages/seedclub-runtime/README.md` and `packages/seedclub-tui/README.md` document package-local ownership boundaries.
|
|
67
66
|
|
|
68
67
|
## Update
|
|
69
68
|
|
|
@@ -78,16 +77,6 @@ npm uninstall -g @clubnet/seedclub
|
|
|
78
77
|
rm -rf ~/.seedclub
|
|
79
78
|
```
|
|
80
79
|
|
|
81
|
-
### Coming from the old version (curl | bash)
|
|
82
|
-
|
|
83
|
-
The previous version installed into `~/.seedclub/bin/` and modified your PATH. The npm package cleans this up automatically, but you can also remove the old PATH line from your shell profile (`~/.zshrc`, `~/.bashrc`, etc.) manually.
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
npm install -g @clubnet/seedclub
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
If you have pi installed globally (`npm install -g @mariozechner/pi-coding-agent`), that's fine — seedclub and pi are completely independent.
|
|
90
|
-
|
|
91
80
|
## License
|
|
92
81
|
|
|
93
82
|
MIT
|
package/assets/SYSTEM.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
You are a general-purpose computer and coding agent operating inside the Seed Club terminal application.
|
|
2
|
+
|
|
3
|
+
Your job is to help users get real work done across code, files, research, and Seed Club platform workflows. Act directly, stay concise, and use the best available interface for the task.
|
|
4
|
+
|
|
5
|
+
Identity:
|
|
6
|
+
|
|
7
|
+
- In user-facing replies, answer directly and naturally. Mention Seed Club only when the user is asking about the product, the platform, auth, installation, or branded workflows.
|
|
8
|
+
- Use exact upstream names such as `pi-coding-agent` and `pi-tui` only when technical precision matters, for example when discussing package names, file paths, vendored code, or repository history.
|
|
9
|
+
|
|
10
|
+
Operating model:
|
|
11
|
+
|
|
12
|
+
- Be a useful general coding and computer agent first, not just a platform assistant.
|
|
13
|
+
- Inspect the real environment before making assumptions.
|
|
14
|
+
- Prefer doing the work directly over describing what you would do.
|
|
15
|
+
- Show file paths clearly when discussing code or local files.
|
|
16
|
+
- Stay within the user's actual access and permissions.
|
|
17
|
+
- If the user is asking about upstream pi internals or SDK behavior, inspect the installed upstream docs and code before answering.
|
|
18
|
+
|
|
19
|
+
Interface selection:
|
|
20
|
+
|
|
21
|
+
- Use local machine tools for code changes, file retrieval, file transforms, shell tasks, exports, and other computer-local workflows.
|
|
22
|
+
- Use Seed Club platform tools when the task is about Seed Club records, workflows, people, meetings, transcripts, media, or network operations.
|
|
23
|
+
- Use external web research only when the task is outside Seed Club's own records or when current external verification is needed.
|
|
24
|
+
- Choose the narrowest tool or workflow that can answer the question or complete the task reliably.
|
|
25
|
+
|
|
26
|
+
Seed Club platform policy:
|
|
27
|
+
|
|
28
|
+
- Prefer Seed Club tools first when the user is asking about Seed Club data or workflows.
|
|
29
|
+
- For Seed Club records questions, treat Seed Club platform tools as the primary source of truth, not the local repo or filesystem.
|
|
30
|
+
- Do not inspect local repo code, extension source, or API client helpers to rediscover Seed Club routes when a registered Seed Club tool already matches the task.
|
|
31
|
+
- Resolve program names, slugs, and shorthand against the user's accessible Seed Club programs when that context is available.
|
|
32
|
+
- Prefer metadata-first exploration before loading large transcript or media payloads.
|
|
33
|
+
- For prior-conversation questions such as "what did we talk about?" or "what were the main topics?", prefer `full_conversation` media assets when available, then use meeting transcripts only as a fallback or when the canonical meeting transcript is specifically needed.
|
|
34
|
+
- Treat local export, download, upload, and publish actions as workflow endpoints: use the purpose-built tools for those actions instead of recreating them indirectly.
|
|
35
|
+
- When the user wants files on disk or assets uploaded, keep the flow anchored in the local machine plus the relevant Seed Club platform tool.
|
|
36
|
+
- Stay scoped to the user's platform permissions and do not imply access the user does not have.
|
|
37
|
+
- Do not claim Seed Club platform data is unavailable unless a relevant Seed Club tool actually returns an auth, permission, or not-found failure.
|
|
38
|
+
|
|
39
|
+
External research policy:
|
|
40
|
+
|
|
41
|
+
- If the user asks an open-ended research question that is not explicitly scoped to Seed Club records, do a fast external research pass before answering.
|
|
42
|
+
- Use `search_web` first for external search and `fetch_web_page` for follow-up reads.
|
|
43
|
+
- Prefer built-in web tools over bash, curl, Python, or ad hoc scraping for external research.
|
|
44
|
+
- Gather at least 2 reputable sources, then answer with a concise synthesis.
|
|
45
|
+
- Include source URLs inline so the user can verify.
|
|
@@ -18,6 +18,7 @@ interface SeedclubDeps {
|
|
|
18
18
|
connect: (args: string | undefined, ctx: any) => Promise<boolean>;
|
|
19
19
|
connectCalendar: (ctx: any) => Promise<void>;
|
|
20
20
|
disconnect: (ctx: any) => Promise<void>;
|
|
21
|
+
showMemoryMenu?: (ctx: any) => Promise<void>;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
async function compactSeedclubContext(ctx: any) {
|
|
@@ -136,6 +137,7 @@ export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
|
|
|
136
137
|
"Use local API/Auth",
|
|
137
138
|
"Use prod API/Auth",
|
|
138
139
|
"Connect personal calendar",
|
|
140
|
+
"Memory",
|
|
139
141
|
"Open CRM prompt",
|
|
140
142
|
"Open meetings prompt",
|
|
141
143
|
"Open transcripts prompt",
|
|
@@ -178,6 +180,10 @@ export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
|
|
|
178
180
|
case "Connect personal calendar":
|
|
179
181
|
await deps.connectCalendar(ctx);
|
|
180
182
|
break;
|
|
183
|
+
case "Memory":
|
|
184
|
+
if (deps.showMemoryMenu) await deps.showMemoryMenu(ctx);
|
|
185
|
+
else ctx.ui.notify("Memory controls are unavailable.", "warning");
|
|
186
|
+
break;
|
|
181
187
|
case "Open CRM prompt":
|
|
182
188
|
await new Promise((r) => setTimeout(r, 300));
|
|
183
189
|
ctx.ui.setEditorText("List CRM records for my workspace.");
|
|
@@ -4,6 +4,7 @@ import { join, resolve } from "node:path";
|
|
|
4
4
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
5
|
import { api } from "../api-client.js";
|
|
6
6
|
import { getSessionContext } from "../tools/utility.js";
|
|
7
|
+
import { setWorkingMessage } from "../tool-utils.js";
|
|
7
8
|
import {
|
|
8
9
|
exactNormalizedTextMatch,
|
|
9
10
|
isLikelyVtt,
|
|
@@ -148,7 +149,15 @@ function extractTranscriptText(obj: any): string | null {
|
|
|
148
149
|
return null;
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
function
|
|
152
|
+
function allowSyntheticVttExport() {
|
|
153
|
+
return /^(1|true|yes)$/i.test(process.env.SEEDCLUB_ALLOW_SYNTHETIC_VTT_EXPORT ?? "");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolveVttContent(
|
|
157
|
+
source: any,
|
|
158
|
+
options?: { allowGeneratedFromText?: boolean },
|
|
159
|
+
): { vtt: string; mode: "native" | "generated_from_text" } | null {
|
|
160
|
+
const allowGeneratedFromText = options?.allowGeneratedFromText === true;
|
|
152
161
|
const explicitVtt = typeof source?.transcript_vtt === "string" ? source.transcript_vtt.trim() : "";
|
|
153
162
|
if (explicitVtt) return { vtt: explicitVtt, mode: "native" };
|
|
154
163
|
|
|
@@ -158,15 +167,17 @@ function resolveVttContent(source: any): { vtt: string; mode: "native" | "genera
|
|
|
158
167
|
const maybeVtt = extractTranscriptText(source);
|
|
159
168
|
if (typeof maybeVtt === "string" && maybeVtt.trim()) {
|
|
160
169
|
if (isLikelyVtt(maybeVtt)) return { vtt: maybeVtt.trim(), mode: "native" };
|
|
161
|
-
|
|
162
|
-
|
|
170
|
+
if (allowGeneratedFromText) {
|
|
171
|
+
const generated = textToSyntheticVtt(maybeVtt);
|
|
172
|
+
if (generated) return { vtt: generated, mode: "generated_from_text" };
|
|
173
|
+
}
|
|
163
174
|
}
|
|
164
175
|
|
|
165
176
|
return null;
|
|
166
177
|
}
|
|
167
178
|
|
|
168
|
-
function ensureVtt(asset: any): string | null {
|
|
169
|
-
return resolveVttContent(asset)?.vtt ?? null;
|
|
179
|
+
function ensureVtt(asset: any, options?: { allowGeneratedFromText?: boolean }): string | null {
|
|
180
|
+
return resolveVttContent(asset, options)?.vtt ?? null;
|
|
170
181
|
}
|
|
171
182
|
|
|
172
183
|
async function listCandidateAssets(
|
|
@@ -238,8 +249,12 @@ function personFromMeetingTitle(title?: string | null): string | null {
|
|
|
238
249
|
return cleaned || null;
|
|
239
250
|
}
|
|
240
251
|
|
|
241
|
-
function toCandidateFromAsset(
|
|
242
|
-
|
|
252
|
+
function toCandidateFromAsset(
|
|
253
|
+
asset: any,
|
|
254
|
+
fallbackPerson: string,
|
|
255
|
+
options?: { allowGeneratedFromText?: boolean },
|
|
256
|
+
): TranscriptCandidate | null {
|
|
257
|
+
const resolvedVtt = resolveVttContent(asset, options);
|
|
243
258
|
if (!resolvedVtt) return null;
|
|
244
259
|
const eventDate = pickDate(asset);
|
|
245
260
|
return {
|
|
@@ -257,7 +272,11 @@ function toCandidateFromAsset(asset: any, fallbackPerson: string): TranscriptCan
|
|
|
257
272
|
};
|
|
258
273
|
}
|
|
259
274
|
|
|
260
|
-
async function listFallbackTranscriptCandidates(
|
|
275
|
+
async function listFallbackTranscriptCandidates(
|
|
276
|
+
programSlug: string,
|
|
277
|
+
intent: TranscriptIntent,
|
|
278
|
+
options?: { allowGeneratedFromText?: boolean },
|
|
279
|
+
): Promise<TranscriptCandidate[]> {
|
|
261
280
|
const response = await api.get<any>("/meetings/transcripts", {
|
|
262
281
|
program_slug: programSlug,
|
|
263
282
|
limit: 20,
|
|
@@ -300,7 +319,7 @@ async function listFallbackTranscriptCandidates(programSlug: string, intent: Tra
|
|
|
300
319
|
|
|
301
320
|
const candidates: TranscriptCandidate[] = [];
|
|
302
321
|
for (const row of rows) {
|
|
303
|
-
const resolvedVtt = resolveVttContent(row?.transcript ?? {});
|
|
322
|
+
const resolvedVtt = resolveVttContent(row?.transcript ?? {}, options);
|
|
304
323
|
if (!resolvedVtt) continue;
|
|
305
324
|
const eventDate = typeof row?.transcript?.event_date === "string" ? row.transcript.event_date : new Date().toISOString().slice(0, 10);
|
|
306
325
|
const person = pickPersonFromTranscriptRow(row, intent.person?.trim() || "unassigned");
|
|
@@ -467,190 +486,194 @@ async function offerOpenDownloadedPath(agent: ExtensionAPI, ctx: any, outDir: st
|
|
|
467
486
|
}
|
|
468
487
|
|
|
469
488
|
export async function exportTranscripts(agent: ExtensionAPI, ctx: any, args: TranscriptExportArgs) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
489
|
+
try {
|
|
490
|
+
const request = args.request?.trim();
|
|
491
|
+
if (!request) {
|
|
492
|
+
return {
|
|
493
|
+
error: "Provide the original transcript request.",
|
|
494
|
+
status: 400,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const intent = {
|
|
499
|
+
...parseIntent(request),
|
|
500
|
+
...(args.person ? { person: args.person } : {}),
|
|
501
|
+
...(args.date ? { date: args.date } : {}),
|
|
502
|
+
...(args.time ? { time: args.time } : {}),
|
|
503
|
+
...(args.outDir ? { outDir: args.outDir } : {}),
|
|
504
|
+
...(args.latestCount ? { latestCount: Math.max(1, Math.min(20, Math.trunc(args.latestCount))) } : {}),
|
|
475
505
|
};
|
|
476
|
-
|
|
506
|
+
const allowGeneratedFromText = allowSyntheticVttExport();
|
|
477
507
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
508
|
+
setWorkingMessage(ctx, "Loading Seed Club session...");
|
|
509
|
+
const session = await getSessionContext();
|
|
510
|
+
if ("error" in session) {
|
|
511
|
+
ctx.ui.notify(`Unable to load Seed Club session: ${session.error}`, "error");
|
|
512
|
+
return {
|
|
513
|
+
error: session.error,
|
|
514
|
+
status: session.status ?? 500,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
486
517
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
518
|
+
const programs = pickPrograms(session);
|
|
519
|
+
setWorkingMessage(ctx, "Resolving transcript program...");
|
|
520
|
+
const program = args.programSlug
|
|
521
|
+
? programs.find((candidate) => candidate.slug === args.programSlug) ?? null
|
|
522
|
+
: await resolveProgram(request, programs, ctx);
|
|
523
|
+
if (!program) {
|
|
524
|
+
ctx.ui.notify("No program selected. Include a program name like 11AM or use /transcripts.", "warning");
|
|
525
|
+
return {
|
|
526
|
+
error: args.programSlug
|
|
527
|
+
? `Program ${args.programSlug} is not accessible for the current user.`
|
|
528
|
+
: "No program selected.",
|
|
529
|
+
status: 404,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
495
532
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
533
|
+
const filterAssetsForIntent = async (inputAssets: any[]) => {
|
|
534
|
+
let filtered = await filterByMeetingTime(inputAssets, intent.time);
|
|
535
|
+
filtered = filtered.filter((asset) => !!ensureVtt(asset, { allowGeneratedFromText }));
|
|
536
|
+
if (!args.partyId) {
|
|
537
|
+
const peopleQueries = Array.from(new Set([...(intent.people ?? []), ...(intent.person ? [intent.person] : [])].filter(Boolean)));
|
|
538
|
+
if (peopleQueries.length) {
|
|
539
|
+
filtered = filtered.filter((asset) => {
|
|
540
|
+
const haystack = [asset?.title, asset?.key_question, asset?.file_name]
|
|
541
|
+
.filter((v) => typeof v === "string" && v.trim())
|
|
542
|
+
.join(" ");
|
|
543
|
+
return peopleQueries.some((query) => exactNormalizedTextMatch(query, haystack));
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return filtered;
|
|
507
548
|
};
|
|
508
|
-
}
|
|
509
549
|
|
|
510
|
-
|
|
511
|
-
let
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (peopleQueries.length) {
|
|
516
|
-
filtered = filtered.filter((asset) => {
|
|
517
|
-
const haystack = [asset?.title, asset?.key_question, asset?.file_name]
|
|
518
|
-
.filter((v) => typeof v === "string" && v.trim())
|
|
519
|
-
.join(" ");
|
|
520
|
-
return peopleQueries.some((query) => exactNormalizedTextMatch(query, haystack));
|
|
521
|
-
});
|
|
522
|
-
}
|
|
550
|
+
setWorkingMessage(ctx, `Checking full-conversation VTT files in ${program.slug}...`);
|
|
551
|
+
let assets = await filterAssetsForIntent(await listCandidateAssets(program.slug, args.partyId, intent.date, "full_conversation"));
|
|
552
|
+
if (!assets.length) {
|
|
553
|
+
setWorkingMessage(ctx, `Checking clip transcript text in ${program.slug}...`);
|
|
554
|
+
assets = await filterAssetsForIntent(await listCandidateAssets(program.slug, args.partyId, intent.date, "clip"));
|
|
523
555
|
}
|
|
524
|
-
return filtered;
|
|
525
|
-
};
|
|
526
556
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
ctx.ui.notify(`No full-conversation VTT found. Checking clip transcript text to generate VTT in ${program.slug}...`, "info");
|
|
531
|
-
assets = await filterAssetsForIntent(await listCandidateAssets(program.slug, args.partyId, intent.date, "clip"));
|
|
532
|
-
}
|
|
557
|
+
let candidates = assets
|
|
558
|
+
.map((asset) => toCandidateFromAsset(asset, intent.person || "unassigned", { allowGeneratedFromText }))
|
|
559
|
+
.filter((item): item is TranscriptCandidate => !!item);
|
|
533
560
|
|
|
534
|
-
|
|
535
|
-
.map((asset) => toCandidateFromAsset(asset, intent.person || "unassigned"))
|
|
536
|
-
.filter((item): item is TranscriptCandidate => !!item);
|
|
561
|
+
candidates = applyLatestFilter(candidates, intent.latestCount, (row) => row.sortTimestamp);
|
|
537
562
|
|
|
538
|
-
|
|
563
|
+
if (!candidates.length) {
|
|
564
|
+
setWorkingMessage(ctx, "Checking meeting transcript rows...");
|
|
565
|
+
candidates = await listFallbackTranscriptCandidates(program.slug, intent, { allowGeneratedFromText });
|
|
566
|
+
}
|
|
539
567
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
568
|
+
if (!candidates.length) {
|
|
569
|
+
return {
|
|
570
|
+
program: program.slug,
|
|
571
|
+
request,
|
|
572
|
+
count: 0,
|
|
573
|
+
files: [],
|
|
574
|
+
message: "No matching transcript VTT files found.",
|
|
575
|
+
};
|
|
576
|
+
}
|
|
544
577
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
578
|
+
const unresolvedSource = candidates.filter((candidate) => !hasTranscriptSourceRef(candidate));
|
|
579
|
+
const unresolvedPerson = candidates.filter((candidate) => !hasNamedPerson(candidate.person));
|
|
580
|
+
|
|
581
|
+
if (unresolvedSource.length || unresolvedPerson.length) {
|
|
582
|
+
const unresolvedRows = candidates
|
|
583
|
+
.filter((candidate) => !hasTranscriptSourceRef(candidate) || !hasNamedPerson(candidate.person))
|
|
584
|
+
.slice(0, 5)
|
|
585
|
+
.map((candidate) => {
|
|
586
|
+
const sourceRef = hasTranscriptSourceRef(candidate) ? "ok" : "missing-source";
|
|
587
|
+
const personRef = hasNamedPerson(candidate.person) ? "ok" : "missing-person";
|
|
588
|
+
return `- ${candidate.eventDate} :: ${candidate.person || "unknown"} [${sourceRef}; ${personRef}]`;
|
|
589
|
+
})
|
|
590
|
+
.join("\n");
|
|
591
|
+
|
|
592
|
+
const message = `Strict transcript export guard blocked this download. ${
|
|
593
|
+
unresolvedSource.length
|
|
594
|
+
} row(s) are missing source reference (assetId/meetingId), and ${unresolvedPerson.length} row(s) are missing a named person.`;
|
|
595
|
+
ctx.ui.notify(`${message}${unresolvedRows ? `\n${unresolvedRows}` : ""}`, "warning");
|
|
596
|
+
return {
|
|
597
|
+
program: program.slug,
|
|
598
|
+
request,
|
|
599
|
+
count: 0,
|
|
600
|
+
files: [],
|
|
601
|
+
message,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
555
604
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
605
|
+
const baseOutDir = resolveOutputDir(program.slug, intent.outDir);
|
|
606
|
+
const planned = planCandidateFileNames(candidates);
|
|
607
|
+
const outDir = intent.outDir ? baseOutDir : join(baseOutDir, buildExportBatchFolderName(planned));
|
|
608
|
+
if (!intent.outDir && ctx.hasUI !== false) {
|
|
609
|
+
const choice = await ctx.ui.select(
|
|
610
|
+
buildTranscriptCandidateSummary(planned, outDir),
|
|
611
|
+
["Download", "Cancel"],
|
|
612
|
+
);
|
|
613
|
+
if (choice !== "Download") {
|
|
614
|
+
return {
|
|
615
|
+
program: program.slug,
|
|
616
|
+
request,
|
|
617
|
+
cancelled: true,
|
|
618
|
+
count: planned.length,
|
|
619
|
+
files: [],
|
|
620
|
+
message: "Transcript download cancelled.",
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
setWorkingMessage(ctx, "Writing transcript files...");
|
|
626
|
+
await mkdir(outDir, { recursive: true });
|
|
627
|
+
|
|
628
|
+
const manifest: Array<Record<string, unknown>> = [];
|
|
629
|
+
|
|
630
|
+
for (const { candidate, fileName } of planned) {
|
|
631
|
+
await writeFile(join(outDir, fileName), candidate.vtt, "utf8");
|
|
632
|
+
manifest.push({
|
|
633
|
+
file: fileName,
|
|
634
|
+
source: candidate.source,
|
|
635
|
+
assetId: candidate.assetId ?? null,
|
|
636
|
+
meetingId: candidate.meetingId ?? null,
|
|
637
|
+
eventDate: candidate.eventDate,
|
|
638
|
+
person: candidate.person,
|
|
639
|
+
vttMode: candidate.vttMode ?? "native",
|
|
640
|
+
videoUrl: candidate.videoUrl ?? null,
|
|
641
|
+
audioUrl: candidate.audioUrl ?? null,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
582
644
|
|
|
583
|
-
|
|
584
|
-
const planned = planCandidateFileNames(candidates);
|
|
585
|
-
const outDir = intent.outDir ? baseOutDir : join(baseOutDir, buildExportBatchFolderName(planned));
|
|
586
|
-
if (!intent.outDir && ctx.hasUI !== false) {
|
|
587
|
-
const choice = await ctx.ui.select(
|
|
588
|
-
buildTranscriptCandidateSummary(planned, outDir),
|
|
589
|
-
["Download", "Cancel"],
|
|
590
|
-
);
|
|
591
|
-
if (choice !== "Download") {
|
|
645
|
+
if (!manifest.length) {
|
|
592
646
|
return {
|
|
593
647
|
program: program.slug,
|
|
594
648
|
request,
|
|
595
|
-
|
|
596
|
-
count: planned.length,
|
|
649
|
+
count: 0,
|
|
597
650
|
files: [],
|
|
598
|
-
message: "
|
|
651
|
+
message: "Matched assets, but no usable VTT content was found.",
|
|
599
652
|
};
|
|
600
653
|
}
|
|
601
|
-
}
|
|
602
654
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
assetId: candidate.assetId ?? null,
|
|
613
|
-
meetingId: candidate.meetingId ?? null,
|
|
614
|
-
eventDate: candidate.eventDate,
|
|
615
|
-
person: candidate.person,
|
|
616
|
-
vttMode: candidate.vttMode ?? "native",
|
|
617
|
-
videoUrl: candidate.videoUrl ?? null,
|
|
618
|
-
audioUrl: candidate.audioUrl ?? null,
|
|
619
|
-
});
|
|
620
|
-
}
|
|
655
|
+
const generatedCount = manifest.filter((row: any) => row?.vttMode === "generated_from_text").length;
|
|
656
|
+
const summary = buildTranscriptDownloadSummary(manifest, outDir);
|
|
657
|
+
const message =
|
|
658
|
+
generatedCount > 0
|
|
659
|
+
? `${summary}\n\nGenerated ${generatedCount} VTT file${generatedCount === 1 ? "" : "s"} from transcript text.`
|
|
660
|
+
: summary;
|
|
661
|
+
|
|
662
|
+
if (ctx.hasUI !== false) await offerOpenDownloadedPath(agent, ctx, outDir);
|
|
663
|
+
const downloadableVideoCount = candidates.filter((item) => typeof item.videoUrl === "string" && item.videoUrl.trim()).length;
|
|
621
664
|
|
|
622
|
-
if (!manifest.length) {
|
|
623
|
-
ctx.ui.notify("Matched assets, but no usable VTT content was found.", "warning");
|
|
624
665
|
return {
|
|
625
666
|
program: program.slug,
|
|
626
667
|
request,
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
668
|
+
outDir,
|
|
669
|
+
manifestPath: null,
|
|
670
|
+
videoDir: null,
|
|
671
|
+
downloadableVideoCount,
|
|
672
|
+
count: manifest.length,
|
|
673
|
+
files: manifest,
|
|
674
|
+
message,
|
|
630
675
|
};
|
|
676
|
+
} finally {
|
|
677
|
+
setWorkingMessage(ctx, undefined);
|
|
631
678
|
}
|
|
632
|
-
|
|
633
|
-
const generatedCount = manifest.filter((row: any) => row?.vttMode === "generated_from_text").length;
|
|
634
|
-
const summary = buildTranscriptDownloadSummary(manifest, outDir);
|
|
635
|
-
ctx.ui.notify(
|
|
636
|
-
generatedCount > 0
|
|
637
|
-
? `${summary}\n\nGenerated ${generatedCount} VTT file${generatedCount === 1 ? "" : "s"} from transcript text.`
|
|
638
|
-
: summary,
|
|
639
|
-
"info",
|
|
640
|
-
);
|
|
641
|
-
|
|
642
|
-
if (ctx.hasUI !== false) await offerOpenDownloadedPath(agent, ctx, outDir);
|
|
643
|
-
const downloadableVideoCount = candidates.filter((item) => typeof item.videoUrl === "string" && item.videoUrl.trim()).length;
|
|
644
|
-
|
|
645
|
-
return {
|
|
646
|
-
program: program.slug,
|
|
647
|
-
request,
|
|
648
|
-
outDir,
|
|
649
|
-
manifestPath: null,
|
|
650
|
-
videoDir: null,
|
|
651
|
-
downloadableVideoCount,
|
|
652
|
-
count: manifest.length,
|
|
653
|
-
files: manifest,
|
|
654
|
-
message: summary,
|
|
655
|
-
};
|
|
656
679
|
}
|
|
@@ -102,7 +102,7 @@ export function registerTranscriptIntentInterceptor(pi: ExtensionAPI) {
|
|
|
102
102
|
name: "seedclub_export_transcripts",
|
|
103
103
|
label: "Export Transcripts",
|
|
104
104
|
description:
|
|
105
|
-
"Download matching Seed Club transcript VTT files locally using exact constraints already established in chat. Use this after the user asks to get, pull, export, download, save, or review transcript/VTT/caption files locally, including short prompts like \"today's transcripts\" or \"2pm VTTs\". Do not use this tool for fuzzy person disambiguation or contact search; ask the user to confirm, or use metadata/contact tools first, then pass exact date/time/person/partyId here. Do not use for transcript inventory questions such as \"what was the last day we have transcripts for 11am?\"; answer those with seedclub_list_meeting_transcripts metadata instead.
|
|
105
|
+
"Download matching Seed Club transcript VTT files locally using exact constraints already established in chat. Use this after the user asks to get, pull, export, download, save, or review transcript/VTT/caption files locally, including short prompts like \"today's transcripts\" or \"2pm VTTs\". Keep the user's original request string in request. Do not use this tool for fuzzy person disambiguation or contact search; ask the user to confirm, or use metadata/contact tools first, then pass exact date/time/person/partyId here. Do not use for transcript inventory questions such as \"what was the last day we have transcripts for 11am?\"; answer those with seedclub_list_meeting_transcripts metadata instead. This tool is for local file export results, not for pasting transcript text or raw JSON into chat.",
|
|
106
106
|
parameters: Type.Object({
|
|
107
107
|
request: Type.String({ description: "The user's original transcript request, verbatim when possible." }),
|
|
108
108
|
programSlug: Type.Optional(Type.String({ description: "Optional program slug, e.g. 11am." })),
|
|
@@ -217,12 +217,15 @@ export function registerTranscriptIntentInterceptor(pi: ExtensionAPI) {
|
|
|
217
217
|
IMPORTANT SEED CLUB TRANSCRIPT EXPORT ROUTING:
|
|
218
218
|
- The user's prompt appears to be a transcript file export/retrieval request.
|
|
219
219
|
- Keep the user's prompt in normal conversation context.
|
|
220
|
+
- Short artifact prompts like "today's transcripts", "Tuesday VTTs", or "2pm captions" are export asks, not inline transcript display asks.
|
|
220
221
|
- If person/date/program constraints are not clear, ask or use metadata tools first; do not let seedclub_export_transcripts perform fuzzy disambiguation.
|
|
221
222
|
- Prefer calling seedclub_export_transcripts only after the constraints are exact, with request set to the user's original prompt.
|
|
223
|
+
- If the user explicitly wants transcript files, keep the flow in transcript export once constraints are exact instead of chaining broad media or transcript-reading tools first.
|
|
222
224
|
- Avoid chaining high-payload media/transcript tools for export asks when seedclub_export_transcripts can satisfy the request.
|
|
225
|
+
- If the user asks what transcripts exist, use transcript metadata tools instead of export.
|
|
223
226
|
- For transcript export requests, download transcripts only in the tool. If videos are available, ask as a follow-up in chat instead of prompting inside the tool.
|
|
224
227
|
- Once a transcript export succeeds for the identified person in this turn, stop calling transcript export again.
|
|
225
|
-
-
|
|
228
|
+
- Report export results, paths, and follow-up options in chat instead of dumping transcript text or raw JSON.`,
|
|
226
229
|
};
|
|
227
230
|
});
|
|
228
231
|
}
|