@ericsanchezok/synergy-plugin 2.1.3 → 2.2.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.
- package/README.md +114 -430
- package/dist/example.d.ts +2 -2
- package/dist/example.js +0 -3
- package/dist/hooks.js +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.js +1 -0
- package/dist/manifest.d.ts +463 -0
- package/dist/manifest.js +422 -0
- package/dist/tool.d.ts +10 -0
- package/dist/ui.d.ts +45 -0
- package/dist/ui.js +0 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -1,487 +1,171 @@
|
|
|
1
1
|
# Synergy Plugin SDK
|
|
2
2
|
|
|
3
|
-
`@ericsanchezok/synergy-plugin` is the
|
|
3
|
+
`@ericsanchezok/synergy-plugin` is the authoring SDK for Synergy plugins.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Plugins extend the Synergy server runtime and can also contribute Web UI surfaces through `plugin.json`. The current API is intentionally strict: a plugin module exports an object descriptor with a canonical `id` and an `init()` method. The descriptor id, `plugin.json.name`, registry id, lockfile key, and approval key must all be the same canonical plugin id.
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- lifecycle hooks around sessions, agenda runs, notes, engram search, and tool execution
|
|
9
|
-
- provider auth integration
|
|
10
|
-
- config and event observers
|
|
7
|
+
Plugin authors should use `@ericsanchezok/synergy-plugin-kit` and this SDK from a standalone plugin project. Cloning the Synergy source repository is only needed when changing or debugging the plugin platform itself.
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
## Recommended Flow
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
```bash
|
|
12
|
+
bunx @ericsanchezok/synergy-plugin-kit create my-plugin --template tool-ui
|
|
13
|
+
cd my-plugin
|
|
14
|
+
bun install
|
|
15
|
+
bun run validate
|
|
16
|
+
bun run build
|
|
17
|
+
bun run pack
|
|
18
|
+
bun run sign my-plugin-0.1.0.synergy-plugin.tgz
|
|
19
|
+
bun run publish:market
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
During local development you can also install directly:
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
```bash
|
|
25
|
+
synergy plugin add file:///absolute/path/to/my-plugin
|
|
26
|
+
```
|
|
21
27
|
|
|
22
|
-
##
|
|
28
|
+
## Runtime Descriptor
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
Each function is initialized once and returns a set of hooks and capabilities.
|
|
26
|
-
In practice, most plugins should export a single default plugin function.
|
|
30
|
+
Every runtime entry exports a `PluginDescriptor` object:
|
|
27
31
|
|
|
28
32
|
```ts
|
|
29
|
-
import type {
|
|
33
|
+
import type { PluginDescriptor } from "@ericsanchezok/synergy-plugin"
|
|
34
|
+
import { tool } from "@ericsanchezok/synergy-plugin/tool"
|
|
30
35
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
export const plugin: PluginDescriptor = {
|
|
37
|
+
id: "my-plugin",
|
|
38
|
+
name: "My Plugin",
|
|
39
|
+
async init(input) {
|
|
40
|
+
return {
|
|
41
|
+
tool: {
|
|
42
|
+
greet: tool({
|
|
43
|
+
description: "Greet a user by name",
|
|
44
|
+
args: {
|
|
45
|
+
name: tool.schema.string(),
|
|
46
|
+
},
|
|
47
|
+
async execute(args, context) {
|
|
48
|
+
return {
|
|
49
|
+
output: `Hello, ${args.name}. Session: ${context.sessionID}`,
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
async "session.turn.after"(event) {
|
|
55
|
+
console.log("turn completed", event.sessionID)
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
},
|
|
35
59
|
}
|
|
36
60
|
|
|
37
|
-
export default
|
|
61
|
+
export default plugin
|
|
38
62
|
```
|
|
39
63
|
|
|
40
|
-
|
|
64
|
+
There is no compatibility layer for legacy descriptor shapes. `plugin.json.name` must match `plugin.id`; Synergy fails validation or loading if they differ.
|
|
65
|
+
|
|
66
|
+
## Plugin Input
|
|
67
|
+
|
|
68
|
+
`init(input)` receives runtime services scoped to the active Synergy Scope:
|
|
41
69
|
|
|
42
70
|
```ts
|
|
43
71
|
type PluginInput = {
|
|
44
72
|
client: ReturnType<typeof createSynergyClient>
|
|
45
|
-
scope:
|
|
46
|
-
type: "global" | "project"
|
|
47
|
-
id: string
|
|
48
|
-
directory: string
|
|
49
|
-
worktree: string
|
|
50
|
-
// ...other scope metadata
|
|
51
|
-
}
|
|
73
|
+
scope: unknown
|
|
52
74
|
directory: string
|
|
53
75
|
worktree: string
|
|
54
76
|
serverUrl: URL
|
|
55
77
|
$: BunShell
|
|
78
|
+
pluginDir: string
|
|
79
|
+
config: { get(): Promise<Record<string, unknown>>; set(values: Record<string, unknown>): Promise<void> }
|
|
80
|
+
auth: { get(key: string): Promise<string | undefined>; set(key: string, value: string): Promise<void> }
|
|
81
|
+
cache: { get<T>(key: string): Promise<T | undefined>; set(key: string, value: unknown, ttl?: number): Promise<void> }
|
|
56
82
|
}
|
|
57
83
|
```
|
|
58
84
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
For an external plugin package:
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
bun add @ericsanchezok/synergy-plugin zod
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Use ESM and export your plugin from your package entrypoint.
|
|
68
|
-
A typical package entry might look like this:
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
import type { Plugin } from "@ericsanchezok/synergy-plugin"
|
|
72
|
-
|
|
73
|
-
const MyPlugin: Plugin = async () => ({})
|
|
74
|
-
|
|
75
|
-
export default MyPlugin
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
If you are writing a local plugin directly in a Synergy config directory such as `.synergy/plugin/` or `~/.synergy/config/plugin/`, Synergy will install `@ericsanchezok/synergy-plugin` in that config directory automatically. Any additional dependencies declared in that directory's `package.json` are installed there as well.
|
|
85
|
+
For isolated worker/process plugins, these services are proxied through the host bridge and checked against the plugin approval record.
|
|
79
86
|
|
|
80
|
-
##
|
|
87
|
+
## Manifest
|
|
81
88
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
### 1. Explicit plugin entries in `synergy.jsonc`
|
|
85
|
-
|
|
86
|
-
The config schema includes a top-level `plugin` field:
|
|
89
|
+
Each distributable plugin has a root `plugin.json`:
|
|
87
90
|
|
|
88
91
|
```jsonc
|
|
89
92
|
{
|
|
90
|
-
"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
- project scope: `<project>/.synergy/plugin/` and `<project>/.synergy/plugins/`
|
|
102
|
-
- global config: `~/.synergy/config/plugin/` and `~/.synergy/config/plugins/`
|
|
103
|
-
|
|
104
|
-
This makes local development straightforward:
|
|
105
|
-
|
|
106
|
-
```text
|
|
107
|
-
my-project/
|
|
108
|
-
.synergy/
|
|
109
|
-
plugin/
|
|
110
|
-
my-plugin.ts
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
A few practical details:
|
|
114
|
-
|
|
115
|
-
- plugins are initialized in the current runtime Scope
|
|
116
|
-
- every exported plugin function in a module is loaded once
|
|
117
|
-
- `default` export is the safest convention unless you intentionally want multiple plugin instances from one file
|
|
118
|
-
- reloading plugin state also reloads plugin-provided tools
|
|
119
|
-
|
|
120
|
-
## Minimal plugin
|
|
121
|
-
|
|
122
|
-
This is the smallest useful plugin: one observation hook and no custom tools.
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
import type { Plugin } from "@ericsanchezok/synergy-plugin"
|
|
126
|
-
|
|
127
|
-
const SessionLogger: Plugin = async () => {
|
|
128
|
-
return {
|
|
129
|
-
async "session.turn.after"(input) {
|
|
130
|
-
if (input.error) {
|
|
131
|
-
console.error("session turn failed", input.sessionID, input.error)
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
console.log("session turn completed", input.sessionID, input.assistantMessageID)
|
|
93
|
+
"name": "my-plugin",
|
|
94
|
+
"version": "0.1.0",
|
|
95
|
+
"description": "Example Synergy plugin",
|
|
96
|
+
"main": "./src/index.ts",
|
|
97
|
+
"permissions": {
|
|
98
|
+
"tools": {
|
|
99
|
+
"invoke": true,
|
|
100
|
+
"filesystem": "none",
|
|
101
|
+
"network": false,
|
|
102
|
+
"shell": false,
|
|
103
|
+
"mcp": "none",
|
|
136
104
|
},
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
```ts
|
|
149
|
-
import type { Plugin } from "@ericsanchezok/synergy-plugin"
|
|
150
|
-
import { tool } from "@ericsanchezok/synergy-plugin/tool"
|
|
151
|
-
|
|
152
|
-
const GitPlugin: Plugin = async (ctx) => {
|
|
153
|
-
return {
|
|
154
|
-
tool: {
|
|
155
|
-
current_branch: tool({
|
|
156
|
-
description: "Get the current git branch",
|
|
157
|
-
args: {},
|
|
158
|
-
async execute(_args, toolCtx) {
|
|
159
|
-
const out = await ctx.$`git rev-parse --abbrev-ref HEAD`.cwd(ctx.worktree).quiet().text()
|
|
160
|
-
|
|
161
|
-
return [`session: ${toolCtx.sessionID}`, `agent: ${toolCtx.agent}`, `branch: ${out.trim()}`].join("\n")
|
|
105
|
+
},
|
|
106
|
+
"contributes": {
|
|
107
|
+
"tools": [
|
|
108
|
+
{
|
|
109
|
+
"name": "greet",
|
|
110
|
+
"title": "Greet",
|
|
111
|
+
"description": "Greet a user by name",
|
|
112
|
+
"capabilities": {
|
|
113
|
+
"filesystem": "none",
|
|
114
|
+
"network": false,
|
|
115
|
+
"shell": false,
|
|
162
116
|
},
|
|
163
|
-
}
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
"ui": {
|
|
120
|
+
"entry": "./dist/ui/index.js",
|
|
121
|
+
"toolRenderers": [{ "tool": "greet" }],
|
|
164
122
|
},
|
|
165
|
-
}
|
|
123
|
+
},
|
|
166
124
|
}
|
|
167
|
-
|
|
168
|
-
export default GitPlugin
|
|
169
125
|
```
|
|
170
126
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
```ts
|
|
174
|
-
type ToolContext = {
|
|
175
|
-
sessionID: string
|
|
176
|
-
messageID: string
|
|
177
|
-
agent: string
|
|
178
|
-
abort: AbortSignal
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
A few things to know about plugin tools:
|
|
183
|
-
|
|
184
|
-
- tool args are defined with Zod shapes
|
|
185
|
-
- the helper returns plain text output; Synergy wraps it into the runtime tool result format
|
|
186
|
-
- long output may be truncated by the runtime, just like built-in tools
|
|
187
|
-
- if you need runtime context like Scope paths or shell access, close over the outer plugin `ctx`
|
|
188
|
-
|
|
189
|
-
## Hooks overview
|
|
190
|
-
|
|
191
|
-
If you want a current CLI list instead of reading source, run `synergy plugin hooks` or `synergy plugin hooks --json`.
|
|
127
|
+
`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.
|
|
192
128
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
### Observation hooks
|
|
196
|
-
|
|
197
|
-
These let you react to runtime events without changing the main result.
|
|
198
|
-
They either have no mutable output object, or their output is not used to drive the main flow.
|
|
199
|
-
|
|
200
|
-
Common examples:
|
|
201
|
-
|
|
202
|
-
- `event`
|
|
203
|
-
- `config`
|
|
204
|
-
- `session.turn.after`
|
|
205
|
-
- `cortex.task.after`
|
|
206
|
-
- `agenda.run.after`
|
|
207
|
-
- `agenda.run.error`
|
|
208
|
-
- `engram.experience.encode.after`
|
|
209
|
-
|
|
210
|
-
Use these for logging, metrics, side effects, external notifications, and indexing.
|
|
211
|
-
|
|
212
|
-
### Mutation hooks
|
|
213
|
-
|
|
214
|
-
These can shape what Synergy does by mutating the `output` object passed as the second argument.
|
|
215
|
-
The runtime passes an object, your hook edits it in place, and the updated object continues through the pipeline.
|
|
216
|
-
|
|
217
|
-
Common examples:
|
|
218
|
-
|
|
219
|
-
- `chat.message`
|
|
220
|
-
- `chat.params`
|
|
221
|
-
- `permission.ask`
|
|
222
|
-
- `tool.execute.before`
|
|
223
|
-
- `tool.execute.after`
|
|
224
|
-
- `agenda.run.before`
|
|
225
|
-
- `note.create.before`
|
|
226
|
-
- `note.update.before`
|
|
227
|
-
- `note.search.before`
|
|
228
|
-
- `note.search.after`
|
|
229
|
-
- `engram.memory.search.before`
|
|
230
|
-
- `engram.memory.search.after`
|
|
231
|
-
- `experimental.*` transform hooks
|
|
232
|
-
|
|
233
|
-
The rule of thumb is simple: treat `input` as context, and treat `output` as the thing you may change.
|
|
234
|
-
|
|
235
|
-
## Hook reference
|
|
236
|
-
|
|
237
|
-
### Core capabilities
|
|
238
|
-
|
|
239
|
-
| Hook / field | Purpose | Typical use |
|
|
240
|
-
| ------------ | ------------------------------------------ | -------------------------------------------- |
|
|
241
|
-
| `tool` | Register custom tools | Runtime-side integrations, project utilities |
|
|
242
|
-
| `auth` | Add provider auth methods and auth loaders | Custom providers, OAuth, API key flows |
|
|
243
|
-
| `config` | Observe loaded config | Initialize plugin state from current config |
|
|
244
|
-
| `event` | Observe 72 runtime bus events (see below) | Logging, metrics, passive integrations |
|
|
245
|
-
|
|
246
|
-
#### `event` — observable bus events
|
|
247
|
-
|
|
248
|
-
The `event` hook lets you subscribe to any of the following runtime bus events by matching `event.type` in your handler. Run `synergy plugin hooks --json` for the full machine-readable list.
|
|
249
|
-
|
|
250
|
-
**Installation & scope**
|
|
251
|
-
|
|
252
|
-
```
|
|
253
|
-
installation.updated installation.update_available
|
|
254
|
-
scope.updated scope.removed
|
|
255
|
-
```
|
|
129
|
+
## UI Types
|
|
256
130
|
|
|
257
|
-
|
|
131
|
+
UI contribution types are exported separately:
|
|
258
132
|
|
|
133
|
+
```ts
|
|
134
|
+
import type { PluginToolRendererProps, PluginPanelProps } from "@ericsanchezok/synergy-plugin/ui"
|
|
259
135
|
```
|
|
260
|
-
config.updated config.set_activated
|
|
261
|
-
server.connected server.instance.disposed global.disposed
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
**File & LSP**
|
|
265
|
-
|
|
266
|
-
```
|
|
267
|
-
file.edited file.watcher.updated
|
|
268
|
-
lsp.updated lsp.client_diagnostics
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
**MCP & command**
|
|
272
|
-
|
|
273
|
-
```
|
|
274
|
-
mcp.ready mcp.tools_changed mcp.prompts_changed mcp.resources_changed
|
|
275
|
-
command.executed
|
|
276
|
-
vcs.branch.updated
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
**Permission & note**
|
|
280
|
-
|
|
281
|
-
```
|
|
282
|
-
permission.asked permission.replied permission.allow_all_changed
|
|
283
|
-
note.created note.updated note.deleted
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
**Session & message**
|
|
287
|
-
|
|
288
|
-
```
|
|
289
|
-
session.created session.updated session.deleted session.diff
|
|
290
|
-
session.error session.status session.idle session.compacted
|
|
291
|
-
message.updated message.removed message.part.updated message.part.removed
|
|
292
|
-
question.asked question.replied question.rejected
|
|
293
|
-
runtime.reloaded
|
|
294
|
-
todo.updated dag.updated
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
**Cortex & agenda**
|
|
298
|
-
|
|
299
|
-
```
|
|
300
|
-
cortex.task.created cortex.task.completed cortex.tasks.updated
|
|
301
|
-
agenda.item.created agenda.item.updated agenda.item.deleted
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
**PTY**
|
|
305
|
-
|
|
306
|
-
```
|
|
307
|
-
pty.created pty.updated pty.exited pty.deleted
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
**Channel**
|
|
311
|
-
|
|
312
|
-
```
|
|
313
|
-
channel.command.executed channel.connected channel.disconnected
|
|
314
|
-
channel.message.received
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
**App & Holos**
|
|
318
|
-
|
|
319
|
-
```
|
|
320
|
-
app.push
|
|
321
|
-
holos.profile.updated
|
|
322
|
-
holos.contact.added holos.contact.removed holos.contact.updated holos.contact.config_updated
|
|
323
|
-
holos.friend_request.created holos.friend_request.updated holos.friend_request.removed
|
|
324
|
-
holos.queue.enqueued holos.queue.delivered holos.queue.expired
|
|
325
|
-
holos.connected holos.connection_status.changed holos.presence
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
### Chat and session hooks
|
|
329
|
-
|
|
330
|
-
| Hook | Mutates output? | Notes |
|
|
331
|
-
| -------------------------------------- | --------------- | ---------------------------------------------------------------- |
|
|
332
|
-
| `chat.message` | Yes | Inspect or rewrite incoming user message parts before processing |
|
|
333
|
-
| `chat.params` | Yes | Adjust model parameters and provider options before LLM calls |
|
|
334
|
-
| `session.turn.after` | No | Observe the completed turn, including error state if present |
|
|
335
|
-
| `experimental.chat.messages.transform` | Yes | Rewrite the message history sent to the model |
|
|
336
|
-
| `experimental.chat.system.transform` | Yes | Rewrite the assembled system prompt |
|
|
337
|
-
| `experimental.session.compacting` | Yes | Add compaction context or replace the compaction prompt |
|
|
338
|
-
| `experimental.text.complete` | Yes | Rewrite a text completion result |
|
|
339
|
-
|
|
340
|
-
### Permission and tool hooks
|
|
341
|
-
|
|
342
|
-
| Hook | Mutates output? | Notes |
|
|
343
|
-
| --------------------- | --------------- | ------------------------------------------------------- |
|
|
344
|
-
| `permission.ask` | Yes | Override `ask` / `deny` / `allow` decisions |
|
|
345
|
-
| `tool.execute.before` | Yes | Rewrite tool args before execution |
|
|
346
|
-
| `tool.execute.after` | Yes | Rewrite tool output, title, or metadata after execution |
|
|
347
136
|
|
|
348
|
-
|
|
137
|
+
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.
|
|
349
138
|
|
|
350
|
-
|
|
351
|
-
| ------------------- | --------------- | --------------------------------------------------------- |
|
|
352
|
-
| `cortex.task.after` | No | Observe completed Cortex task execution |
|
|
353
|
-
| `agenda.run.before` | Yes | Skip a run or replace the `AgendaItem` used for execution |
|
|
354
|
-
| `agenda.run.after` | No | Observe a successful agenda run |
|
|
355
|
-
| `agenda.run.error` | No | Observe a failed agenda run |
|
|
139
|
+
## Runtime Modes
|
|
356
140
|
|
|
357
|
-
|
|
141
|
+
Synergy resolves each plugin to one runtime mode:
|
|
358
142
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
| `note.create.after` | Usually no | Observe the created note after persistence |
|
|
363
|
-
| `note.update.before` | Yes | Rewrite the patch before update logic runs |
|
|
364
|
-
| `note.update.after` | Usually no | Observe the updated note after persistence |
|
|
365
|
-
| `note.search.before` | Yes | Rewrite search pattern, scope, date filters, tags, or pinned filter |
|
|
366
|
-
| `note.search.after` | Yes | Filter or reorder returned notes |
|
|
143
|
+
- `in-process` for trusted local or built-in plugins.
|
|
144
|
+
- `worker` for isolated plugins that do not need a separate OS process.
|
|
145
|
+
- `process` for third-party, high-risk, or policy-forced isolation.
|
|
367
146
|
|
|
368
|
-
|
|
147
|
+
Worker and process plugins are started through Synergy's plugin runner. The runner imports the descriptor, calls `init()`, reports tools and hooks to the host, and proxies tool and hook calls over the runtime protocol.
|
|
369
148
|
|
|
370
|
-
|
|
371
|
-
| -------------------------------- | --------------- | -------------------------------------------------------------------------- |
|
|
372
|
-
| `engram.memory.search.before` | Yes | Rewrite query, vector, top-k, categories, recall modes, or rerank behavior |
|
|
373
|
-
| `engram.memory.search.after` | Yes | Filter or rerank returned engram memory results |
|
|
374
|
-
| `engram.experience.encode.after` | No | Observe whether an experience was encoded, skipped, or deduplicated |
|
|
149
|
+
## Packaging
|
|
375
150
|
|
|
376
|
-
|
|
151
|
+
`synergy-plugin build` writes a distributable `dist/` directory:
|
|
377
152
|
|
|
378
|
-
|
|
153
|
+
- `dist/plugin.json`
|
|
154
|
+
- `dist/runtime/index.js`
|
|
155
|
+
- `dist/ui/index.js` when UI entry is declared
|
|
156
|
+
- copied theme/icon/assets files
|
|
157
|
+
- `dist/permissions.summary.json`
|
|
158
|
+
- `dist/integrity.json`
|
|
379
159
|
|
|
380
|
-
|
|
160
|
+
`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, running registry validation, and opening a PR when `gh` is available.
|
|
381
161
|
|
|
382
|
-
|
|
162
|
+
For local marketplace UX testing, the Synergy runtime still provides `synergy plugin publish <tarball>` to publish into the local development registry.
|
|
383
163
|
|
|
384
|
-
|
|
385
|
-
- `note.search.after` for result shaping
|
|
164
|
+
## Exports
|
|
386
165
|
|
|
387
166
|
```ts
|
|
388
|
-
import type {
|
|
167
|
+
import type { PluginDescriptor, PluginInput } from "@ericsanchezok/synergy-plugin"
|
|
389
168
|
import { tool } from "@ericsanchezok/synergy-plugin/tool"
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return {
|
|
393
|
-
tool: {
|
|
394
|
-
scope_info: tool({
|
|
395
|
-
description: "Show the current Scope and worktree",
|
|
396
|
-
args: {},
|
|
397
|
-
async execute() {
|
|
398
|
-
return [
|
|
399
|
-
`scope: ${ctx.scope.id}`,
|
|
400
|
-
`scopeType: ${ctx.scope.type}`,
|
|
401
|
-
`directory: ${ctx.directory}`,
|
|
402
|
-
`worktree: ${ctx.worktree}`,
|
|
403
|
-
].join("\n")
|
|
404
|
-
},
|
|
405
|
-
}),
|
|
406
|
-
},
|
|
407
|
-
|
|
408
|
-
async "session.turn.after"(input) {
|
|
409
|
-
if (!input.error) {
|
|
410
|
-
console.log("assistant replied in session", input.sessionID)
|
|
411
|
-
}
|
|
412
|
-
},
|
|
413
|
-
|
|
414
|
-
async "note.search.after"(_input, output) {
|
|
415
|
-
output.notes = output.notes.filter((note) => !note.tags.includes("private"))
|
|
416
|
-
},
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
export default ExamplePlugin
|
|
169
|
+
import type { BunShell } from "@ericsanchezok/synergy-plugin/shell"
|
|
170
|
+
import type { PluginToolRendererProps } from "@ericsanchezok/synergy-plugin/ui"
|
|
421
171
|
```
|
|
422
|
-
|
|
423
|
-
## Best practices
|
|
424
|
-
|
|
425
|
-
### Keep hooks fast
|
|
426
|
-
|
|
427
|
-
Most hooks run inline with real user work. If a hook blocks, the user feels it.
|
|
428
|
-
Do the minimum in the hook path. Push slow work to another system when possible.
|
|
429
|
-
|
|
430
|
-
### Be conservative with mutation
|
|
431
|
-
|
|
432
|
-
If you mutate an output object, make the change narrow and obvious.
|
|
433
|
-
A good plugin should be easy to reason about six months later.
|
|
434
|
-
|
|
435
|
-
### Treat Scope as real context
|
|
436
|
-
|
|
437
|
-
Plugins run inside a resolved Scope. Prefer `ctx.scope`, `ctx.directory`, and `ctx.worktree` over guessing from process state.
|
|
438
|
-
|
|
439
|
-
### Use `ctx.$` carefully
|
|
440
|
-
|
|
441
|
-
`ctx.$` is useful for local runtime-side commands, but it also makes it easy to couple a plugin to one machine or one repository layout. Shell out when that is the simplest correct answer, not by default.
|
|
442
|
-
|
|
443
|
-
### Prefer observation hooks for telemetry and indexing
|
|
444
|
-
|
|
445
|
-
If you only need to watch what happened, use an after-hook or `event` hook instead of intercepting earlier stages.
|
|
446
|
-
|
|
447
|
-
### Be explicit about note and engram behavior
|
|
448
|
-
|
|
449
|
-
Hooks like `note.search.after` and `engram.memory.search.after` can quietly change what users see. Document those policies in the plugin itself and keep them predictable.
|
|
450
|
-
|
|
451
|
-
### Expect experimental hooks to move
|
|
452
|
-
|
|
453
|
-
If your plugin depends on an `experimental.*` hook, isolate that logic so future API changes are easy to update.
|
|
454
|
-
|
|
455
|
-
### Export one plugin by default
|
|
456
|
-
|
|
457
|
-
A single default export keeps module behavior obvious. Multiple exported plugin functions are supported, but they are loaded independently.
|
|
458
|
-
|
|
459
|
-
## Auth plugins
|
|
460
|
-
|
|
461
|
-
A plugin can also provide `auth` for a custom provider. That is how plugins participate in provider-specific API key or OAuth flows and, when needed, compute provider options through an auth loader.
|
|
462
|
-
|
|
463
|
-
If you only need custom tools or hooks, you can ignore `auth` entirely.
|
|
464
|
-
|
|
465
|
-
## When to use a plugin vs other extension points
|
|
466
|
-
|
|
467
|
-
Use a plugin when you need runtime behavior:
|
|
468
|
-
|
|
469
|
-
- add a tool
|
|
470
|
-
- intercept a session, agenda, note, or engram lifecycle step
|
|
471
|
-
- integrate provider auth
|
|
472
|
-
- react to runtime events
|
|
473
|
-
|
|
474
|
-
Use other extension points when the problem is simpler:
|
|
475
|
-
|
|
476
|
-
- use `.synergy/command/` for reusable prompt commands
|
|
477
|
-
- use `.synergy/skill/` for domain instructions and workflows
|
|
478
|
-
- use config in `synergy.jsonc` for static runtime configuration
|
|
479
|
-
|
|
480
|
-
## Development notes
|
|
481
|
-
|
|
482
|
-
- local plugin files can live under `.synergy/plugin/` or `.synergy/plugins/`
|
|
483
|
-
- global plugin files can live under `~/.synergy/config/plugin/` or `~/.synergy/config/plugins/`
|
|
484
|
-
- explicit package plugins can be listed in `synergy.jsonc` under `plugin`
|
|
485
|
-
- plugin reload also refreshes plugin-provided tools
|
|
486
|
-
|
|
487
|
-
That is the core model: a plugin is an async server-side module that receives runtime context, returns hooks and tools, and participates directly in Scope-aware execution.
|
package/dist/example.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare const ExamplePlugin:
|
|
1
|
+
import type { PluginDescriptor } from "./index";
|
|
2
|
+
export declare const ExamplePlugin: PluginDescriptor;
|
|
3
3
|
export default ExamplePlugin;
|
package/dist/example.js
CHANGED
|
@@ -78,9 +78,6 @@ export const ExamplePlugin = {
|
|
|
78
78
|
async "note.create.before"(_, output) {
|
|
79
79
|
output.note.title = output.note.title.trim() || "Untitled note";
|
|
80
80
|
output.note.tags = mergeTags(output.note.tags);
|
|
81
|
-
if (!output.note.contentText?.trim()) {
|
|
82
|
-
output.note.contentText = preview(output.note.title);
|
|
83
|
-
}
|
|
84
81
|
},
|
|
85
82
|
async "engram.memory.search.after"(input, output) {
|
|
86
83
|
output.results = output.results
|
package/dist/hooks.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -3,16 +3,25 @@ import type { BunShell } from "./shell";
|
|
|
3
3
|
import type { ToolDefinition, ToolResult } from "./tool";
|
|
4
4
|
export * from "./tool";
|
|
5
5
|
export type { ToolResult };
|
|
6
|
+
export * from "./manifest";
|
|
7
|
+
export type { BunShell, BunShellOutput, BunShellPromise, ShellExpression, ShellFunction } from "./shell";
|
|
6
8
|
export interface PluginConfigAccessor {
|
|
7
9
|
/** Get the plugin's full config object */
|
|
8
10
|
get(): Promise<Record<string, any>>;
|
|
9
11
|
/** Set one or more config values (deep-merged into the plugin's namespace) */
|
|
10
12
|
set(values: Record<string, any>): Promise<void>;
|
|
11
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Plugin credential store (plaintext JSON on disk).
|
|
16
|
+
*
|
|
17
|
+
* WARNING: Credentials are stored as unencrypted JSON in the plugin data directory
|
|
18
|
+
* at ~/.synergy/data/plugin/{id}/auth.json. Protect your filesystem.
|
|
19
|
+
* Future versions will use system keychain encryption.
|
|
20
|
+
*/
|
|
12
21
|
export interface PluginAuthStore {
|
|
13
22
|
/** Read a credential by key */
|
|
14
23
|
get(key: string): Promise<string | undefined>;
|
|
15
|
-
/** Persist a credential
|
|
24
|
+
/** Persist a credential. WARNING: stored as plaintext JSON on disk. Protect your filesystem. */
|
|
16
25
|
set(key: string, value: string): Promise<void>;
|
|
17
26
|
/** Remove a credential */
|
|
18
27
|
delete(key: string): Promise<void>;
|
|
@@ -178,7 +187,7 @@ export type AuthOuathResult = {
|
|
|
178
187
|
export type PluginInput = {
|
|
179
188
|
client: ReturnType<typeof createSynergyClient>;
|
|
180
189
|
scope: {
|
|
181
|
-
type: "
|
|
190
|
+
type: "home" | "project";
|
|
182
191
|
id: string;
|
|
183
192
|
directory: string;
|
|
184
193
|
worktree: string;
|
|
@@ -205,7 +214,7 @@ export type PluginInput = {
|
|
|
205
214
|
/** Absolute path to this plugin's package root (where package.json lives) */
|
|
206
215
|
pluginDir: string;
|
|
207
216
|
};
|
|
208
|
-
export interface
|
|
217
|
+
export interface PluginDescriptor {
|
|
209
218
|
/** Unique identifier for this plugin (used as config/auth/cache namespace) */
|
|
210
219
|
id: string;
|
|
211
220
|
/** Human-readable display name */
|
package/dist/index.js
CHANGED