@clipbus/plugin-sdk 0.8.6 → 0.9.1

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.
@@ -1330,6 +1330,10 @@ function emitActionBootstrap(p) {
1330
1330
  globalThis["__CLIPBUS_PLUGIN_DRAFT__"] = p.draft;
1331
1331
  globalThis.dispatchEvent(new CustomEvent("clipbus-plugin-draft", { detail: p.draft }));
1332
1332
  }
1333
+ if (p.actionInput !== void 0) {
1334
+ globalThis["__CLIPBUS_PLUGIN_ACTION_INPUT__"] = p.actionInput;
1335
+ globalThis.dispatchEvent(new CustomEvent("clipbus-plugin-action-input", { detail: p.actionInput }));
1336
+ }
1333
1337
  if (p.theme !== void 0) {
1334
1338
  globalThis["__CLIPBUS_PLUGIN_THEME__"] = p.theme;
1335
1339
  globalThis.dispatchEvent(new CustomEvent("clipbus-plugin-theme", { detail: p.theme }));
@@ -1367,6 +1371,7 @@ function computeWirePayloads(scenario, themeKey = DEFAULT_THEME_KEY) {
1367
1371
  context,
1368
1372
  item: scenario.item,
1369
1373
  draft: scenario.draft ?? {},
1374
+ actionInput: scenario.actionInput,
1370
1375
  theme,
1371
1376
  locale
1372
1377
  };
@@ -1385,6 +1390,7 @@ function injectWire(_target, payloads) {
1385
1390
  context: payloads.context,
1386
1391
  item: payloads.item,
1387
1392
  draft: payloads.draft,
1393
+ actionInput: payloads.actionInput,
1388
1394
  theme: payloads.theme,
1389
1395
  locale: payloads.locale
1390
1396
  });
@@ -1441,6 +1447,12 @@ function classifyWireSections(payloads) {
1441
1447
  defaultOpen: true,
1442
1448
  value: payloads.draft
1443
1449
  });
1450
+ sections.push({
1451
+ key: "actionInput",
1452
+ kind: "content",
1453
+ defaultOpen: true,
1454
+ value: payloads.actionInput
1455
+ });
1444
1456
  }
1445
1457
  sections.push({
1446
1458
  key: "theme",
@@ -16,7 +16,7 @@ import type { WirePayloads } from './wire.js';
16
16
  export interface WireSection {
17
17
  /** The payload key name (e.g. 'context', 'item', 'attachment'). */
18
18
  key: string;
19
- /** content = item + attachment/draft (user-facing data; open by default).
19
+ /** content = item + attachment/draft/actionInput (user-facing data; open by default).
20
20
  * infra = context + theme + locale (plumbing; collapsed by default). */
21
21
  kind: 'content' | 'infra';
22
22
  /** Whether the section is expanded when first rendered. */
@@ -27,8 +27,8 @@ export interface WireSection {
27
27
  /**
28
28
  * Classify wire payload sections into display order with open/collapsed defaults.
29
29
  *
30
- * Fixed order: context · item · attachment(renderer)/draft(action) · theme · locale
31
- * content: item + attachment/draft — defaultOpen true (user compares these with output)
30
+ * Fixed order: context · item · attachment(renderer)/draft+actionInput(action) · theme · locale
31
+ * content: item + attachment/draft/actionInput — defaultOpen true
32
32
  * infra: context + theme + locale — defaultOpen false (plumbing, low signal)
33
33
  */
34
34
  export declare function classifyWireSections(payloads: WirePayloads): WireSection[];
@@ -1,4 +1,4 @@
1
- import type { PluginClipboardItem, PluginAttachmentPayload } from '../generated/data.generated.js';
1
+ import type { PluginActionUIInput, PluginAttachmentPayload, PluginClipboardItem } from '../generated/data.generated.js';
2
2
  export interface PreviewViewport {
3
3
  /** Initial viewport width (px). Defaults to the per-mode width when omitted. */
4
4
  width?: number;
@@ -27,6 +27,8 @@ export interface PreviewScenario {
27
27
  attachment?: PluginAttachmentPayload;
28
28
  /** Action mode draft payload. */
29
29
  draft?: Record<string, unknown>;
30
+ /** Sanitized current Action value; never include real filesystem paths. */
31
+ actionInput?: PluginActionUIInput;
30
32
  buttons?: {
31
33
  id: string;
32
34
  title: string;
@@ -1,6 +1,6 @@
1
1
  import type { PluginThemeTokenSnapshot } from '../generated/data.generated.js';
2
2
  import type { PluginContextPayload, PluginLocalePayload } from '../generated/hostBootstrap.generated.js';
3
- import type { PluginClipboardItem, PluginAttachmentPayload } from '../generated/data.generated.js';
3
+ import type { PluginActionUIInput, PluginAttachmentPayload, PluginClipboardItem } from '../generated/data.generated.js';
4
4
  import type { PreviewScenario } from './types.js';
5
5
  /**
6
6
  * Backward-compat alias. Now accepts any preset key string ('graphite', 'ember',
@@ -13,6 +13,7 @@ export interface WirePayloads {
13
13
  item: PluginClipboardItem;
14
14
  attachment?: PluginAttachmentPayload;
15
15
  draft?: Record<string, unknown>;
16
+ actionInput?: PluginActionUIInput;
16
17
  theme: PluginThemeTokenSnapshot;
17
18
  locale: PluginLocalePayload;
18
19
  }
@@ -79,7 +79,9 @@ var host = {
79
79
  openFilePath: (payload) => ipcBus.request("navigation.openFilePath", payload)
80
80
  },
81
81
  action: {
82
- allocateImageTempPath: (payload) => ipcBus.request("action.allocateImageTempPath", payload)
82
+ allocateImageTempPath: (payload) => ipcBus.request("action.allocateImageTempPath", payload),
83
+ materializeInputImagePath: () => ipcBus.request("action.materializeInputImagePath", {}),
84
+ allocateOutputFilePath: (payload) => ipcBus.request("action.allocateOutputFilePath", payload)
83
85
  },
84
86
  settings: {
85
87
  get: (payload) => ipcBus.request("settings.get", payload),
@@ -132,6 +134,10 @@ var actionResult = {
132
134
  result: { resultKind: "image", imageTempPath, ...options && "imageFormatHint" in options ? { imageFormatHint: options.imageFormatHint } : {} },
133
135
  userMessage: options?.userMessage
134
136
  }),
137
+ path_reference: (entries, options) => ({
138
+ result: { resultKind: "path_reference", entries },
139
+ userMessage: options?.userMessage
140
+ }),
135
141
  none: (options) => ({
136
142
  result: { resultKind: "none" },
137
143
  userMessage: options?.userMessage
@@ -49,7 +49,9 @@ var host = {
49
49
  openFilePath: (payload) => ipcBus.request("navigation.openFilePath", payload)
50
50
  },
51
51
  action: {
52
- allocateImageTempPath: (payload) => ipcBus.request("action.allocateImageTempPath", payload)
52
+ allocateImageTempPath: (payload) => ipcBus.request("action.allocateImageTempPath", payload),
53
+ materializeInputImagePath: () => ipcBus.request("action.materializeInputImagePath", {}),
54
+ allocateOutputFilePath: (payload) => ipcBus.request("action.allocateOutputFilePath", payload)
53
55
  },
54
56
  settings: {
55
57
  get: (payload) => ipcBus.request("settings.get", payload),
@@ -102,6 +104,10 @@ var actionResult = {
102
104
  result: { resultKind: "image", imageTempPath, ...options && "imageFormatHint" in options ? { imageFormatHint: options.imageFormatHint } : {} },
103
105
  userMessage: options?.userMessage
104
106
  }),
107
+ path_reference: (entries, options) => ({
108
+ result: { resultKind: "path_reference", entries },
109
+ userMessage: options?.userMessage
110
+ }),
105
111
  none: (options) => ({
106
112
  result: { resultKind: "none" },
107
113
  userMessage: options?.userMessage
package/dist/ui/index.cjs CHANGED
@@ -132,6 +132,8 @@ async function callPluginMethod(method, payload) {
132
132
  var callItemReadAttachment = (payload) => callPluginMethod("item.readAttachment", payload);
133
133
  var callAssetCurrentItemImageUrl = () => callPluginMethod("asset.currentItemImageUrl", {});
134
134
  var callAssetPathReferenceImageUrl = (payload) => callPluginMethod("asset.pathReferenceImageUrl", payload);
135
+ var callAssetCurrentActionInputImageUrl = () => callPluginMethod("asset.currentActionInputImageUrl", {});
136
+ var callAssetCurrentActionInputPathReferenceImageUrl = (payload) => callPluginMethod("asset.currentActionInputPathReferenceImageUrl", payload);
135
137
  var callClipboardCopyText = (payload) => callPluginMethod("clipboard.copyText", payload);
136
138
  var callNavigationOpenUrl = (payload) => callPluginMethod("navigation.openUrl", payload);
137
139
  var callNavigationRevealInFinder = (payload) => callPluginMethod("navigation.revealInFinder", payload);
@@ -212,6 +214,11 @@ function onDraft(listener) {
212
214
  globalThis.addEventListener("clipbus-plugin-draft", handler);
213
215
  return () => globalThis.removeEventListener("clipbus-plugin-draft", handler);
214
216
  }
217
+ function onActionInput(listener) {
218
+ const handler = (e) => listener(e.detail);
219
+ globalThis.addEventListener("clipbus-plugin-action-input", handler);
220
+ return () => globalThis.removeEventListener("clipbus-plugin-action-input", handler);
221
+ }
215
222
  function onTheme(listener) {
216
223
  const handler = (e) => listener(e.detail);
217
224
  globalThis.addEventListener("clipbus-plugin-theme", handler);
@@ -252,6 +259,8 @@ var _itemAttachmentTopic = createTopic(readWindowGlobal("__CLIPBUS_PLUGIN_ATTACH
252
259
  onAttachment((p) => _itemAttachmentTopic.set(p));
253
260
  var _actionDraftTopic = createTopic(readWindowGlobal("__CLIPBUS_PLUGIN_DRAFT__"));
254
261
  onDraft((p) => _actionDraftTopic.set(p));
262
+ var _actionInputTopic = createTopic(readWindowGlobal("__CLIPBUS_PLUGIN_ACTION_INPUT__"));
263
+ onActionInput((p) => _actionInputTopic.set(p));
255
264
  var _themeTopic = createTopic(readWindowGlobal("__CLIPBUS_PLUGIN_THEME__"));
256
265
  onTheme((p) => _themeTopic.set(p));
257
266
  var _attachmentRendererOnHostInvokeStream = createStream();
@@ -299,7 +308,9 @@ var clipbus = {
299
308
  },
300
309
  asset: {
301
310
  currentItemImageUrl: () => callAssetCurrentItemImageUrl(),
302
- pathReferenceImageUrl: (payload) => callAssetPathReferenceImageUrl(payload)
311
+ pathReferenceImageUrl: (payload) => callAssetPathReferenceImageUrl(payload),
312
+ currentActionInputImageUrl: guardContext("action", () => callAssetCurrentActionInputImageUrl()),
313
+ currentActionInputPathReferenceImageUrl: guardContext("action", (payload) => callAssetCurrentActionInputPathReferenceImageUrl(payload))
303
314
  },
304
315
  clipboard: {
305
316
  copyText: (payload) => callClipboardCopyText(payload)
@@ -316,6 +327,10 @@ var clipbus = {
316
327
  current: () => _actionDraftTopic.current(),
317
328
  on: (fn) => _actionDraftTopic.on(fn)
318
329
  },
330
+ input: {
331
+ current: () => _actionInputTopic.current(),
332
+ on: (fn) => _actionInputTopic.on(fn)
333
+ },
319
334
  onHostInvoke: {
320
335
  on: (fn) => _actionOnHostInvokeStream.on(fn)
321
336
  }
package/dist/ui/index.js CHANGED
@@ -103,6 +103,8 @@ async function callPluginMethod(method, payload) {
103
103
  var callItemReadAttachment = (payload) => callPluginMethod("item.readAttachment", payload);
104
104
  var callAssetCurrentItemImageUrl = () => callPluginMethod("asset.currentItemImageUrl", {});
105
105
  var callAssetPathReferenceImageUrl = (payload) => callPluginMethod("asset.pathReferenceImageUrl", payload);
106
+ var callAssetCurrentActionInputImageUrl = () => callPluginMethod("asset.currentActionInputImageUrl", {});
107
+ var callAssetCurrentActionInputPathReferenceImageUrl = (payload) => callPluginMethod("asset.currentActionInputPathReferenceImageUrl", payload);
106
108
  var callClipboardCopyText = (payload) => callPluginMethod("clipboard.copyText", payload);
107
109
  var callNavigationOpenUrl = (payload) => callPluginMethod("navigation.openUrl", payload);
108
110
  var callNavigationRevealInFinder = (payload) => callPluginMethod("navigation.revealInFinder", payload);
@@ -183,6 +185,11 @@ function onDraft(listener) {
183
185
  globalThis.addEventListener("clipbus-plugin-draft", handler);
184
186
  return () => globalThis.removeEventListener("clipbus-plugin-draft", handler);
185
187
  }
188
+ function onActionInput(listener) {
189
+ const handler = (e) => listener(e.detail);
190
+ globalThis.addEventListener("clipbus-plugin-action-input", handler);
191
+ return () => globalThis.removeEventListener("clipbus-plugin-action-input", handler);
192
+ }
186
193
  function onTheme(listener) {
187
194
  const handler = (e) => listener(e.detail);
188
195
  globalThis.addEventListener("clipbus-plugin-theme", handler);
@@ -223,6 +230,8 @@ var _itemAttachmentTopic = createTopic(readWindowGlobal("__CLIPBUS_PLUGIN_ATTACH
223
230
  onAttachment((p) => _itemAttachmentTopic.set(p));
224
231
  var _actionDraftTopic = createTopic(readWindowGlobal("__CLIPBUS_PLUGIN_DRAFT__"));
225
232
  onDraft((p) => _actionDraftTopic.set(p));
233
+ var _actionInputTopic = createTopic(readWindowGlobal("__CLIPBUS_PLUGIN_ACTION_INPUT__"));
234
+ onActionInput((p) => _actionInputTopic.set(p));
226
235
  var _themeTopic = createTopic(readWindowGlobal("__CLIPBUS_PLUGIN_THEME__"));
227
236
  onTheme((p) => _themeTopic.set(p));
228
237
  var _attachmentRendererOnHostInvokeStream = createStream();
@@ -270,7 +279,9 @@ var clipbus = {
270
279
  },
271
280
  asset: {
272
281
  currentItemImageUrl: () => callAssetCurrentItemImageUrl(),
273
- pathReferenceImageUrl: (payload) => callAssetPathReferenceImageUrl(payload)
282
+ pathReferenceImageUrl: (payload) => callAssetPathReferenceImageUrl(payload),
283
+ currentActionInputImageUrl: guardContext("action", () => callAssetCurrentActionInputImageUrl()),
284
+ currentActionInputPathReferenceImageUrl: guardContext("action", (payload) => callAssetCurrentActionInputPathReferenceImageUrl(payload))
274
285
  },
275
286
  clipboard: {
276
287
  copyText: (payload) => callClipboardCopyText(payload)
@@ -287,6 +298,10 @@ var clipbus = {
287
298
  current: () => _actionDraftTopic.current(),
288
299
  on: (fn) => _actionDraftTopic.on(fn)
289
300
  },
301
+ input: {
302
+ current: () => _actionInputTopic.current(),
303
+ on: (fn) => _actionInputTopic.on(fn)
304
+ },
290
305
  onHostInvoke: {
291
306
  on: (fn) => _actionOnHostInvokeStream.on(fn)
292
307
  }
@@ -46,6 +46,7 @@ var MANIFEST_VALIDATION_CODES = [
46
46
  "duplicateDetectorID",
47
47
  "duplicateActionID",
48
48
  "emptyPluginID",
49
+ "invalidPluginID",
49
50
  "emptyPluginTitle",
50
51
  "installEntryMissing",
51
52
  "installEntryOutsideRoot",
@@ -58,11 +59,11 @@ var MANIFEST_VALIDATION_CODES = [
58
59
  "detectorAttachmentTypeOutsideNamespace",
59
60
  "rendererUIEntryMissing",
60
61
  "rendererUIEntryOutsideRoot",
61
- "actionMissingSupportedItemTypes",
62
+ "actionMissingSupportedInputKinds",
62
63
  "actionUIEntryMissing",
63
64
  "actionUIEntryOutsideRoot",
64
65
  "detectorInvalidSupportedInputKind",
65
- "actionInvalidSupportedItemType"
66
+ "actionInvalidSupportedInputKind"
66
67
  ];
67
68
  var CANONICAL_ITEM_TYPES = ["text", "image", "path_reference"];
68
69
  function asRecord(val) {
@@ -89,25 +90,41 @@ function isInsideRoot(entryRelative, rootAbsolute) {
89
90
  const resolvedEntry = import_node_path.default.resolve(rootAbsolute, entryRelative);
90
91
  return resolvedEntry === rootAbsolute || resolvedEntry.startsWith(rootAbsolute + import_node_path.default.sep);
91
92
  }
93
+ var PLUGIN_ID_MAX_LENGTH = 128;
94
+ var PLUGIN_ID_PATTERN = /^[a-z0-9._-]+$/;
95
+ function isValidPluginID(id) {
96
+ if (id.length === 0 || id.length > PLUGIN_ID_MAX_LENGTH) return false;
97
+ if (id.startsWith(".") || id.includes("..")) return false;
98
+ return PLUGIN_ID_PATTERN.test(id);
99
+ }
92
100
  function validateSchemaVersion(raw, issues) {
93
101
  const version = typeof raw["schemaVersion"] === "number" ? raw["schemaVersion"] : 0;
94
- if (version !== 2) {
102
+ if (version !== 3) {
95
103
  issues.push(issue("unsupportedSchemaVersion", `Unsupported plugin schema version: ${version}`));
96
104
  }
97
105
  }
98
106
  var LEGACY_RENDERER_FIELDS = ["displayName", "defaultEnabled", "defaultOrderKey", "actions"];
99
107
  var LEGACY_DETECTOR_FIELDS = ["displayName", "capabilityID", "defaultEnabled", "timeoutMs"];
100
- var LEGACY_ACTION_FIELDS = ["displayName", "actionID", "defaultEnabled", "defaultOrderKey", "revealButtons"];
108
+ var LEGACY_ACTION_FIELDS = [
109
+ "displayName",
110
+ "actionID",
111
+ "defaultEnabled",
112
+ "defaultOrderKey",
113
+ "revealButtons",
114
+ "supportedItemTypes",
115
+ "inputConstraints",
116
+ "possibleResultKinds"
117
+ ];
101
118
  function validateLegacyFields(raw, issues) {
102
119
  const plugin = asRecord(raw["plugin"]);
103
120
  if (plugin && "displayName" in plugin) {
104
- issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v2: plugin.displayName`));
121
+ issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v3: plugin.displayName`));
105
122
  }
106
123
  const renderers = asRecordArray(raw["attachmentRenderers"]);
107
124
  for (let i = 0; i < renderers.length; i++) {
108
125
  for (const field of LEGACY_RENDERER_FIELDS) {
109
126
  if (field in renderers[i]) {
110
- issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v2: attachmentRenderers[${i}].${field}`));
127
+ issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v3: attachmentRenderers[${i}].${field}`));
111
128
  }
112
129
  }
113
130
  }
@@ -115,7 +132,7 @@ function validateLegacyFields(raw, issues) {
115
132
  for (let i = 0; i < detectors.length; i++) {
116
133
  for (const field of LEGACY_DETECTOR_FIELDS) {
117
134
  if (field in detectors[i]) {
118
- issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v2: detectors[${i}].${field}`));
135
+ issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v3: detectors[${i}].${field}`));
119
136
  }
120
137
  }
121
138
  }
@@ -123,7 +140,7 @@ function validateLegacyFields(raw, issues) {
123
140
  for (let i = 0; i < actions.length; i++) {
124
141
  for (const field of LEGACY_ACTION_FIELDS) {
125
142
  if (field in actions[i]) {
126
- issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v2: actions[${i}].${field}`));
143
+ issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v3: actions[${i}].${field}`));
127
144
  }
128
145
  }
129
146
  }
@@ -139,9 +156,9 @@ function validateCanonicalItemTypeSpellings(raw, issues) {
139
156
  }
140
157
  for (const action of asRecordArray(raw["actions"])) {
141
158
  const actionID = asString(action["id"]) ?? "";
142
- for (const value of asStringArray(action["supportedItemTypes"])) {
159
+ for (const value of asStringArray(action["supportedInputKinds"])) {
143
160
  if (!CANONICAL_ITEM_TYPES.includes(value)) {
144
- issues.push(issue("actionInvalidSupportedItemType", `Action supported item types contain unsupported value: ${actionID} -> ${value}`));
161
+ issues.push(issue("actionInvalidSupportedInputKind", `Action supported input kinds contain unsupported value: ${actionID} -> ${value}`));
145
162
  }
146
163
  }
147
164
  }
@@ -190,9 +207,9 @@ function validateActions(actions, issues) {
190
207
  if (title.trim().length === 0) {
191
208
  issues.push(issue("actionTitleEmpty", `Action title is empty: ${id}`));
192
209
  }
193
- const supportedItemTypes = asStringArray(action["supportedItemTypes"]);
194
- if (supportedItemTypes.length === 0) {
195
- issues.push(issue("actionMissingSupportedItemTypes", `Action is missing supported item types: ${id}`));
210
+ const supportedInputKinds = asStringArray(action["supportedInputKinds"]);
211
+ if (supportedInputKinds.length === 0) {
212
+ issues.push(issue("actionMissingSupportedInputKinds", `Action is missing supported input kinds: ${id}`));
196
213
  }
197
214
  }
198
215
  }
@@ -240,6 +257,8 @@ function validateManifest(manifest, options = {}) {
240
257
  const pluginTitle = asString(plugin["title"]) ?? "";
241
258
  if (pluginID.trim().length === 0) {
242
259
  issues.push(issue("emptyPluginID", "Plugin id is empty"));
260
+ } else if (!isValidPluginID(pluginID)) {
261
+ issues.push(issue("invalidPluginID", `Invalid plugin id: ${pluginID}`));
243
262
  }
244
263
  if (pluginTitle.trim().length === 0) {
245
264
  issues.push(issue("emptyPluginTitle", "Plugin title is empty"));
@@ -31,10 +31,10 @@ export interface ValidateManifestOptions {
31
31
  rootDir?: string;
32
32
  }
33
33
  /**
34
- * All 26 validation error codes, verbatim camelCase mirror of Swift ValidationError cases.
34
+ * All validation error codes, verbatim camelCase mirror of Swift ValidationError cases.
35
35
  * Parity with native is enforced by scripts/checks/check-plugin-manifest-validator-parity.sh.
36
36
  */
37
- export declare const MANIFEST_VALIDATION_CODES: readonly ["unsupportedSchemaVersion", "missingManifest", "missingRuntimeEntry", "missingUIRoot", "legacyFieldNotSupported", "duplicateAttachmentRendererID", "duplicateDetectorID", "duplicateActionID", "emptyPluginID", "emptyPluginTitle", "installEntryMissing", "installEntryOutsideRoot", "rendererTitleEmpty", "detectorTitleEmpty", "actionTitleEmpty", "detectorMissingSupportedInputKinds", "detectorAttachmentTypesEmpty", "detectorAttachmentTypeNotRenderable", "detectorAttachmentTypeOutsideNamespace", "rendererUIEntryMissing", "rendererUIEntryOutsideRoot", "actionMissingSupportedItemTypes", "actionUIEntryMissing", "actionUIEntryOutsideRoot", "detectorInvalidSupportedInputKind", "actionInvalidSupportedItemType"];
37
+ export declare const MANIFEST_VALIDATION_CODES: readonly ["unsupportedSchemaVersion", "missingManifest", "missingRuntimeEntry", "missingUIRoot", "legacyFieldNotSupported", "duplicateAttachmentRendererID", "duplicateDetectorID", "duplicateActionID", "emptyPluginID", "invalidPluginID", "emptyPluginTitle", "installEntryMissing", "installEntryOutsideRoot", "rendererTitleEmpty", "detectorTitleEmpty", "actionTitleEmpty", "detectorMissingSupportedInputKinds", "detectorAttachmentTypesEmpty", "detectorAttachmentTypeNotRenderable", "detectorAttachmentTypeOutsideNamespace", "rendererUIEntryMissing", "rendererUIEntryOutsideRoot", "actionMissingSupportedInputKinds", "actionUIEntryMissing", "actionUIEntryOutsideRoot", "detectorInvalidSupportedInputKind", "actionInvalidSupportedInputKind"];
38
38
  /**
39
39
  * Validate a parsed manifest JSON object against the Clipbus plugin manifest rules.
40
40
  *
@@ -31,10 +31,10 @@ export interface ValidateManifestOptions {
31
31
  rootDir?: string;
32
32
  }
33
33
  /**
34
- * All 26 validation error codes, verbatim camelCase mirror of Swift ValidationError cases.
34
+ * All validation error codes, verbatim camelCase mirror of Swift ValidationError cases.
35
35
  * Parity with native is enforced by scripts/checks/check-plugin-manifest-validator-parity.sh.
36
36
  */
37
- export declare const MANIFEST_VALIDATION_CODES: readonly ["unsupportedSchemaVersion", "missingManifest", "missingRuntimeEntry", "missingUIRoot", "legacyFieldNotSupported", "duplicateAttachmentRendererID", "duplicateDetectorID", "duplicateActionID", "emptyPluginID", "emptyPluginTitle", "installEntryMissing", "installEntryOutsideRoot", "rendererTitleEmpty", "detectorTitleEmpty", "actionTitleEmpty", "detectorMissingSupportedInputKinds", "detectorAttachmentTypesEmpty", "detectorAttachmentTypeNotRenderable", "detectorAttachmentTypeOutsideNamespace", "rendererUIEntryMissing", "rendererUIEntryOutsideRoot", "actionMissingSupportedItemTypes", "actionUIEntryMissing", "actionUIEntryOutsideRoot", "detectorInvalidSupportedInputKind", "actionInvalidSupportedItemType"];
37
+ export declare const MANIFEST_VALIDATION_CODES: readonly ["unsupportedSchemaVersion", "missingManifest", "missingRuntimeEntry", "missingUIRoot", "legacyFieldNotSupported", "duplicateAttachmentRendererID", "duplicateDetectorID", "duplicateActionID", "emptyPluginID", "invalidPluginID", "emptyPluginTitle", "installEntryMissing", "installEntryOutsideRoot", "rendererTitleEmpty", "detectorTitleEmpty", "actionTitleEmpty", "detectorMissingSupportedInputKinds", "detectorAttachmentTypesEmpty", "detectorAttachmentTypeNotRenderable", "detectorAttachmentTypeOutsideNamespace", "rendererUIEntryMissing", "rendererUIEntryOutsideRoot", "actionMissingSupportedInputKinds", "actionUIEntryMissing", "actionUIEntryOutsideRoot", "detectorInvalidSupportedInputKind", "actionInvalidSupportedInputKind"];
38
38
  /**
39
39
  * Validate a parsed manifest JSON object against the Clipbus plugin manifest rules.
40
40
  *
@@ -11,6 +11,7 @@ var MANIFEST_VALIDATION_CODES = [
11
11
  "duplicateDetectorID",
12
12
  "duplicateActionID",
13
13
  "emptyPluginID",
14
+ "invalidPluginID",
14
15
  "emptyPluginTitle",
15
16
  "installEntryMissing",
16
17
  "installEntryOutsideRoot",
@@ -23,11 +24,11 @@ var MANIFEST_VALIDATION_CODES = [
23
24
  "detectorAttachmentTypeOutsideNamespace",
24
25
  "rendererUIEntryMissing",
25
26
  "rendererUIEntryOutsideRoot",
26
- "actionMissingSupportedItemTypes",
27
+ "actionMissingSupportedInputKinds",
27
28
  "actionUIEntryMissing",
28
29
  "actionUIEntryOutsideRoot",
29
30
  "detectorInvalidSupportedInputKind",
30
- "actionInvalidSupportedItemType"
31
+ "actionInvalidSupportedInputKind"
31
32
  ];
32
33
  var CANONICAL_ITEM_TYPES = ["text", "image", "path_reference"];
33
34
  function asRecord(val) {
@@ -54,25 +55,41 @@ function isInsideRoot(entryRelative, rootAbsolute) {
54
55
  const resolvedEntry = path.resolve(rootAbsolute, entryRelative);
55
56
  return resolvedEntry === rootAbsolute || resolvedEntry.startsWith(rootAbsolute + path.sep);
56
57
  }
58
+ var PLUGIN_ID_MAX_LENGTH = 128;
59
+ var PLUGIN_ID_PATTERN = /^[a-z0-9._-]+$/;
60
+ function isValidPluginID(id) {
61
+ if (id.length === 0 || id.length > PLUGIN_ID_MAX_LENGTH) return false;
62
+ if (id.startsWith(".") || id.includes("..")) return false;
63
+ return PLUGIN_ID_PATTERN.test(id);
64
+ }
57
65
  function validateSchemaVersion(raw, issues) {
58
66
  const version = typeof raw["schemaVersion"] === "number" ? raw["schemaVersion"] : 0;
59
- if (version !== 2) {
67
+ if (version !== 3) {
60
68
  issues.push(issue("unsupportedSchemaVersion", `Unsupported plugin schema version: ${version}`));
61
69
  }
62
70
  }
63
71
  var LEGACY_RENDERER_FIELDS = ["displayName", "defaultEnabled", "defaultOrderKey", "actions"];
64
72
  var LEGACY_DETECTOR_FIELDS = ["displayName", "capabilityID", "defaultEnabled", "timeoutMs"];
65
- var LEGACY_ACTION_FIELDS = ["displayName", "actionID", "defaultEnabled", "defaultOrderKey", "revealButtons"];
73
+ var LEGACY_ACTION_FIELDS = [
74
+ "displayName",
75
+ "actionID",
76
+ "defaultEnabled",
77
+ "defaultOrderKey",
78
+ "revealButtons",
79
+ "supportedItemTypes",
80
+ "inputConstraints",
81
+ "possibleResultKinds"
82
+ ];
66
83
  function validateLegacyFields(raw, issues) {
67
84
  const plugin = asRecord(raw["plugin"]);
68
85
  if (plugin && "displayName" in plugin) {
69
- issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v2: plugin.displayName`));
86
+ issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v3: plugin.displayName`));
70
87
  }
71
88
  const renderers = asRecordArray(raw["attachmentRenderers"]);
72
89
  for (let i = 0; i < renderers.length; i++) {
73
90
  for (const field of LEGACY_RENDERER_FIELDS) {
74
91
  if (field in renderers[i]) {
75
- issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v2: attachmentRenderers[${i}].${field}`));
92
+ issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v3: attachmentRenderers[${i}].${field}`));
76
93
  }
77
94
  }
78
95
  }
@@ -80,7 +97,7 @@ function validateLegacyFields(raw, issues) {
80
97
  for (let i = 0; i < detectors.length; i++) {
81
98
  for (const field of LEGACY_DETECTOR_FIELDS) {
82
99
  if (field in detectors[i]) {
83
- issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v2: detectors[${i}].${field}`));
100
+ issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v3: detectors[${i}].${field}`));
84
101
  }
85
102
  }
86
103
  }
@@ -88,7 +105,7 @@ function validateLegacyFields(raw, issues) {
88
105
  for (let i = 0; i < actions.length; i++) {
89
106
  for (const field of LEGACY_ACTION_FIELDS) {
90
107
  if (field in actions[i]) {
91
- issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v2: actions[${i}].${field}`));
108
+ issues.push(issue("legacyFieldNotSupported", `Legacy manifest field is not supported in v3: actions[${i}].${field}`));
92
109
  }
93
110
  }
94
111
  }
@@ -104,9 +121,9 @@ function validateCanonicalItemTypeSpellings(raw, issues) {
104
121
  }
105
122
  for (const action of asRecordArray(raw["actions"])) {
106
123
  const actionID = asString(action["id"]) ?? "";
107
- for (const value of asStringArray(action["supportedItemTypes"])) {
124
+ for (const value of asStringArray(action["supportedInputKinds"])) {
108
125
  if (!CANONICAL_ITEM_TYPES.includes(value)) {
109
- issues.push(issue("actionInvalidSupportedItemType", `Action supported item types contain unsupported value: ${actionID} -> ${value}`));
126
+ issues.push(issue("actionInvalidSupportedInputKind", `Action supported input kinds contain unsupported value: ${actionID} -> ${value}`));
110
127
  }
111
128
  }
112
129
  }
@@ -155,9 +172,9 @@ function validateActions(actions, issues) {
155
172
  if (title.trim().length === 0) {
156
173
  issues.push(issue("actionTitleEmpty", `Action title is empty: ${id}`));
157
174
  }
158
- const supportedItemTypes = asStringArray(action["supportedItemTypes"]);
159
- if (supportedItemTypes.length === 0) {
160
- issues.push(issue("actionMissingSupportedItemTypes", `Action is missing supported item types: ${id}`));
175
+ const supportedInputKinds = asStringArray(action["supportedInputKinds"]);
176
+ if (supportedInputKinds.length === 0) {
177
+ issues.push(issue("actionMissingSupportedInputKinds", `Action is missing supported input kinds: ${id}`));
161
178
  }
162
179
  }
163
180
  }
@@ -205,6 +222,8 @@ function validateManifest(manifest, options = {}) {
205
222
  const pluginTitle = asString(plugin["title"]) ?? "";
206
223
  if (pluginID.trim().length === 0) {
207
224
  issues.push(issue("emptyPluginID", "Plugin id is empty"));
225
+ } else if (!isValidPluginID(pluginID)) {
226
+ issues.push(issue("invalidPluginID", `Invalid plugin id: ${pluginID}`));
208
227
  }
209
228
  if (pluginTitle.trim().length === 0) {
210
229
  issues.push(issue("emptyPluginTitle", "Plugin title is empty"));
package/docs/authoring.md CHANGED
@@ -156,9 +156,9 @@ UI 端完整 topic/verb 列表见 [API.md](../API.md)。
156
156
  ### resolveSession 入参
157
157
 
158
158
  ```ts
159
- input.item // PluginClipboardItem
159
+ input.sourceItem // 原始 PluginClipboardItem 身份
160
160
  input.content // PluginContentEnvelope
161
- input.attachments // PluginAttachmentRef[]
161
+ input.attachments // SDK 0.9.0 中 Action 当前固定为 []
162
162
  ```
163
163
 
164
164
  ### resolveSession 返回值
@@ -179,11 +179,16 @@ return {
179
179
  ### runAutoAction 入参
180
180
 
181
181
  ```ts
182
- input.item // PluginClipboardItem
182
+ input.sourceItem // 原始 PluginClipboardItem 身份
183
183
  input.content // PluginContentEnvelope
184
- input.attachments // PluginAttachmentRef[]
184
+ input.attachments // SDK 0.9.0 中 Action 当前固定为 []
185
185
  ```
186
186
 
187
+ `sourceItem` 始终指向打开 Action Panel 时的原始 item;`content` 是当前
188
+ 级联值,可能已经从 text 变成 image 或 path_reference。Action 的适用性
189
+ 和处理逻辑必须以 `content.kind` 为准,item mutation 仍作用于
190
+ `sourceItem.id`。
191
+
187
192
  ### runAutoAction 返回值(用 `actionResult`)
188
193
 
189
194
  ```ts
@@ -196,6 +201,12 @@ actionResult.text('hello world', { userMessage: 'Copied' });
196
201
  // image 结果(注意:第一个参数是 imageTempPath 字符串,不是对象)
197
202
  actionResult.image(imageTempPath, { imageFormatHint: 'png', userMessage: 'Saved' });
198
203
 
204
+ // path_reference:复用当前输入,或返回 host 分配的文件
205
+ actionResult.path_reference([
206
+ { source: 'input', inputIndex: 0 },
207
+ { source: 'allocated_file', path: outputPath, displayName: 'result.txt' },
208
+ ]);
209
+
199
210
  // 仅副作用,无输出
200
211
  actionResult.none({ userMessage: 'Applied' });
201
212
  ```
@@ -205,6 +216,7 @@ actionResult.none({ userMessage: 'Applied' });
205
216
  ```ts
206
217
  actionResult.text(text: string, options?: { userMessage?: string })
207
218
  actionResult.image(imageTempPath: string, options?: { imageFormatHint?: string; userMessage?: string })
219
+ actionResult.path_reference(entries: PluginActionPathResultEntry[], options?: { userMessage?: string })
208
220
  actionResult.none(options?: { userMessage?: string })
209
221
  ```
210
222
 
@@ -221,6 +233,9 @@ interface MyDraft { subject: string; note: string; }
221
233
  const initial = clipbus.action.draft.current() as MyDraft | undefined;
222
234
  const form = reactive({ subject: initial?.subject ?? '', note: initial?.note ?? '' });
223
235
 
236
+ // 当前级联输入;path_reference 只含 kind/displayName,不暴露 path
237
+ const currentInput = clipbus.action.input.current();
238
+
224
239
  // 订阅 host 推送的后续更新(罕见;通常 initialDraft 之后就由 UI 接管)
225
240
  clipbus.action.draft.on(next => {
226
241
  Object.assign(form, next);
@@ -247,6 +262,9 @@ async function handleApply() {
247
262
  }
248
263
  ```
249
264
 
265
+ Draft 完成后宿主展示结果供复制或关闭,但不会把结果移交到下一个 Action;
266
+ 只有 auto-run Action 的非 `none` 成功结果可以继续级联。
267
+
250
268
  ### Draft Action:UI 需要 runtime 协助时
251
269
 
252
270
  `clipbus.action.complete` 接收 `{result: {resultKind: 'text' | 'image' | 'none', …}}`。**image 结果的 `imageTempPath` 必须由 runtime 端通过 `host.action.allocateImageTempPath` 分配**(UI 端没有这个能力),所以图片类 draft 的典型流程是:
@@ -285,4 +303,16 @@ async function handleApplyImage(prompt: string) {
285
303
 
286
304
  > **不要**尝试通过 `runtime.invoke` 把图片字节传回 UI。postMessage 经 base64 + JSON 双重序列化,几 MB 图片就会卡顿数百 ms。让 runtime 直接写文件、只把路径返回给 UI。
287
305
 
306
+ ### Action 当前图片与安全文件结果
307
+
308
+ 读取当前 Action 图片使用 `ctx.host.action.materializeInputImagePath()`,不要
309
+ 用 `ctx.host.item.materializeImagePath()`:后者读取原始 item 图片,而级联
310
+ 后的当前值可能来自上一步 Action。
311
+
312
+ 普通文件结果必须先调用
313
+ `ctx.host.action.allocateOutputFilePath({suggestedName})`,写入返回路径,再
314
+ 通过 `{source: 'allocated_file', path, displayName}` 返回。若只是保留当前
315
+ path_reference 的某一项,使用 `{source: 'input', inputIndex}`。任意绝对
316
+ 路径不会被宿主接受。
317
+
288
318
  RPC 机制(`defineMessage` / `messageHandlers`)的完整说明见 [rpc.md](./rpc.md)。入参结构(`PluginContentEnvelope`、`PluginClipboardItem`)详见 [item-context.md](./item-context.md)。
@@ -60,7 +60,7 @@ import type { PluginActionHandler } from '@clipbus/plugin-sdk/runtime';
60
60
  const action: PluginActionHandler = {
61
61
  async runAutoAction(input, ctx) {
62
62
  if (ctx.host.capabilities.has('item.setTags')) {
63
- await ctx.host.item.setTags({ itemID: input.item.id, tags: ['processed'] });
63
+ await ctx.host.item.setTags({ tags: ['processed'] });
64
64
  }
65
65
  // 不支持时跳过,不报错
66
66
  },
@@ -73,7 +73,7 @@ Node 侧错误来自**跨进程 IPC**,抛出的是反序列化的普通对象
73
73
 
74
74
  ```ts
75
75
  try {
76
- await ctx.host.item.setTags({ itemID: input.item.id, tags: ['processed'] });
76
+ await ctx.host.item.setTags({ tags: ['processed'] });
77
77
  } catch (err: unknown) {
78
78
  const e = err as { name?: string; data?: { capability?: string } };
79
79
  if (e.name === 'PluginCapabilityUnsupported') {
package/docs/entry.md CHANGED
@@ -34,10 +34,10 @@ import { PluginContextError } from '@clipbus/plugin-sdk/ui';
34
34
  | Namespace | 形态 | 说明 |
35
35
  |---|---|---|
36
36
  | `clipbus.item` | Topic | `current()`, `on()`, `readAttachment()`,子 OptionalTopic `clipbus.item.attachment` |
37
- | `clipbus.asset` | Verb | `currentItemImageUrl()`、`pathReferenceImageUrl({index})` —— 按身份取本地图片的 `clipbus-asset://` URL 供 WebView `<img>` 渲染(真实路径不出宿主);Node 产图经 `host.asset.registerImage({path})` |
37
+ | `clipbus.asset` | Verb | renderer 使用 `currentItemImageUrl()` / `pathReferenceImageUrl({index})`;Action 使用 `currentActionInputImageUrl()` / `currentActionInputPathReferenceImageUrl({index})`。均返回不透明 `clipbus-asset://` URL |
38
38
  | `clipbus.theme` | Topic | `current()`, `on()` |
39
39
  | `clipbus.pluginContext` | Topic | `current()`, `on()`,返回 `{mode, pluginID}` |
40
- | `clipbus.action` | Verb + OptionalTopic | action 上下文专属;`setButtons`、`complete`、`draft`、`onHostInvoke` |
40
+ | `clipbus.action` | Verb + Topic | action 上下文专属;`setButtons`、`complete`、`draft`、`input`、`onHostInvoke` |
41
41
  | `clipbus.attachmentRenderer` | Verb + Stream | attachment 上下文专属;`setButtons`、`onHostInvoke` |
42
42
  | `clipbus.window` | Verb | `setHeight`, `autoFit` |
43
43
  | `clipbus.clipboard` | Verb | `copyText` |