alt-plugin-sdk 0.2.2 → 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 +93 -38
  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
 
@@ -445,8 +449,12 @@ Limits:
445
449
  - `markdown` body (memo/summary) — ≤ 500,000 chars.
446
450
  - `appendTranscriptLine.text` — 1–10,000 chars.
447
451
  - `upsertComponent.contentText` — ≤ 1,000,000 chars.
448
- - `setMemo` / `setSummary` / `appendTranscriptLine` and `upsertComponent`
449
- 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 }`.
450
458
 
451
459
  ### 6.5 `alt.folders` — manage the note tree
452
460
 
@@ -531,11 +539,25 @@ fileHandle.cancel();
531
539
 
532
540
  Error codes (`PluginTranscriptionStreamErrorCode`):
533
541
 
534
- - `FORBIDDEN` — plugin lacks `transcription:run`
535
- - `INVALID_REQUEST` — bad params (e.g. unknown file path, invalid noteId)
536
- - `RECORDING_ACTIVE` Alt is currently recording; can't run transcription
537
- - `FILE_UNAVAILABLE` the file was deleted or unreadable
538
- - `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.
539
561
 
540
562
  ### 6.8 `alt.ai` — chat completions and summarize
541
563
 
@@ -584,8 +606,11 @@ const handle = await alt.ai.chat.stream(
584
606
  );
585
607
  // handle.cancel() to abort.
586
608
 
587
- // Run Alt's own summarize prompt against a note. Pulls the transcript+memo,
588
- // 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.
589
614
  const sum = await alt.ai.summarize({
590
615
  noteId,
591
616
  style: 'Focus on definitions and worked examples.',
@@ -619,38 +644,48 @@ const bytes: ArrayBuffer = await someFile.arrayBuffer();
619
644
  const attached = await alt.files.attach({
620
645
  noteId,
621
646
  fileName: 'lecture12.pdf',
622
- data: bytes, // max 256 MB
647
+ data: bytes, // max 256 MiB (256 * 1024 * 1024 bytes)
623
648
  mimeType: 'application/pdf',
624
649
  componentType: 'slides', // 'slides' | 'recording'
625
650
  title: 'Lecture 12 — Slides',
626
651
  displayOrder: 0,
627
652
  });
628
653
  // attached: { componentId, fileId, fileName, mimeType, sizeBytes }
654
+ // mimeType is `string | null` — null when the caller omitted it.
629
655
 
630
656
  const list = await alt.files.list({ noteId });
631
657
  const blob = await alt.files.read({ fileId: list[0].fileId });
632
- // { fileName, mimeType, sizeBytes, data: ArrayBuffer }
658
+ // { fileName, mimeType: string | null, sizeBytes, data: ArrayBuffer }
633
659
 
634
660
  await alt.files.delete({ componentId: attached.componentId });
635
661
  ```
636
662
 
637
663
  Constraints:
638
664
 
639
- - `fileName` is 260 chars and **must not contain** path separators or
640
- control characters. Host re-validates with a strict regex; bad names
641
- 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.
642
670
  - `componentType` is restricted to `slides | recording` because the database
643
671
  only allows file-backed components for those types. Use
644
672
  `notes.upsertComponent` for text-based components instead.
645
- - `data` is an `ArrayBuffer` capped at 256 MiB.
673
+ - `data` is an `ArrayBuffer` capped at 256 MiB (`256 * 1024 * 1024`).
646
674
 
647
675
  ### 6.10 `alt.settings` — read curated app settings
648
676
 
649
- Permission: `settings:read`. Read-only allowlist. There is **no
650
- 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`.
651
681
 
652
682
  ```ts
653
- 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
+
654
689
  const all = await alt.settings.list();
655
690
  // {
656
691
  // theme: 'dark',
@@ -694,9 +729,13 @@ const res = await altFetch('https://alt-plugin.invalid/v1/chat/completions', {
694
729
  // Standard streaming Response — use res.body.getReader() or res.text().
695
730
  ```
696
731
 
697
- `Authorization` and `x-machine-id` headers are stripped before the request
698
- reaches the host, so SDKs that try to inject API keys won't accidentally
699
- 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.
700
739
 
701
740
  ### `createAltProvider(options?)`
702
741
 
@@ -731,8 +770,11 @@ Use this whenever you'd normally instantiate `@ai-sdk/openai` etc. Plugins
731
770
 
732
771
  ### 8.1 Live transcript subscriber
733
772
 
734
- Watch active-note changes, fetch its transcript whenever it updates, render
735
- 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.
736
778
 
737
779
  ```ts
738
780
  // permissions: ['notes:read', 'events:subscribe', 'appState:read']
@@ -749,21 +791,33 @@ const refresh = async (noteId: number) => {
749
791
  render(lines);
750
792
  };
751
793
 
752
- 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 => {
753
798
  currentNoteId = note?.id ?? null;
754
799
  if (currentNoteId !== null) void refresh(currentNoteId);
755
800
  else render(['(no active note)']);
756
801
  });
757
802
 
758
- await alt.events.subscribe('transcriptUpdated', ({ noteId }) => {
759
- if (noteId === currentNoteId) void refresh(noteId);
760
- });
803
+ const unsubTranscript = await alt.events.subscribe(
804
+ 'transcriptUpdated',
805
+ ({ noteId }) => {
806
+ if (noteId === currentNoteId) void refresh(noteId);
807
+ },
808
+ );
761
809
 
762
810
  const active = await alt.state.getActiveNoteSummary();
763
811
  if (active) {
764
812
  currentNoteId = active.id;
765
813
  await refresh(active.id);
766
814
  }
815
+
816
+ // On teardown:
817
+ window.addEventListener('beforeunload', () => {
818
+ void unsubActive();
819
+ void unsubTranscript();
820
+ });
767
821
  ```
768
822
 
769
823
  ### 8.2 Quiz from a note (AI + storage)
@@ -800,7 +854,9 @@ export async function buildFlashcards(noteId: number) {
800
854
  ### 8.3 Transcribe an arbitrary audio file, then save as a new note
801
855
 
802
856
  ```ts
803
- // 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`.
804
860
  import { alt } from 'alt-plugin-sdk';
805
861
 
806
862
  export async function importAudio(filePath: string, title: string) {
@@ -823,7 +879,6 @@ export async function importAudio(filePath: string, title: string) {
823
879
  title: 'Imported transcript',
824
880
  contentText: segments.join('\n'),
825
881
  });
826
- await alt.notes.select({ noteId });
827
882
  },
828
883
  onError: err => console.error(err.code, err.message),
829
884
  },
@@ -834,7 +889,7 @@ export async function importAudio(filePath: string, title: string) {
834
889
  ### 8.4 Attach a PDF chosen via `<input type="file">`
835
890
 
836
891
  ```ts
837
- // permissions: ['files:write']
892
+ // permissions: ['files:write', 'appState:read']
838
893
  import { alt } from 'alt-plugin-sdk';
839
894
 
840
895
  document.querySelector<HTMLInputElement>('#picker')!.addEventListener(
@@ -1002,7 +1057,7 @@ The npm package follows semver:
1002
1057
  also subscribe to. Polling `getStatus()` 10× a second is wasteful when
1003
1058
  `recordingStatusChanged` exists.
1004
1059
  - **Cap your storage.** `alt.storage` is plugin-scoped but lives in the
1005
- 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
1006
1061
  `alt.files.attach`.
1007
1062
  - **Plate JSON is not exposed.** Memo/summary content is round-tripped
1008
1063
  through Alt's internal Plate-rich-text codec; the SDK gives you the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alt-plugin-sdk",
3
- "version": "0.2.2",
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)",