@ericsanchezok/synergy-plugin 2.4.2 → 2.4.4

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/README.md CHANGED
@@ -65,15 +65,14 @@ export default plugin
65
65
 
66
66
  Tools can return user-facing files through `attachments`. Use the generated SDK `asset.upload()` route or the public `/asset` endpoint to upload binary data, then return the resulting `asset://...` URL. Do not import Synergy internal asset modules from a plugin.
67
67
 
68
- For visual tools whose output belongs in the main answer area, set `metadata.display.presentation` to `artifact-only` and list the attachment ids to promote:
68
+ For visual tools whose output belongs in the main answer area, hide the tool card and set presentation on the returned attachment:
69
69
 
70
70
  ```ts
71
71
  return {
72
72
  output: "",
73
73
  metadata: {
74
74
  display: {
75
- presentation: "artifact-only",
76
- primaryAttachmentIds: [partId],
75
+ toolCard: "hidden",
77
76
  },
78
77
  },
79
78
  attachments: [
@@ -81,30 +80,27 @@ return {
81
80
  id: partId,
82
81
  sessionID: context.sessionID,
83
82
  messageID: context.messageID,
84
- type: "file",
83
+ type: "attachment",
85
84
  mime: "image/svg+xml",
86
85
  filename: "result.svg",
87
86
  url: uploaded.url,
87
+ presentation: { renderer: "image", size: "medium", crop: false },
88
+ model: { mode: "summary", summary: "Generated SVG result." },
88
89
  },
89
90
  ],
90
91
  }
91
92
  ```
92
93
 
93
- Running and failed tool states still render normally, so progress, approvals, and errors remain visible.
94
+ Each attachment controls its own display with `presentation.hidden`, `presentation.renderer`, `presentation.size`, and `presentation.crop`. Omit `renderer` to let Synergy choose from the MIME type.
94
95
 
95
- For image, video, or audio generation tools, declare the display protocol on the tool definition as well. This lets Synergy show its built-in media generation placeholder as soon as the tool starts, then replace it with the promoted attachment when the tool completes:
96
+ For image, video, or audio generation tools, declare the display protocol on the tool definition as well. This lets Synergy show its built-in media generation placeholder as soon as the tool starts, then replace it with the completed attachment in the original message order:
96
97
 
97
98
  ```ts
98
99
  const mediaDisplay = {
99
100
  kind: "media-generation",
100
- visibility: "media",
101
- presentation: "artifact-only",
101
+ toolCard: "hidden",
102
102
  media: {
103
103
  type: "image",
104
- actionLabel: "Create image",
105
- pendingTitle: "Generating image",
106
- pendingDescription: "Preparing the image...",
107
- promptField: "prompt",
108
104
  aspectRatio: "1:1",
109
105
  },
110
106
  } as const
@@ -116,12 +112,12 @@ tool({
116
112
  prompt: tool.schema.string(),
117
113
  },
118
114
  async execute(args, context) {
119
- // Upload the generated image, then return metadata.display with primaryAttachmentIds.
115
+ // Upload the generated image, then return it with attachment-level presentation.
120
116
  },
121
117
  })
122
118
  ```
123
119
 
124
- Use `visibility: "media"` for tools whose running and completed success states belong on the media surface. Error states still fall back to normal tool cards.
120
+ Use `toolCard: "hidden"` for tools whose running and completed states belong on a dedicated surface instead of a tool card. Optional media labels are for accessibility and host-specific status surfaces; Synergy does not show tool input parameters as transcript copy.
125
121
 
126
122
  ## Internal Tools And Delegated Tasks
127
123
 
@@ -152,7 +148,7 @@ const plan = await context.task?.run({
152
148
  "plugin__my-plugin__private_helper": true,
153
149
  },
154
150
  visibility: "hidden",
155
- timeoutMs: 30_000,
151
+ timeoutMs: 120_000,
156
152
  output: {
157
153
  mode: "structured",
158
154
  schema: {
@@ -188,7 +184,7 @@ type PluginInput = {
188
184
  }
189
185
  ```
190
186
 
191
- For isolated worker/process plugins, these services are proxied through the host bridge and checked against the plugin approval record.
187
+ For isolated worker/process plugins, these services are proxied through the host bridge and checked against the plugin approval record. Workspace file and shell bridge calls require an active plugin tool context; read plugin package assets directly from `input.pluginDir`.
192
188
 
193
189
  ## Manifest
194
190
 
@@ -200,9 +196,11 @@ Each distributable plugin has a root `plugin.json`:
200
196
  "version": "0.1.0",
201
197
  "description": "Example Synergy plugin",
202
198
  "main": "./src/index.ts",
199
+ "engines": {
200
+ "synergy": ">=2.4.3",
201
+ },
203
202
  "permissions": {
204
203
  "tools": {
205
- "invoke": true,
206
204
  "filesystem": "none",
207
205
  "network": false,
208
206
  "shell": false,
@@ -235,15 +233,50 @@ Each distributable plugin has a root `plugin.json`:
235
233
 
236
234
  `contributes.ui.entry` is a runtime-loadable JavaScript asset. Source files such as `src/ui.tsx` are only build inputs. `synergy-plugin build` uses the conventional UI source path and writes the compiled bundle to the declared entry.
237
235
 
236
+ Session workbench UI uses `contributes.ui.workbenchPanels`. A workbench panel declares which surface it belongs to and how its tabs behave:
237
+
238
+ ```jsonc
239
+ {
240
+ "contributes": {
241
+ "ui": {
242
+ "entry": "./dist/ui/index.js",
243
+ "workbenchPanels": [
244
+ {
245
+ "id": "build-log",
246
+ "label": "Build Log",
247
+ "icon": "terminal",
248
+ "exportName": "BuildLogPanel",
249
+ "surface": "bottom",
250
+ "cardinality": "multi",
251
+ "requiresSession": true,
252
+ },
253
+ ],
254
+ },
255
+ },
256
+ "permissions": {
257
+ "ui": {
258
+ "workbenchPanels": true,
259
+ },
260
+ },
261
+ }
262
+ ```
263
+
264
+ `surface` is `"side"` or `"bottom"`. `cardinality` is `"exclusive"` for one active panel on that surface, `"singleton"` for one tab per panel id, or `"multi"` for a new tab each time. `requiresSession` hides the panel until the user is in a concrete session. App panels remain separate under `contributes.ui.appPanels` and create top-level sidebar entries.
265
+
238
266
  ## UI Types
239
267
 
240
268
  UI contribution types are exported separately:
241
269
 
242
270
  ```ts
243
- import type { PluginToolRendererProps, PluginPanelProps } from "@ericsanchezok/synergy-plugin/ui"
271
+ import type {
272
+ PluginToolRendererProps,
273
+ PluginWorkbenchPanel,
274
+ PluginPanelProps,
275
+ PluginMessageSlotProps,
276
+ } from "@ericsanchezok/synergy-plugin/ui"
244
277
  ```
245
278
 
246
- Supported UI surfaces are tool renderers, part renderers, workspace panels, global panels, settings sections, chat components, themes, icons, routes, and commands. The Web client loads aggregated UI metadata with the generated SDK method `plugin.listUiContributions()`, which maps to `/plugin/ui/contributions`; plugin JS and assets are still loaded through browser-native asset URLs.
279
+ Supported UI surfaces are tool renderers, part renderers, workbench panels, app panels, settings sections, message slots, themes, icons, app routes, and commands. The Web client loads aggregated UI metadata with the generated SDK method `plugin.listUiContributions()`, which maps to `/plugin/ui/contributions`; plugin JS and assets are still loaded through browser-native asset URLs.
247
280
 
248
281
  ## Runtime Modes
249
282
 
@@ -266,7 +299,7 @@ Worker and process plugins are started through Synergy's plugin runner. The runn
266
299
  - `dist/permissions.summary.json`
267
300
  - `dist/integrity.json`
268
301
 
269
- `synergy-plugin pack` archives `dist/` into `<name>-<version>.synergy-plugin.tgz`. `synergy-plugin sign` writes `<tarball>.sig`. `synergy-plugin publish-market` prepares the official marketplace submission by uploading or checking GitHub Release assets, writing a `SII-Holos/synergy-plugins` entry with the signer public key, regenerating the registry index, running registry validation, and opening a PR when `gh` is available.
302
+ `synergy-plugin pack` archives `dist/` into `<name>-<version>.synergy-plugin.tgz`. `synergy-plugin sign` writes `<tarball>.sig`. `synergy-plugin publish-market` prepares the official marketplace submission by uploading or checking GitHub Release assets, writing a `SII-Holos/synergy-plugins` entry with the signer public key and `compatibility.synergy` from `plugin.json` `engines.synergy`, regenerating the registry index, running registry validation, and opening a PR when `gh` is available.
270
303
 
271
304
  For local marketplace UX testing, the Synergy runtime still provides `synergy plugin publish <tarball>` to publish into the local development registry.
272
305
 
@@ -0,0 +1,15 @@
1
+ export declare namespace PluginArtifact {
2
+ const manifestFile = "plugin.json";
3
+ const normalizedManifestFile = "plugin.normalized.json";
4
+ const runtimeEntry = "runtime/index.js";
5
+ const integrityFile = "integrity.json";
6
+ const permissionsSummaryFile = "permissions.summary.json";
7
+ const requiredFiles: readonly ["plugin.json", "runtime/index.js", "integrity.json", "permissions.summary.json"];
8
+ const assetRoutePrefix = "/plugin/assets";
9
+ const allowedAssetRoots: readonly ["dist", "public", "assets", "ui", "themes", "icons"];
10
+ }
11
+ export declare function normalizePluginArtifactPath(filePath: string): string;
12
+ export declare function normalizePluginArchiveEntry(entry: string): string | undefined;
13
+ export declare function pluginArtifactAssetRoot(filePath: string): string;
14
+ export declare function isAllowedPluginAssetPath(filePath: string): boolean;
15
+ export declare function pluginAssetUrl(pluginId: string, versionHash: string, filePath: string): string;
@@ -0,0 +1,52 @@
1
+ export var PluginArtifact;
2
+ (function (PluginArtifact) {
3
+ PluginArtifact.manifestFile = "plugin.json";
4
+ PluginArtifact.normalizedManifestFile = "plugin.normalized.json";
5
+ PluginArtifact.runtimeEntry = "runtime/index.js";
6
+ PluginArtifact.integrityFile = "integrity.json";
7
+ PluginArtifact.permissionsSummaryFile = "permissions.summary.json";
8
+ PluginArtifact.requiredFiles = [PluginArtifact.manifestFile, PluginArtifact.runtimeEntry, PluginArtifact.integrityFile, PluginArtifact.permissionsSummaryFile];
9
+ PluginArtifact.assetRoutePrefix = "/plugin/assets";
10
+ PluginArtifact.allowedAssetRoots = ["dist", "public", "assets", "ui", "themes", "icons"];
11
+ })(PluginArtifact || (PluginArtifact = {}));
12
+ export function normalizePluginArtifactPath(filePath) {
13
+ const normalized = filePath.replace(/\\/g, "/").replace(/^\.\//, "");
14
+ if (!normalized || normalized === ".")
15
+ throw new Error("Plugin artifact path cannot be empty");
16
+ if (normalized.startsWith("/") || /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(normalized)) {
17
+ throw new Error(`Plugin artifact path must be relative: ${filePath}`);
18
+ }
19
+ const parts = normalized.split("/").filter(Boolean);
20
+ if (parts.includes(".."))
21
+ throw new Error(`Plugin artifact path cannot escape the plugin root: ${filePath}`);
22
+ return parts.join("/");
23
+ }
24
+ export function normalizePluginArchiveEntry(entry) {
25
+ let normalized = entry.replace(/\r$/, "").replace(/\\/g, "/");
26
+ while (normalized.startsWith("./"))
27
+ normalized = normalized.slice(2);
28
+ normalized = normalized.replace(/\/+$/, "");
29
+ if (!normalized || normalized === ".")
30
+ return undefined;
31
+ return normalizePluginArtifactPath(normalized);
32
+ }
33
+ export function pluginArtifactAssetRoot(filePath) {
34
+ return normalizePluginArtifactPath(filePath).split("/")[0] ?? "";
35
+ }
36
+ export function isAllowedPluginAssetPath(filePath) {
37
+ try {
38
+ const root = pluginArtifactAssetRoot(filePath);
39
+ return PluginArtifact.allowedAssetRoots.includes(root);
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ export function pluginAssetUrl(pluginId, versionHash, filePath) {
46
+ const normalized = normalizePluginArtifactPath(filePath);
47
+ const encodedPath = normalized
48
+ .split("/")
49
+ .map((part) => encodeURIComponent(part))
50
+ .join("/");
51
+ return `${PluginArtifact.assetRoutePrefix}/${encodeURIComponent(pluginId)}/${encodeURIComponent(versionHash)}/${encodedPath}`;
52
+ }
package/dist/display.d.ts CHANGED
@@ -3,13 +3,10 @@ export interface ToolMediaDisplay {
3
3
  actionLabel?: string;
4
4
  pendingTitle?: string;
5
5
  pendingDescription?: string;
6
- promptField?: string;
7
6
  aspectRatio?: "1:1" | "4:3" | "16:9" | "auto";
8
7
  }
9
8
  export interface ToolDisplay {
10
9
  kind?: "default" | "media-generation";
11
- visibility?: "default" | "media" | "hidden-unless-error";
12
- presentation?: "default" | "artifact-only";
10
+ toolCard?: "visible" | "hidden";
13
11
  media?: ToolMediaDisplay;
14
- primaryAttachmentIds?: string[];
15
12
  }
package/dist/ids.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export declare namespace PluginToolId {
2
+ function format(pluginId: string, toolId: string): string;
3
+ function parse(id: string): {
4
+ pluginId: string;
5
+ toolId: string;
6
+ } | undefined;
7
+ function is(id: string): boolean;
8
+ }
9
+ export declare namespace PluginId {
10
+ function isValid(id: string): boolean;
11
+ function normalize(id: string): string;
12
+ function mcpServerKey(pluginId: string, serverKey: string): string;
13
+ }
package/dist/ids.js ADDED
@@ -0,0 +1,33 @@
1
+ export var PluginToolId;
2
+ (function (PluginToolId) {
3
+ function format(pluginId, toolId) {
4
+ return `plugin__${pluginId}__${toolId}`;
5
+ }
6
+ PluginToolId.format = format;
7
+ function parse(id) {
8
+ const match = id.match(/^plugin__([^_]+(?:_[^_]+)*?)__([^_].*)$/);
9
+ if (!match)
10
+ return undefined;
11
+ return { pluginId: match[1], toolId: match[2] };
12
+ }
13
+ PluginToolId.parse = parse;
14
+ function is(id) {
15
+ return id.startsWith("plugin__");
16
+ }
17
+ PluginToolId.is = is;
18
+ })(PluginToolId || (PluginToolId = {}));
19
+ export var PluginId;
20
+ (function (PluginId) {
21
+ function isValid(id) {
22
+ return /^[a-z][a-z0-9_-]*$/i.test(id);
23
+ }
24
+ PluginId.isValid = isValid;
25
+ function normalize(id) {
26
+ return id.trim().toLowerCase();
27
+ }
28
+ PluginId.normalize = normalize;
29
+ function mcpServerKey(pluginId, serverKey) {
30
+ return `${pluginId}::${serverKey}`;
31
+ }
32
+ PluginId.mcpServerKey = mcpServerKey;
33
+ })(PluginId || (PluginId = {}));
package/dist/index.d.ts CHANGED
@@ -5,11 +5,15 @@ export * from "./tool";
5
5
  export type { ToolDisplay, ToolMediaDisplay } from "./display";
6
6
  export type { ToolResult };
7
7
  export * from "./manifest";
8
+ export * from "./policy";
9
+ export * from "./spec";
10
+ export * from "./version";
11
+ export * from "./artifact";
8
12
  export type { BunShell, BunShellOutput, BunShellPromise, ShellExpression, ShellFunction } from "./shell";
9
13
  export interface PluginConfigAccessor {
10
14
  /** Get the plugin's full config object */
11
15
  get(): Promise<Record<string, any>>;
12
- /** Set one or more config values (deep-merged into the plugin's namespace) */
16
+ /** Replace the plugin's config namespace with these values */
13
17
  set(values: Record<string, any>): Promise<void>;
14
18
  }
15
19
  /**
@@ -310,6 +314,15 @@ export type ProviderProfileHook = {
310
314
  modelID: string;
311
315
  options?: Record<string, any>;
312
316
  }) => Promise<any>;
317
+ fetchModelCatalog?: (input: {
318
+ auth?: Auth;
319
+ fetch?: typeof fetch;
320
+ baseURL?: string;
321
+ }) => Promise<Array<{
322
+ id: string;
323
+ rank?: number;
324
+ model?: Partial<Model>;
325
+ }>>;
313
326
  fetchModels?: (input: {
314
327
  auth?: Auth;
315
328
  fetch?: typeof fetch;
package/dist/index.js CHANGED
@@ -1,2 +1,6 @@
1
1
  export * from "./tool";
2
2
  export * from "./manifest";
3
+ export * from "./policy";
4
+ export * from "./spec";
5
+ export * from "./version";
6
+ export * from "./artifact";