@notegen/plugin-api 0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NoteGen contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # `@notegen/plugin-api`
2
+
3
+ UI lifecycle: forms retain values by field ID until removed, closed, or explicitly
4
+ reset with a new `resetKey`. `ui.openDialog(options)` returns `{ id }`;
5
+ `ui.closeDialog(id)` only closes that instance. To replace an existing dialog,
6
+ pass its ID as `replaceId`. Another plugin's dialog cannot be replaced.
7
+ `ui.onDidCloseDialog` reports `{ id, reason }`. Form commands inside dialogs
8
+ also receive `dialogId` alongside `formId` and `values`; capture it before
9
+ awaiting work so a stale submission cannot close a newer dialog.
10
+
11
+ The public TypeScript contract for NoteGen plugins.
12
+
13
+ ```bash
14
+ pnpm add -D @notegen/plugin-api
15
+ ```
16
+
17
+ Use type-only imports whenever possible:
18
+
19
+ ```ts
20
+ import type { PluginActivate } from '@notegen/plugin-api'
21
+
22
+ export const activate: PluginActivate = async (context) => {
23
+ context.commands.handle('com.example.hello.open', async () => {
24
+ await context.ui.showNotice('Hello from NoteGen')
25
+ })
26
+ }
27
+ ```
28
+
29
+ The package contains manifest, permission, contribution, lifecycle, host-context,
30
+ and stable error types. It has no runtime dependencies and does not require DOM
31
+ types.
32
+
33
+ Values that cross the runtime boundary use `PluginJsonValue`. Command arguments
34
+ and results, storage values, and declarative UI action arguments therefore accept
35
+ only finite JSON-compatible data. Convert class instances such as `Date`, maps,
36
+ sets, and custom errors to plain objects before passing them to the host.
37
+ `undefined` is allowed only at the outer command boundary to mean “no argument”
38
+ or “no result,” and as the missing-key result of storage `get`; it cannot appear
39
+ inside a stored, returned, or action-argument JSON value.
40
+
41
+ The package also publishes an editor-oriented manifest schema at
42
+ `@notegen/plugin-api/plugin-manifest-v1.schema.json`. The schema catches shape
43
+ errors while editing; `notegen-plugin validate` remains authoritative for
44
+ namespace ownership, localization files, API/app compatibility, and packaged
45
+ file checks.
46
+
47
+ API 0.1.0 covers commands, settings, storage, calendar resolution, active-editor
48
+ snapshots and revision-checked edits, note listing/read/write/move/delete and
49
+ change events, declarative sidebars/dialogs, and text-only requests to exact
50
+ user-approved HTTPS origins. Every sensitive operation is permission scoped.
51
+
52
+ API 0.1.0 adds declarative forms (text, textarea, number, select, checkbox), tables,
53
+ trees, editor-area plugin tabs, view visibility events and close/focus methods.
54
+ Forms submit `{ formId, values }` to a declared command; its result may contain
55
+ `fieldErrors` and `message`. The UI remains host-rendered without DOM access.
56
+ `editor.applyEdits` and `editor.setSelection` use UTF-16 Markdown offsets and
57
+ currently require source mode. Batch edits cannot overlap and form one undo step.
58
+ `notes.search` searches saved Markdown within both list and read grants (up to
59
+ 200 files, 16 MiB scanned, 100 matching lines); inspect `truncated`.
60
+ `attachments.read/create` require separate grants, accept PNG/JPEG/GIF/WebP,
61
+ PDF/TXT/CSV, and transport standard Base64 up to 1 MiB decoded. Creation never
62
+ overwrites an existing file. There is no attachment delete, arbitrary-file API,
63
+ or automatic preview/open operation.
64
+
65
+ File mutations (`notes.write`, `notes.move`, and `notes.delete`) run only from
66
+ the main window and require every affected note to be closed in all tabs,
67
+ panes, and separate editor windows. Otherwise they return `EditorBusy`.
68
+ Use `editor.applyEdit` for the active document. The host drains pending saves
69
+ before checking a file's `expectedRevision`; re-read after a stale-revision
70
+ error. If a mutation error includes `details.committed: true`, the disk change
71
+ completed and only its UI reconciliation failed. Read the file state before
72
+ retrying.
73
+
74
+ The current community runtime loads one self-contained ESM entry. Type-only
75
+ imports disappear during compilation. Any value import or other dependency must
76
+ be bundled into that entry by the authoring build.
77
+
78
+ See the [NoteGen documentation](https://notegen.top) for the complete plugin
79
+ development guide.
80
+
81
+ ## Local diagnostics and folder permission bindings
82
+
83
+ `context.log.info/warning/error(message)` writes local diagnostics. The QuickJS
84
+ host truncates messages to 1,000 characters and drops entries beyond 50 per
85
+ 10 seconds. Use `error.stack` explicitly when a stack trace helps; do not log
86
+ credentials or note contents. The developer panel can export its filtered log.
87
+ The in-process test host records these calls but does not emulate the quota.
88
+
89
+ One `string` setting with `scope: "workspace"` may declare
90
+ `permissionPaths: ["notes.create", "notes.open"]`. Each listed permission must
91
+ be declared, required, unique and use `workspace-folder` scope (at most 20).
92
+ The permission dialog uses the setting's fixed folder prefix, lets the user
93
+ choose another folder, and preserves `{{...}}` date subfolders. This is a UI
94
+ suggestion only: permissions still require explicit user approval. Plugins
95
+ without this declaration use the normal per-permission inputs.
96
+
97
+ Production KV storage now follows the installed package content fingerprint.
98
+ A new package starts with a copy of the current package's data; rollback returns
99
+ to the old data branch. Reinstalling an existing fingerprint reuses its branch.
100
+ This does not roll back note writes, attachments, settings or remote effects.
101
+ Use explicit data schema versions and idempotent migration steps. The test host
102
+ does not simulate package installation or versioned storage.
@@ -0,0 +1,617 @@
1
+ /** The public API version implemented by this release of NoteGen. */
2
+ export declare const PLUGIN_API_VERSION: "0.1.0";
3
+ export type PluginPlatform = 'desktop' | 'ios' | 'android';
4
+ export type PluginActivationEvent = `onCommand:${string}` | 'onEditor:markdown' | 'onWorkspace:open' | 'onNotes:change';
5
+ export type PluginPermissionScope = 'active-editor' | 'workspace-file' | 'workspace-files' | 'workspace-folder' | 'network-origins';
6
+ export interface PluginPermissionDeclaration<Scope extends PluginPermissionScope = PluginPermissionScope> {
7
+ scope: Scope;
8
+ optional?: boolean;
9
+ description?: string;
10
+ }
11
+ export interface PluginPermissionDeclarations {
12
+ 'editor.read'?: PluginPermissionDeclaration<'active-editor'>;
13
+ 'editor.write'?: PluginPermissionDeclaration<'active-editor'>;
14
+ 'notes.read'?: PluginPermissionDeclaration<'workspace-file' | 'workspace-files' | 'workspace-folder'>;
15
+ 'notes.create'?: PluginPermissionDeclaration<'workspace-folder'>;
16
+ 'notes.open'?: PluginPermissionDeclaration<'workspace-folder'>;
17
+ 'notes.list'?: PluginPermissionDeclaration<'workspace-folder'>;
18
+ 'notes.write'?: PluginPermissionDeclaration<'workspace-file' | 'workspace-files' | 'workspace-folder'>;
19
+ 'notes.delete'?: PluginPermissionDeclaration<'workspace-file' | 'workspace-files' | 'workspace-folder'>;
20
+ 'notes.move'?: PluginPermissionDeclaration<'workspace-folder'>;
21
+ 'network.fetch'?: PluginPermissionDeclaration<'network-origins'>;
22
+ 'attachments.read'?: PluginPermissionDeclaration<'workspace-file' | 'workspace-files' | 'workspace-folder'>;
23
+ 'attachments.create'?: PluginPermissionDeclaration<'workspace-folder'>;
24
+ }
25
+ export type PluginPermissionName = keyof PluginPermissionDeclarations;
26
+ export interface PluginCommandContribution {
27
+ id: string;
28
+ title: string;
29
+ description?: string;
30
+ icon?: string;
31
+ suggestedShortcut?: string;
32
+ }
33
+ export interface PluginSettingOption {
34
+ readonly label: string;
35
+ readonly value: string;
36
+ }
37
+ export type PluginSettingContribution = {
38
+ key: string;
39
+ type: 'boolean';
40
+ scope: 'device' | 'workspace';
41
+ title: string;
42
+ description?: string;
43
+ default: boolean;
44
+ } | {
45
+ key: string;
46
+ type: 'string';
47
+ /** One workspace folder template may bind required workspace-folder grants. */
48
+ permissionPaths?: readonly PluginPermissionName[];
49
+ scope: 'device' | 'workspace';
50
+ title: string;
51
+ description?: string;
52
+ default: string;
53
+ placeholder?: string;
54
+ /** Maximum UTF-8 byte length accepted by the host. */
55
+ maxLength?: number;
56
+ } | {
57
+ key: string;
58
+ type: 'number';
59
+ scope: 'device' | 'workspace';
60
+ title: string;
61
+ description?: string;
62
+ default: number;
63
+ min?: number;
64
+ max?: number;
65
+ step?: number;
66
+ } | {
67
+ key: string;
68
+ type: 'select';
69
+ scope: 'device' | 'workspace';
70
+ title: string;
71
+ description?: string;
72
+ default: string;
73
+ options: readonly PluginSettingOption[];
74
+ } | {
75
+ key: string;
76
+ type: 'workspace-file' | 'workspace-folder';
77
+ scope: 'workspace';
78
+ title: string;
79
+ description?: string;
80
+ default: string;
81
+ };
82
+ export interface PluginStatusBarContribution {
83
+ id: string;
84
+ alignment: 'left' | 'right';
85
+ priority?: number;
86
+ command?: string;
87
+ }
88
+ export interface PluginViewContribution {
89
+ id: string;
90
+ title: string;
91
+ location: 'left-sidebar' | 'right-sidebar' | 'editor-tab';
92
+ icon?: string;
93
+ }
94
+ export type PluginMenuLocation = 'editor/slash' | 'editor/context' | 'file/context' | 'mobile/writing/overflow';
95
+ export interface PluginMenuContribution {
96
+ location: PluginMenuLocation;
97
+ command: string;
98
+ when?: string;
99
+ group?: string;
100
+ }
101
+ export interface PluginContributions {
102
+ commands?: readonly PluginCommandContribution[];
103
+ settings?: readonly PluginSettingContribution[];
104
+ statusBar?: readonly PluginStatusBarContribution[];
105
+ menus?: readonly PluginMenuContribution[];
106
+ views?: readonly PluginViewContribution[];
107
+ }
108
+ export interface PluginAuthor {
109
+ name: string;
110
+ url?: string;
111
+ }
112
+ export interface PluginManifestV1 {
113
+ manifestVersion: 1;
114
+ id: string;
115
+ name: string;
116
+ description?: string;
117
+ version: string;
118
+ apiVersion: string;
119
+ minAppVersion: string;
120
+ platforms: readonly PluginPlatform[];
121
+ entry: string;
122
+ activationEvents: readonly PluginActivationEvent[];
123
+ permissions: Readonly<PluginPermissionDeclarations>;
124
+ contributes: PluginContributions;
125
+ defaultLocale?: string;
126
+ locales?: Readonly<Record<string, string>>;
127
+ author?: PluginAuthor;
128
+ repository?: string;
129
+ license?: string;
130
+ }
131
+ export type PluginSettingValue = string | number | boolean;
132
+ /**
133
+ * Values that can cross the NoteGen plugin boundary without losing meaning.
134
+ *
135
+ * Plugin commands, storage, and declarative UI are transported as JSON. Class
136
+ * instances, functions, symbols, bigint values, `undefined` inside containers,
137
+ * and cyclic objects are therefore intentionally excluded from this type.
138
+ */
139
+ export type PluginJsonValue = null | boolean | number | string | readonly PluginJsonValue[] | {
140
+ readonly [key: string]: PluginJsonValue;
141
+ };
142
+ export type PluginCommandArgument = PluginJsonValue | undefined;
143
+ export type PluginCommandResult = PluginJsonValue | void;
144
+ export interface WorkspaceInfo {
145
+ id: string;
146
+ name: string;
147
+ }
148
+ export interface ResolveDayOptions {
149
+ timeZone: 'system' | string;
150
+ dayStartsAt: string;
151
+ }
152
+ export interface ResolvedDay {
153
+ instant: string;
154
+ logicalDate: string;
155
+ timeZone: string;
156
+ localDateTime: string;
157
+ }
158
+ export interface NoteSnapshot {
159
+ id: string;
160
+ path: string;
161
+ revision: number;
162
+ content: string;
163
+ }
164
+ export interface ReadNoteOptions {
165
+ path: string;
166
+ }
167
+ export interface OpenOrCreateNoteOptions {
168
+ workspaceId: string;
169
+ path: string;
170
+ initialContent: string;
171
+ conflict: 'open-existing';
172
+ open: boolean;
173
+ idempotencyKey: string;
174
+ }
175
+ export interface OpenOrCreateResult {
176
+ status: 'created' | 'opened-existing';
177
+ workspaceId: string;
178
+ path: string;
179
+ opened: boolean;
180
+ }
181
+ export interface NoteEntry {
182
+ path: string;
183
+ name: string;
184
+ size: number;
185
+ }
186
+ export interface ListNotesOptions {
187
+ folder?: string;
188
+ recursive?: boolean;
189
+ limit?: number;
190
+ /** Opaque continuation returned by notes.list; keep folder/recursive unchanged. */
191
+ cursor?: string;
192
+ }
193
+ export interface ListNotesResult {
194
+ entries: readonly NoteEntry[];
195
+ truncated: boolean;
196
+ nextCursor?: string;
197
+ }
198
+ export interface SearchNotesOptions {
199
+ query: string;
200
+ folder?: string;
201
+ caseSensitive?: boolean;
202
+ limit?: number;
203
+ }
204
+ export interface SearchNotesResult {
205
+ matches: readonly {
206
+ path: string;
207
+ revision: number;
208
+ line: number;
209
+ preview: string;
210
+ }[];
211
+ /** True when the scan or result quota was reached. Searches saved Markdown only. */
212
+ truncated: boolean;
213
+ }
214
+ export interface WriteNoteOptions {
215
+ path: string;
216
+ content: string;
217
+ /** Required when the target exists, including with create: true. Read before writing. */
218
+ expectedRevision?: number;
219
+ create?: boolean;
220
+ }
221
+ export interface WriteNoteResult {
222
+ path: string;
223
+ revision: number;
224
+ created: boolean;
225
+ }
226
+ export interface MoveNoteOptions {
227
+ from: string;
228
+ to: string;
229
+ overwrite?: false;
230
+ }
231
+ export interface DeleteNoteOptions {
232
+ path: string;
233
+ /** Desktop moves to system trash; mobile deletion is unavailable. */
234
+ expectedRevision: number;
235
+ }
236
+ export interface NoteChangeEvent {
237
+ type: 'created' | 'changed' | 'deleted' | 'moved';
238
+ path: string;
239
+ previousPath?: string;
240
+ }
241
+ export interface ActiveEditorContext {
242
+ windowId: string;
243
+ editorId: string;
244
+ documentId: string;
245
+ kind: 'markdown';
246
+ mode: 'visual' | 'source' | 'sectioned';
247
+ revision: number;
248
+ composing: boolean;
249
+ size: {
250
+ utf16Length: number;
251
+ bytes: number;
252
+ lines: number;
253
+ };
254
+ }
255
+ export interface EditorSelection {
256
+ editorId: string;
257
+ revision: number;
258
+ from?: number;
259
+ to?: number;
260
+ offsetsAvailable: boolean;
261
+ empty: boolean;
262
+ text: string;
263
+ }
264
+ export interface EditorTextSnapshot {
265
+ editorId: string;
266
+ documentId: string;
267
+ revision: number;
268
+ format: 'markdown';
269
+ text: string;
270
+ }
271
+ export interface GetEditorTextSnapshotOptions {
272
+ editorId: string;
273
+ expectedRevision: number;
274
+ format: 'markdown';
275
+ }
276
+ export interface EditorActiveChangeEvent {
277
+ previous: ActiveEditorContext | null;
278
+ current: ActiveEditorContext | null;
279
+ }
280
+ export interface EditorContentChangeEvent {
281
+ editorId: string;
282
+ documentId: string;
283
+ revision: number;
284
+ composing: boolean;
285
+ size: ActiveEditorContext['size'];
286
+ }
287
+ export interface ApplyEditorEditOptions {
288
+ editorId: string;
289
+ expectedRevision: number;
290
+ text: string;
291
+ target: 'cursor' | 'selection';
292
+ }
293
+ export interface ApplyEditorEditResult {
294
+ applied: true;
295
+ insertedLength: number;
296
+ }
297
+ /** All ranges use UTF-16 offsets into the Markdown snapshot at expectedRevision. */
298
+ export interface EditorRangeEdit {
299
+ from: number;
300
+ to: number;
301
+ text: string;
302
+ }
303
+ export interface ApplyEditorEditsOptions {
304
+ editorId: string;
305
+ expectedRevision: number;
306
+ edits: readonly EditorRangeEdit[];
307
+ }
308
+ export interface SetEditorSelectionOptions {
309
+ editorId: string;
310
+ expectedRevision: number;
311
+ from: number;
312
+ to: number;
313
+ }
314
+ export type PluginFormValue = string | number | boolean;
315
+ export interface PluginFormCondition {
316
+ field: string;
317
+ equals: PluginFormValue;
318
+ }
319
+ export type PluginFormField = {
320
+ id: string;
321
+ label: string;
322
+ description?: string;
323
+ required?: boolean;
324
+ disabled?: boolean;
325
+ visibleWhen?: PluginFormCondition;
326
+ } & ({
327
+ type: 'text' | 'textarea';
328
+ value?: string;
329
+ placeholder?: string;
330
+ maxLength?: number;
331
+ } | {
332
+ type: 'number';
333
+ value?: number;
334
+ min?: number;
335
+ max?: number;
336
+ } | {
337
+ type: 'select';
338
+ value?: string;
339
+ options: readonly {
340
+ label: string;
341
+ value: string;
342
+ }[];
343
+ } | {
344
+ type: 'checkbox';
345
+ value?: boolean;
346
+ });
347
+ export interface PluginFormBlock {
348
+ type: 'form';
349
+ id: string;
350
+ /** Change this token to explicitly reset values and feedback. */
351
+ resetKey?: string;
352
+ /** Debounced user changes; receives { formId, fieldId, values, revision, generation, dialogId? }. Return value is ignored. */
353
+ changeCommand?: string;
354
+ submitDisabled?: boolean;
355
+ fields: readonly PluginFormField[];
356
+ submitLabel: string;
357
+ /** Receives { formId, values, dialogId? }. Hidden/disabled fields are omitted. May return { fieldErrors, message }. */
358
+ command: string;
359
+ }
360
+ export interface PluginViewState {
361
+ id: string;
362
+ location: PluginViewContribution['location'];
363
+ visible: boolean;
364
+ }
365
+ export interface WorkspaceChangeEvent {
366
+ previous: WorkspaceInfo | null;
367
+ current: WorkspaceInfo;
368
+ }
369
+ export interface PluginNetworkRequest {
370
+ url: string;
371
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
372
+ headers?: Readonly<Record<string, string>>;
373
+ body?: string;
374
+ timeoutMs?: number;
375
+ }
376
+ export interface PluginNetworkResponse {
377
+ url: string;
378
+ status: number;
379
+ headers: Readonly<Record<string, string>>;
380
+ body: string;
381
+ }
382
+ export type PluginTableCell = string | {
383
+ text: string;
384
+ command: string;
385
+ argument?: PluginCommandArgument;
386
+ disabled?: boolean;
387
+ };
388
+ export type PluginUiBlock = PluginFormBlock | {
389
+ type: 'separator';
390
+ } | {
391
+ type: 'callout';
392
+ title: string;
393
+ text: string;
394
+ tone?: 'default' | 'destructive';
395
+ } | {
396
+ type: 'progress';
397
+ label: string;
398
+ value: number;
399
+ } | {
400
+ type: 'table';
401
+ id?: string;
402
+ rowIds?: readonly string[];
403
+ columns: readonly string[];
404
+ rows: readonly (readonly PluginTableCell[])[];
405
+ } | {
406
+ type: 'tree';
407
+ items: readonly {
408
+ id: string;
409
+ parentId?: string;
410
+ label: string;
411
+ command?: string;
412
+ argument?: PluginCommandArgument;
413
+ }[];
414
+ } | {
415
+ type: 'heading';
416
+ text: string;
417
+ } | {
418
+ type: 'text';
419
+ text: string;
420
+ tone?: 'default' | 'muted' | 'warning';
421
+ } | {
422
+ type: 'list';
423
+ items: readonly string[];
424
+ } | {
425
+ type: 'key-value';
426
+ items: readonly {
427
+ label: string;
428
+ value: string;
429
+ }[];
430
+ } | {
431
+ type: 'actions';
432
+ actions: readonly {
433
+ id: string;
434
+ label: string;
435
+ command: string;
436
+ argument?: PluginCommandArgument;
437
+ variant?: 'default' | 'secondary' | 'destructive';
438
+ disabled?: boolean;
439
+ }[];
440
+ };
441
+ export interface PluginUiDocument {
442
+ blocks: readonly PluginUiBlock[];
443
+ /** Apply only while this form input snapshot is current in the target surface. */
444
+ expectedForm?: {
445
+ formId: string;
446
+ generation: string;
447
+ revision: number;
448
+ };
449
+ }
450
+ export interface PluginDialogOptions {
451
+ /** Replace only this still-open dialog owned by this plugin. Otherwise a busy dialog returns Conflict. */
452
+ replaceId?: string;
453
+ title: string;
454
+ description?: string;
455
+ content: PluginUiDocument;
456
+ closeLabel?: string;
457
+ }
458
+ export interface PluginDisposable {
459
+ readonly dispose: () => void;
460
+ }
461
+ export interface PluginStorageArea {
462
+ readonly get: (key: string) => Promise<PluginJsonValue | undefined>;
463
+ readonly set: (key: string, value: PluginJsonValue) => Promise<void>;
464
+ readonly delete: (key: string) => Promise<void>;
465
+ }
466
+ export type PluginAbortListener = () => void;
467
+ /** The AbortSignal subset implemented by NoteGen marketplace and development runtimes. */
468
+ export interface PluginAbortSignal {
469
+ readonly aborted: boolean;
470
+ readonly reason: unknown;
471
+ readonly throwIfAborted: () => void;
472
+ readonly addEventListener: (type: 'abort', listener: PluginAbortListener, options?: {
473
+ once?: boolean;
474
+ }) => void;
475
+ readonly removeEventListener: (type: 'abort', listener: PluginAbortListener) => void;
476
+ }
477
+ export interface PluginStatusBarUpdate {
478
+ visible: boolean;
479
+ text?: string;
480
+ compactText?: string;
481
+ tooltip?: string;
482
+ accessibleLabel?: string;
483
+ busy?: boolean;
484
+ }
485
+ export interface PluginDialogHandle {
486
+ id: string;
487
+ }
488
+ export type PluginDialogUpdate = Omit<PluginDialogOptions, 'replaceId'>;
489
+ /** Non-destructive main-window navigation only; no arbitrary command execution. */
490
+ export type PluginHostCommand = 'app.openSearch' | 'app.openSettings' | 'app.openPluginSettings';
491
+ export interface PluginDialogCloseEvent {
492
+ id: string;
493
+ reason: 'user' | 'programmatic' | 'replaced' | 'disposed';
494
+ }
495
+ export interface PluginAttachment {
496
+ /** Workspace-relative path; never an absolute filesystem path or file URL. */
497
+ path: string;
498
+ size: number;
499
+ /** Standard padded Base64, limited to 1 MiB of decoded bytes. */
500
+ base64: string;
501
+ }
502
+ export interface PluginContext {
503
+ readonly plugin: {
504
+ readonly id: string;
505
+ readonly version: string;
506
+ /** The concrete host API version, which may be newer than this SDK release. */
507
+ readonly apiVersion: string;
508
+ };
509
+ /** Local diagnostics only. Messages are truncated to 1,000 characters and rate limited. Never log secrets. */
510
+ readonly log: {
511
+ readonly info: (message: string) => void;
512
+ readonly warning: (message: string) => void;
513
+ readonly error: (message: string) => void;
514
+ };
515
+ readonly signal: PluginAbortSignal;
516
+ readonly commands: {
517
+ readonly executeHost: (command: PluginHostCommand) => Promise<void>;
518
+ readonly handle: (commandId: string, handler: (argument?: PluginCommandArgument) => PluginCommandResult | Promise<PluginCommandResult>) => PluginDisposable;
519
+ };
520
+ readonly workspace: {
521
+ readonly getCurrent: () => Promise<WorkspaceInfo>;
522
+ readonly onDidChange: (listener: (event: WorkspaceChangeEvent) => void | Promise<void>) => PluginDisposable;
523
+ };
524
+ readonly calendar: {
525
+ readonly resolveDay: (options: ResolveDayOptions) => Promise<ResolvedDay>;
526
+ };
527
+ readonly attachments: {
528
+ readonly read: (options: {
529
+ path: string;
530
+ }) => Promise<PluginAttachment>;
531
+ /** Creates a new file only. Existing files are never overwritten. */
532
+ readonly create: (options: {
533
+ path: string;
534
+ base64: string;
535
+ }) => Promise<{
536
+ path: string;
537
+ size: number;
538
+ }>;
539
+ };
540
+ readonly notes: {
541
+ readonly read: (options: ReadNoteOptions) => Promise<NoteSnapshot>;
542
+ readonly openOrCreate: (options: OpenOrCreateNoteOptions) => Promise<OpenOrCreateResult>;
543
+ readonly list: (options?: ListNotesOptions) => Promise<ListNotesResult>;
544
+ /** Requires both notes.list and notes.read within the granted folder. */
545
+ readonly search: (options: SearchNotesOptions) => Promise<SearchNotesResult>;
546
+ /** Main window only. Close every tab, pane, and separate window for the target first; otherwise EditorBusy. */
547
+ readonly write: (options: WriteNoteOptions) => Promise<WriteNoteResult>;
548
+ /** Main window only. Source and destination must be closed in every editor; otherwise EditorBusy. */
549
+ readonly move: (options: MoveNoteOptions) => Promise<void>;
550
+ /** Main window only. Close every tab, pane, and separate window for the target first; otherwise EditorBusy. */
551
+ readonly delete: (options: DeleteNoteOptions) => Promise<void>;
552
+ readonly onDidChange: (listener: (event: NoteChangeEvent) => void | Promise<void>) => PluginDisposable;
553
+ };
554
+ readonly editor: {
555
+ readonly getActiveEditor: () => Promise<ActiveEditorContext | null>;
556
+ readonly getSelection: () => Promise<EditorSelection | null>;
557
+ readonly getTextSnapshot: (options: GetEditorTextSnapshotOptions) => Promise<EditorTextSnapshot>;
558
+ readonly applyEdit: (options: ApplyEditorEditOptions) => Promise<ApplyEditorEditResult>;
559
+ /** Atomic, non-overlapping Markdown edits. Currently available in source mode. */
560
+ readonly applyEdits: (options: ApplyEditorEditsOptions) => Promise<ApplyEditorEditResult>;
561
+ /** Canonical Markdown offsets; currently available in source mode. */
562
+ readonly setSelection: (options: SetEditorSelectionOptions) => Promise<void>;
563
+ readonly onDidChangeActiveEditor: (listener: (event: EditorActiveChangeEvent) => void | Promise<void>) => PluginDisposable;
564
+ readonly onDidChangeContent: (listener: (event: EditorContentChangeEvent) => void | Promise<void>) => PluginDisposable;
565
+ };
566
+ readonly storage: {
567
+ readonly device: PluginStorageArea;
568
+ readonly workspace: PluginStorageArea;
569
+ };
570
+ readonly ui: {
571
+ readonly showNotice: (message: string) => Promise<void>;
572
+ readonly statusBar: {
573
+ readonly update: (id: string, state: PluginStatusBarUpdate) => Promise<void>;
574
+ };
575
+ readonly views: {
576
+ readonly update: (id: string, content: PluginUiDocument) => Promise<void>;
577
+ readonly open: (id: string) => Promise<void>;
578
+ readonly close: (id: string) => Promise<void>;
579
+ readonly focus: (id: string) => Promise<void>;
580
+ readonly getState: (id: string) => Promise<PluginViewState>;
581
+ readonly onDidChange: (listener: (state: PluginViewState) => void | Promise<void>) => PluginDisposable;
582
+ };
583
+ readonly openDialog: (options: PluginDialogOptions) => Promise<PluginDialogHandle>;
584
+ readonly updateDialog: (id: string, options: PluginDialogUpdate) => Promise<void>;
585
+ /** Closes only the calling plugin's dialog. */
586
+ readonly closeDialog: (id: string) => Promise<void>;
587
+ readonly onDidCloseDialog: (listener: (event: PluginDialogCloseEvent) => void | Promise<void>) => PluginDisposable;
588
+ };
589
+ readonly network: {
590
+ readonly fetch: (request: PluginNetworkRequest) => Promise<PluginNetworkResponse>;
591
+ };
592
+ readonly i18n: {
593
+ readonly t: (key: string, values?: Record<string, string | number>) => string;
594
+ };
595
+ readonly settings: {
596
+ readonly get: (key: string) => PluginSettingValue | undefined;
597
+ readonly onDidChange: (listener: (key: string, value: PluginSettingValue) => void | Promise<void>) => PluginDisposable;
598
+ };
599
+ }
600
+ export interface PluginModule {
601
+ activate: (context: PluginContext) => void | Promise<void>;
602
+ deactivate?: () => void | Promise<void>;
603
+ }
604
+ export type PluginActivate = PluginModule['activate'];
605
+ export type PluginDeactivate = NonNullable<PluginModule['deactivate']>;
606
+ export declare const PLUGIN_ERROR_CODES: readonly ["PermissionDenied", "AlreadyRegistered", "UnavailableOnPlatform", "QuotaExceeded", "StaleRevision", "Conflict", "NotFound", "InvalidTimeZone", "InvalidPath", "EditorBusy", "WorkspaceChanged", "CreatedNotOpened", "ReadOnly", "NoSpace", "Timeout", "Cancelled", "InvalidManifest", "Incompatible", "RuntimeFailure", "SignatureInvalid", "IntegrityMismatch"];
607
+ export type PluginErrorCode = typeof PLUGIN_ERROR_CODES[number];
608
+ /** Errors crossing the plugin boundary expose a stable code and message. */
609
+ export declare class PluginError extends Error {
610
+ readonly code: PluginErrorCode;
611
+ readonly details?: Readonly<Record<string, PluginJsonValue>>;
612
+ constructor(code: PluginErrorCode, message: string, details?: Readonly<Record<string, PluginJsonValue>>);
613
+ }
614
+ /** Works for both local errors and errors received across the Worker boundary. */
615
+ export declare function isPluginError(value: unknown): value is PluginError;
616
+ /** Preserve literal manifest values while checking the v1 shape at compile time. */
617
+ export declare function definePluginManifest<const Manifest extends PluginManifestV1>(manifest: Manifest): Manifest;
package/dist/index.js ADDED
@@ -0,0 +1,47 @@
1
+ /** The public API version implemented by this release of NoteGen. */
2
+ export const PLUGIN_API_VERSION = '0.1.0';
3
+ export const PLUGIN_ERROR_CODES = Object.freeze([
4
+ 'PermissionDenied',
5
+ 'AlreadyRegistered',
6
+ 'UnavailableOnPlatform',
7
+ 'QuotaExceeded',
8
+ 'StaleRevision',
9
+ 'Conflict',
10
+ 'NotFound',
11
+ 'InvalidTimeZone',
12
+ 'InvalidPath',
13
+ 'EditorBusy',
14
+ 'WorkspaceChanged',
15
+ 'CreatedNotOpened',
16
+ 'ReadOnly',
17
+ 'NoSpace',
18
+ 'Timeout',
19
+ 'Cancelled',
20
+ 'InvalidManifest',
21
+ 'Incompatible',
22
+ 'RuntimeFailure',
23
+ 'SignatureInvalid',
24
+ 'IntegrityMismatch',
25
+ ]);
26
+ /** Errors crossing the plugin boundary expose a stable code and message. */
27
+ export class PluginError extends Error {
28
+ constructor(code, message, details) {
29
+ super(message);
30
+ this.name = 'PluginError';
31
+ this.code = code;
32
+ this.details = details;
33
+ }
34
+ }
35
+ /** Works for both local errors and errors received across the Worker boundary. */
36
+ export function isPluginError(value) {
37
+ if (typeof value !== 'object' || value === null)
38
+ return false;
39
+ const candidate = value;
40
+ return typeof candidate.message === 'string'
41
+ && typeof candidate.code === 'string'
42
+ && PLUGIN_ERROR_CODES.includes(candidate.code);
43
+ }
44
+ /** Preserve literal manifest values while checking the v1 shape at compile time. */
45
+ export function definePluginManifest(manifest) {
46
+ return manifest;
47
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@notegen/plugin-api",
3
+ "version": "0.1.0",
4
+ "description": "Public TypeScript contract for NoteGen plugins.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./plugin-manifest-v1.schema.json": "./schema/plugin-manifest-v1.schema.json",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "schema",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public",
28
+ "provenance": true
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/codexu/note-gen-plugin-sdk.git",
33
+ "directory": "packages/plugin-api"
34
+ },
35
+ "homepage": "https://notegen.top",
36
+ "bugs": {
37
+ "url": "https://github.com/codexu/note-gen-plugin-sdk/issues"
38
+ },
39
+ "notegen": {
40
+ "pluginApiVersion": "0.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "typescript": "^5.8.3"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "scripts": {
49
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
50
+ "check:contract": "node -e \"const fs=require('node:fs');const pkg=require('./package.json');const source=fs.readFileSync('src/index.ts','utf8');const match=source.match(/PLUGIN_API_VERSION = '([^']+)'/);if(!match)throw new Error('Missing PLUGIN_API_VERSION');if(match[1]!==pkg.notegen.pluginApiVersion)throw new Error('Package contract metadata '+pkg.notegen.pluginApiVersion+' does not match exported plugin API '+match[1]);\"",
51
+ "build": "pnpm run clean && pnpm run check:contract && tsc -p tsconfig.json"
52
+ }
53
+ }
@@ -0,0 +1,309 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://notegen.top/schemas/plugin-manifest-v1.schema.json",
4
+ "title": "NoteGen Plugin Manifest v1",
5
+ "description": "Editor schema for plugin.json. The NoteGen CLI remains authoritative for semantic, localization, compatibility, and package-file validation.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "manifestVersion",
10
+ "id",
11
+ "name",
12
+ "version",
13
+ "apiVersion",
14
+ "minAppVersion",
15
+ "platforms",
16
+ "entry",
17
+ "activationEvents",
18
+ "permissions",
19
+ "contributes"
20
+ ],
21
+ "properties": {
22
+ "manifestVersion": { "const": 1 },
23
+ "id": {
24
+ "type": "string",
25
+ "minLength": 3,
26
+ "maxLength": 160,
27
+ "pattern": "^(?!app\\.notegen(?:\\.|$))(?=.{3,160}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$"
28
+ },
29
+ "name": { "$ref": "#/$defs/pluginName" },
30
+ "description": { "$ref": "#/$defs/pluginDescription" },
31
+ "version": { "$ref": "#/$defs/semver" },
32
+ "apiVersion": {
33
+ "type": "string",
34
+ "maxLength": 80,
35
+ "pattern": "^(?:(?:>=|<=|\\^|~|>|<) *)?(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$"
36
+ },
37
+ "minAppVersion": { "$ref": "#/$defs/semver" },
38
+ "platforms": {
39
+ "type": "array",
40
+ "minItems": 1,
41
+ "maxItems": 3,
42
+ "uniqueItems": true,
43
+ "items": { "enum": ["desktop", "ios", "android"] },
44
+ "contains": { "const": "desktop" }
45
+ },
46
+ "entry": { "$ref": "#/$defs/entryPath" },
47
+ "activationEvents": {
48
+ "type": "array",
49
+ "maxItems": 100,
50
+ "uniqueItems": true,
51
+ "items": {
52
+ "anyOf": [
53
+ { "enum": ["onEditor:markdown", "onWorkspace:open", "onNotes:change"] },
54
+ { "type": "string", "pattern": "^onCommand:[A-Za-z0-9._-]+$", "maxLength": 240 }
55
+ ]
56
+ }
57
+ },
58
+ "permissions": { "$ref": "#/$defs/permissions" },
59
+ "contributes": { "$ref": "#/$defs/contributions" },
60
+ "defaultLocale": { "$ref": "#/$defs/localeTag" },
61
+ "locales": {
62
+ "type": "object",
63
+ "maxProperties": 50,
64
+ "propertyNames": { "$ref": "#/$defs/localeTag" },
65
+ "additionalProperties": { "$ref": "#/$defs/packagePath" }
66
+ },
67
+ "author": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "required": ["name"],
71
+ "properties": {
72
+ "name": { "type": "string", "minLength": 1, "maxLength": 120 },
73
+ "url": { "$ref": "#/$defs/publicUrl" }
74
+ }
75
+ },
76
+ "repository": { "$ref": "#/$defs/publicUrl" },
77
+ "license": { "type": "string", "minLength": 1, "maxLength": 80 }
78
+ },
79
+ "$defs": {
80
+ "pluginName": {
81
+ "type": "string",
82
+ "minLength": 1,
83
+ "maxLength": 100
84
+ },
85
+ "pluginDescription": {
86
+ "type": "string",
87
+ "minLength": 1,
88
+ "maxLength": 500
89
+ },
90
+ "localizedText": {
91
+ "type": "string",
92
+ "minLength": 1,
93
+ "maxLength": 240
94
+ },
95
+ "semver": {
96
+ "type": "string",
97
+ "maxLength": 80,
98
+ "pattern": "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$"
99
+ },
100
+ "namespacedId": {
101
+ "type": "string",
102
+ "minLength": 3,
103
+ "maxLength": 220,
104
+ "pattern": "^[A-Za-z0-9._-]+$"
105
+ },
106
+ "packagePath": {
107
+ "type": "string",
108
+ "minLength": 1,
109
+ "maxLength": 1024,
110
+ "pattern": "^(?!/)(?![A-Za-z]:)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$"
111
+ },
112
+ "entryPath": {
113
+ "type": "string",
114
+ "minLength": 4,
115
+ "maxLength": 240,
116
+ "pattern": "^(?!/)(?![A-Za-z]:)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+\\.js$"
117
+ },
118
+ "localeTag": {
119
+ "type": "string",
120
+ "minLength": 2,
121
+ "maxLength": 35,
122
+ "pattern": "^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$"
123
+ },
124
+ "publicUrl": {
125
+ "type": "string",
126
+ "maxLength": 500,
127
+ "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://"
128
+ },
129
+ "permission": {
130
+ "type": "object",
131
+ "additionalProperties": false,
132
+ "required": ["scope"],
133
+ "properties": {
134
+ "scope": { "type": "string" },
135
+ "optional": { "type": "boolean" },
136
+ "description": { "$ref": "#/$defs/localizedText" }
137
+ }
138
+ },
139
+ "activeEditorPermission": {
140
+ "allOf": [
141
+ { "$ref": "#/$defs/permission" },
142
+ { "properties": { "scope": { "const": "active-editor" } } }
143
+ ]
144
+ },
145
+ "folderPermission": {
146
+ "allOf": [
147
+ { "$ref": "#/$defs/permission" },
148
+ { "properties": { "scope": { "const": "workspace-folder" } } }
149
+ ]
150
+ },
151
+ "filePermission": {
152
+ "allOf": [
153
+ { "$ref": "#/$defs/permission" },
154
+ { "properties": { "scope": { "enum": ["workspace-file", "workspace-files", "workspace-folder"] } } }
155
+ ]
156
+ },
157
+ "permissions": {
158
+ "type": "object",
159
+ "additionalProperties": false,
160
+ "properties": {
161
+ "editor.read": { "$ref": "#/$defs/activeEditorPermission" },
162
+ "editor.write": { "$ref": "#/$defs/activeEditorPermission" },
163
+ "notes.read": { "$ref": "#/$defs/filePermission" },
164
+ "notes.create": { "$ref": "#/$defs/folderPermission" },
165
+ "notes.open": { "$ref": "#/$defs/folderPermission" },
166
+ "notes.list": { "$ref": "#/$defs/folderPermission" },
167
+ "notes.write": { "$ref": "#/$defs/filePermission" },
168
+ "attachments.read": { "$ref": "#/$defs/filePermission" },
169
+ "attachments.create": { "$ref": "#/$defs/folderPermission" },
170
+ "notes.delete": { "$ref": "#/$defs/filePermission" },
171
+ "notes.move": { "$ref": "#/$defs/folderPermission" },
172
+ "network.fetch": {
173
+ "allOf": [
174
+ { "$ref": "#/$defs/permission" },
175
+ { "properties": { "scope": { "const": "network-origins" } } }
176
+ ]
177
+ }
178
+ }
179
+ },
180
+ "command": {
181
+ "type": "object",
182
+ "additionalProperties": false,
183
+ "required": ["id", "title"],
184
+ "properties": {
185
+ "id": { "$ref": "#/$defs/namespacedId" },
186
+ "title": { "$ref": "#/$defs/localizedText" },
187
+ "description": { "$ref": "#/$defs/localizedText" },
188
+ "icon": { "type": "string", "pattern": "^[A-Za-z0-9-]+$", "maxLength": 80 },
189
+ "suggestedShortcut": { "type": "string", "minLength": 1, "maxLength": 80 }
190
+ }
191
+ },
192
+ "settingCommon": {
193
+ "type": "object",
194
+ "required": ["key", "type", "scope", "title", "default"],
195
+ "properties": {
196
+ "key": { "$ref": "#/$defs/namespacedId" },
197
+ "scope": { "enum": ["device", "workspace"] },
198
+ "title": { "$ref": "#/$defs/localizedText" },
199
+ "description": { "$ref": "#/$defs/localizedText" }
200
+ }
201
+ },
202
+ "setting": {
203
+ "oneOf": [
204
+ {
205
+ "allOf": [{ "$ref": "#/$defs/settingCommon" }],
206
+ "additionalProperties": false,
207
+ "properties": {
208
+ "key": { "$ref": "#/$defs/namespacedId" }, "type": { "const": "boolean" },
209
+ "scope": { "enum": ["device", "workspace"] }, "title": { "$ref": "#/$defs/localizedText" },
210
+ "description": { "$ref": "#/$defs/localizedText" }, "default": { "type": "boolean" }
211
+ }
212
+ },
213
+ {
214
+ "allOf": [{ "$ref": "#/$defs/settingCommon" }],
215
+ "additionalProperties": false,
216
+ "properties": {
217
+ "key": { "$ref": "#/$defs/namespacedId" }, "type": { "const": "string" },
218
+ "scope": { "enum": ["device", "workspace"] }, "title": { "$ref": "#/$defs/localizedText" },
219
+ "description": { "$ref": "#/$defs/localizedText" }, "default": { "type": "string" },
220
+ "placeholder": { "$ref": "#/$defs/localizedText" },
221
+ "maxLength": { "type": "integer", "minimum": 1, "maximum": 65536 },
222
+ "permissionPaths": { "type": "array", "minItems": 1, "maxItems": 20, "uniqueItems": true, "items": { "type": "string" } }
223
+ }
224
+ },
225
+ {
226
+ "allOf": [{ "$ref": "#/$defs/settingCommon" }],
227
+ "additionalProperties": false,
228
+ "properties": {
229
+ "key": { "$ref": "#/$defs/namespacedId" }, "type": { "const": "number" },
230
+ "scope": { "enum": ["device", "workspace"] }, "title": { "$ref": "#/$defs/localizedText" },
231
+ "description": { "$ref": "#/$defs/localizedText" }, "default": { "type": "number" },
232
+ "min": { "type": "number" }, "max": { "type": "number" },
233
+ "step": { "type": "number", "exclusiveMinimum": 0 }
234
+ }
235
+ },
236
+ {
237
+ "allOf": [{ "$ref": "#/$defs/settingCommon" }],
238
+ "additionalProperties": false,
239
+ "properties": {
240
+ "key": { "$ref": "#/$defs/namespacedId" }, "type": { "const": "select" },
241
+ "scope": { "enum": ["device", "workspace"] }, "title": { "$ref": "#/$defs/localizedText" },
242
+ "description": { "$ref": "#/$defs/localizedText" }, "default": { "type": "string" },
243
+ "options": {
244
+ "type": "array", "minItems": 1, "maxItems": 100,
245
+ "items": {
246
+ "type": "object", "additionalProperties": false, "required": ["label", "value"],
247
+ "properties": { "label": { "$ref": "#/$defs/localizedText" }, "value": { "type": "string", "minLength": 1, "maxLength": 160 } }
248
+ }
249
+ }
250
+ },
251
+ "required": ["options"]
252
+ },
253
+ {
254
+ "allOf": [{ "$ref": "#/$defs/settingCommon" }],
255
+ "additionalProperties": false,
256
+ "properties": {
257
+ "key": { "$ref": "#/$defs/namespacedId" }, "type": { "enum": ["workspace-file", "workspace-folder"] },
258
+ "scope": { "const": "workspace" }, "title": { "$ref": "#/$defs/localizedText" },
259
+ "description": { "$ref": "#/$defs/localizedText" }, "default": { "type": "string" }
260
+ }
261
+ }
262
+ ]
263
+ },
264
+ "statusBar": {
265
+ "type": "object",
266
+ "additionalProperties": false,
267
+ "required": ["id", "alignment"],
268
+ "properties": {
269
+ "id": { "$ref": "#/$defs/namespacedId" },
270
+ "alignment": { "enum": ["left", "right"] },
271
+ "priority": { "type": "integer", "minimum": -10000, "maximum": 10000 },
272
+ "command": { "$ref": "#/$defs/namespacedId" }
273
+ }
274
+ },
275
+ "menu": {
276
+ "type": "object",
277
+ "additionalProperties": false,
278
+ "required": ["location", "command"],
279
+ "properties": {
280
+ "location": { "enum": ["editor/slash", "editor/context", "file/context", "mobile/writing/overflow"] },
281
+ "command": { "$ref": "#/$defs/namespacedId" },
282
+ "when": { "type": "string", "minLength": 1, "maxLength": 240 },
283
+ "group": { "type": "string", "minLength": 1, "maxLength": 80 }
284
+ }
285
+ },
286
+ "view": {
287
+ "type": "object",
288
+ "additionalProperties": false,
289
+ "required": ["id", "title", "location"],
290
+ "properties": {
291
+ "id": { "$ref": "#/$defs/namespacedId" },
292
+ "title": { "$ref": "#/$defs/localizedText" },
293
+ "location": { "enum": ["left-sidebar", "right-sidebar", "editor-tab"] },
294
+ "icon": { "type": "string", "pattern": "^[A-Za-z0-9-]+$", "maxLength": 80 }
295
+ }
296
+ },
297
+ "contributions": {
298
+ "type": "object",
299
+ "additionalProperties": false,
300
+ "properties": {
301
+ "commands": { "type": "array", "maxItems": 100, "items": { "$ref": "#/$defs/command" } },
302
+ "settings": { "type": "array", "maxItems": 100, "items": { "$ref": "#/$defs/setting" } },
303
+ "statusBar": { "type": "array", "maxItems": 30, "items": { "$ref": "#/$defs/statusBar" } },
304
+ "menus": { "type": "array", "maxItems": 100, "items": { "$ref": "#/$defs/menu" } },
305
+ "views": { "type": "array", "maxItems": 30, "items": { "$ref": "#/$defs/view" } }
306
+ }
307
+ }
308
+ }
309
+ }