xyne-plugin 1.0.2 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xyne-plugin",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "SDK for writing Xyne plugins — define custom tools, hooks, and extensions",
5
5
  "type": "module",
6
6
  "exports": {
package/src/hooks.ts CHANGED
@@ -18,6 +18,115 @@ export interface ModelSpec {
18
18
  /** LLM message — opaque to plugins, passed through for transformation. */
19
19
  export type Message = Record<string, unknown>
20
20
 
21
+ /** How a provider's API key was resolved. */
22
+ export type CredentialSource = "config" | "env" | "auth"
23
+
24
+ /** Provider metadata exposed to plugin hooks. */
25
+ export interface ProviderInfo {
26
+ id: string
27
+ name: string
28
+ source: CredentialSource
29
+ env: string[]
30
+ options: Record<string, unknown>
31
+ models: Record<string, { name: string; contextLength: number; maxOutputTokens: number; [key: string]: unknown }>
32
+ }
33
+
34
+ // ─── Message & Part types for hook signatures ────────────────────────────────
35
+
36
+ /** Token usage breakdown. */
37
+ export interface TokenInfo {
38
+ input: number
39
+ output: number
40
+ cacheRead: number
41
+ cacheWrite: number
42
+ reasoning: number
43
+ }
44
+
45
+ /** Error descriptor attached to failed messages. */
46
+ export type MessageError =
47
+ | { name: "AuthError"; providerID: string; message: string }
48
+ | { name: "APIError"; message: string; statusCode?: number; isRetryable: boolean }
49
+ | { name: "ContextOverflowError"; message: string }
50
+ | { name: "AbortedError"; message: string }
51
+ | { name: "OutputLengthError" }
52
+ | { name: "StructuredOutputError"; message: string; retries: number }
53
+ | { name: "Unknown"; message: string }
54
+
55
+ /** A message in the session (user or assistant turn). */
56
+ export interface MessageInfo {
57
+ id: string
58
+ sessionID: string
59
+ role: "user" | "assistant"
60
+ created: number
61
+ completed?: number
62
+ parentID?: string
63
+ agentID?: string
64
+ modelID?: string
65
+ providerID?: string
66
+ path?: { cwd: string; root: string }
67
+ finish?: string
68
+ error?: MessageError
69
+ cost?: number
70
+ tokens?: TokenInfo
71
+ requestedModel?: { providerID: string; modelID: string }
72
+ format?: { type: string; [key: string]: unknown }
73
+ structured?: unknown
74
+ summary?: string
75
+ }
76
+
77
+ /** Tool call state. */
78
+ export type ToolState =
79
+ | { status: "pending" }
80
+ | { status: "running"; input: unknown; startTime: number; title?: string; metadata?: unknown }
81
+ | { status: "completed"; input: unknown; output: string; title: string; metadata: unknown; startTime: number; endTime: number; attachments?: Array<{ type: string; url: string; mime: string }>; compacted?: number }
82
+ | { status: "error"; input: unknown; error: string; startTime: number; endTime: number }
83
+
84
+ /** Discriminated union of all part types. */
85
+ export type PartInfo =
86
+ | { type: "text"; id: string; messageID: string; sessionID: string; text: string; startTime: number; endTime?: number; metadata?: Record<string, unknown>; synthetic?: boolean; ignored?: boolean }
87
+ | { type: "reasoning"; id: string; messageID: string; sessionID: string; text: string; startTime: number; endTime?: number; metadata?: Record<string, unknown> }
88
+ | { type: "tool"; id: string; messageID: string; sessionID: string; callID: string; tool: string; state: ToolState }
89
+ | { type: "step-start"; id: string; messageID: string; sessionID: string }
90
+ | { type: "step-finish"; id: string; messageID: string; sessionID: string; reason: string; tokens?: TokenInfo; cost?: number }
91
+ | { type: "retry"; id: string; messageID: string; sessionID: string; attempt: number; error: MessageError; time: { created: number } }
92
+ | { type: "compaction"; id: string; messageID: string; sessionID: string; auto: boolean; overflow?: boolean }
93
+ | { type: "subtask"; id: string; messageID: string; sessionID: string; prompt: string; description: string; agent: string; model?: { providerID: string; modelID: string }; command?: string }
94
+ | { type: "patch"; id: string; messageID: string; sessionID: string; hash: string; files: string[] }
95
+ | { type: "media"; id: string; messageID: string; sessionID: string; mime: string; filename?: string; url: string }
96
+ | { type: "agent"; id: string; messageID: string; sessionID: string; name: string }
97
+
98
+ // ─── Plugin agent types ─────────────────────────────────────────────────────
99
+
100
+ /**
101
+ * Simplified agent definition for plugins.
102
+ *
103
+ * Plugin agents follow the same Agent.Info shape but use a reduced surface:
104
+ * - `name` is derived from the record key
105
+ * - `tools` is a record of ToolDefinitions that are converted to Tool.Info[] at registration time
106
+ * - `permission` accepts a simple string map instead of a raw Permission.Ruleset
107
+ */
108
+ export interface PluginAgentDef {
109
+ description?: string
110
+ mode?: "primary" | "subagent" | "all"
111
+ system?: string
112
+ /** Model override as "providerID/modelID" string or structured spec. */
113
+ model?: string | { providerID: string; modelID: string }
114
+ /** Tools scoped to this agent. These are added on top of session tools. */
115
+ tools?: Record<string, ToolDefinition>
116
+ /**
117
+ * If true, this agent ONLY has access to its own `tools` (built-in session tools are excluded).
118
+ * If false (default), the agent's tools are merged with the session's tool set.
119
+ */
120
+ isolatedTools?: boolean
121
+ /** Permission overrides. Keys are tool IDs or "*", values are "allow" | "deny" | "ask". */
122
+ permission?: Record<string, "allow" | "deny" | "ask" | Record<string, "allow" | "deny" | "ask">>
123
+ maxSteps?: number
124
+ color?: string
125
+ hidden?: boolean
126
+ temperature?: number
127
+ topP?: number
128
+ }
129
+
21
130
  // ─── Plugin function types ───────────────────────────────────────────────────
22
131
 
23
132
  export type PluginInput = {
@@ -26,10 +135,106 @@ export type PluginInput = {
26
135
  appName: string
27
136
  /** SDK client — available for plugins that need session/message/tool APIs. */
28
137
  client?: unknown
138
+ /** Base URL for the local server (if running). */
139
+ serverUrl?: URL
140
+ /** Bun shell for running commands. */
141
+ $?: unknown
29
142
  }
30
143
 
31
144
  export type Plugin = (input: PluginInput) => Promise<Hooks>
32
145
 
146
+ // ─── Auth types ─────────────────────────────────────────────────────────────
147
+
148
+ export type AuthPrompt =
149
+ | {
150
+ type: "text"
151
+ key: string
152
+ message: string
153
+ placeholder?: string
154
+ validate?: (value: string) => string | undefined
155
+ condition?: (inputs: Record<string, string>) => boolean
156
+ }
157
+ | {
158
+ type: "select"
159
+ key: string
160
+ message: string
161
+ options: Array<{ label: string; value: string; hint?: string }>
162
+ condition?: (inputs: Record<string, string>) => boolean
163
+ }
164
+
165
+ export type AuthOAuthResult = { url: string; instructions: string } & (
166
+ | {
167
+ method: "auto"
168
+ callback(): Promise<
169
+ | ({ type: "success"; provider?: string } & (
170
+ | { refresh: string; access: string; expires: number; accountId?: string }
171
+ | { key: string }
172
+ ))
173
+ | { type: "failed" }
174
+ >
175
+ }
176
+ | {
177
+ method: "code"
178
+ callback(code: string): Promise<
179
+ | ({ type: "success"; provider?: string } & (
180
+ | { refresh: string; access: string; expires: number; accountId?: string }
181
+ | { key: string }
182
+ ))
183
+ | { type: "failed" }
184
+ >
185
+ }
186
+ )
187
+
188
+ export type AuthMethod =
189
+ | {
190
+ type: "oauth"
191
+ label: string
192
+ prompts?: AuthPrompt[]
193
+ authorize(inputs?: Record<string, string>): Promise<AuthOAuthResult>
194
+ }
195
+ | {
196
+ type: "api"
197
+ label: string
198
+ prompts?: AuthPrompt[]
199
+ authorize?(inputs?: Record<string, string>): Promise<
200
+ | { type: "success"; key: string; provider?: string }
201
+ | { type: "failed" }
202
+ >
203
+ }
204
+ | {
205
+ type: "custom"
206
+ label: string
207
+ prompts?: AuthPrompt[]
208
+ /** Plugin handles the entire auth flow. UI shows "Connecting..." while this runs. */
209
+ authorize(inputs?: Record<string, string>): Promise<
210
+ | { type: "success"; data: Record<string, unknown> }
211
+ | { type: "failed" }
212
+ >
213
+ }
214
+
215
+ /**
216
+ * Options returned by an auth loader to configure the provider SDK.
217
+ */
218
+ export interface AuthLoaderResult {
219
+ baseURL?: string
220
+ apiKey?: string
221
+ fetch?: typeof globalThis.fetch
222
+ /** Custom model selector — receives the SDK instance, model ID, and options. */
223
+ getModel?: (sdk: unknown, modelID: string, options: Record<string, unknown>) => Promise<unknown>
224
+ [key: string]: unknown
225
+ }
226
+
227
+ export type AuthHook = {
228
+ provider: string
229
+ /**
230
+ * Called after auth succeeds to configure the provider SDK.
231
+ * Receives a getter for the auth entry and the provider info.
232
+ * Returns SDK options (baseURL, apiKey, fetch, getModel, etc.)
233
+ */
234
+ loader?: (auth: () => Promise<unknown>, provider: unknown) => Promise<AuthLoaderResult>
235
+ methods: AuthMethod[]
236
+ }
237
+
33
238
  // ─── Hook definitions ────────────────────────────────────────────────────────
34
239
 
35
240
  export interface Hooks {
@@ -42,18 +247,34 @@ export interface Hooks {
42
247
  /** Register custom tools. Keyed by tool ID. */
43
248
  tool?: Record<string, ToolDefinition>
44
249
 
45
- /** Auth provider stub expand later with full OAuth / API key flows. */
46
- auth?: { provider: string }
250
+ /** Register custom agents. Keyed by agent name. */
251
+ agent?: Record<string, PluginAgentDef>
252
+
253
+ /** Auth provider — OAuth, API key, or custom auth flows. */
254
+ auth?: AuthHook
255
+
256
+ /** Called when a new message is created (user or assistant). */
257
+ "chat.message"?: (
258
+ input: {
259
+ sessionID: string
260
+ messageID: string
261
+ role: string
262
+ agent?: string
263
+ model?: { providerID: string; modelID: string }
264
+ variant?: string
265
+ },
266
+ output: { message: MessageInfo; parts: PartInfo[] },
267
+ ) => Promise<void>
47
268
 
48
269
  /** Mutate LLM sampling parameters before a chat request. */
49
270
  "chat.params"?: (
50
- input: { sessionID: string; agent: string; model: ModelSpec },
51
- output: { temperature?: number; topP?: number; topK?: number },
271
+ input: { sessionID: string; agent: string; model: ModelSpec; provider?: ProviderInfo; message?: MessageInfo },
272
+ output: { temperature?: number; topP?: number; topK?: number; options?: Record<string, unknown> },
52
273
  ) => Promise<void>
53
274
 
54
275
  /** Inject extra HTTP headers into the provider request. */
55
276
  "chat.headers"?: (
56
- input: { sessionID: string; agent: string; model: ModelSpec },
277
+ input: { sessionID: string; agent: string; model: ModelSpec; provider?: ProviderInfo; message?: MessageInfo },
57
278
  output: { headers: Record<string, string> },
58
279
  ) => Promise<void>
59
280
 
@@ -75,6 +296,12 @@ export interface Hooks {
75
296
  output: { status: "ask" | "deny" | "allow" },
76
297
  ) => Promise<void>
77
298
 
299
+ /** Called before a slash command executes. */
300
+ "command.execute.before"?: (
301
+ input: { command: string; sessionID: string; arguments: string },
302
+ output: { parts: PartInfo[] },
303
+ ) => Promise<void>
304
+
78
305
  /** Modify tool args before execution. */
79
306
  "tool.execute.before"?: (
80
307
  input: { tool: string; sessionID: string; callID: string },
@@ -104,4 +331,10 @@ export interface Hooks {
104
331
  input: { sessionID: string },
105
332
  output: { context: string[]; prompt?: string },
106
333
  ) => Promise<void>
334
+
335
+ /** Text completion hook — modify or provide text completions. */
336
+ "text.complete"?: (
337
+ input: { text: string; cursorOffset: number },
338
+ output: { completions: string[] },
339
+ ) => Promise<void>
107
340
  }
package/src/index.ts CHANGED
@@ -22,11 +22,11 @@
22
22
  */
23
23
 
24
24
  // Plugin types
25
- export type { Plugin, PluginInput, Hooks } from "./hooks"
25
+ export type { Plugin, PluginInput, Hooks, PluginAgentDef, AuthHook, AuthMethod, AuthPrompt, AuthOAuthResult, AuthLoaderResult, ModelSpec, ProviderInfo, CredentialSource, MessageInfo, PartInfo, ToolState, TokenInfo, MessageError } from "./hooks"
26
26
 
27
27
  // Tool authoring
28
28
  export { tool } from "./tool"
29
- export type { ToolDefinition, ToolContext } from "./tool"
29
+ export type { ToolDefinition, ToolContext, ToolAuthConfig, ServiceCredentials, ToolAttachment, PluginToolResult } from "./tool"
30
30
 
31
31
  // Zod re-export for convenience
32
32
  export { z } from "zod"
package/src/tool.ts CHANGED
@@ -1,4 +1,47 @@
1
1
  import { z } from "zod"
2
+ import type { AuthMethod } from "./hooks"
3
+
4
+ // ─── Service credentials ────────────────────────────────────────────────────
5
+
6
+ /** Resolved service credentials injected into tool context. */
7
+ export type ServiceCredentials =
8
+ | { type: "oauth"; access: string; refresh: string; expires: number }
9
+ | { type: "api"; key: string }
10
+ | { type: "custom"; data: Record<string, unknown> }
11
+
12
+ // ─── Tool auth config ───────────────────────────────────────────────────────
13
+
14
+ /**
15
+ * Declares that a tool requires authentication with an external service.
16
+ *
17
+ * When a tool with `auth` is executed:
18
+ * 1. The system checks stored credentials for `serviceID`
19
+ * 2. If found and not expired → credentials injected into `ctx.credentials`
20
+ * 3. If expired → `refresh` callback is tried first (silent refresh)
21
+ * 4. If missing or refresh fails → auth flow triggered via bus event, execution blocks
22
+ * until user completes auth, then resumes with credentials
23
+ */
24
+ export interface ToolAuthConfig {
25
+ /** Unique service identifier used as the storage key (e.g. "google-gmail", "jira"). */
26
+ serviceID: string
27
+ /** Human-readable label shown in auth prompts (e.g. "Google Gmail"). */
28
+ label: string
29
+ /** Auth methods — reuses the same AuthMethod type as provider auth (OAuth, API key, custom). */
30
+ methods: AuthMethod[]
31
+ /** Optional refresh callback — called when stored OAuth token is expired.
32
+ * Receives the current credentials, returns refreshed credentials.
33
+ * If this throws, falls through to full re-auth flow. */
34
+ refresh?: (entry: ServiceCredentials) => Promise<ServiceCredentials>
35
+ /** Optional custom credential storage. Defaults to auth.json (0o600 permissions).
36
+ * Plugins can provide their own storage (e.g. system keychain, encrypted vault). */
37
+ storage?: {
38
+ get(): Promise<ServiceCredentials | undefined>
39
+ set(entry: ServiceCredentials): Promise<void>
40
+ remove(): Promise<void>
41
+ }
42
+ }
43
+
44
+ // ─── Tool context ───────────────────────────────────────────────────────────
2
45
 
3
46
  /**
4
47
  * Execution context passed to every custom tool's execute function.
@@ -20,6 +63,19 @@ export type ToolContext = {
20
63
  abort: AbortSignal
21
64
  metadata(input: { title?: string; metadata?: { [key: string]: unknown } }): void
22
65
  ask(input: AskInput): Promise<void>
66
+ /**
67
+ * Service credentials for this tool's declared `auth` service.
68
+ * Populated automatically before execute() when the tool has a `ToolAuthConfig`
69
+ * and the user has authenticated. Undefined if no auth is declared.
70
+ */
71
+ credentials?: ServiceCredentials
72
+ /**
73
+ * Clear cached credentials and re-trigger the auth flow.
74
+ * Call this when the external API returns 401/403 (token revoked).
75
+ * Returns fresh credentials after the user re-authenticates.
76
+ * Only available on tools that declare `auth`.
77
+ */
78
+ reauth?: () => Promise<ServiceCredentials>
23
79
  }
24
80
 
25
81
  type AskInput = {
@@ -29,6 +85,28 @@ type AskInput = {
29
85
  metadata: { [key: string]: unknown }
30
86
  }
31
87
 
88
+ // ─── Tool definition ────────────────────────────────────────────────────────
89
+
90
+ export interface ToolAttachment {
91
+ type: "file"
92
+ mime: string
93
+ /** Data URL (data:mime;base64,...) or file:// URL */
94
+ url: string
95
+ }
96
+
97
+ export type PluginToolResult = string | { text: string; attachments: ToolAttachment[] }
98
+
99
+ /**
100
+ * The wide storage type used in Hooks.tool and internal registries.
101
+ * Plugin authors don't construct this directly — use the `tool()` factory.
102
+ */
103
+ export interface ToolDefinition {
104
+ description: string
105
+ args: Record<string, z.ZodType>
106
+ auth?: ToolAuthConfig
107
+ execute(args: Record<string, unknown>, context: ToolContext): Promise<PluginToolResult>
108
+ }
109
+
32
110
  /**
33
111
  * Define a custom tool. Use this in plugin packages or local tool files.
34
112
  *
@@ -50,11 +128,11 @@ type AskInput = {
50
128
  export function tool<Args extends z.ZodRawShape>(input: {
51
129
  description: string
52
130
  args: Args
53
- execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<string>
54
- }) {
131
+ /** Optional auth requirement — tool execution will pause for auth if credentials are missing. */
132
+ auth?: ToolAuthConfig
133
+ execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<PluginToolResult>
134
+ }): ToolDefinition {
55
135
  return input
56
136
  }
57
137
 
58
138
  tool.schema = z
59
-
60
- export type ToolDefinition = ReturnType<typeof tool>