@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.
Files changed (75) hide show
  1. package/README.md +18 -29
  2. package/assets/SYSTEM.md +45 -0
  3. package/assets/extensions/seedclub/commands/seedclub.ts +6 -0
  4. package/assets/extensions/seedclub/commands/transcript-export.ts +191 -168
  5. package/assets/extensions/seedclub/commands/transcript-intent.ts +5 -2
  6. package/assets/extensions/seedclub/gate-state.ts +31 -14
  7. package/assets/extensions/seedclub/index.ts +326 -14
  8. package/assets/extensions/seedclub/memory-client.js +57 -0
  9. package/assets/extensions/seedclub/memory-helpers.js +78 -0
  10. package/assets/extensions/seedclub/memory.ts +364 -0
  11. package/assets/extensions/seedclub/platform-routing.js +351 -0
  12. package/assets/extensions/seedclub/recent-entities.js +786 -0
  13. package/assets/extensions/seedclub/tool-utils.ts +37 -6
  14. package/assets/extensions/seedclub/tools/media.ts +45 -237
  15. package/assets/extensions/seedclub/tools/meetings.ts +22 -12
  16. package/assets/extensions/seedclub/tools/web.ts +475 -0
  17. package/assets/extensions/seedclub/ui-copy.ts +4 -2
  18. package/assets/theme/dark.json +9 -7
  19. package/assets/theme/light.json +9 -7
  20. package/bin/cli.js +38 -110
  21. package/package.json +9 -2
  22. package/packages/seedclub-runtime/README.md +13 -0
  23. package/packages/seedclub-runtime/package.json +5 -0
  24. package/packages/seedclub-runtime/pi-contract-baseline.json +62 -0
  25. package/packages/seedclub-runtime/src/contract.test.mjs +90 -0
  26. package/packages/seedclub-runtime/src/index.mjs +259 -0
  27. package/packages/seedclub-runtime/src/index.test.mjs +49 -0
  28. package/packages/seedclub-runtime/src/workspace-deps.mjs +66 -0
  29. package/packages/seedclub-tui/README.md +19 -0
  30. package/packages/seedclub-tui/package.json +5 -0
  31. package/packages/seedclub-tui/src/app/interactive-mode.mjs +2469 -0
  32. package/packages/seedclub-tui/src/cli.mjs +14 -0
  33. package/packages/seedclub-tui/src/index.mjs +48 -0
  34. package/packages/seedclub-tui/src/vendor/coding-agent/README.md +11 -0
  35. package/packages/seedclub-tui/src/vendor/coding-agent/config.js +76 -0
  36. package/packages/seedclub-tui/src/vendor/coding-agent/core/footer-data-provider.js +143 -0
  37. package/packages/seedclub-tui/src/vendor/coding-agent/core/keybindings.js +204 -0
  38. package/packages/seedclub-tui/src/vendor/coding-agent/core/tools/edit-diff.js +243 -0
  39. package/packages/seedclub-tui/src/vendor/coding-agent/core/tools/path-utils.js +81 -0
  40. package/packages/seedclub-tui/src/vendor/coding-agent/core/tools/truncate.js +205 -0
  41. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/assistant-message.js +96 -0
  42. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/bordered-loader.js +51 -0
  43. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/countdown-timer.js +34 -0
  44. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/custom-editor.js +70 -0
  45. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/diff.js +133 -0
  46. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/dynamic-border.js +21 -0
  47. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/extension-editor.js +95 -0
  48. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/extension-input.js +69 -0
  49. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/extension-selector.js +92 -0
  50. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/footer.js +213 -0
  51. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/keybinding-hints.js +61 -0
  52. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/login-dialog.js +132 -0
  53. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/oauth-selector.js +80 -0
  54. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/tool-execution.js +712 -0
  55. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/user-message.js +31 -0
  56. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/components/visual-truncate.js +33 -0
  57. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/theme/dark.json +85 -0
  58. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/theme/light.json +84 -0
  59. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/theme/theme-schema.json +335 -0
  60. package/packages/seedclub-tui/src/vendor/coding-agent/modes/interactive/theme/theme.js +944 -0
  61. package/packages/seedclub-tui/src/vendor/coding-agent/utils/image-convert.js +35 -0
  62. package/packages/seedclub-tui/src/vendor/coding-agent/utils/photon.js +121 -0
  63. package/packages/seedclub-tui/src/vendor/coding-agent/utils/shell.js +38 -0
  64. package/postinstall.js +18 -73
  65. package/assets/extensions/seedclub/branding.ts +0 -52
  66. package/assets/extensions/seedclub/package-lock.json +0 -72
  67. package/assets/extensions/seedclub/package.json +0 -14
  68. package/assets/extensions/seedclub-ui/editor.ts +0 -110
  69. package/assets/extensions/seedclub-ui/footer.ts +0 -126
  70. package/assets/extensions/seedclub-ui/index.ts +0 -19
  71. package/assets/extensions/seedclub-ui/state.ts +0 -18
  72. package/assets/extensions/seedclub-ui/tool-progress.ts +0 -24
  73. package/assets/extensions/seedclub-ui/update.ts +0 -210
  74. package/assets/extensions/seedclub-ui/welcome.ts +0 -748
  75. 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/meeting/media tools, retrieving transcripts, and working with Seed Club-backed app surfaces.
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
- Then:
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
- ### First run
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 the browser opens
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 connect, inspect access, and jump into CRM/meetings/media/headlines workflows |
64
- | `/transcripts` | Export transcript VTT files with filters (date, person, time, output dir) |
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
- 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.
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
@@ -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 resolveVttContent(source: any): { vtt: string; mode: "native" | "generated_from_text" } | null {
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
- const generated = textToSyntheticVtt(maybeVtt);
162
- if (generated) return { vtt: generated, mode: "generated_from_text" };
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(asset: any, fallbackPerson: string): TranscriptCandidate | null {
242
- const resolvedVtt = resolveVttContent(asset);
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(programSlug: string, intent: TranscriptIntent): Promise<TranscriptCandidate[]> {
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
- const request = args.request?.trim();
471
- if (!request) {
472
- return {
473
- error: "Provide the original transcript request.",
474
- status: 400,
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
- const intent = {
479
- ...parseIntent(request),
480
- ...(args.person ? { person: args.person } : {}),
481
- ...(args.date ? { date: args.date } : {}),
482
- ...(args.time ? { time: args.time } : {}),
483
- ...(args.outDir ? { outDir: args.outDir } : {}),
484
- ...(args.latestCount ? { latestCount: Math.max(1, Math.min(20, Math.trunc(args.latestCount))) } : {}),
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
- const session = await getSessionContext();
488
- if ("error" in session) {
489
- ctx.ui.notify(`Unable to load Seed Club session: ${session.error}`, "error");
490
- return {
491
- error: session.error,
492
- status: session.status ?? 500,
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
- const programs = pickPrograms(session);
497
- const program = args.programSlug
498
- ? programs.find((candidate) => candidate.slug === args.programSlug) ?? null
499
- : await resolveProgram(request, programs, ctx);
500
- if (!program) {
501
- ctx.ui.notify("No program selected. Include a program name like 11AM or use /transcripts.", "warning");
502
- return {
503
- error: args.programSlug
504
- ? `Program ${args.programSlug} is not accessible for the current user.`
505
- : "No program selected.",
506
- status: 404,
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
- const filterAssetsForIntent = async (inputAssets: any[]) => {
511
- let filtered = await filterByMeetingTime(inputAssets, intent.time);
512
- filtered = filtered.filter((asset) => !!ensureVtt(asset));
513
- if (!args.partyId) {
514
- const peopleQueries = Array.from(new Set([...(intent.people ?? []), ...(intent.person ? [intent.person] : [])].filter(Boolean)));
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
- ctx.ui.notify(`Looking up full-conversation VTT files in ${program.slug}...`, "info");
528
- let assets = await filterAssetsForIntent(await listCandidateAssets(program.slug, args.partyId, intent.date, "full_conversation"));
529
- if (!assets.length) {
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
- let candidates = assets
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
- candidates = applyLatestFilter(candidates, intent.latestCount, (row) => row.sortTimestamp);
563
+ if (!candidates.length) {
564
+ setWorkingMessage(ctx, "Checking meeting transcript rows...");
565
+ candidates = await listFallbackTranscriptCandidates(program.slug, intent, { allowGeneratedFromText });
566
+ }
539
567
 
540
- if (!candidates.length) {
541
- ctx.ui.notify("No matching asset transcript content found. Checking meeting transcript rows...", "info");
542
- candidates = await listFallbackTranscriptCandidates(program.slug, intent);
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
- if (!candidates.length) {
546
- ctx.ui.notify("No matching transcript VTT files found.", "info");
547
- return {
548
- program: program.slug,
549
- request,
550
- count: 0,
551
- files: [],
552
- message: "No matching transcript VTT files found.",
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
- const unresolvedSource = candidates.filter((candidate) => !hasTranscriptSourceRef(candidate));
557
- const unresolvedPerson = candidates.filter((candidate) => !hasNamedPerson(candidate.person));
558
-
559
- if (unresolvedSource.length || unresolvedPerson.length) {
560
- const unresolvedRows = candidates
561
- .filter((candidate) => !hasTranscriptSourceRef(candidate) || !hasNamedPerson(candidate.person))
562
- .slice(0, 5)
563
- .map((candidate) => {
564
- const sourceRef = hasTranscriptSourceRef(candidate) ? "ok" : "missing-source";
565
- const personRef = hasNamedPerson(candidate.person) ? "ok" : "missing-person";
566
- return `- ${candidate.eventDate} :: ${candidate.person || "unknown"} [${sourceRef}; ${personRef}]`;
567
- })
568
- .join("\n");
569
-
570
- const message = `Strict transcript export guard blocked this download. ${
571
- unresolvedSource.length
572
- } row(s) are missing source reference (assetId/meetingId), and ${unresolvedPerson.length} row(s) are missing a named person.`;
573
- ctx.ui.notify(`${message}${unresolvedRows ? `\n${unresolvedRows}` : ""}`, "warning");
574
- return {
575
- program: program.slug,
576
- request,
577
- count: 0,
578
- files: [],
579
- message,
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
- const baseOutDir = resolveOutputDir(program.slug, intent.outDir);
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
- cancelled: true,
596
- count: planned.length,
649
+ count: 0,
597
650
  files: [],
598
- message: "Transcript download cancelled.",
651
+ message: "Matched assets, but no usable VTT content was found.",
599
652
  };
600
653
  }
601
- }
602
654
 
603
- await mkdir(outDir, { recursive: true });
604
-
605
- const manifest: Array<Record<string, unknown>> = [];
606
-
607
- for (const { candidate, fileName } of planned) {
608
- await writeFile(join(outDir, fileName), candidate.vtt, "utf8");
609
- manifest.push({
610
- file: fileName,
611
- source: candidate.source,
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
- count: 0,
628
- files: [],
629
- message: "Matched assets, but no usable VTT content was found.",
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. Always pass the user's original request string in request.",
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
- - Do not answer by dumping transcript text or raw JSON into chat.`,
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
  }