@muninmd/munin-sdk 0.0.0-stage → 1.1.0

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.
@@ -0,0 +1,779 @@
1
+ import type * as CMState from '@codemirror/state'
2
+ import type * as CMView from '@codemirror/view'
3
+ import type { Permission, PluginPlatform } from './plugin-package.js'
4
+ /**
5
+ * The plugin API, version 1 — the one surface the app supports for plugins.
6
+ *
7
+ * A plugin's entry file is an ES module whose default export is a function of
8
+ * this shape; the app calls it with a {@link PluginContext} when the person
9
+ * switches the plugin on:
10
+ *
11
+ * ```js
12
+ * export default function activate(ctx) {
13
+ * ctx.commands.register({ id: 'hello', title: 'Say hello', run: () => ctx.ui.notice('Hello') })
14
+ * }
15
+ * export function deactivate() {} // optional
16
+ * ```
17
+ *
18
+ * Everything the plugin registers goes through `ctx`, is stamped with the
19
+ * plugin's id as its owner, and is taken back when the plugin is switched off —
20
+ * so a plugin that uses only this file leaves nothing behind. Anything it
21
+ * reaches by another road (`window`, the DOM, `window.api`) is its own risk and
22
+ * may change without notice.
23
+ *
24
+ * This file is also the source of the declaration file given to authors
25
+ * (`plugins/sdk/plugin-api.d.ts`), so it imports nothing from the app.
26
+ */
27
+ /** Undoes one registration. Calling it twice is harmless. */
28
+ export type Disposer = () => void
29
+ export declare const PLUGIN_API_VERSION = 1
30
+ export interface PluginCommand {
31
+ /** Short id, unique within the plugin. The palette shows it under the plugin's
32
+ * own name, so two plugins can both have `refresh`. */
33
+ id: string
34
+ title: string
35
+ /** Optional heading to group commands under in the palette. */
36
+ section?: string
37
+ run: () => void | Promise<void>
38
+ }
39
+ export interface PluginView {
40
+ /** Short id, unique within the plugin. */
41
+ id: string
42
+ title: string
43
+ /** An SVG, as a string, for the tool stripe. Drawn with `currentColor`. */
44
+ icon: string
45
+ /**
46
+ * Fill `el` with the panel. Called when the panel opens; return a function to
47
+ * clean up when it closes. The app owns `el` — it is yours to render into, not
48
+ * to keep a reference to after the cleanup runs.
49
+ */
50
+ mount: (el: HTMLElement, instance: PluginViewInstance) => void | (() => void)
51
+ /**
52
+ * How the view opens on a phone. `sheet`: a sheet from a button in the
53
+ * library, for something glanced at. `page`: the whole screen, from the dock's
54
+ * menu, for something worked in. Without either the view has no way in on a
55
+ * phone. On a computer it is an ordinary panel in every case.
56
+ */
57
+ mobile?: 'sheet' | 'page'
58
+ /**
59
+ * The panel can be expanded over the whole workspace. The app then puts a
60
+ * control for it in the panel's header — one look and one Escape key for every
61
+ * plugin — and puts the arrangement back afterwards. Expanding is never saved: a
62
+ * restart shows the normal layout. Opening a note, or anything else that moves
63
+ * the person's attention, collapses it by itself.
64
+ *
65
+ * Only on a computer. On a phone a `page` already fills the screen.
66
+ */
67
+ expandable?: boolean
68
+ }
69
+ /**
70
+ * One open panel of a view. Two panels of the same view are two instances, and
71
+ * each keeps its own state: on a computer it is saved with the layout and comes
72
+ * back with it, on a phone it lasts while the sheet or page is open. The state
73
+ * must survive `JSON.stringify`; anything that does not is dropped.
74
+ */
75
+ export interface PluginViewInstance {
76
+ state<T = unknown>(): T | undefined
77
+ setState(value: unknown): void
78
+ /**
79
+ * Whether the panel fills the workspace right now. On a phone, true for a
80
+ * `page` and false for a `sheet`.
81
+ */
82
+ expanded(): boolean
83
+ /**
84
+ * Expand or collapse the panel, as its header control does. Does nothing — and
85
+ * warns in the plugin's log — for a view that did not declare `expandable`, and
86
+ * does nothing on a phone.
87
+ */
88
+ setExpanded(on: boolean): void
89
+ /**
90
+ * Told when the panel is expanded or collapsed, after it has its new size, so a
91
+ * measurement taken in the callback is the new one. Use it to change *what* is
92
+ * drawn (labels shown only when there is room); how big it is, a
93
+ * `ResizeObserver` reports as it does for any resize. Returns what stops the
94
+ * calls; whatever is left is dropped when the panel closes.
95
+ */
96
+ onExpandedChange(callback: (expanded: boolean) => void): Disposer
97
+ }
98
+ interface FieldBase {
99
+ /** Short id, unique within the section; also the key {@link PluginSettings.get} reads. */
100
+ id: string
101
+ label: string
102
+ hint?: string
103
+ /**
104
+ * Where the value is kept. `device` (the default) is this computer; `vault` is
105
+ * the open vault — a folder name means something only in the vault it names,
106
+ * and the value follows whichever vault is open.
107
+ */
108
+ scope?: 'device' | 'vault'
109
+ }
110
+ export interface ToggleSetting extends FieldBase {
111
+ kind: 'toggle'
112
+ default: boolean
113
+ }
114
+ export interface TextSetting extends FieldBase {
115
+ kind: 'text'
116
+ default: string
117
+ /** Why a value is not acceptable, or null when it is. */
118
+ validate?: (value: string) => string | null
119
+ /** Ready-made values that fill the field in one gesture. */
120
+ presets?: {
121
+ value: string
122
+ label?: string
123
+ }[]
124
+ }
125
+ export interface SettingOption {
126
+ id: string
127
+ label: string
128
+ }
129
+ export interface ChoiceSetting extends FieldBase {
130
+ kind: 'choice'
131
+ default: string
132
+ /**
133
+ * The choices — fixed, or worked out when needed. A function is asked again
134
+ * whenever the vault's notes change, so a list of folders keeps up with the
135
+ * folders.
136
+ */
137
+ options: SettingOption[] | (() => SettingOption[] | Promise<SettingOption[]>)
138
+ }
139
+ /** A button in the settings, for something that is not a value. */
140
+ export interface ActionSetting {
141
+ kind: 'action'
142
+ id: string
143
+ label: string
144
+ hint?: string
145
+ run: () => void | Promise<void>
146
+ }
147
+ /**
148
+ * A value the person types and nobody reads back — an access key, a token. It
149
+ * is kept as one of the plugin's {@link PluginSecrets} under the field's id, so
150
+ * `ctx.secrets.get(id)` reads it. The settings show only whether it is set; the
151
+ * person can type a new one or clear it, never see the old one.
152
+ *
153
+ * {@link PluginSettings.get} answers `''` for it, and `onChange` is told `true`
154
+ * when a value was set and `false` when it was cleared — never the value.
155
+ */
156
+ export interface SecretSetting {
157
+ kind: 'secret'
158
+ /** Short id, unique within the section; also the secret's name. */
159
+ id: string
160
+ label: string
161
+ hint?: string
162
+ placeholder?: string
163
+ }
164
+ export type SettingField =
165
+ ToggleSetting | TextSetting | ChoiceSetting | ActionSetting | SecretSetting
166
+ export interface SettingsGroupDef {
167
+ /** Short id, unique within the section. */
168
+ id: string
169
+ /** Heading over the group; leave it out for a single unlabelled group. */
170
+ label?: string
171
+ fields: SettingField[]
172
+ }
173
+ export interface SettingsSectionDef {
174
+ /** Short id, unique within the plugin. */
175
+ id: string
176
+ title: string
177
+ /** The fields, when there is one group of them. */
178
+ fields?: SettingField[]
179
+ /** Several groups, each under its own heading. Used instead of `fields`. */
180
+ groups?: SettingsGroupDef[]
181
+ /**
182
+ * Draw the section yourself, for settings that are not a list of values — a
183
+ * list of connections, say. Used instead of `fields` and `groups`. Called when
184
+ * the section is shown; return a function to clean up when it is closed. As
185
+ * with a panel, `el` is the app's: render into it, do not keep it.
186
+ *
187
+ * Draw with the app's colours (`var(--text)`, `var(--text2)`, `var(--island)`,
188
+ * `var(--border-soft)`, `var(--accent)` …) so the section looks like its
189
+ * neighbours in both themes.
190
+ */
191
+ mount?: (el: HTMLElement) => void | (() => void)
192
+ }
193
+ export interface PluginSettings {
194
+ /** Add a section to the settings, after the built-in ones. */
195
+ register(section: SettingsSectionDef): Disposer
196
+ /** The current value of a field, or its default. */
197
+ get<T extends boolean | string = boolean | string>(fieldId: string): T
198
+ /** Change a field's value from code — to select the folder a plugin has just
199
+ * created, say. Runs the field's change handlers like a change by the person. */
200
+ set(fieldId: string, value: boolean | string): void
201
+ /** Called after the person changes a field. */
202
+ onChange(fieldId: string, handler: (value: boolean | string) => void): Disposer
203
+ /**
204
+ * Show the settings at one of this plugin's sections — the first one it
205
+ * registered when `sectionId` is left out. On a computer the settings window
206
+ * opens on it; on a phone the plugin's settings page does, and back returns
207
+ * to where the person was.
208
+ */
209
+ open(sectionId?: string): void
210
+ }
211
+ export interface PluginTheme {
212
+ id: string
213
+ name: string
214
+ base: 'dark' | 'light'
215
+ /** CSS custom properties, e.g. `{ '--canvas': '#242933' }`. */
216
+ tokens?: Record<string, string>
217
+ }
218
+ /** What the app publishes. Anything else is a plugin's own event, untyped. */
219
+ export interface PluginEvents {
220
+ 'file-open': {
221
+ path: string
222
+ }
223
+ 'active-note-change': {
224
+ path: string | null
225
+ }
226
+ 'metadata-updated': void
227
+ 'tree-changed': void
228
+ /** A note or folder appeared, changed, moved or went. `origin` is `app` when
229
+ * this app did it (the person, or a plugin), `external` when another program
230
+ * or a sync client did. A full reread of the vault gives no per-file event. */
231
+ 'file-created': PluginFileEvent
232
+ 'file-modified': PluginFileEvent
233
+ /** Renamed or moved; `oldPath` is where it was. */
234
+ 'file-renamed': PluginFileEvent & {
235
+ oldPath: string
236
+ }
237
+ 'file-deleted': PluginFileEvent
238
+ }
239
+ export interface PluginFileEvent {
240
+ path: string
241
+ /** Whether it is a folder; absent when the app could not tell — something
242
+ * another program did that it has not looked at yet. */
243
+ folder?: boolean
244
+ origin: 'app' | 'external'
245
+ }
246
+ export interface PluginVault {
247
+ /** The open note's path (vault-relative, `/`-separated), or null. */
248
+ activePath(): string | null
249
+ /** Every note's path. Needs `vault:read`. */
250
+ list(): Promise<string[]>
251
+ /** A note's text — from its editor when it is open, unsaved typing
252
+ * included. Needs `vault:read`. */
253
+ read(path: string): Promise<string>
254
+ /** Replace a note's text, or create it. An open note changes in its editor
255
+ * (undoable, saved like typing); any other is written to disk. Needs
256
+ * `vault:write`. */
257
+ write(path: string, text: string): Promise<void>
258
+ /** Open a note in the active pane. */
259
+ open(path: string): void
260
+ /** Every folder in the vault, vault-relative. Needs `vault:read`. */
261
+ folders(): Promise<string[]>
262
+ /**
263
+ * The project a note belongs to, when there is one: in a workspace every
264
+ * note's path starts with `@<id>/`, the project's; a project opened by itself
265
+ * holds all of its notes. Null for a plain folder. Needs `vault:read`.
266
+ */
267
+ projectOf(path: string): Promise<{
268
+ id: string
269
+ name: string
270
+ } | null>
271
+ /** Whether a note is there, asked of the disk and not of a list that may lag
272
+ * behind it. Needs `vault:read`. */
273
+ exists(path: string): Promise<boolean>
274
+ /**
275
+ * Create a note in `folder` ('' is the vault root) named `name`, with `content`,
276
+ * and return its path. If the name is taken the note gets the next free one —
277
+ * so the returned path, not the requested one, is where it is. Needs `vault:write`.
278
+ */
279
+ createNote(folder: string, name: string, content?: string): Promise<string>
280
+ /** Create a folder and return its path. Needs `vault:write`. */
281
+ createFolder(parent: string, name: string): Promise<string>
282
+ /**
283
+ * Rename a note (`newName` without `.md`) or a folder, in place, and return
284
+ * the new path. Links to it are rewritten and open tabs follow it, as when
285
+ * the person renames it. Needs `vault:write`.
286
+ */
287
+ rename(path: string, newName: string): Promise<string>
288
+ /** Move a note or folder into `folder` ('' is the root); returns the new
289
+ * path. Needs `vault:write`. */
290
+ move(path: string, folder: string): Promise<string>
291
+ /** Delete a note or a folder with everything in it. Asks nothing — the person
292
+ * agreed to `vault:write` when switching the plugin on. Tabs showing it close. */
293
+ delete(path: string): Promise<void>
294
+ /** Any file of the vault as bytes, with its media type. Needs `vault:read`. */
295
+ readBinary(path: string): Promise<{
296
+ data: Uint8Array
297
+ mime: string
298
+ }>
299
+ /**
300
+ * Save a file into the vault's attachments folder, named by the vault's rule
301
+ * the way a pasted image is; returns its path, ready for `![[…]]`. Needs
302
+ * `vault:write`.
303
+ */
304
+ saveAttachment(name: string, data: Uint8Array): Promise<string>
305
+ /** What the plugin keeps about the open vault, inside that vault. Unlike
306
+ * {@link PluginContext.data}, it differs between vaults and follows the open one. */
307
+ readonly data: {
308
+ load<T = unknown>(): Promise<T | undefined>
309
+ save(value: unknown): Promise<void>
310
+ }
311
+ }
312
+ /**
313
+ * A link to a note — `[[link]]` or `[text](path.md)`: what it says, and where in
314
+ * the text it stands.
315
+ */
316
+ export interface PluginLink {
317
+ /**
318
+ * What it names: for `[[…]]` the name or path inside the brackets without
319
+ * `#heading` and `|alias` (`projects/plan`); for a markdown link the path as
320
+ * written, decoded, from the note it is in (`../projects/plan.md`).
321
+ */
322
+ target: string
323
+ /** Offset of the link's first character (`[[`, `[`, or an embed's `!`). */
324
+ from: number
325
+ /** Offset just after the link. */
326
+ to: number
327
+ }
328
+ /** What the index knows about one note. */
329
+ export interface PluginNoteMeta {
330
+ path: string
331
+ title: string
332
+ links: PluginLink[]
333
+ /** Without the `#`. Both `#inline` tags and frontmatter `tags:`. */
334
+ tags: string[]
335
+ aliases: string[]
336
+ headings: {
337
+ level: number
338
+ text: string
339
+ }[]
340
+ /** The frontmatter, as scalars and lists of strings. */
341
+ properties: Record<string, string | string[]>
342
+ }
343
+ export interface PluginBacklink {
344
+ path: string
345
+ title: string
346
+ /** Text around the mention. */
347
+ snippet: string
348
+ }
349
+ export type PluginResolveResult =
350
+ | {
351
+ status: 'ok'
352
+ path: string
353
+ }
354
+ | {
355
+ status: 'missing'
356
+ }
357
+ | {
358
+ status: 'ambiguous'
359
+ candidates: string[]
360
+ }
361
+ /** `[[id:…]]` names a project that is not open here. */
362
+ | {
363
+ status: 'unavailable'
364
+ project: string
365
+ }
366
+ export interface PluginGraph {
367
+ /** `id` is the note's path; `current` marks the note the graph was asked about. */
368
+ nodes: {
369
+ id: string
370
+ title: string
371
+ current: boolean
372
+ }[]
373
+ edges: {
374
+ source: string
375
+ target: string
376
+ }[]
377
+ }
378
+ /**
379
+ * The app's index of what notes mean — read only. Everything here needs
380
+ * `vault:read`. The answers come from the same index the app's own panels read,
381
+ * so they agree with what the person sees; after `metadata-updated` they reflect
382
+ * the change.
383
+ */
384
+ export interface PluginMetadata {
385
+ /** A note's metadata, or null when there is no such note. */
386
+ note(path: string): Promise<PluginNoteMeta | null>
387
+ /** The notes linking to `path`. */
388
+ backlinks(path: string): Promise<PluginBacklink[]>
389
+ /**
390
+ * Which note a link's text leads to, by the app's own rules. `from` is the
391
+ * note the link is written in; in a workspace a bare name is looked for in
392
+ * its project — without it, in every project.
393
+ */
394
+ resolve(target: string, from?: string): Promise<PluginResolveResult>
395
+ /** The note, the notes it links to and from, and the links among them. */
396
+ graph(path: string): Promise<PluginGraph>
397
+ /** Every note and resolved link; `current`, when given, is marked. */
398
+ vaultGraph(current?: string): Promise<PluginGraph>
399
+ /** Every tag, with how many notes carry it. */
400
+ tags(): Promise<
401
+ {
402
+ tag: string
403
+ count: number
404
+ }[]
405
+ >
406
+ notesWithTag(tag: string): Promise<
407
+ {
408
+ path: string
409
+ title: string
410
+ }[]
411
+ >
412
+ }
413
+ export interface PluginTemplates {
414
+ /** The vault's templates. Needs `vault:read`. */
415
+ list(): {
416
+ path: string
417
+ name: string
418
+ }[]
419
+ /** A template's text with its variables filled in for a note at `intoPath`.
420
+ * Needs `vault:read`. */
421
+ render(templatePath: string, intoPath: string): Promise<string>
422
+ }
423
+ /** Dates as this app reads and writes them. */
424
+ export interface PluginDates {
425
+ /** Tokens `YYYY MM DD dddd LL…`; with no format, the one the person chose for
426
+ * showing dates. The language is the interface's. */
427
+ format(date: Date, format?: string): string
428
+ /** The date a text holds under `format`, or null. */
429
+ parse(text: string, format: string): Date | null
430
+ /** Whether a date written in `format` can be read back — the test for a format
431
+ * used in file names. */
432
+ isParsableFormat(format: string): boolean
433
+ /** The format the person chose for showing dates. */
434
+ appFormat(): string
435
+ /** Which day a week starts on, numbered as `Date.getDay()` (0 is Sunday). */
436
+ weekStart(): number
437
+ /** Weekday names in the interface language, already rotated to start the week
438
+ * on `weekStart()`. */
439
+ weekdayNames(style?: 'short' | 'long'): string[]
440
+ }
441
+ /**
442
+ * The note open in the active pane, as its editor has it — unsaved typing
443
+ * included. Positions are offsets into the text, as in {@link PluginLink}.
444
+ *
445
+ * Each call looks the note up again, so a handle kept after its tab was closed
446
+ * answers with an error rather than with text that is no longer on screen.
447
+ */
448
+ export interface PluginActiveEditor {
449
+ readonly path: string
450
+ /** The text in the editor. Needs `vault:read`. */
451
+ text(): string
452
+ /** The main selection; `from` = `to` for a bare caret. Needs `vault:read`. */
453
+ selection(): {
454
+ from: number
455
+ to: number
456
+ text: string
457
+ }
458
+ /** Insert at the caret, replacing the selection. Needs `vault:write`. */
459
+ insert(text: string): void
460
+ /** Replace `from`–`to`. Needs `vault:write`. */
461
+ replace(from: number, to: number, text: string): void
462
+ /** Move the selection (`head` defaults to `anchor`). */
463
+ select(anchor: number, head?: number): void
464
+ /** True in reading mode, where edits are refused. */
465
+ readonly readOnly: boolean
466
+ }
467
+ export interface PluginEditor {
468
+ /**
469
+ * Add a CodeMirror extension to every editor, open now or opened later.
470
+ * Build it from {@link PluginEditor.libs} — the app's own copies — and not from
471
+ * your own `@codemirror/*` import: two copies of the library in one window do
472
+ * not recognise each other's objects.
473
+ */
474
+ extension(ext: CMState.Extension): Disposer
475
+ /** The note being edited now, or null when none is open. Every edit goes
476
+ * through the editor: it can be undone and is saved like typing. */
477
+ active(): PluginActiveEditor | null
478
+ readonly libs: {
479
+ readonly state: typeof CMState
480
+ readonly view: typeof CMView
481
+ }
482
+ }
483
+ export interface PluginResponse {
484
+ readonly ok: boolean
485
+ readonly status: number
486
+ readonly statusText: string
487
+ readonly headers: Record<string, string>
488
+ text(): Promise<string>
489
+ json(): Promise<unknown>
490
+ /** The body as it came, byte for byte — for pictures and other files. */
491
+ bytes(): Promise<Uint8Array>
492
+ }
493
+ export interface PluginRequestInit {
494
+ method?: string
495
+ headers?: Record<string, string>
496
+ body?: string
497
+ /**
498
+ * Cancels the request. Waiting for the answer, or for its next part, then
499
+ * fails with an error named `AbortError`, and the connection is closed.
500
+ */
501
+ signal?: AbortSignal
502
+ /**
503
+ * How long to wait, in milliseconds, without anything arriving — for the
504
+ * answer to begin, or for its next part — before giving up with an error
505
+ * named `TimeoutError`. A long answer that keeps arriving is never cut off.
506
+ * By default 60 s for `fetch` and 120 s for `stream`.
507
+ */
508
+ timeoutMs?: number
509
+ }
510
+ /**
511
+ * An answer that is read as it arrives. The promise that gives it resolves as
512
+ * soon as the status and headers are in, before any of the body.
513
+ *
514
+ * The body is read once, by one of `chunks`, `lines` or `text`; a second read is
515
+ * an error. Leaving `chunks` or `lines` early (a `break`) closes the connection.
516
+ */
517
+ export interface PluginStreamResponse {
518
+ readonly ok: boolean
519
+ readonly status: number
520
+ readonly statusText: string
521
+ readonly headers: Record<string, string>
522
+ /** The body in the parts it arrives in. */
523
+ chunks(): AsyncIterable<Uint8Array>
524
+ /**
525
+ * The body as UTF-8 lines, without their `\n` or `\r\n` — what server-sent
526
+ * events and newline-delimited JSON are read with. A character split between
527
+ * two parts arrives whole.
528
+ */
529
+ lines(): AsyncIterable<string>
530
+ /** The rest of the body as text — for an error's explanation, say. */
531
+ text(): Promise<string>
532
+ }
533
+ export interface PluginNet {
534
+ /** Needs `network`. The body is read whole, and an answer over 10 MB fails as
535
+ * soon as it passes that size. Only `http:` and `https:`. */
536
+ fetch(url: string, init?: PluginRequestInit): Promise<PluginResponse>
537
+ /**
538
+ * Needs `network`. The same request as {@link fetch}, with the body handed
539
+ * over as it arrives — for an answer that is written while it is sent, like a
540
+ * language model's. The 10 MB limit is counted as it goes.
541
+ *
542
+ * The request is made outside the window — by the app on a computer, by the
543
+ * system on a phone — so a server's CORS rules do not apply, and `http:`
544
+ * reaches machines on the local network on both.
545
+ */
546
+ stream(url: string, init?: PluginRequestInit): Promise<PluginStreamResponse>
547
+ }
548
+ /**
549
+ * Values the plugin must keep apart from its data: access keys, tokens. On a
550
+ * computer, a file only this user can read (0600, outside the plugin's own
551
+ * folder, no keychain prompt); on a phone, encrypted by the Android keystore.
552
+ * The plugin's alone. They survive switching off and updating, and go when the
553
+ * plugin is removed with its data.
554
+ *
555
+ * On a phone without keystore encryption, `set` fails rather than keep the
556
+ * value in the clear.
557
+ */
558
+ export interface PluginSecrets {
559
+ /** The value, or null when none is set. Names: letters, digits, `. _ : -`. */
560
+ get(name: string): Promise<string | null>
561
+ set(name: string, value: string): Promise<void>
562
+ delete(name: string): Promise<void>
563
+ }
564
+ /**
565
+ * The plugin's own storage, by key — for many records that change one at a
566
+ * time, where {@link PluginContext.data} would rewrite all of them on every
567
+ * save. On this device, apart from the notes; kept and removed with the
568
+ * plugin's data.
569
+ *
570
+ * A key is 1–128 letters, digits, `.`, `_` or `-`. A value must survive
571
+ * `JSON.stringify` and be at most 5 MB written out; a save is whole — after a
572
+ * crash the key holds the old value or the new one.
573
+ */
574
+ export interface PluginKeyedStore {
575
+ get<T = unknown>(key: string): Promise<T | undefined>
576
+ set(key: string, value: unknown): Promise<void>
577
+ delete(key: string): Promise<void>
578
+ /** The keys there are, or only those that start with `prefix`. */
579
+ keys(prefix?: string): Promise<string[]>
580
+ }
581
+ export interface PluginSearchResult {
582
+ path: string
583
+ title: string
584
+ /** Text around what matched. */
585
+ snippet: string
586
+ }
587
+ export interface PluginSearch {
588
+ /**
589
+ * The notes that match `query`, best first: the app's own search, with its
590
+ * operators and its order. `limit` defaults to 20 and is at most 200. Needs
591
+ * `vault:read`.
592
+ */
593
+ query(
594
+ query: string,
595
+ options?: {
596
+ limit?: number
597
+ }
598
+ ): Promise<PluginSearchResult[]>
599
+ }
600
+ /** Where the app's built-in MCP server is, and the token that opens it. */
601
+ export interface PluginMcpEndpoint {
602
+ /** The server's address, `http://127.0.0.1:<port>/mcp`. */
603
+ url: string
604
+ /** Sent as `Authorization: Bearer <token>`. Treat it as a secret: it reads the person's notes. */
605
+ token: string
606
+ }
607
+ export interface PluginApp {
608
+ /**
609
+ * The address and token of the app's own MCP server, to connect it as one of a
610
+ * client's servers — or null when the server is off, has no token yet, or this
611
+ * is a phone. Asked again each time, so it follows the person turning the
612
+ * server off or making a new token. Needs `network`.
613
+ */
614
+ mcp(): Promise<PluginMcpEndpoint | null>
615
+ }
616
+ export interface PluginContext {
617
+ readonly id: string
618
+ readonly platform: PluginPlatform
619
+ readonly apiVersion: typeof PLUGIN_API_VERSION
620
+ readonly appVersion: string
621
+ /** The permissions the person agreed to — what the plugin's manifest declared. */
622
+ readonly permissions: readonly Permission[]
623
+ /** True when this activation is part of the app starting up, false when the
624
+ * person switched the plugin on while the app was running. */
625
+ readonly startup: boolean
626
+ readonly commands: {
627
+ register(command: PluginCommand): Disposer
628
+ }
629
+ readonly views: {
630
+ register(view: PluginView): Disposer
631
+ /** Show one of this plugin's views: as a panel on a computer (focusing one
632
+ * that is already open), and on a phone as its sheet or page. */
633
+ open(id: string): void
634
+ }
635
+ readonly settings: PluginSettings
636
+ readonly themes: {
637
+ register(theme: PluginTheme): Disposer
638
+ }
639
+ /**
640
+ * Entries for the app's context menus, after the app's own. `when` decides,
641
+ * each time a menu opens, whether the entry applies; keep it quick — it runs
642
+ * while the menu is being drawn.
643
+ */
644
+ readonly menus: {
645
+ /** A note or folder: the file tree and a tab on a computer, the long-press
646
+ * actions on a phone. */
647
+ readonly file: {
648
+ add(entry: PluginMenuEntry<PluginFileTarget>): Disposer
649
+ }
650
+ /** A right click in a note's text, on a computer. */
651
+ readonly editor: {
652
+ add(entry: PluginMenuEntry<PluginEditorTarget>): Disposer
653
+ }
654
+ }
655
+ /** Items in the status bar, on a computer. On a phone `add` works and shows
656
+ * nothing: there is no status bar. */
657
+ readonly statusBar: {
658
+ add(item: { id: string; text: string; title?: string; onClick?: () => void }): {
659
+ set(patch: { text?: string; title?: string }): void
660
+ remove(): void
661
+ }
662
+ }
663
+ readonly keys: {
664
+ /**
665
+ * Give one of this plugin's commands a shortcut: `Mod+Shift+D` (`Mod` is ⌘
666
+ * on macOS and Ctrl elsewhere), or several. A combination the app or another
667
+ * plugin already holds stays theirs; the log says so.
668
+ */
669
+ bind(commandId: string, combo: string | string[]): Disposer
670
+ }
671
+ readonly events: {
672
+ on<K extends keyof PluginEvents>(
673
+ event: K,
674
+ handler: (payload: PluginEvents[K]) => void
675
+ ): Disposer
676
+ on(event: string, handler: (payload: unknown) => void): Disposer
677
+ }
678
+ readonly vault: PluginVault
679
+ readonly metadata: PluginMetadata
680
+ readonly templates: PluginTemplates
681
+ readonly dates: PluginDates
682
+ readonly editor: PluginEditor
683
+ readonly net: PluginNet
684
+ readonly secrets: PluginSecrets
685
+ readonly search: PluginSearch
686
+ readonly app: PluginApp
687
+ /** The plugin's own storage, on this device, apart from the notes. Survives
688
+ * switching off, updating and — if the person asks — removing. */
689
+ readonly data: {
690
+ load<T = unknown>(): Promise<T | undefined>
691
+ save(value: unknown): Promise<void>
692
+ }
693
+ /** Storage by key, beside {@link data}; see {@link PluginKeyedStore}. */
694
+ readonly store: PluginKeyedStore
695
+ readonly ui: {
696
+ notice(message: string, kind?: 'info' | 'error'): void
697
+ /** The interface language code (`en`, `ru`, …). */
698
+ language(): string
699
+ /** Ask the person for a line of text. Resolves with it, or null if they cancel. */
700
+ prompt(options: {
701
+ title: string
702
+ initial?: string
703
+ placeholder?: string
704
+ confirmLabel?: string
705
+ /** Why a value is not acceptable, or null when it is. */
706
+ validate?: (value: string) => string | null
707
+ }): Promise<string | null>
708
+ /**
709
+ * Offer a list to choose from, narrowed as the person types (by the label,
710
+ * then the detail). Resolves to the item chosen — the very object passed
711
+ * in, extra fields and all — or null when the list is closed.
712
+ */
713
+ pick<T extends PluginPickItem>(options: { items: T[]; placeholder?: string }): Promise<T | null>
714
+ }
715
+ /**
716
+ * Publish an API for the plugins that depend on this one. `factory` is called
717
+ * once for each of them, with who is asking; what a dependent registers through
718
+ * `client.register` is undone when *it* is switched off, so the provider never
719
+ * has to follow its dependents' lives. Call it once, while activating.
720
+ */
721
+ provide(factory: (client: PluginClient) => unknown): void
722
+ readonly plugins: {
723
+ /**
724
+ * The API a plugin this one depends on published. Only for plugins named in
725
+ * the manifest's `dependencies` — they are guaranteed to be running first;
726
+ * anything else is undefined. Undefined, too, when the plugin published
727
+ * nothing. The type is the provider's to document.
728
+ */
729
+ get<T = unknown>(id: string): T | undefined
730
+ }
731
+ /** Hand over anything of yours that needs undoing; it runs when the plugin is
732
+ * switched off, after the registrations above are taken back. */
733
+ register(disposer: Disposer): void
734
+ readonly log: {
735
+ info(...args: unknown[]): void
736
+ warn(...args: unknown[]): void
737
+ error(...args: unknown[]): void
738
+ }
739
+ }
740
+ export interface PluginFileTarget {
741
+ path: string
742
+ isFolder: boolean
743
+ }
744
+ export interface PluginEditorTarget {
745
+ path: string
746
+ /** The selection, as offsets; `from` = `to` for a bare caret. */
747
+ from: number
748
+ to: number
749
+ text: string
750
+ }
751
+ export interface PluginMenuEntry<T> {
752
+ /** Short id, unique within the plugin. */
753
+ id: string
754
+ title: string
755
+ when?: (target: T) => boolean
756
+ run: (target: T) => void | Promise<void>
757
+ }
758
+ /** An entry of {@link PluginContext.ui.pick}'s list. */
759
+ export interface PluginPickItem {
760
+ label: string
761
+ /** A second, quieter line. */
762
+ detail?: string
763
+ }
764
+ /** The plugin asking for a provider's API. */
765
+ export interface PluginClient {
766
+ readonly id: string
767
+ /** Undo `disposer` when this client is switched off, updated or removed. */
768
+ register(disposer: Disposer): void
769
+ /** Write to the client's own log — for a failure that is the client's doing,
770
+ * so its author finds it where they look. */
771
+ log(level: 'info' | 'warn' | 'error', ...args: unknown[]): void
772
+ }
773
+ /** What a plugin's entry module exports. */
774
+ export interface PluginModule {
775
+ default?: (ctx: PluginContext) => void | Promise<void>
776
+ /** Called when the plugin is switched off, before its registrations go. */
777
+ deactivate?: () => void | Promise<void>
778
+ }
779
+ export {}