@ericsanchezok/synergy-plugin 2.2.2 → 2.3.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/README.md CHANGED
@@ -61,6 +61,68 @@ export default plugin
61
61
 
62
62
  There is no compatibility layer for legacy descriptor shapes. `plugin.json.name` must match `plugin.id`; Synergy fails validation or loading if they differ.
63
63
 
64
+ ## Tool Results And Attachments
65
+
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
+
68
+ For visual tools whose output should appear as the main answer instead of a tool card, set `metadata.display.presentation` to `artifact-only` and list the attachment ids to promote:
69
+
70
+ ```ts
71
+ return {
72
+ output: "",
73
+ metadata: {
74
+ display: {
75
+ presentation: "artifact-only",
76
+ primaryAttachmentIds: [partId],
77
+ },
78
+ },
79
+ attachments: [
80
+ {
81
+ id: partId,
82
+ sessionID: context.sessionID,
83
+ messageID: context.messageID,
84
+ type: "file",
85
+ mime: "image/svg+xml",
86
+ filename: "result.svg",
87
+ url: uploaded.url,
88
+ },
89
+ ],
90
+ }
91
+ ```
92
+
93
+ Running and failed tool states still render normally, so progress, approvals, and errors remain visible.
94
+
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
+
97
+ ```ts
98
+ const mediaDisplay = {
99
+ kind: "media-generation",
100
+ visibility: "media",
101
+ presentation: "artifact-only",
102
+ media: {
103
+ type: "image",
104
+ actionLabel: "Create image",
105
+ pendingTitle: "Generating image",
106
+ pendingDescription: "Preparing the image...",
107
+ promptField: "prompt",
108
+ aspectRatio: "1:1",
109
+ },
110
+ } as const
111
+
112
+ tool({
113
+ description: "Generate an image",
114
+ display: mediaDisplay,
115
+ args: {
116
+ prompt: tool.schema.string(),
117
+ },
118
+ async execute(args, context) {
119
+ // Upload the generated image, then return metadata.display with primaryAttachmentIds.
120
+ },
121
+ })
122
+ ```
123
+
124
+ Use `visibility: "media"` for tools whose running and completed success states should be represented by the media surface instead of the ordinary tool transcript. Error states still fall back to normal tool cards.
125
+
64
126
  ## Plugin Input
65
127
 
66
128
  `init(input)` receives runtime services scoped to the active Synergy Scope:
@@ -107,6 +169,9 @@ Each distributable plugin has a root `plugin.json`:
107
169
  "name": "greet",
108
170
  "title": "Greet",
109
171
  "description": "Greet a user by name",
172
+ "display": {
173
+ "kind": "default",
174
+ },
110
175
  "capabilities": {
111
176
  "filesystem": "none",
112
177
  "network": false,
@@ -0,0 +1,15 @@
1
+ export interface ToolMediaDisplay {
2
+ type: "image" | "video" | "audio";
3
+ actionLabel?: string;
4
+ pendingTitle?: string;
5
+ pendingDescription?: string;
6
+ promptField?: string;
7
+ aspectRatio?: "1:1" | "4:3" | "16:9" | "auto";
8
+ }
9
+ export interface ToolDisplay {
10
+ kind?: "default" | "media-generation";
11
+ visibility?: "default" | "media" | "hidden-unless-error";
12
+ presentation?: "default" | "artifact-only";
13
+ media?: ToolMediaDisplay;
14
+ primaryAttachmentIds?: string[];
15
+ }
File without changes
package/dist/hooks.js CHANGED
@@ -11,6 +11,12 @@ export const HOOKS = [
11
11
  mutatesOutput: false,
12
12
  summary: "Add provider auth methods and auth loaders",
13
13
  },
14
+ {
15
+ name: "provider",
16
+ category: "core",
17
+ mutatesOutput: false,
18
+ summary: "Register provider catalog/runtime profiles",
19
+ },
14
20
  {
15
21
  name: "config",
16
22
  category: "core",
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { AgendaItem, AgendaRunLog, CortexTask, Event, createSynergyClient,
2
2
  import type { BunShell } from "./shell";
3
3
  import type { ToolDefinition, ToolResult } from "./tool";
4
4
  export * from "./tool";
5
+ export type { ToolDisplay, ToolMediaDisplay } from "./display";
5
6
  export type { ToolResult };
6
7
  export * from "./manifest";
7
8
  export type { BunShell, BunShellOutput, BunShellPromise, ShellExpression, ShellFunction } from "./shell";
@@ -150,8 +151,43 @@ export type AuthHook = {
150
151
  } | {
151
152
  type: "failed";
152
153
  }>;
154
+ } | {
155
+ type: "import";
156
+ label: string;
157
+ prompts?: Array<{
158
+ type: "text";
159
+ key: string;
160
+ message: string;
161
+ placeholder?: string;
162
+ validate?: (value: string) => string | undefined;
163
+ condition?: (inputs: Record<string, string>) => boolean;
164
+ } | {
165
+ type: "select";
166
+ key: string;
167
+ message: string;
168
+ options: Array<{
169
+ label: string;
170
+ value: string;
171
+ hint?: string;
172
+ }>;
173
+ condition?: (inputs: Record<string, string>) => boolean;
174
+ }>;
175
+ import(inputs?: Record<string, string>): Promise<AuthImportResult>;
153
176
  })[];
154
177
  };
178
+ export type AuthImportResult = ({
179
+ type: "success";
180
+ provider?: string;
181
+ } & ({
182
+ refresh: string;
183
+ access: string;
184
+ expires: number;
185
+ } | {
186
+ key: string;
187
+ })) | {
188
+ type: "failed";
189
+ message?: string;
190
+ };
155
191
  export type AuthOuathResult = {
156
192
  url: string;
157
193
  instructions: string;
@@ -184,6 +220,88 @@ export type AuthOuathResult = {
184
220
  type: "failed";
185
221
  }>;
186
222
  });
223
+ export type ProviderProfileHook = {
224
+ id: string;
225
+ name: string;
226
+ aliases?: string[];
227
+ description?: string;
228
+ signupUrl?: string;
229
+ env?: string[];
230
+ baseURL?: string;
231
+ modelsURL?: string;
232
+ apiMode?: "chat_completions" | "responses" | "anthropic_messages" | "codex_responses" | "external_process";
233
+ authKind?: "api_key" | "oauth" | "oauth_external" | "copilot" | "wellknown" | "none";
234
+ aiSdkPackage?: string;
235
+ modelFactory?: "languageModel" | "openaiResponses" | "openaiChat" | "openaiResponsesUnlessCompletionUrls" | "call" | "copilotAuto" | "anthropicMessages";
236
+ modelsDevProviderID?: string;
237
+ fallbackModels?: string[];
238
+ defaultAuxModel?: string;
239
+ usageKind?: "codex" | "anthropic-oauth" | "openrouter" | "unsupported";
240
+ healthCheck?: "models" | "none";
241
+ requestQuirks?: string[];
242
+ autoload?: (input: {
243
+ providerID: string;
244
+ auth?: Auth;
245
+ provider?: Provider;
246
+ }) => Promise<boolean>;
247
+ resolveAuth?: (input: {
248
+ providerID: string;
249
+ auth?: Auth;
250
+ provider?: Provider;
251
+ }) => Promise<Auth | undefined>;
252
+ refreshAuth?: (input: {
253
+ providerID: string;
254
+ auth?: Auth;
255
+ provider?: Provider;
256
+ }) => Promise<Auth | undefined>;
257
+ buildHeaders?: (input: {
258
+ providerID: string;
259
+ auth?: Auth;
260
+ url: string;
261
+ headers: Headers;
262
+ body?: unknown;
263
+ }) => Promise<Record<string, string> | Headers | undefined>;
264
+ rewriteBody?: (input: {
265
+ providerID: string;
266
+ auth?: Auth;
267
+ url: string;
268
+ headers: Headers;
269
+ body?: unknown;
270
+ }) => Promise<unknown>;
271
+ modelOptions?: (input: {
272
+ providerID: string;
273
+ auth?: Auth;
274
+ provider?: Provider;
275
+ }) => Promise<Record<string, any>>;
276
+ classifyError?: (input: {
277
+ providerID: string;
278
+ status?: number;
279
+ error?: unknown;
280
+ body?: unknown;
281
+ }) => {
282
+ code: string;
283
+ retryable: boolean;
284
+ reloginRequired?: boolean;
285
+ exhausted?: boolean;
286
+ cooldownUntil?: number;
287
+ resetAt?: number;
288
+ } | undefined;
289
+ runtimeOptions?: (input: {
290
+ providerID: string;
291
+ auth?: Auth;
292
+ provider?: Provider;
293
+ }) => Promise<Record<string, any>>;
294
+ getModel?: (input: {
295
+ sdk: any;
296
+ modelID: string;
297
+ options?: Record<string, any>;
298
+ }) => Promise<any>;
299
+ fetchModels?: (input: {
300
+ auth?: Auth;
301
+ fetch?: typeof fetch;
302
+ baseURL?: string;
303
+ }) => Promise<string[]>;
304
+ };
187
305
  export type PluginInput = {
188
306
  client: ReturnType<typeof createSynergyClient>;
189
307
  scope: {
@@ -235,6 +353,8 @@ export interface PluginHooks {
235
353
  tool?: Record<string, ToolDefinition>;
236
354
  /** Provider auth integration */
237
355
  auth?: AuthHook;
356
+ /** Provider runtime/catalog profiles */
357
+ provider?: ProviderProfileHook | ProviderProfileHook[];
238
358
  /** Observe runtime bus events */
239
359
  event?(input: {
240
360
  event: Event;
@@ -53,6 +53,7 @@ export declare const PluginManifest: z.ZodObject<{
53
53
  config: z.ZodDefault<z.ZodEnum<{
54
54
  plugin: "plugin";
55
55
  global: "global";
56
+ none: "none";
56
57
  }>>;
57
58
  secrets: z.ZodDefault<z.ZodEnum<{
58
59
  none: "none";
@@ -107,6 +108,52 @@ export declare const PluginManifest: z.ZodObject<{
107
108
  icon: z.ZodOptional<z.ZodString>;
108
109
  category: z.ZodOptional<z.ZodString>;
109
110
  kind: z.ZodOptional<z.ZodString>;
111
+ exposure: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
112
+ mode: z.ZodLiteral<"resident">;
113
+ }, z.core.$strict>, z.ZodObject<{
114
+ mode: z.ZodLiteral<"group">;
115
+ group: z.ZodString;
116
+ title: z.ZodOptional<z.ZodString>;
117
+ description: z.ZodOptional<z.ZodString>;
118
+ whenToExpand: z.ZodOptional<z.ZodString>;
119
+ }, z.core.$strict>, z.ZodObject<{
120
+ mode: z.ZodLiteral<"search">;
121
+ title: z.ZodOptional<z.ZodString>;
122
+ keywords: z.ZodOptional<z.ZodArray<z.ZodString>>;
123
+ }, z.core.$strict>], "mode">>;
124
+ display: z.ZodOptional<z.ZodObject<{
125
+ kind: z.ZodOptional<z.ZodEnum<{
126
+ default: "default";
127
+ "media-generation": "media-generation";
128
+ }>>;
129
+ visibility: z.ZodOptional<z.ZodEnum<{
130
+ default: "default";
131
+ media: "media";
132
+ "hidden-unless-error": "hidden-unless-error";
133
+ }>>;
134
+ presentation: z.ZodOptional<z.ZodEnum<{
135
+ default: "default";
136
+ "artifact-only": "artifact-only";
137
+ }>>;
138
+ media: z.ZodOptional<z.ZodObject<{
139
+ type: z.ZodEnum<{
140
+ image: "image";
141
+ video: "video";
142
+ audio: "audio";
143
+ }>;
144
+ actionLabel: z.ZodOptional<z.ZodString>;
145
+ pendingTitle: z.ZodOptional<z.ZodString>;
146
+ pendingDescription: z.ZodOptional<z.ZodString>;
147
+ promptField: z.ZodOptional<z.ZodString>;
148
+ aspectRatio: z.ZodOptional<z.ZodEnum<{
149
+ "1:1": "1:1";
150
+ "4:3": "4:3";
151
+ "16:9": "16:9";
152
+ auto: "auto";
153
+ }>>;
154
+ }, z.core.$strict>>;
155
+ primaryAttachmentIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
156
+ }, z.core.$strict>>;
110
157
  capabilities: z.ZodOptional<z.ZodObject<{
111
158
  filesystem: z.ZodOptional<z.ZodEnum<{
112
159
  read: "read";
@@ -315,6 +362,7 @@ export declare const PluginManifest: z.ZodObject<{
315
362
  config: z.ZodDefault<z.ZodEnum<{
316
363
  plugin: "plugin";
317
364
  global: "global";
365
+ none: "none";
318
366
  }>>;
319
367
  secrets: z.ZodDefault<z.ZodEnum<{
320
368
  none: "none";
package/dist/manifest.js CHANGED
@@ -87,6 +87,48 @@ const UICommandDef = z
87
87
  icon: z.string().optional(),
88
88
  })
89
89
  .strict();
90
+ const ToolExposureDef = z.discriminatedUnion("mode", [
91
+ z
92
+ .object({
93
+ mode: z.literal("resident"),
94
+ })
95
+ .strict(),
96
+ z
97
+ .object({
98
+ mode: z.literal("group"),
99
+ group: z.string().min(1),
100
+ title: z.string().optional(),
101
+ description: z.string().optional(),
102
+ whenToExpand: z.string().optional(),
103
+ })
104
+ .strict(),
105
+ z
106
+ .object({
107
+ mode: z.literal("search"),
108
+ title: z.string().optional(),
109
+ keywords: z.array(z.string()).optional(),
110
+ })
111
+ .strict(),
112
+ ]);
113
+ const ToolDisplayDef = z
114
+ .object({
115
+ kind: z.enum(["default", "media-generation"]).optional(),
116
+ visibility: z.enum(["default", "media", "hidden-unless-error"]).optional(),
117
+ presentation: z.enum(["default", "artifact-only"]).optional(),
118
+ media: z
119
+ .object({
120
+ type: z.enum(["image", "video", "audio"]),
121
+ actionLabel: z.string().min(1).max(80).optional(),
122
+ pendingTitle: z.string().min(1).max(120).optional(),
123
+ pendingDescription: z.string().min(1).max(200).optional(),
124
+ promptField: z.string().min(1).max(64).optional(),
125
+ aspectRatio: z.enum(["1:1", "4:3", "16:9", "auto"]).optional(),
126
+ })
127
+ .strict()
128
+ .optional(),
129
+ primaryAttachmentIds: z.array(z.string().min(1)).optional(),
130
+ })
131
+ .strict();
90
132
  const UIContribution = z
91
133
  .object({
92
134
  entry: z
@@ -134,7 +176,7 @@ const PluginPermissionsSchema = z
134
176
  .object({
135
177
  session: z.enum(["none", "metadata", "read"]).default("none"),
136
178
  workspace: z.enum(["none", "metadata", "read"]).default("none"),
137
- config: z.enum(["plugin", "global"]).default("plugin"),
179
+ config: z.enum(["none", "plugin", "global"]).default("plugin"),
138
180
  secrets: z.enum(["none", "own"]).default("none"),
139
181
  })
140
182
  .optional(),
@@ -221,6 +263,8 @@ export const PluginManifest = z
221
263
  icon: z.string().optional(),
222
264
  category: z.string().optional(),
223
265
  kind: z.string().optional(),
266
+ exposure: ToolExposureDef.optional(),
267
+ display: ToolDisplayDef.optional(),
224
268
  capabilities: z
225
269
  .object({
226
270
  filesystem: z.enum(["none", "read", "write"]).optional(),
package/dist/tool.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { z } from "zod";
2
+ import type { ToolDisplay } from "./display";
3
+ export type { ToolDisplay, ToolMediaDisplay } from "./display";
2
4
  export type ToolContext = {
3
5
  sessionID: string;
4
6
  messageID: string;
@@ -12,10 +14,14 @@ export type ToolContext = {
12
14
  metadata?: Record<string, any>;
13
15
  }): Promise<void>;
14
16
  };
17
+ export type ToolResultMetadata = Record<string, any> & {
18
+ display?: ToolDisplay;
19
+ primaryAttachmentIds?: string[];
20
+ };
15
21
  export interface ToolResult {
16
22
  title?: string;
17
23
  output: string;
18
- metadata?: Record<string, any>;
24
+ metadata?: ToolResultMetadata;
19
25
  attachments?: Array<{
20
26
  type: "file";
21
27
  id: string;
@@ -27,12 +33,29 @@ export interface ToolResult {
27
33
  localPath?: string;
28
34
  }>;
29
35
  }
36
+ export type ToolExposure = {
37
+ mode: "resident";
38
+ } | {
39
+ mode: "group";
40
+ group: string;
41
+ title?: string;
42
+ description?: string;
43
+ whenToExpand?: string;
44
+ } | {
45
+ mode: "search";
46
+ title?: string;
47
+ keywords?: string[];
48
+ };
30
49
  export declare function tool<Args extends z.ZodRawShape>(input: {
31
50
  description: string;
51
+ exposure?: ToolExposure;
52
+ display?: ToolDisplay;
32
53
  args: Args;
33
54
  execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<string | ToolResult>;
34
55
  }): {
35
56
  description: string;
57
+ exposure?: ToolExposure;
58
+ display?: ToolDisplay;
36
59
  args: Args;
37
60
  execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<string | ToolResult>;
38
61
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@ericsanchezok/synergy-plugin",
4
- "version": "2.2.2",
4
+ "version": "2.3.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -25,6 +25,10 @@
25
25
  "import": "./dist/tool.js",
26
26
  "types": "./dist/tool.d.ts"
27
27
  },
28
+ "./display": {
29
+ "import": "./dist/display.js",
30
+ "types": "./dist/display.d.ts"
31
+ },
28
32
  "./hooks": {
29
33
  "import": "./dist/hooks.js",
30
34
  "types": "./dist/hooks.d.ts"