@clipbus/plugin-sdk 0.7.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.
Files changed (54) hide show
  1. package/API.md +641 -0
  2. package/LICENSE +21 -0
  3. package/README.md +466 -0
  4. package/SPECIFICATION.md +355 -0
  5. package/dist/dom/autoFit.d.ts +15 -0
  6. package/dist/dom/consolePatch.d.ts +1 -0
  7. package/dist/dom/index.cjs +211 -0
  8. package/dist/dom/index.d.cts +6 -0
  9. package/dist/dom/index.d.ts +6 -0
  10. package/dist/dom/index.js +188 -0
  11. package/dist/dom/textInputState.d.ts +1 -0
  12. package/dist/dom/topicAdapter.d.ts +30 -0
  13. package/dist/generated/INDEX.runtime.generated.d.ts +6 -0
  14. package/dist/generated/INDEX.ui.generated.d.ts +4 -0
  15. package/dist/generated/capabilityClients.generated.d.ts +199 -0
  16. package/dist/generated/data.generated.d.ts +193 -0
  17. package/dist/generated/hostClients.generated.d.ts +38 -0
  18. package/dist/generated/runtime.actionResult.generated.d.ts +28 -0
  19. package/dist/generated/runtime.definePlugin.generated.d.ts +16 -0
  20. package/dist/generated/runtime.handlers.generated.d.ts +20 -0
  21. package/dist/generated/runtime.host.generated.d.ts +34 -0
  22. package/dist/generated/topicSubscribers.generated.d.ts +32 -0
  23. package/dist/generated/ui.bootstrap.generated.d.ts +15 -0
  24. package/dist/generated/ui.clipbus.generated.d.ts +79 -0
  25. package/dist/generated/wireConstants.generated.d.ts +3 -0
  26. package/dist/internal/capabilities.d.ts +31 -0
  27. package/dist/internal/index.cjs +68 -0
  28. package/dist/internal/index.d.ts +1 -0
  29. package/dist/internal/internalConsole.d.ts +7 -0
  30. package/dist/internal/ipcBus.d.ts +48 -0
  31. package/dist/internal/runtimeInvokeClient.d.ts +3 -0
  32. package/dist/internal/topic.d.ts +20 -0
  33. package/dist/runtime/defineMessage.d.ts +6 -0
  34. package/dist/runtime/index.cjs +163 -0
  35. package/dist/runtime/index.d.cts +4 -0
  36. package/dist/runtime/index.d.ts +4 -0
  37. package/dist/runtime/index.js +132 -0
  38. package/dist/shared/defineMessage.d.ts +7 -0
  39. package/dist/ui/defineMessage.d.ts +7 -0
  40. package/dist/ui/index.cjs +362 -0
  41. package/dist/ui/index.d.cts +4 -0
  42. package/dist/ui/index.d.ts +4 -0
  43. package/dist/ui/index.js +339 -0
  44. package/docs/README.md +34 -0
  45. package/docs/authoring.md +288 -0
  46. package/docs/capability-detection.md +105 -0
  47. package/docs/concepts.md +80 -0
  48. package/docs/entry.md +137 -0
  49. package/docs/faq.md +65 -0
  50. package/docs/item-context.md +186 -0
  51. package/docs/manifest.md +149 -0
  52. package/docs/permissions.md +32 -0
  53. package/docs/rpc.md +84 -0
  54. package/package.json +76 -0
@@ -0,0 +1,355 @@
1
+ # Clipbus Plugin SDK — Specification
2
+
3
+ ## Chapter 1: Wire Shapes and Strict API Form
4
+
5
+ The SDK exposes API calls in strict wire shape (no sugar). Every public symbol belongs to exactly one of four shapes.
6
+
7
+ ### 1.1 Topic\<T\>
8
+
9
+ A Topic holds a current value and notifies listeners when it changes.
10
+
11
+ ```
12
+ topic.current() → T read current value synchronously
13
+ topic.on(listener) → Unsubscribe register a change listener; returns unsub fn
14
+ ```
15
+
16
+ Use a Topic when the plugin needs to both read the current value and react to future changes (e.g. `clipbus.item`, `clipbus.theme`).
17
+
18
+ ### 1.2 OptionalTopic\<T\>
19
+
20
+ Like Topic but the value may be absent until the host provides it.
21
+
22
+ ```
23
+ topic.current() → T | undefined
24
+ topic.on(listener) → Unsubscribe
25
+ ```
26
+
27
+ Use an OptionalTopic when a value is context-dependent and may never be set in the current run (e.g. `clipbus.item.attachment` in an action context, `clipbus.action` in an attachment renderer context).
28
+
29
+ ### 1.3 Stream\<T\>
30
+
31
+ A Stream has no current value; it only fans out discrete events to listeners.
32
+
33
+ ```
34
+ stream.on(listener) → Unsubscribe
35
+ ```
36
+
37
+ Use a Stream for one-shot or repeated events with no persistent state (e.g. `clipbus.attachmentRenderer.onHostInvoke`).
38
+
39
+ ### 1.4 Verb
40
+
41
+ A Verb is an async function that triggers a side-effect or host operation.
42
+
43
+ ```
44
+ verb() → Promise<Result>
45
+ verb(args) → Promise<Result>
46
+ ```
47
+
48
+ Verbs that are illegal in the current context (e.g. calling `clipbus.action.complete` from an attachment renderer) reject immediately with `PluginContextError`.
49
+
50
+ ### 1.5 Applicability Decision Flow
51
+
52
+ ```
53
+ Does the value have persistent state the plugin can read synchronously?
54
+ No → Stream (events only)
55
+ Yes → Does it exist in every context this module loads in?
56
+ Yes → Topic
57
+ No → OptionalTopic
58
+
59
+ Is this a side-effect/operation rather than state?
60
+ Yes → Verb
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Chapter 1.6: Shared Type Re-exports
66
+
67
+ The SDK runtime and UI modules re-export core data types for plugin author convenience:
68
+
69
+ ### From `@clipbus/plugin-sdk/runtime` and `@clipbus/plugin-sdk/ui`
70
+
71
+ ```ts
72
+ // Strong content and item types
73
+ export type { PluginContentEnvelope, TextContent, ImageContent, PathReferenceContent } from '@clipbus/plugin-sdk/runtime';
74
+ export type { PluginClipboardItem, ItemContext, AttachmentRef } from '@clipbus/plugin-sdk/runtime';
75
+ ```
76
+
77
+ Use these directly in plugin payload and draft type definitions instead of declaring plugin-local wrapper types.
78
+
79
+ Note: The convenience aliases `ContentEnvelope`, `PathEntry`, `ClipboardItem`, and `DetectorArtifact`
80
+ were removed in the plugin-api-shrink rollout. Use the canonical `Plugin*`-prefixed names:
81
+ `PluginContentEnvelope`, `PluginClipboardItem`, `PluginDetectorArtifact`.
82
+ `PathEntry` has no replacement — decode `payload: unknown` directly from `path_reference` content.
83
+
84
+ ---
85
+
86
+ ## Chapter 1.7: Action Handler Shape and Typed Drafts
87
+
88
+ The SDK exposes a **single** action handler interface, generated from the catalog:
89
+
90
+ ```ts
91
+ // Generated in runtime.handlers.generated.ts
92
+ export interface PluginAutoRunActionHandler {
93
+ resolveSession(input: PluginResolveActionSessionInput, ctx?: { host?: HostClient }): Promise<PluginActionResolveResult>;
94
+ runAutoAction(input: PluginAutoRunActionInput, ctx?: { host?: HostClient }): Promise<PluginActionOperationResult>;
95
+ }
96
+ ```
97
+
98
+ Both methods are required on the type. The **lifecycle in `manifest.json`** decides which method the host actually calls:
99
+
100
+ - `lifecycle: 'auto-run'` → host calls `runAutoAction`; `resolveSession` is invoked once for session initialization if present
101
+ - `lifecycle: 'draft'` → host calls `resolveSession` to seed `initialDraft` + buttons; `runAutoAction` is never called for draft lifecycle (you can `throw` from it to make the contract explicit, or leave it as a no-op)
102
+
103
+ There is no `PluginDraftActionHandler` convenience type — both lifecycles use the same interface.
104
+
105
+ ### Strongly typed drafts
106
+
107
+ `clipbus.action.draft.current()` returns `Record<string, unknown> | undefined`. Cast at the call site:
108
+
109
+ ```ts
110
+ import { clipbus } from '@clipbus/plugin-sdk/ui';
111
+
112
+ interface MyDraft {
113
+ title: string;
114
+ note: string;
115
+ }
116
+
117
+ const draft = clipbus.action.draft.current() as MyDraft | undefined;
118
+ ```
119
+
120
+ `clipbus.action.draft` has **no `update()` verb** in the current SDK. The UI manages its own form state from `initialDraft`, then submits the final result via `clipbus.action.complete({result, userMessage?})`. If a runtime-side step is required (e.g. allocating an image temp path), the UI calls `clipbus.runtime.invoke(...)` against its own `messageHandlers`.
121
+
122
+ ### Canonical type names
123
+
124
+ All publicly exported types carry the `Plugin` prefix. The pre-`plugin-api-shrink` aliases without the prefix (`DraftActionHandler`, `AutoRunActionHandler`, `DetectorHandler`, `AttachmentRendererHandler`, `ActionOperationResult`, `ResolveAttachmentInput`, etc.) **have been removed**. Use the canonical names:
125
+
126
+ | Use this | Not this (removed alias) |
127
+ |---|---|
128
+ | `PluginAutoRunActionHandler` | `AutoRunActionHandler` / `DraftActionHandler` |
129
+ | `PluginDetectorHandler` | `DetectorHandler` |
130
+ | `PluginAttachmentRendererHandler` | `AttachmentRendererHandler` |
131
+ | `PluginActionOperationResult` | `ActionOperationResult` |
132
+ | `PluginActionResolveResult` | `ActionResolveResult` |
133
+ | `PluginResolveAttachmentInput` | `ResolveAttachmentInput` |
134
+ | `PluginClipboardItem` | `ClipboardItem` |
135
+ | `PluginAttachmentPayload` | `AttachmentPayload` |
136
+ | `PluginContentEnvelope` | `ContentEnvelope` |
137
+ | `PluginPathEntry` | `PathEntry` |
138
+ | `PluginDetectorArtifact` | `DetectorArtifact` |
139
+
140
+ ---
141
+
142
+ ## Chapter 2: Capability Mirror Table
143
+
144
+ Capabilities are split by catalog `surface.side`: runtime capabilities are exposed through `ctx.host.*`, UI capabilities are exposed through `clipbus.*`, and `side:'both'` capabilities appear in both trees. All calls use strict wire shape (no sugar).
145
+
146
+ > The **authoritative, auto-generated** list of every capability with its full payload / response shape lives in [API.md §2 Capabilities](./API.md#2-capabilities) and the alphabetical matrix in [§3 Capability Matrix](./API.md#3-capability-matrix). The mirror table below adds a "Notes" column with migration-related rationale (which APIs were removed, permission gates, etc.). If the two disagree, **API.md wins** — file a doc-fix PR.
147
+
148
+ | Capability | Wire method | Runtime (ctx.host) | UI (clipbus. | Notes |
149
+ |---|---|---|---|---|
150
+ | Read clipboard item | — | `ctx.host` input param | `clipbus.item.current()` | Topic |
151
+ | Subscribe item changes | — | — | `clipbus.item.on(fn)` | Topic |
152
+ | Set tags | `item.setTags` | `ctx.host.item.setTags({tags})` | — | Verb; runtime-only via user RPC bridge; removed from UI in plugin-api-shrink |
153
+ | Add tags | `item.addTags` | `ctx.host.item.addTags({tags})` | — | Verb; runtime-only via user RPC bridge |
154
+ | Remove tags | `item.removeTags` | `ctx.host.item.removeTags({tags})` | — | Verb; runtime-only via user RPC bridge |
155
+ | Pin/unpin | `item.setPinned` | `ctx.host.item.setPinned({pinned})` | — | Verb; runtime-only via user RPC bridge |
156
+ | Set attachments | `item.setAttachments` | `ctx.host.item.setAttachments(p)` | — | Verb; runtime-only via user RPC bridge; `owner` and `attachmentType` now required |
157
+ | Set search extension | `item.setSearchExtension` | `ctx.host.item.setSearchExtension(p)` | — | Verb; runtime-only via user RPC bridge; `owner` now required |
158
+ | Materialize image | `item.materializeImagePath` | `ctx.host.item.materializeImagePath()` | — | Verb; runtime only; returns `{path}` |
159
+ | Read attachment | `item.readAttachment` | `ctx.host.item.readAttachment({type, key})` | `clipbus.item.readAttachment({type, key})` | Verb; UI-callable; returns `{payloadJson?}` |
160
+ | Attachment payload | — | `input.attachment.payloadJson` | `clipbus.item.attachment.current()` | OptionalTopic |
161
+ | Replace renderer button list | `attachmentRenderer.setButtons` | — | `clipbus.attachmentRenderer.setButtons({buttons})` | Verb; buttons have `isEnabled?: boolean` field |
162
+ | Host-side renderer button click | `clipbus-plugin-attachment-host-invoke` (host event) | — | `clipbus.attachmentRenderer.onHostInvoke(fn)` | Stream; receives `{ buttonID }` |
163
+ | Theme snapshot | theme host event | via `__CLIPBUS_PLUGIN_THEME__` | `clipbus.theme.current()` | Topic |
164
+ | Theme change | `themeHostEvent` | — | `clipbus.theme.on(fn)` | Topic (via host event) |
165
+ | Window height | `window.setHeight` | — | `clipbus.window.setHeight({px})` | Verb; strict wire shape |
166
+ | Auto-fit height | `window.autoFit` | — | `clipbus.window.autoFit()` | Verb |
167
+ | Draft | draftHostEvent | input param | `clipbus.action.draft.current()` | OptionalTopic; UI is read-only — no `update()` verb |
168
+ | Replace action button list | `action.setButtons` | — | `clipbus.action.setButtons({buttons})` | Verb; buttons have `isEnabled?: boolean` field |
169
+ | Submit draft result | `action.complete` | — | `clipbus.action.complete({result, userMessage?})` | Verb; draft lifecycle only |
170
+ | Allocate image temp path | `action.allocateImageTempPath` | `ctx.host.action.allocateImageTempPath({formatHint?})` | — | Verb; runtime-only via user RPC bridge; removed from UI in plugin-api-shrink |
171
+ | UI → Runtime RPC | `runtime.invoke` | — (handler registered via `messageHandlers` in `definePlugin`) | `clipbus.runtime.invoke(key, payload, {timeoutMs?})` | Verb; UI side only; routes to plugin's own Node runtime; default timeout 30 s |
172
+ | Host-side action button click | `clipbus-plugin-action-host-invoke` (host event) | — | `clipbus.action.onHostInvoke(fn)` | Stream; receives `{ buttonID }` |
173
+ | Copy text | `clipboard.copyText` | `ctx.host.clipboard.copyText({text})` | `clipbus.clipboard.copyText({text})` | Verb; strict wire shape |
174
+ | Open URL | `navigation.openUrl` | `ctx.host.navigation.openUrl({url})` | `clipbus.navigation.openUrl({url})` | Verb; strict wire shape |
175
+ | Reveal in Finder | `navigation.revealInFinder` | `ctx.host.navigation.revealInFinder({path})` | `clipbus.navigation.revealInFinder({path})` | Verb; strict wire shape |
176
+ | Open file | `navigation.openFilePath` | `ctx.host.navigation.openFilePath({path})` | `clipbus.navigation.openFilePath({path})` | Verb; strict wire shape |
177
+ | Settings get | `settings.get` | `ctx.host.settings.get({key})` | `clipbus.settings.get({key})` | Verb; strict wire shape |
178
+ | Settings get all | `settings.getAll` | `ctx.host.settings.getAll()` | `clipbus.settings.getAll()` | Verb |
179
+
180
+ ### 2.1 `side:'both'` symmetry
181
+
182
+ Every capability whose catalog declares `surface: { side: 'both', ... }` is **callable from either process** with identical behavior:
183
+
184
+ - **Runtime side** — `ctx.host.<domain>.<verb>(payload)` (Node IPC wire). Returns `Promise<Response>`; permission errors reject.
185
+ - **UI side** — `clipbus.<domain>.<verb>(payload)` (WebView Call wire). Same payload shape, same response, same permission gate.
186
+
187
+ Both sides go through the same base host bridge; there is no parallel runtime-only or UI-only code path.
188
+
189
+ **plugin-api-shrink changes**: `item.setTags`, `item.addTags`, `item.removeTags`, `item.setPinned`,
190
+ `item.setAttachments`, `item.setSearchExtension`, `item.materializeImagePath`, and
191
+ `action.allocateImageTempPath` were moved to **runtime-only** (`side:'runtime'`). Their UI-side
192
+ `clipbus.*` wrappers were removed. Plugins that need to invoke these from a UI context must route
193
+ through `clipbus.runtime.invoke(key, payload)` → their own Node runtime messageHandler.
194
+
195
+ The remaining UI-only capabilities are **surface-bound** to specific WebView panes and are exposed only on the UI tree (`clipbus.*`):
196
+
197
+ | Wire method | UI path | Why UI-only |
198
+ |---|---|---|
199
+ | `runtime.invoke` | `clipbus.runtime.invoke(key, payload, opts?)` | UI initiates a call to its own Node runtime; no runtime-side wire exposure needed |
200
+ | `action.setButtons` | `clipbus.action.setButtons` | Targets action workspace native button bar |
201
+ | `action.complete` | `clipbus.action.complete` | Submits draft lifecycle, action workspace only |
202
+ | `attachmentRenderer.setButtons` | `clipbus.attachmentRenderer.setButtons` | Targets attachment renderer native button bar |
203
+ | `window.setHeight` | `clipbus.window.setHeight` | WebView size only meaningful in WebView |
204
+ | `window.autoFit` | `clipbus.window.autoFit` | Same |
205
+ | `textInput.stateChanged` | `clipbus.textInput.stateChanged` | Notifies WebView native text-input focus chain |
206
+
207
+ Node IPC inbound router rejects any UI-only method names at `PluginRuntimeHostMethod(rawValue:)` parse time with reply `"Unknown method <name>"`; they cannot be dispatched even if a runtime entry attempts to invoke them by raw method name.
208
+
209
+ ---
210
+
211
+ ## Chapter 2.5: Host Events (the canonical 7)
212
+
213
+ Host events declared in the catalog drive every state Topic / Stream the SDK exposes to plugin authors. Each event optionally carries a `windowGlobal` (host injects JSON for synchronous `.current()` read at WebView startup) and a `surface.feed` with `topic` (SDK Topic key) + `shape` (`'topic'` for state, `'stream'` for fire-and-forget events) + `context`.
214
+
215
+ | Wire name | SDK Topic / Stream | Window global | Shape | Context | Payload |
216
+ |---|---|---|---|---|---|
217
+ | `clipbus-plugin-context` | `clipbus.pluginContext` | `__CLIPBUS_PLUGIN_CONTEXT__` | topic | any | `{ mode: 'attachmentRenderer' \| 'action', pluginID: string }` |
218
+ | `clipbus-plugin-item` | `clipbus.item` | `__CLIPBUS_PLUGIN_ITEM__` | topic | any | `PluginClipboardItem` |
219
+ | `clipbus-plugin-attachment` | `clipbus.item.attachment` | `__CLIPBUS_PLUGIN_ATTACHMENT__` | topic | attachmentRenderer | `PluginAttachmentPayload` |
220
+ | `clipbus-plugin-draft` | `clipbus.action.draft` | `__CLIPBUS_PLUGIN_DRAFT__` | topic | action | `Record<string, unknown>` |
221
+ | `clipbus-plugin-theme` | `clipbus.theme` | `__CLIPBUS_PLUGIN_THEME__` | topic | any | `PluginThemeTokenSnapshot` |
222
+ | `clipbus-plugin-attachment-host-invoke` | `clipbus.attachmentRenderer.onHostInvoke` | — | stream | attachmentRenderer | `{ buttonID: string }` |
223
+ | `clipbus-plugin-action-host-invoke` | `clipbus.action.onHostInvoke` | — | stream | action | `{ buttonID: string }` |
224
+
225
+ The previous "bootstrap vs change-event" split was removed in plugin-api-shrink: every Topic now uses the same `windowGlobal` for synchronous initial read **and** a CustomEvent for subsequent updates. The SDK wires both ends — plugin authors only see Topic / OptionalTopic / Stream.
226
+
227
+ The canonical list is regenerated into [`API.md` §4 and §5](./API.md#4-host-events) from `protocol/plugin/src/catalog.ts` — if a name in this chapter ever diverges from API.md, API.md wins.
228
+
229
+ ---
230
+
231
+ ## Chapter 3: Process for Adding New Capabilities or Host Events
232
+
233
+ The plugin wire is fully codegen-driven. Choosing between `defineCapability` and `defineHostEvent`:
234
+
235
+ - **Use `defineCapability`** when the plugin requests an action from the host (e.g. set tags, copy text, open URL).
236
+ - **Use `defineHostEvent`** when the host pushes state or events to the plugin (e.g. theme tokens, button clicks, attachment updates). Decorate with `surface.feed` to wire 1:1 to SDK Topic.
237
+
238
+ ### Adding a Capability
239
+
240
+ 1. **Declare the contract** — add a `defineCapability({name, payload, response, surface})` call in `protocol/plugin/src/domains/<domain>.ts`. Choose a `<domain>.<verb>` name and describe payload / response via `t.*` schema primitives.
241
+ - For typed maps, use `t.record(T)` (e.g. `t.record(t.json())` for `Record<string, JSONValue>`).
242
+ - Prefer `t.object` and `t.discriminatedUnion` for nested structures; use `t.json()` only as an escape hatch.
243
+ - Set `surface: {side: 'ui' | 'runtime' | 'both', path: '<domain>.<verb>', context?: 'any' | 'action' | 'attachment', optimisticUpdate?: {topic, fromPayload}}`
244
+
245
+ 2. **Register in the catalog** — append the descriptor to `protocol/plugin/src/catalog.ts` (`capabilities` array).
246
+
247
+ 3. **Regenerate** — `cd protocol/plugin && npm run codegen`. The host sync target and the SDK sync target under `packages/plugin-sdk/src/generated/` are auto-updated. Generated call clients (`callItemSetTags`, etc.) and clipbus.tree are produced automatically.
248
+
249
+ 4. **Implement the host method** — add the implementation on the host bridge per `surface.side`:
250
+ - `'both'`: implement on the base host bridge (satisfies `PluginRuntimeHostHandler` and inherits into `PluginUIHostBridge`).
251
+ - `'ui'`: implement on the UI host bridge (satisfies `PluginUIHostHandler`).
252
+ - `'runtime'`: implement on the base host bridge (satisfies `PluginRuntimeHostHandler`).
253
+ The host build will refuse to compile until the corresponding handler protocol is satisfied.
254
+
255
+ 5. **Wire the SDK surface** — SDK surface is fully codegen-generated. No hand-wrapping needed; `clipbus.<domain>.<verb>` and `ctx.host.<domain>.<verb>` automatically appear in the generated index files.
256
+
257
+ 6. **Test** — add host bridge tests for the new method. Do not write pure UI unit tests; codegen snapshot tests cover the surface.
258
+
259
+ 7. **Verify locally** — `./scripts/checks/check-plugin-contract.sh` and the host platform test flow must both exit 0.
260
+
261
+ 8. **PR** — see Chapter 5 for the PR checklist.
262
+
263
+ ### Adding a Host Event
264
+
265
+ 1. **Declare the contract** — add a `defineHostEvent({name, payload, windowGlobal, surface})` call in `protocol/plugin/src/domains/<domain>.ts`. Specify:
266
+ - `name`: plugin-internal identifier (mapped to wire `windowGlobal` name via codegen)
267
+ - `payload`: shape via `t.*` schema primitives
268
+ - `windowGlobal` (optional): if set, host injects JSON to this window global for sync read (e.g. `__CLIPBUS_PLUGIN_ITEM__`)
269
+ - `surface.feed`: `{topic: '<domain>' | '<domain>.<subdomain>', shape: 'topic' | 'stream', context?: 'any' | 'action' | 'attachment'}`
270
+
271
+ 2. **Register in the catalog** — append the descriptor to `protocol/plugin/src/catalog.ts` (`hostEvents` array).
272
+
273
+ 3. **Regenerate** — same as capability step 3.
274
+
275
+ 4. **Implement host dispatch** — call the codegen-emitted emitter for the event's surface shape: `PluginHostBootstrapEmitter.generated.swift` for `windowGlobal` bootstrap and `PluginHostTopicEmitter.generated.swift` for topic/stream feeds. Codegen provides the `emitX(payload)` methods and payload types.
276
+
277
+ 5. **Wire the SDK surface** — codegen automatically emits SDK bootstrap wiring. `clipbus.<domain>.on(fn)`, `.current()` are generated; no hand-wrapping needed.
278
+
279
+ 6. **Test** — add tests for event dispatch. Snapshot tests cover SDK wiring.
280
+
281
+ 7. **Verify locally** — same as capability step 7.
282
+
283
+ 8. **PR** — same as capability step 8.
284
+
285
+ ---
286
+
287
+ ## Chapter 4: Naming Rules
288
+
289
+ ### 4.1 Plugin Type Prefix
290
+
291
+ All codegen-emitted type names carry the `Plugin` prefix:
292
+
293
+ **带 Plugin 前缀(仅 type 名):**
294
+ - Wire payload / response interface: `PluginItemSetTagsPayload` / `PluginItemSetTagsResponse`
295
+ - `defineType` named types: `PluginAttachmentRef` / `PluginClipboardItem` / `PluginDetectorArtifact` / `PluginAttachmentMutationEntry` / `PluginSearchExtensionEntry` / `PluginConsoleLogLevel` / etc. (必须以 Plugin 开头,DSL validation 校验)
296
+ - Handler interfaces: `PluginDetectorHandler` / `PluginAttachmentRendererHandler` / `PluginAutoRunActionHandler` (the single action handler covers both `auto-run` and `draft` lifecycles; see Chapter 1.7)
297
+ - Host event payload alias: `PluginItemPayload` / `PluginAttachmentPayload` / `PluginThemeTokenSnapshot` / etc.
298
+ - Host-side Codable struct / enum: `PluginItemSetTagsPayload` / `PluginContentEnvelope` / etc. (已有,保持)
299
+
300
+ **已删除的 typeRef**(plugin-api-shrink):
301
+ - `PluginActionSession` — 随 `clipbus-plugin-action-session` host event 一起删除
302
+ - `PluginActionDescriptor` — 同上
303
+ - `PluginActionInvocationTrigger` — 同上
304
+
305
+ **不加 Plugin 前缀(动词 / 值命名空间):**
306
+ - TS 函数名: `callItemSetTags` / `onItemEvent` / `guardContext` — 动词前缀足够区分
307
+ - 主机端 method 名: `emitItem` / `setBootstrap` — 同上
308
+ - TS 顶层值导出: `clipbus` / `actionResult` — 是值不是类型
309
+ - 主机端 enum case 名: `itemSetTags`(在 `PluginUIHostMethod` / `PluginRuntimeHostMethod` enum 里)— enum 自身带 Plugin 前缀
310
+
311
+ ### 4.2 Module namespaces
312
+
313
+ Top-level namespaces on `clipbus.*` mirror the host capability domain:
314
+ - `clipbus.item` — clipboard item data and mutations
315
+ - `clipbus.theme` — appearance tokens
316
+ - `clipbus.action` — draft action session and controls (optional topic in action context)
317
+ - `clipbus.window` — WebView layout (height, auto-fit)
318
+ - `clipbus.clipboard` — system clipboard write
319
+ - `clipbus.navigation` — navigation and file operations
320
+ - `clipbus.settings` — plugin settings read access
321
+
322
+ ### 4.3 Method names
323
+
324
+ - Topics: `current()` to read, `on(fn)` to subscribe (matches Vue's reactive conventions)
325
+ - Verbs: strict wire shape with single object parameter: `setTags({tags})`, `addTags({tags})`, `update({draft?, defaultButtonID?})`
326
+ - Streams: `onHostInvoke` — `on` prefix + noun phrase
327
+
328
+ ### 4.4 Forbidden patterns
329
+
330
+ - No `get` prefix on Topics (use `current()` instead)
331
+ - No `subscribe` — use `on()`
332
+ - No `dispatch` — use `emit()` internally, `invoke()` or named verbs externally
333
+ - No `bridge` in public names — that's an implementation detail
334
+ - No sugar wrapping (parameter unwrapping, response unwrapping) — strict wire shapes only
335
+
336
+ ### 4.5 Error naming
337
+
338
+ Context errors throw `PluginContextError` (exported from `@clipbus/plugin-sdk/ui`). All other SDK errors use `Error` with descriptive messages prefixed `[clipbus.sdk]`.
339
+
340
+ ---
341
+
342
+ ## Chapter 5: PR Checklist
343
+
344
+ Before merging any PR that touches `sdk/`:
345
+
346
+ - [ ] New capability is documented in Chapter 2 mirror table
347
+ - [ ] TypeScript types added to `ctx.ts` and/or module interface
348
+ - [ ] Failing test written first (TDD), then implementation
349
+ - [ ] `npm run build` passes in `sdk/`
350
+ - [ ] `npm test` passes in `sdk/` (runtime + ui + surface)
351
+ - [ ] Surface snapshot updated with `SNAPSHOT_UPDATE=1` and golden files committed
352
+ - [ ] No references to host-internal paths, WebKit handler names, or host implementation class names in any `*.md` file under `sdk/` (run the doc-grep CI step to verify)
353
+ - [ ] PR description cites the SPECIFICATION.md chapter number justifying shape choice
354
+ - [ ] `npm test` passes in `plugins/template-plugin/` (template plugin integration tests)
355
+ - [ ] `./scripts/checks/check-plugin-contract.sh` exits 0
@@ -0,0 +1,15 @@
1
+ export interface AutoFitOptions {
2
+ /** Minimum height in px (default: 0). */
3
+ min?: number;
4
+ /** Maximum height in px (default: Infinity). */
5
+ max?: number;
6
+ /** Element to observe. Defaults to document.body. */
7
+ target?: HTMLElement | null;
8
+ }
9
+ /**
10
+ * Attach ResizeObserver + MutationObserver to track content height and
11
+ * call window.setHeight automatically.
12
+ *
13
+ * @returns A disconnect function. Call it in onUnmounted to clean up.
14
+ */
15
+ export declare function autoFit(options?: AutoFitOptions): () => void;
@@ -0,0 +1 @@
1
+ export declare function patchConsole(): void;
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/dom/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ autoFit: () => autoFit,
24
+ bindTopicTo: () => bindTopicTo,
25
+ patchConsole: () => patchConsole,
26
+ patchTextInputState: () => patchTextInputState
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/generated/wireConstants.generated.ts
31
+ var STRUCTURED_ERROR_PREFIX = "__clipbus_structured_error__:";
32
+ var CAPABILITY_UNSUPPORTED_ERROR_NAME = "PluginCapabilityUnsupported";
33
+
34
+ // src/internal/capabilities.ts
35
+ var CapabilityUnsupportedError = class extends Error {
36
+ capability;
37
+ constructor(capability) {
38
+ super(`Capability not supported by host: ${capability}`);
39
+ this.name = "CapabilityUnsupportedError";
40
+ this.capability = capability;
41
+ Object.setPrototypeOf(this, new.target.prototype);
42
+ }
43
+ };
44
+ function mapToCapabilityError(err, unsupportedName) {
45
+ if (err.name === unsupportedName) {
46
+ const cap = err.data?.capability ?? "";
47
+ return new CapabilityUnsupportedError(cap);
48
+ }
49
+ const m = /^Unknown method:\s*(.+)$/.exec(err.message);
50
+ if (m) return new CapabilityUnsupportedError(m[1]);
51
+ return err;
52
+ }
53
+
54
+ // src/internal/runtimeInvokeClient.ts
55
+ function parseReplyError(raw) {
56
+ const message = extractMessage(raw);
57
+ if (message.startsWith(STRUCTURED_ERROR_PREFIX)) {
58
+ const json = message.slice(STRUCTURED_ERROR_PREFIX.length);
59
+ try {
60
+ const parsed = JSON.parse(json);
61
+ const err = new Error(typeof parsed.message === "string" ? parsed.message : "");
62
+ if (typeof parsed.name === "string" && parsed.name.length > 0) {
63
+ err.name = parsed.name;
64
+ }
65
+ if (parsed.data !== void 0) {
66
+ err.data = parsed.data;
67
+ }
68
+ return mapToCapabilityError(err, CAPABILITY_UNSUPPORTED_ERROR_NAME);
69
+ } catch {
70
+ return new Error(message);
71
+ }
72
+ }
73
+ return mapToCapabilityError(new Error(message), CAPABILITY_UNSUPPORTED_ERROR_NAME);
74
+ }
75
+ function extractMessage(raw) {
76
+ if (typeof raw === "string") return raw;
77
+ if (raw instanceof Error) return raw.message;
78
+ if (raw && typeof raw === "object" && "message" in raw) {
79
+ const m = raw.message;
80
+ if (typeof m === "string") return m;
81
+ }
82
+ return String(raw);
83
+ }
84
+
85
+ // src/generated/capabilityClients.generated.ts
86
+ var HANDLER_NAME = "clipbusPluginCall";
87
+ async function callPluginMethod(method, payload) {
88
+ const handler = globalThis.webkit?.messageHandlers?.[HANDLER_NAME];
89
+ if (!handler) {
90
+ const err = new Error("clipbus." + method + " is only available inside a Clipbus plugin WebView");
91
+ err.name = "PluginHostBridgeUnavailable";
92
+ throw err;
93
+ }
94
+ const normalized = JSON.parse(JSON.stringify(payload));
95
+ let reply;
96
+ try {
97
+ reply = await handler.postMessage({ method, payload: normalized });
98
+ } catch (err) {
99
+ throw parseReplyError(err);
100
+ }
101
+ return reply;
102
+ }
103
+ var callWindowSetHeight = (payload) => callPluginMethod("window.setHeight", payload);
104
+ var callConsoleLog = (payload) => callPluginMethod("console.log", payload);
105
+ var callTextInputStateChanged = (payload) => callPluginMethod("textInput.stateChanged", payload);
106
+
107
+ // src/internal/internalConsole.ts
108
+ function pick(method) {
109
+ if (typeof globalThis === "undefined") return () => {
110
+ };
111
+ const g = globalThis;
112
+ const fn = g.console?.[method];
113
+ if (typeof fn !== "function") return () => {
114
+ };
115
+ return fn.bind(g.console);
116
+ }
117
+ var internalConsole = Object.freeze({
118
+ log: pick("log"),
119
+ warn: pick("warn"),
120
+ error: pick("error")
121
+ });
122
+
123
+ // src/dom/autoFit.ts
124
+ function autoFit(options) {
125
+ if (typeof window === "undefined" || typeof document === "undefined") {
126
+ return () => {
127
+ };
128
+ }
129
+ const min = options?.min ?? 0;
130
+ const max = options?.max ?? Infinity;
131
+ const target = options?.target ?? document.body;
132
+ let pending = false;
133
+ function post() {
134
+ if (pending) return;
135
+ pending = true;
136
+ requestAnimationFrame(() => {
137
+ pending = false;
138
+ const raw = target.scrollHeight;
139
+ const clamped = Math.min(max, Math.max(min, raw));
140
+ callWindowSetHeight({ height: clamped }).catch((err) => {
141
+ internalConsole.warn("[clipbus-sdk] autoFit: window.setHeight failed:", err);
142
+ });
143
+ });
144
+ }
145
+ const ro = new ResizeObserver(post);
146
+ ro.observe(target);
147
+ const mo = new MutationObserver(post);
148
+ mo.observe(target, { childList: true, subtree: true, characterData: true, attributes: true });
149
+ post();
150
+ return () => {
151
+ ro.disconnect();
152
+ mo.disconnect();
153
+ };
154
+ }
155
+
156
+ // src/dom/consolePatch.ts
157
+ var _patched = false;
158
+ var _inFlight = 0;
159
+ function patchConsole() {
160
+ if (_patched) return;
161
+ _patched = true;
162
+ const wrap = (level, original) => (...args) => {
163
+ original(...args);
164
+ if (_inFlight > 0) return;
165
+ _inFlight++;
166
+ Promise.resolve().then(() => callConsoleLog({ level, message: args.map(String).join(" ") })).catch((err) => {
167
+ internalConsole.warn("[clipbus-sdk] consolePatch: forward to host failed:", err);
168
+ }).finally(() => {
169
+ _inFlight--;
170
+ });
171
+ };
172
+ console.log = wrap("info", internalConsole.log);
173
+ console.warn = wrap("warn", internalConsole.warn);
174
+ console.error = wrap("error", internalConsole.error);
175
+ }
176
+
177
+ // src/dom/textInputState.ts
178
+ var _patched2 = false;
179
+ function patchTextInputState() {
180
+ if (_patched2 || typeof window === "undefined") return;
181
+ _patched2 = true;
182
+ let isFocused = false;
183
+ let isComposing = false;
184
+ function post() {
185
+ callTextInputStateChanged({ isFocused, isComposing }).catch((err) => {
186
+ internalConsole.warn("[clipbus-sdk] textInputState: dispatch failed:", err);
187
+ });
188
+ }
189
+ document.addEventListener("focusin", () => {
190
+ isFocused = true;
191
+ post();
192
+ });
193
+ document.addEventListener("focusout", () => {
194
+ isFocused = false;
195
+ post();
196
+ });
197
+ document.addEventListener("compositionstart", () => {
198
+ isComposing = true;
199
+ post();
200
+ });
201
+ document.addEventListener("compositionend", () => {
202
+ isComposing = false;
203
+ post();
204
+ });
205
+ }
206
+
207
+ // src/dom/topicAdapter.ts
208
+ function bindTopicTo(topic, set) {
209
+ set(topic.current());
210
+ return topic.on((value) => set(value));
211
+ }
@@ -0,0 +1,6 @@
1
+ export { autoFit } from './autoFit.js';
2
+ export type { AutoFitOptions } from './autoFit.js';
3
+ export { patchConsole } from './consolePatch.js';
4
+ export { patchTextInputState } from './textInputState.js';
5
+ export { bindTopicTo } from './topicAdapter.js';
6
+ export type { TopicLike } from './topicAdapter.js';
@@ -0,0 +1,6 @@
1
+ export { autoFit } from './autoFit.js';
2
+ export type { AutoFitOptions } from './autoFit.js';
3
+ export { patchConsole } from './consolePatch.js';
4
+ export { patchTextInputState } from './textInputState.js';
5
+ export { bindTopicTo } from './topicAdapter.js';
6
+ export type { TopicLike } from './topicAdapter.js';