@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.
- package/LICENSE +21 -0
- package/README.md +315 -2
- package/pack-plugin.d.mts +4 -0
- package/pack-plugin.mjs +67 -0
- package/package.json +62 -4
- package/plugin-api.d.ts +779 -0
- package/plugin-package.d.ts +121 -0
- package/plugin-package.js +214 -0
package/plugin-api.d.ts
ADDED
|
@@ -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 {}
|