alt-plugin-sdk 0.2.1 → 0.2.3

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 (2) hide show
  1. package/llms.txt +250 -41
  2. package/package.json +1 -1
package/llms.txt CHANGED
@@ -193,8 +193,8 @@ major `1` semantics until you opt in.
193
193
  ## 5. Permissions
194
194
 
195
195
  A plugin only gets exactly what its manifest declares. Calling a method
196
- without its permission throws `Plugin {id} lacks permission "{perm}"` at the
197
- IPC boundary and never touches the database, file system, or models.
196
+ without its permission throws `Plugin permission required: {permission}` at
197
+ the IPC boundary and never touches the database, file system, or models.
198
198
 
199
199
  | Permission | Unlocks |
200
200
  | ------------------- | ------------------------------------------------------------------------------------------------------------------------ |
@@ -205,13 +205,14 @@ IPC boundary and never touches the database, file system, or models.
205
205
  | `notes:write` | `alt.notes.create / update / delete / setMemo / setSummary / appendTranscriptLine / upsertComponent / deleteComponent` |
206
206
  | `notes:select` | `alt.notes.select(...)` — focuses a note in Alt's main window |
207
207
  | `folders:write` | `alt.folders.create / rename / move / delete` |
208
- | `ai:chat` | `alt.ai.models.list / stream / chat.stream / complete / summarize` |
208
+ | `ai:chat` | `alt.ai.stream / chat.stream / complete / summarize`. (Note: `alt.ai.models.list` is NOT gated — any plugin can call it.) |
209
209
  | `recording:control` | `alt.recording.start / stop / getStatus` |
210
210
  | `transcription:run` | `alt.transcription.transcribeFile / transcribeNote` |
211
211
  | `files:read` | `alt.files.list / read` |
212
212
  | `files:write` | `alt.files.attach / delete` |
213
213
  | `settings:read` | `alt.settings.get / list` |
214
214
  | `actions:notes` | **Legacy.** Old plugins that declare this auto-get `notes:write` + `notes:select`. New plugins should declare the modern names. |
215
+ | `settings:write` | **Reserved.** Accepted by the manifest schema for forward compatibility, but no method currently requires it. Don't declare it. |
215
216
 
216
217
  Declare the **minimum** permissions you need. Users see the list before
217
218
  install and can later disable a plugin if its permissions feel off.
@@ -232,9 +233,12 @@ keeps working even if the host bridge is initialized after your code starts.
232
233
 
233
234
  ### 6.1 `alt.storage` — JSON KV per plugin
234
235
 
235
- Plugin-scoped, persisted to electron-store inside the host. Values are deep
236
- JSON: strings, numbers, booleans, `null`, arrays, and plain objects. No
237
- binary, no Map/Set, no circular refs.
236
+ Plugin-scoped, persisted to Alt's SQLite database (in the `plugin_storage`
237
+ table, one row per `(pluginId, key)`). Values are deep JSON: strings,
238
+ numbers, booleans, `null`, arrays, and plain objects. No binary, no
239
+ Map/Set, no circular refs. Large blobs belong in `alt.files.attach` —
240
+ the storage row is JSON-encoded and meant for small structured state
241
+ (preferences, indices, drafts).
238
242
 
239
243
  ```ts
240
244
  type Counter = { count: number; updatedAt: string };
@@ -300,7 +304,7 @@ Event types and their payload (`PluginHostEventPayload` union):
300
304
  | `folderCreated` | `PluginFolderNode` | New folder |
301
305
  | `folderUpdated` | `PluginFolderNode` | Folder renamed or moved |
302
306
  | `folderDeleted` | `{ folderId }` | Folder removed |
303
- | `componentUpdated` | `PluginNoteComponentSummary` | Component (memo/summary/transcript/etc) changed |
307
+ | `componentUpdated` | `PluginNoteComponentSummary` | Component (memo/summary/transcript/etc) created or updated. Note: NOT fired on `deleteComponent` — there's no `componentDeleted` event today; subscribe to `noteUpdated` or re-list if you need to track deletions. |
304
308
  | `settingChanged` | `{ key, value }` | One of the curated app settings updated |
305
309
  | `recordingLevel` | `{ micDb, systemDb, timestamp }` | Reserved — not currently emitted |
306
310
 
@@ -326,6 +330,23 @@ const recent = await alt.notes.list({ limit: 20 });
326
330
  const filtered = await alt.notes.list({ folderId: 7, query: 'midterm', limit: 50 });
327
331
  const content = await alt.notes.getContent(recent[0].id);
328
332
  // { id, title, transcript, memo, summary }
333
+ //
334
+ // `transcript`, `memo`, `summary` are all PLAIN STRINGS, already converted
335
+ // from the host's internal storage format for easy LLM consumption:
336
+ //
337
+ // transcript: LLM-formatted plaintext, one line per entry. Examples:
338
+ // [0:00] Hello everyone, welcome to today's meeting.
339
+ // [0:06 | Speaker 1] I have the numbers ready.
340
+ // [1:23:45 | SPEAKER_0] And in chapter four...
341
+ // (`[m:ss]` or `[h:mm:ss]` for >= 1h. Speaker is appended after
342
+ // ` | ` when present. This is NOT structured JSON — if you need
343
+ // raw timing/speaker data, use getComponent on the transcript
344
+ // component instead, see below.)
345
+ //
346
+ // memo: Markdown, already converted from Alt's internal Plate-JSON
347
+ // rich-text format. Pass back to setMemo as-is.
348
+ //
349
+ // summary: Markdown, same treatment as memo.
329
350
 
330
351
  // Write
331
352
  const created = await alt.notes.create({
@@ -348,12 +369,61 @@ await alt.notes.appendTranscriptLine({
348
369
  // Focus a note in Alt's main window (requires notes:select)
349
370
  await alt.notes.select({ noteId });
350
371
 
351
- // Components — fine-grained content blocks attached to a note
372
+ // Components — fine-grained content blocks attached to a note.
373
+ // `getComponent().contentText` is the RAW value as stored by the host — it
374
+ // is NOT pre-converted like getContent() is. The exact shape depends on
375
+ // componentType:
376
+ //
377
+ // transcript JSON-stringified `TranscriptEntry[]`. Parse with
378
+ // JSON.parse to get structured speaker/timing data.
379
+ // This is the right source for anything that needs to
380
+ // line up with the recording — getContent().transcript
381
+ // has already been collapsed to plaintext.
382
+ // memo, summary Plate-JSON (Alt's internal rich-text format). Treat
383
+ // as opaque — there are no SDK helpers to manipulate
384
+ // Plate nodes. To read these as markdown, use
385
+ // getContent() / getMemo() instead. To write, use
386
+ // setMemo() / setSummary() with markdown.
387
+ // slide_summary, Plain text/markdown — exactly what was last upserted
388
+ // meeting_notes via upsertComponent({ contentText, ... }).
389
+ // slides, File-backed components. `contentText` is `null`. Use
390
+ // recording alt.files.read({ fileId }) for the bytes; the fileId
391
+ // comes from alt.files.list({ noteId }).
392
+
352
393
  const components = await alt.notes.listComponents({ noteId });
394
+
353
395
  const memoComponent = await alt.notes.getComponent({
354
396
  componentId: components.find(c => c.componentType === 'memo')!.id,
355
397
  });
356
- // memoComponent.contentText holds the markdown.
398
+ // memoComponent.contentText is Plate JSON; use alt.notes.getMemo() or
399
+ // alt.notes.getContent() if you want markdown instead.
400
+
401
+ // Structured transcript (speaker + ms timing) — see recipe in §8 below.
402
+ // The raw transcript contentText is JSON-stringified entries with this
403
+ // internal shape (NOT the SDK's `PluginTranscriptionSegment`, which uses
404
+ // `startMs`/`endMs` — the stored shape uses `start`/`end`):
405
+ interface RawTranscriptSegment {
406
+ start: number; // ms from recording start
407
+ end: number; // ms from recording start
408
+ text: string;
409
+ speaker?: string; // e.g. 'SPEAKER_0', 'Speaker 1'
410
+ translatedText?: string;
411
+ }
412
+ interface RawTranscriptEntry {
413
+ relativeStart?: number; // ms from recording start (entry-level)
414
+ speaker?: string;
415
+ segments?: RawTranscriptSegment[];
416
+ originalText?: string;
417
+ translatedText?: string;
418
+ createdAt: number; // unix ms when the entry was first appended
419
+ }
420
+
421
+ const transcriptComp = components.find(c => c.componentType === 'transcript');
422
+ if (transcriptComp) {
423
+ const raw = await alt.notes.getComponent({ componentId: transcriptComp.id });
424
+ const entries: RawTranscriptEntry[] = JSON.parse(raw.contentText ?? '[]');
425
+ // entries[i].relativeStart, entries[i].speaker, entries[i].segments[j].text
426
+ }
357
427
 
358
428
  // Singleton component types (memo / summary / transcript) auto-upsert —
359
429
  // calling upsertComponent for type=memo on a note that already has a memo
@@ -379,8 +449,12 @@ Limits:
379
449
  - `markdown` body (memo/summary) — ≤ 500,000 chars.
380
450
  - `appendTranscriptLine.text` — 1–10,000 chars.
381
451
  - `upsertComponent.contentText` — ≤ 1,000,000 chars.
382
- - `setMemo` / `setSummary` / `appendTranscriptLine` and `upsertComponent`
383
- return `{ componentId }` of the (possibly newly created) component.
452
+ - `setMemo` / `setSummary` / `appendTranscriptLine` return `{ componentId }`
453
+ of the (possibly newly created) component.
454
+ - `upsertComponent` returns the full `PluginNoteComponentSummary` (`id`,
455
+ `noteId`, `componentType`, `title`, `displayOrder`, `hasFile`,
456
+ `createdAt`, `updatedAt`) of the upserted component — not just
457
+ `{ componentId }`.
384
458
 
385
459
  ### 6.5 `alt.folders` — manage the note tree
386
460
 
@@ -465,11 +539,25 @@ fileHandle.cancel();
465
539
 
466
540
  Error codes (`PluginTranscriptionStreamErrorCode`):
467
541
 
468
- - `FORBIDDEN` — plugin lacks `transcription:run`
469
- - `INVALID_REQUEST` — bad params (e.g. unknown file path, invalid noteId)
470
- - `RECORDING_ACTIVE` Alt is currently recording; can't run transcription
471
- - `FILE_UNAVAILABLE` the file was deleted or unreadable
472
- - `PIPELINE_ERROR` Whisper/diarization pipeline crashed
542
+ - `FORBIDDEN` — plugin lacks `transcription:run`.
543
+ - `INVALID_REQUEST` — request fails schema validation (missing/empty
544
+ `filePath`, non-positive `noteId`, etc.). Note: this is shape-only
545
+ the host does NOT pre-check that a `filePath` exists; a non-existent
546
+ or unreadable path surfaces later as `PIPELINE_ERROR`.
547
+ - `RECORDING_ACTIVE` — Alt is currently recording; can't run transcription.
548
+ - `FILE_UNAVAILABLE` — only from `transcribeNote`. Fires when the target
549
+ note has no `recording` component or the file_inode can't be resolved.
550
+ A deleted file passed to `transcribeFile` does NOT fire this code —
551
+ it surfaces as `PIPELINE_ERROR` (the Whisper pipeline can't open it).
552
+ - `PIPELINE_ERROR` — Whisper/diarization pipeline crashed, OR the file
553
+ couldn't be opened (see `FILE_UNAVAILABLE` note above).
554
+
555
+ Also note the defaults the host applies when you omit optional fields:
556
+
557
+ - `language` falls back to the user's `transcription.lectureLanguage`
558
+ setting (typically `'en'`). You don't need to pass it unless you want
559
+ to override.
560
+ - `diarization` defaults to `false` when omitted.
473
561
 
474
562
  ### 6.8 `alt.ai` — chat completions and summarize
475
563
 
@@ -518,8 +606,11 @@ const handle = await alt.ai.chat.stream(
518
606
  );
519
607
  // handle.cancel() to abort.
520
608
 
521
- // Run Alt's own summarize prompt against a note. Pulls the transcript+memo,
522
- // runs through the configured model, returns markdown.
609
+ // Run Alt's own summarize prompt against a note. The host composes the
610
+ // prompt from up to three sources on the note — memo, transcript, and any
611
+ // existing summary (so re-running it refines an earlier output rather than
612
+ // starting from scratch). Each is included only if non-empty. Throws if
613
+ // all three are empty. Runs through the chosen model, returns markdown.
523
614
  const sum = await alt.ai.summarize({
524
615
  noteId,
525
616
  style: 'Focus on definitions and worked examples.',
@@ -553,38 +644,48 @@ const bytes: ArrayBuffer = await someFile.arrayBuffer();
553
644
  const attached = await alt.files.attach({
554
645
  noteId,
555
646
  fileName: 'lecture12.pdf',
556
- data: bytes, // max 256 MB
647
+ data: bytes, // max 256 MiB (256 * 1024 * 1024 bytes)
557
648
  mimeType: 'application/pdf',
558
649
  componentType: 'slides', // 'slides' | 'recording'
559
650
  title: 'Lecture 12 — Slides',
560
651
  displayOrder: 0,
561
652
  });
562
653
  // attached: { componentId, fileId, fileName, mimeType, sizeBytes }
654
+ // mimeType is `string | null` — null when the caller omitted it.
563
655
 
564
656
  const list = await alt.files.list({ noteId });
565
657
  const blob = await alt.files.read({ fileId: list[0].fileId });
566
- // { fileName, mimeType, sizeBytes, data: ArrayBuffer }
658
+ // { fileName, mimeType: string | null, sizeBytes, data: ArrayBuffer }
567
659
 
568
660
  await alt.files.delete({ componentId: attached.componentId });
569
661
  ```
570
662
 
571
663
  Constraints:
572
664
 
573
- - `fileName` is 260 chars and **must not contain** path separators or
574
- control characters. Host re-validates with a strict regex; bad names
575
- throw.
665
+ - `fileName` is 1–260 chars and must match `^[^\\/\x00\n\r:*?"<>|]+$`
666
+ blocks `\ / : * ? " < > |` and the NUL/LF/CR control bytes (Windows
667
+ reserved chars plus path separators). Other control bytes are
668
+ technically allowed by this regex but are still a bad idea. Host
669
+ re-validates; bad names throw.
576
670
  - `componentType` is restricted to `slides | recording` because the database
577
671
  only allows file-backed components for those types. Use
578
672
  `notes.upsertComponent` for text-based components instead.
579
- - `data` is an `ArrayBuffer` capped at 256 MiB.
673
+ - `data` is an `ArrayBuffer` capped at 256 MiB (`256 * 1024 * 1024`).
580
674
 
581
675
  ### 6.10 `alt.settings` — read curated app settings
582
676
 
583
- Permission: `settings:read`. Read-only allowlist. There is **no
584
- settings:write**; if you need plugin-owned settings, use `alt.storage`.
677
+ Permission: `settings:read`. Read-only allowlist there is no
678
+ write surface on the host today (the `settings:write` permission is
679
+ reserved but unused). If you need plugin-owned settings, use
680
+ `alt.storage`.
585
681
 
586
682
  ```ts
587
- const theme = await alt.settings.get('theme'); // 'system' | 'light' | 'dark' | null
683
+ const theme = await alt.settings.get('theme');
684
+ // theme is typed `PluginAppSettingValue` = `string | number | boolean | null`.
685
+ // The SDK does NOT narrow per-key — you have to check at runtime.
686
+ // Typical values seen here: 'system' | 'light' | 'dark'. `null` if unset.
687
+ if (typeof theme === 'string') applyTheme(theme);
688
+
588
689
  const all = await alt.settings.list();
589
690
  // {
590
691
  // theme: 'dark',
@@ -628,9 +729,13 @@ const res = await altFetch('https://alt-plugin.invalid/v1/chat/completions', {
628
729
  // Standard streaming Response — use res.body.getReader() or res.text().
629
730
  ```
630
731
 
631
- `Authorization` and `x-machine-id` headers are stripped before the request
632
- reaches the host, so SDKs that try to inject API keys won't accidentally
633
- exfiltrate them.
732
+ Headers are filtered twice on the way to the cloud. First in the SDK,
733
+ `Authorization` and `x-machine-id` are stripped so AI-SDK clients that
734
+ auto-inject API keys can't exfiltrate them. Then the host applies a
735
+ strict allowlist: only `accept` and `content-type` survive — everything
736
+ else (custom routing hints, request IDs, telemetry headers) is silently
737
+ dropped. Don't rely on passing arbitrary headers through `createAltFetch`;
738
+ put any plugin-side metadata in the request body instead.
634
739
 
635
740
  ### `createAltProvider(options?)`
636
741
 
@@ -665,8 +770,11 @@ Use this whenever you'd normally instantiate `@ai-sdk/openai` etc. Plugins
665
770
 
666
771
  ### 8.1 Live transcript subscriber
667
772
 
668
- Watch active-note changes, fetch its transcript whenever it updates, render
669
- the last 10 lines.
773
+ Watch active-note changes, fetch its transcript whenever it updates,
774
+ render the last 10 transcript lines. Note `getContent().transcript` is
775
+ the LLM-formatted plaintext (`[m:ss | Speaker] text` per line) — slicing
776
+ by `\n` is fine here since each entry is exactly one line, but if you
777
+ need structured timing data go through §8.5 instead.
670
778
 
671
779
  ```ts
672
780
  // permissions: ['notes:read', 'events:subscribe', 'appState:read']
@@ -683,21 +791,33 @@ const refresh = async (noteId: number) => {
683
791
  render(lines);
684
792
  };
685
793
 
686
- await alt.events.subscribe('activeNoteChanged', note => {
794
+ // IMPORTANT: hold on to the unsubscribe handles. Leaked subscriptions
795
+ // stay alive for the lifetime of the plugin window. Call them in your
796
+ // teardown path (e.g. `beforeunload`, your framework's unmount hook).
797
+ const unsubActive = await alt.events.subscribe('activeNoteChanged', note => {
687
798
  currentNoteId = note?.id ?? null;
688
799
  if (currentNoteId !== null) void refresh(currentNoteId);
689
800
  else render(['(no active note)']);
690
801
  });
691
802
 
692
- await alt.events.subscribe('transcriptUpdated', ({ noteId }) => {
693
- if (noteId === currentNoteId) void refresh(noteId);
694
- });
803
+ const unsubTranscript = await alt.events.subscribe(
804
+ 'transcriptUpdated',
805
+ ({ noteId }) => {
806
+ if (noteId === currentNoteId) void refresh(noteId);
807
+ },
808
+ );
695
809
 
696
810
  const active = await alt.state.getActiveNoteSummary();
697
811
  if (active) {
698
812
  currentNoteId = active.id;
699
813
  await refresh(active.id);
700
814
  }
815
+
816
+ // On teardown:
817
+ window.addEventListener('beforeunload', () => {
818
+ void unsubActive();
819
+ void unsubTranscript();
820
+ });
701
821
  ```
702
822
 
703
823
  ### 8.2 Quiz from a note (AI + storage)
@@ -734,7 +854,9 @@ export async function buildFlashcards(noteId: number) {
734
854
  ### 8.3 Transcribe an arbitrary audio file, then save as a new note
735
855
 
736
856
  ```ts
737
- // permissions: ['notes:write', 'transcription:run', 'notes:select']
857
+ // permissions: ['notes:write', 'transcription:run']
858
+ // Note: no `notes:select` needed — the host auto-focuses freshly created
859
+ // notes in the main window as part of `notes.create`.
738
860
  import { alt } from 'alt-plugin-sdk';
739
861
 
740
862
  export async function importAudio(filePath: string, title: string) {
@@ -757,7 +879,6 @@ export async function importAudio(filePath: string, title: string) {
757
879
  title: 'Imported transcript',
758
880
  contentText: segments.join('\n'),
759
881
  });
760
- await alt.notes.select({ noteId });
761
882
  },
762
883
  onError: err => console.error(err.code, err.message),
763
884
  },
@@ -768,7 +889,7 @@ export async function importAudio(filePath: string, title: string) {
768
889
  ### 8.4 Attach a PDF chosen via `<input type="file">`
769
890
 
770
891
  ```ts
771
- // permissions: ['files:write']
892
+ // permissions: ['files:write', 'appState:read']
772
893
  import { alt } from 'alt-plugin-sdk';
773
894
 
774
895
  document.querySelector<HTMLInputElement>('#picker')!.addEventListener(
@@ -790,6 +911,76 @@ document.querySelector<HTMLInputElement>('#picker')!.addEventListener(
790
911
  );
791
912
  ```
792
913
 
914
+ ### 8.5 Structured transcript with speaker + ms timing
915
+
916
+ `alt.notes.getContent(noteId).transcript` returns LLM-formatted plaintext
917
+ (`[0:06 | Speaker 1] ...`). That's the wrong source when you need to line
918
+ up text with the recording, or render a speaker-labeled UI similar to
919
+ Alt's own transcript view. For that, go through the raw transcript
920
+ component:
921
+
922
+ ```ts
923
+ // permissions: ['notes:read']
924
+ import { alt } from 'alt-plugin-sdk';
925
+
926
+ interface RawTranscriptSegment {
927
+ start: number; // ms from recording start
928
+ end: number; // ms from recording start
929
+ text: string;
930
+ speaker?: string; // 'SPEAKER_0', 'Speaker 1', etc.
931
+ translatedText?: string;
932
+ }
933
+ interface RawTranscriptEntry {
934
+ relativeStart?: number; // ms; entry-level start
935
+ speaker?: string;
936
+ segments?: RawTranscriptSegment[];
937
+ originalText?: string;
938
+ translatedText?: string;
939
+ createdAt: number; // unix ms
940
+ }
941
+
942
+ async function getStructuredTranscript(
943
+ noteId: number,
944
+ ): Promise<RawTranscriptEntry[]> {
945
+ const components = await alt.notes.listComponents({ noteId });
946
+ const transcript = components.find(c => c.componentType === 'transcript');
947
+ if (!transcript) return [];
948
+
949
+ const raw = await alt.notes.getComponent({ componentId: transcript.id });
950
+ if (!raw.contentText) return [];
951
+
952
+ try {
953
+ const parsed = JSON.parse(raw.contentText);
954
+ return Array.isArray(parsed) ? (parsed as RawTranscriptEntry[]) : [];
955
+ } catch {
956
+ return [];
957
+ }
958
+ }
959
+
960
+ // Use it:
961
+ const entries = await getStructuredTranscript(noteId);
962
+ for (const entry of entries) {
963
+ for (const seg of entry.segments ?? []) {
964
+ console.log(
965
+ `${seg.start}ms - ${seg.end}ms [${seg.speaker ?? '?'}]:`,
966
+ seg.text,
967
+ );
968
+ }
969
+ }
970
+ ```
971
+
972
+ Two important caveats:
973
+
974
+ - Diarization is opt-in. If the user recorded without diarization
975
+ enabled, `speaker` will be absent on most entries — fall back to a
976
+ single-speaker render.
977
+ - The internal segment shape is `{ start, end }` (milliseconds), NOT the
978
+ SDK's `PluginTranscriptionSegment` (which uses `startMs` / `endMs`).
979
+ The plugin SDK type is for the streaming transcription API; the stored
980
+ format is older and predates that naming choice. Use the inline
981
+ `RawTranscriptSegment` type above (or copy it into your code) rather
982
+ than reusing the SDK type — the field names don't match.
983
+
793
984
  ---
794
985
 
795
986
  ## 9. Errors
@@ -866,13 +1057,31 @@ The npm package follows semver:
866
1057
  also subscribe to. Polling `getStatus()` 10× a second is wasteful when
867
1058
  `recordingStatusChanged` exists.
868
1059
  - **Cap your storage.** `alt.storage` is plugin-scoped but lives in the
869
- user's electron-store on disk. Anything over a few MB belongs in
1060
+ user's SQLite database on disk. Anything over a few MB belongs in
870
1061
  `alt.files.attach`.
871
1062
  - **Plate JSON is not exposed.** Memo/summary content is round-tripped
872
1063
  through Alt's internal Plate-rich-text codec; the SDK gives you the
873
1064
  markdown view via `getContent` / `getMemo` and accepts markdown back via
874
1065
  `setMemo` / `setSummary`. There is no API to manipulate Plate nodes
875
- directly.
1066
+ directly. If you call `getComponent` on a memo or summary you get the
1067
+ raw Plate JSON in `contentText` — don't try to parse it, use the
1068
+ markdown surfaces instead.
1069
+ - **`getContent().transcript` is plaintext, not JSON.** It's the same
1070
+ LLM-friendly format Alt uses internally to feed transcripts to LLMs
1071
+ (lines like `[0:06 | Speaker 1] text`). If you need structured
1072
+ speaker/timing data (to render a transcript UI, jump to a timestamp,
1073
+ group by speaker, etc.), don't try to parse `getContent().transcript`
1074
+ — go through `listComponents` → find the `transcript` component →
1075
+ `getComponent` → `JSON.parse(contentText)`. See §8.5 for a worked
1076
+ example. The inline JSON shape is internal and differs from
1077
+ `PluginTranscriptionSegment` (it uses `start`/`end` ms, not `startMs`/
1078
+ `endMs`).
1079
+ - **Components have different `contentText` shapes per type.** The same
1080
+ `getComponent` API gives you Plate JSON for `memo`/`summary`,
1081
+ JSON-stringified entries for `transcript`, plain text for
1082
+ `slide_summary`/`meeting_notes`, and `null` for file-backed types
1083
+ (`slides`/`recording`). Always branch on `componentType` before
1084
+ touching `contentText`.
876
1085
 
877
1086
  ---
878
1087
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alt-plugin-sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Type-safe browser SDK and runtime contracts for Alt plugins.",
5
5
  "license": "MIT",
6
6
  "author": "Alt (https://altalt.io)",