xyne-plugin 1.0.2 → 1.2.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.2.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,110 @@ 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
+ /** Message shown in the TUI while authorize() runs (default: "Connecting..."). */
208
+ pendingMessage?: string
209
+ /** Hint shown below the pending message. */
210
+ pendingHint?: string
211
+ prompts?: AuthPrompt[]
212
+ /** Plugin handles the entire auth flow. UI shows pendingMessage while this runs. */
213
+ authorize(inputs?: Record<string, string>): Promise<
214
+ | { type: "success"; data: Record<string, unknown> }
215
+ | { type: "failed" }
216
+ >
217
+ }
218
+
219
+ /**
220
+ * Options returned by an auth loader to configure the provider SDK.
221
+ */
222
+ export interface AuthLoaderResult {
223
+ baseURL?: string
224
+ apiKey?: string
225
+ fetch?: typeof globalThis.fetch
226
+ /** Custom model selector — receives the SDK instance, model ID, and options. */
227
+ getModel?: (sdk: unknown, modelID: string, options: Record<string, unknown>) => Promise<unknown>
228
+ [key: string]: unknown
229
+ }
230
+
231
+ export type AuthHook = {
232
+ provider: string
233
+ /**
234
+ * Called after auth succeeds to configure the provider SDK.
235
+ * Receives a getter for the auth entry and the provider info.
236
+ * Returns SDK options (baseURL, apiKey, fetch, getModel, etc.)
237
+ */
238
+ loader?: (auth: () => Promise<unknown>, provider: unknown) => Promise<AuthLoaderResult>
239
+ methods: AuthMethod[]
240
+ }
241
+
33
242
  // ─── Hook definitions ────────────────────────────────────────────────────────
34
243
 
35
244
  export interface Hooks {
@@ -42,18 +251,34 @@ export interface Hooks {
42
251
  /** Register custom tools. Keyed by tool ID. */
43
252
  tool?: Record<string, ToolDefinition>
44
253
 
45
- /** Auth provider stub expand later with full OAuth / API key flows. */
46
- auth?: { provider: string }
254
+ /** Register custom agents. Keyed by agent name. */
255
+ agent?: Record<string, PluginAgentDef>
256
+
257
+ /** Auth provider — OAuth, API key, or custom auth flows. */
258
+ auth?: AuthHook
259
+
260
+ /** Called when a new message is created (user or assistant). */
261
+ "chat.message"?: (
262
+ input: {
263
+ sessionID: string
264
+ messageID: string
265
+ role: string
266
+ agent?: string
267
+ model?: { providerID: string; modelID: string }
268
+ variant?: string
269
+ },
270
+ output: { message: MessageInfo; parts: PartInfo[] },
271
+ ) => Promise<void>
47
272
 
48
273
  /** Mutate LLM sampling parameters before a chat request. */
49
274
  "chat.params"?: (
50
- input: { sessionID: string; agent: string; model: ModelSpec },
51
- output: { temperature?: number; topP?: number; topK?: number },
275
+ input: { sessionID: string; agent: string; model: ModelSpec; provider?: ProviderInfo; message?: MessageInfo },
276
+ output: { temperature?: number; topP?: number; topK?: number; options?: Record<string, unknown> },
52
277
  ) => Promise<void>
53
278
 
54
279
  /** Inject extra HTTP headers into the provider request. */
55
280
  "chat.headers"?: (
56
- input: { sessionID: string; agent: string; model: ModelSpec },
281
+ input: { sessionID: string; agent: string; model: ModelSpec; provider?: ProviderInfo; message?: MessageInfo },
57
282
  output: { headers: Record<string, string> },
58
283
  ) => Promise<void>
59
284
 
@@ -75,6 +300,12 @@ export interface Hooks {
75
300
  output: { status: "ask" | "deny" | "allow" },
76
301
  ) => Promise<void>
77
302
 
303
+ /** Called before a slash command executes. */
304
+ "command.execute.before"?: (
305
+ input: { command: string; sessionID: string; arguments: string },
306
+ output: { parts: PartInfo[] },
307
+ ) => Promise<void>
308
+
78
309
  /** Modify tool args before execution. */
79
310
  "tool.execute.before"?: (
80
311
  input: { tool: string; sessionID: string; callID: string },
@@ -104,4 +335,10 @@ export interface Hooks {
104
335
  input: { sessionID: string },
105
336
  output: { context: string[]; prompt?: string },
106
337
  ) => Promise<void>
338
+
339
+ /** Text completion hook — modify or provide text completions. */
340
+ "text.complete"?: (
341
+ input: { text: string; cursorOffset: number },
342
+ output: { completions: string[] },
343
+ ) => Promise<void>
107
344
  }
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>