@openacp/cli 2026.326.4 → 2026.327.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.
Files changed (72) hide show
  1. package/README.md +2 -2
  2. package/dist/{adapter-77ZCVABT.js → adapter-IZNL6AK2.js} +13 -13
  3. package/dist/{adapter-6ANPBSVU.js → adapter-Z435XYBQ.js} +2 -2
  4. package/dist/{api-server-CHVSUDBX.js → api-server-2I7B3MXR.js} +2 -2
  5. package/dist/{api-server-3PYLRBCN.js → api-server-5VEESFOT.js} +2 -2
  6. package/dist/{chunk-Y64XWMJ4.js → chunk-366FOUJG.js} +2 -2
  7. package/dist/{chunk-WVLDNYOJ.js → chunk-5RO42TWV.js} +2 -2
  8. package/dist/{chunk-UNJUWWQO.js → chunk-5YW56UJK.js} +1 -10
  9. package/dist/chunk-5YW56UJK.js.map +1 -0
  10. package/dist/{chunk-NBFIBGAT.js → chunk-7KGWYNWE.js} +1 -1
  11. package/dist/chunk-CDAUYTVP.js +41 -0
  12. package/dist/chunk-CDAUYTVP.js.map +1 -0
  13. package/dist/{chunk-Q6ZXJTZB.js → chunk-CFM4GJ74.js} +7 -11
  14. package/dist/chunk-CFM4GJ74.js.map +1 -0
  15. package/dist/{chunk-RKB2ZK6S.js → chunk-FDLQ5M6W.js} +17 -136
  16. package/dist/chunk-FDLQ5M6W.js.map +1 -0
  17. package/dist/{chunk-FQEBWOZR.js → chunk-QUXTZU36.js} +45 -126
  18. package/dist/chunk-QUXTZU36.js.map +1 -0
  19. package/dist/{chunk-QSDZDHNS.js → chunk-VO3A2NI4.js} +4 -4
  20. package/dist/{chunk-V5JT5TPD.js → chunk-ZPTM4NGK.js} +2 -2
  21. package/dist/cli.js +145 -46
  22. package/dist/cli.js.map +1 -1
  23. package/dist/{config-editor-HNEKXRLQ.js → config-editor-2GYL2SSZ.js} +2 -2
  24. package/dist/{core-plugins-VEUNFTMB.js → core-plugins-I6UPXQBL.js} +7 -10
  25. package/dist/{discord-NOJQ5PZO.js → discord-DXDTGVGS.js} +2 -2
  26. package/dist/index.d.ts +14 -62
  27. package/dist/index.js +15 -21
  28. package/dist/index.js.map +1 -1
  29. package/dist/{integrate-5C6KSU6D.js → integrate-APK4OEQF.js} +2 -2
  30. package/dist/integrate-APK4OEQF.js.map +1 -0
  31. package/dist/{main-T5WVCCFN.js → main-EJBK65NS.js} +33 -52
  32. package/dist/main-EJBK65NS.js.map +1 -0
  33. package/dist/{new-session-AVQCNXRG.js → new-session-HFO5GHSZ.js} +3 -3
  34. package/dist/plugin-create-LCXXNDK6.js +950 -0
  35. package/dist/plugin-create-LCXXNDK6.js.map +1 -0
  36. package/dist/plugin-search-HQ4WQKOF.js +40 -0
  37. package/dist/plugin-search-HQ4WQKOF.js.map +1 -0
  38. package/dist/{post-upgrade-XLHZ6ZB7.js → post-upgrade-2MG3VUDV.js} +2 -2
  39. package/dist/registry-client-AVGRE4CF.js +8 -0
  40. package/dist/{setup-BAI2F24H.js → setup-N7KT56O7.js} +7 -7
  41. package/dist/{telegram-ZDC3JQF2.js → telegram-QWMJU3A6.js} +2 -2
  42. package/package.json +2 -2
  43. package/dist/chunk-2CX4IEEC.js +0 -124
  44. package/dist/chunk-2CX4IEEC.js.map +0 -1
  45. package/dist/chunk-FQEBWOZR.js.map +0 -1
  46. package/dist/chunk-Q6ZXJTZB.js.map +0 -1
  47. package/dist/chunk-RKB2ZK6S.js.map +0 -1
  48. package/dist/chunk-UNJUWWQO.js.map +0 -1
  49. package/dist/chunk-WAAD23KY.js +0 -222
  50. package/dist/chunk-WAAD23KY.js.map +0 -1
  51. package/dist/integrate-5C6KSU6D.js.map +0 -1
  52. package/dist/main-T5WVCCFN.js.map +0 -1
  53. package/dist/plugin-create-AQ3B22BZ.js +0 -334
  54. package/dist/plugin-create-AQ3B22BZ.js.map +0 -1
  55. package/dist/usage-WYNK6ZC5.js +0 -10
  56. /package/dist/{adapter-77ZCVABT.js.map → adapter-IZNL6AK2.js.map} +0 -0
  57. /package/dist/{adapter-6ANPBSVU.js.map → adapter-Z435XYBQ.js.map} +0 -0
  58. /package/dist/{api-server-3PYLRBCN.js.map → api-server-2I7B3MXR.js.map} +0 -0
  59. /package/dist/{api-server-CHVSUDBX.js.map → api-server-5VEESFOT.js.map} +0 -0
  60. /package/dist/{chunk-Y64XWMJ4.js.map → chunk-366FOUJG.js.map} +0 -0
  61. /package/dist/{chunk-WVLDNYOJ.js.map → chunk-5RO42TWV.js.map} +0 -0
  62. /package/dist/{chunk-NBFIBGAT.js.map → chunk-7KGWYNWE.js.map} +0 -0
  63. /package/dist/{chunk-QSDZDHNS.js.map → chunk-VO3A2NI4.js.map} +0 -0
  64. /package/dist/{chunk-V5JT5TPD.js.map → chunk-ZPTM4NGK.js.map} +0 -0
  65. /package/dist/{config-editor-HNEKXRLQ.js.map → config-editor-2GYL2SSZ.js.map} +0 -0
  66. /package/dist/{core-plugins-VEUNFTMB.js.map → core-plugins-I6UPXQBL.js.map} +0 -0
  67. /package/dist/{discord-NOJQ5PZO.js.map → discord-DXDTGVGS.js.map} +0 -0
  68. /package/dist/{new-session-AVQCNXRG.js.map → new-session-HFO5GHSZ.js.map} +0 -0
  69. /package/dist/{post-upgrade-XLHZ6ZB7.js.map → post-upgrade-2MG3VUDV.js.map} +0 -0
  70. /package/dist/{telegram-ZDC3JQF2.js.map → registry-client-AVGRE4CF.js.map} +0 -0
  71. /package/dist/{setup-BAI2F24H.js.map → setup-N7KT56O7.js.map} +0 -0
  72. /package/dist/{usage-WYNK6ZC5.js.map → telegram-QWMJU3A6.js.map} +0 -0
@@ -0,0 +1,950 @@
1
+ import {
2
+ getCurrentVersion
3
+ } from "./chunk-S64CB6J3.js";
4
+ import "./chunk-VUNV25KB.js";
5
+
6
+ // src/cli/commands/plugin-create.ts
7
+ import * as p from "@clack/prompts";
8
+ import fs from "fs";
9
+ import path from "path";
10
+
11
+ // src/cli/plugin-template/package-json.ts
12
+ function generatePackageJson(params) {
13
+ const packageJson = {
14
+ name: params.pluginName,
15
+ version: "0.1.0",
16
+ description: params.description || "",
17
+ type: "module",
18
+ main: "dist/index.js",
19
+ types: "dist/index.d.ts",
20
+ scripts: {
21
+ build: "tsc",
22
+ dev: "tsc --watch",
23
+ test: "vitest",
24
+ prepublishOnly: "npm run build"
25
+ },
26
+ author: params.author || "",
27
+ license: params.license,
28
+ keywords: ["openacp", "openacp-plugin"],
29
+ engines: {
30
+ openacp: `>=${params.cliVersion}`
31
+ },
32
+ peerDependencies: {
33
+ "@openacp/cli": `>=${params.cliVersion}`
34
+ },
35
+ devDependencies: {
36
+ "@openacp/plugin-sdk": params.cliVersion,
37
+ typescript: "^5.4.0",
38
+ vitest: "^3.0.0"
39
+ }
40
+ };
41
+ return JSON.stringify(packageJson, null, 2) + "\n";
42
+ }
43
+
44
+ // src/cli/plugin-template/tsconfig.ts
45
+ function generateTsconfig() {
46
+ const tsconfig = {
47
+ compilerOptions: {
48
+ target: "ES2022",
49
+ module: "NodeNext",
50
+ moduleResolution: "NodeNext",
51
+ declaration: true,
52
+ outDir: "dist",
53
+ rootDir: "src",
54
+ strict: true,
55
+ esModuleInterop: true,
56
+ skipLibCheck: true,
57
+ forceConsistentCasingInFileNames: true
58
+ },
59
+ include: ["src"],
60
+ exclude: ["node_modules", "dist", "src/**/__tests__"]
61
+ };
62
+ return JSON.stringify(tsconfig, null, 2) + "\n";
63
+ }
64
+
65
+ // src/cli/plugin-template/dotfiles.ts
66
+ function generateGitignore() {
67
+ return ["node_modules/", "dist/", "*.tsbuildinfo", ".DS_Store", ""].join("\n");
68
+ }
69
+ function generateNpmignore() {
70
+ return ["src/", "tsconfig.json", ".editorconfig", ".gitignore", "*.test.ts", "__tests__/", ""].join("\n");
71
+ }
72
+ function generateEditorconfig() {
73
+ return [
74
+ "root = true",
75
+ "",
76
+ "[*]",
77
+ "indent_style = space",
78
+ "indent_size = 2",
79
+ "end_of_line = lf",
80
+ "charset = utf-8",
81
+ "trim_trailing_whitespace = true",
82
+ "insert_final_newline = true",
83
+ ""
84
+ ].join("\n");
85
+ }
86
+
87
+ // src/cli/plugin-template/readme.ts
88
+ function generateReadme(params) {
89
+ return [
90
+ `# ${params.pluginName}`,
91
+ "",
92
+ params.description || "An OpenACP plugin.",
93
+ "",
94
+ "## Installation",
95
+ "",
96
+ "```bash",
97
+ `openacp plugin add ${params.pluginName}`,
98
+ "```",
99
+ "",
100
+ "## Development",
101
+ "",
102
+ "```bash",
103
+ "npm install",
104
+ "npm run build",
105
+ "npm test",
106
+ "",
107
+ "# Live development with hot-reload:",
108
+ `openacp dev .`,
109
+ "```",
110
+ "",
111
+ "## License",
112
+ "",
113
+ params.license,
114
+ ""
115
+ ].join("\n");
116
+ }
117
+
118
+ // src/cli/plugin-template/plugin-source.ts
119
+ function generatePluginSource(params) {
120
+ const dirName = params.pluginName.replace(/^@[^/]+\//, "");
121
+ const escapedDescription = (params.description || "").replace(/'/g, "\\'");
122
+ return `import type { OpenACPPlugin, PluginContext, InstallContext, MigrateContext } from '@openacp/plugin-sdk'
123
+
124
+ const plugin: OpenACPPlugin = {
125
+ name: '${params.pluginName}',
126
+ version: '0.1.0',
127
+ description: '${escapedDescription}',
128
+
129
+ // Declare which permissions your plugin needs.
130
+ // Available: events:read, events:emit, services:register, services:use,
131
+ // middleware:register, commands:register, storage:read, storage:write, kernel:access
132
+ permissions: ['events:read', 'services:register'],
133
+
134
+ // Dependencies on other plugins (loaded before this one).
135
+ // pluginDependencies: { '@openacp/security': '>=1.0.0' },
136
+
137
+ // Optional dependencies (used if available, gracefully degrade if not).
138
+ // optionalPluginDependencies: { '@openacp/usage': '>=1.0.0' },
139
+
140
+ /**
141
+ * Called during server startup in dependency order.
142
+ * Register services, middleware, commands, and event listeners here.
143
+ */
144
+ async setup(ctx: PluginContext): Promise<void> {
145
+ ctx.log.info('Plugin setup started')
146
+
147
+ // Example: register a service
148
+ // ctx.registerService('my-service', myServiceImpl)
149
+
150
+ // Example: listen to events
151
+ // ctx.on('session:created', (event) => { ... })
152
+
153
+ // Example: register a slash command
154
+ // ctx.registerCommand({
155
+ // name: 'mycommand',
156
+ // description: 'Does something useful',
157
+ // category: 'plugin',
158
+ // async handler(args) {
159
+ // return { type: 'text', text: 'Hello from ${params.pluginName}!' }
160
+ // },
161
+ // })
162
+
163
+ ctx.log.info('Plugin setup complete')
164
+ },
165
+
166
+ /**
167
+ * Called during server shutdown in reverse dependency order.
168
+ * Clean up resources, close connections, stop timers here.
169
+ * Has a 10-second timeout.
170
+ */
171
+ async teardown(): Promise<void> {
172
+ // Clean up resources here
173
+ },
174
+
175
+ /**
176
+ * Called when user runs \`openacp plugin add ${params.pluginName}\`.
177
+ * Use ctx.terminal for interactive prompts to gather configuration.
178
+ */
179
+ async install(ctx: InstallContext): Promise<void> {
180
+ ctx.terminal.log.info('Installing ${params.pluginName}...')
181
+
182
+ // Example: prompt for configuration
183
+ // const apiKey = await ctx.terminal.text({
184
+ // message: 'Enter your API key',
185
+ // validate: (v) => v.length === 0 ? 'Required' : undefined,
186
+ // })
187
+ // await ctx.settings.set('apiKey', apiKey)
188
+
189
+ ctx.terminal.log.success('Installation complete!')
190
+ },
191
+
192
+ /**
193
+ * Called when user runs \`openacp plugin configure ${params.pluginName}\`.
194
+ * Re-run configuration prompts to update settings.
195
+ */
196
+ async configure(ctx: InstallContext): Promise<void> {
197
+ ctx.terminal.log.info('Configuring ${params.pluginName}...')
198
+
199
+ // Re-run configuration prompts, pre-filling with current values
200
+ // const current = await ctx.settings.getAll()
201
+ // ...
202
+
203
+ ctx.terminal.log.success('Configuration updated!')
204
+ },
205
+
206
+ /**
207
+ * Called during boot when the plugin version has changed.
208
+ * Migrate settings from the old format to the new format.
209
+ */
210
+ async migrate(ctx: MigrateContext, oldSettings: unknown, oldVersion: string): Promise<unknown> {
211
+ ctx.log.info(\`Migrating from v\${oldVersion}\`)
212
+ // Return the migrated settings object
213
+ return oldSettings
214
+ },
215
+
216
+ /**
217
+ * Called when user runs \`openacp plugin remove ${params.pluginName}\`.
218
+ * Clean up any external resources. If opts.purge is true, delete all data.
219
+ */
220
+ async uninstall(ctx: InstallContext, opts: { purge: boolean }): Promise<void> {
221
+ ctx.terminal.log.info('Uninstalling ${params.pluginName}...')
222
+ if (opts.purge) {
223
+ await ctx.settings.clear()
224
+ }
225
+ ctx.terminal.log.success('Uninstalled!')
226
+ },
227
+ }
228
+
229
+ export default plugin
230
+ `;
231
+ }
232
+
233
+ // src/cli/plugin-template/plugin-test.ts
234
+ function generatePluginTest(params) {
235
+ return `import { describe, it, expect } from 'vitest'
236
+ import { createTestContext, createTestInstallContext } from '@openacp/plugin-sdk/testing'
237
+ import plugin from '../index.js'
238
+
239
+ describe('${params.pluginName}', () => {
240
+ it('has correct metadata', () => {
241
+ expect(plugin.name).toBe('${params.pluginName}')
242
+ expect(plugin.version).toBeDefined()
243
+ expect(plugin.setup).toBeInstanceOf(Function)
244
+ })
245
+
246
+ it('sets up without errors', async () => {
247
+ const ctx = createTestContext({
248
+ pluginName: '${params.pluginName}',
249
+ pluginConfig: { enabled: true },
250
+ permissions: plugin.permissions,
251
+ })
252
+ await expect(plugin.setup(ctx)).resolves.not.toThrow()
253
+ })
254
+
255
+ it('tears down without errors', async () => {
256
+ if (plugin.teardown) {
257
+ await expect(plugin.teardown()).resolves.not.toThrow()
258
+ }
259
+ })
260
+
261
+ it('installs without errors', async () => {
262
+ if (plugin.install) {
263
+ const ctx = createTestInstallContext({
264
+ pluginName: '${params.pluginName}',
265
+ terminalResponses: { password: [''], confirm: [true], select: ['apiKey'] },
266
+ })
267
+ await expect(plugin.install(ctx)).resolves.not.toThrow()
268
+ }
269
+ })
270
+ })
271
+ `;
272
+ }
273
+
274
+ // src/cli/plugin-template/claude-md.ts
275
+ function generateClaudeMd(params) {
276
+ return `# CLAUDE.md
277
+
278
+ This file provides context for AI coding agents (Claude, Cursor, etc.) working on this plugin.
279
+
280
+ ## Project Overview
281
+
282
+ This is an OpenACP plugin. OpenACP bridges AI coding agents to messaging platforms via the Agent Client Protocol (ACP). Plugins extend OpenACP with new adapters, services, commands, and middleware.
283
+
284
+ - **Package**: ${params.pluginName}
285
+ - **SDK**: \`@openacp/plugin-sdk\` (types, base classes, testing utilities)
286
+ - **Entry point**: \`src/index.ts\` (default export of \`OpenACPPlugin\` object)
287
+
288
+ ## Build & Run
289
+
290
+ \`\`\`bash
291
+ npm install # Install dependencies
292
+ npm run build # Compile TypeScript (tsc)
293
+ npm run dev # Watch mode (tsc --watch)
294
+ npm test # Run tests (vitest)
295
+ \`\`\`
296
+
297
+ ### Development with hot-reload
298
+
299
+ \`\`\`bash
300
+ openacp dev . # Compiles, watches, and reloads plugin on changes
301
+ \`\`\`
302
+
303
+ ## File Structure
304
+
305
+ \`\`\`
306
+ src/
307
+ index.ts \u2014 Plugin entry point (exports OpenACPPlugin)
308
+ __tests__/
309
+ index.test.ts \u2014 Tests using @openacp/plugin-sdk/testing
310
+ package.json \u2014 engines.openacp declares minimum CLI version
311
+ tsconfig.json \u2014 ES2022, NodeNext, strict mode
312
+ CLAUDE.md \u2014 This file (AI agent context)
313
+ PLUGIN_GUIDE.md \u2014 Human-readable developer guide
314
+ \`\`\`
315
+
316
+ ## Architecture: How OpenACP Plugins Work
317
+
318
+ ### Plugin Lifecycle
319
+
320
+ \`\`\`
321
+ install \u2500\u2500> [reboot] \u2500\u2500> migrate? \u2500\u2500> setup \u2500\u2500> [running] \u2500\u2500> teardown \u2500\u2500> uninstall
322
+ \`\`\`
323
+
324
+ | Hook | Trigger | Interactive? | Has Services? |
325
+ |------|---------|-------------|---------------|
326
+ | \`install(ctx)\` | \`openacp plugin add <name>\` | Yes | No |
327
+ | \`migrate(ctx, oldSettings, oldVersion)\` | Boot \u2014 stored version differs from plugin version | No | No |
328
+ | \`configure(ctx)\` | \`openacp plugin configure <name>\` | Yes | No |
329
+ | \`setup(ctx)\` | Every boot, after migrate | No | Yes |
330
+ | \`teardown()\` | Shutdown (10s timeout) | No | Yes |
331
+ | \`uninstall(ctx, opts)\` | \`openacp plugin remove <name>\` | Yes | No |
332
+
333
+ ### OpenACPPlugin Interface
334
+
335
+ \`\`\`typescript
336
+ interface OpenACPPlugin {
337
+ name: string // unique identifier, e.g. '@myorg/my-plugin'
338
+ version: string // semver
339
+ description?: string
340
+ permissions?: PluginPermission[]
341
+ pluginDependencies?: Record<string, string> // name -> semver range
342
+ optionalPluginDependencies?: Record<string, string> // used if available
343
+ overrides?: string // replace a built-in plugin entirely
344
+ settingsSchema?: ZodSchema // Zod validation for settings
345
+ essential?: boolean // true = needs setup before system can run
346
+
347
+ setup(ctx: PluginContext): Promise<void>
348
+ teardown?(): Promise<void>
349
+ install?(ctx: InstallContext): Promise<void>
350
+ configure?(ctx: InstallContext): Promise<void>
351
+ migrate?(ctx: MigrateContext, oldSettings: unknown, oldVersion: string): Promise<unknown>
352
+ uninstall?(ctx: InstallContext, opts: { purge: boolean }): Promise<void>
353
+ }
354
+ \`\`\`
355
+
356
+ ### PluginContext API (available in setup)
357
+
358
+ \`\`\`typescript
359
+ interface PluginContext {
360
+ pluginName: string
361
+ pluginConfig: Record<string, unknown> // from settings.json
362
+
363
+ // Events (requires 'events:read' / 'events:emit')
364
+ on(event: string, handler: (...args: unknown[]) => void): void
365
+ off(event: string, handler: (...args: unknown[]) => void): void
366
+ emit(event: string, payload: unknown): void
367
+
368
+ // Services (requires 'services:register' / 'services:use')
369
+ registerService<T>(name: string, implementation: T): void
370
+ getService<T>(name: string): T | undefined
371
+
372
+ // Middleware (requires 'middleware:register')
373
+ registerMiddleware<H extends MiddlewareHook>(hook: H, opts: MiddlewareOptions<MiddlewarePayloadMap[H]>): void
374
+
375
+ // Commands (requires 'commands:register')
376
+ registerCommand(def: CommandDef): void
377
+
378
+ // Storage (requires 'storage:read' / 'storage:write')
379
+ storage: PluginStorage // get, set, delete, list, getDataDir
380
+
381
+ // Messaging (requires 'services:use')
382
+ sendMessage(sessionId: string, content: OutgoingMessage): Promise<void>
383
+
384
+ // Kernel access (requires 'kernel:access')
385
+ sessions: SessionManager
386
+ config: ConfigManager
387
+ eventBus: EventBus
388
+
389
+ // Always available
390
+ log: Logger // trace, debug, info, warn, error, fatal, child
391
+ }
392
+ \`\`\`
393
+
394
+ ### CommandDef and CommandResponse
395
+
396
+ \`\`\`typescript
397
+ interface CommandDef {
398
+ name: string // command name without slash
399
+ description: string // shown in /help
400
+ usage?: string // e.g. '<city>' or 'on|off'
401
+ category: 'system' | 'plugin'
402
+ handler(args: CommandArgs): Promise<CommandResponse | void>
403
+ }
404
+
405
+ interface CommandArgs {
406
+ raw: string // text after command name
407
+ sessionId: string | null
408
+ channelId: string // 'telegram', 'discord', 'slack'
409
+ userId: string
410
+ reply(content: string | CommandResponse): Promise<void>
411
+ }
412
+
413
+ type CommandResponse =
414
+ | { type: 'text'; text: string }
415
+ | { type: 'menu'; title: string; options: MenuOption[] }
416
+ | { type: 'list'; title: string; items: ListItem[] }
417
+ | { type: 'confirm'; question: string; onYes: string; onNo: string }
418
+ | { type: 'error'; message: string }
419
+ | { type: 'silent' }
420
+ \`\`\`
421
+
422
+ ### Settings System
423
+
424
+ - \`settingsSchema\`: Zod schema for validation
425
+ - \`SettingsAPI\` (in InstallContext): get, set, getAll, setAll, delete, clear, has
426
+ - Settings stored at \`~/.openacp/plugins/@scope/name/settings.json\`
427
+ - \`PluginStorage\` (in PluginContext): key-value store at \`~/.openacp/plugins/data/@scope/name/kv.json\`
428
+ - \`storage.getDataDir()\`: returns path for large files, databases, caches
429
+
430
+ ### InstallContext (for install/configure/uninstall)
431
+
432
+ \`\`\`typescript
433
+ interface InstallContext {
434
+ pluginName: string
435
+ terminal: TerminalIO // text, select, confirm, password, multiselect, log, spinner, note
436
+ settings: SettingsAPI
437
+ legacyConfig?: Record<string, unknown>
438
+ dataDir: string
439
+ log: Logger
440
+ }
441
+ \`\`\`
442
+
443
+ ### Service Interfaces (available via ctx.getService)
444
+
445
+ | Service name | Interface | Description |
446
+ |---|---|---|
447
+ | \`security\` | \`SecurityService\` | Access control, session limits, user roles |
448
+ | \`file-service\` | \`FileServiceInterface\` | File saving, resolving, format conversion |
449
+ | \`notifications\` | \`NotificationService\` | Send notifications to users |
450
+ | \`usage\` | \`UsageService\` | Token/cost tracking and budget checking |
451
+ | \`speech\` | \`SpeechServiceInterface\` | Text-to-speech and speech-to-text |
452
+ | \`tunnel\` | \`TunnelServiceInterface\` | Port tunneling and public URL management |
453
+ | \`context\` | \`ContextService\` | Context building for agent sessions |
454
+
455
+ ## Plugin Permissions
456
+
457
+ Declare in \`permissions\` array. Only request what you need.
458
+
459
+ | Permission | Allows |
460
+ |---|---|
461
+ | \`events:read\` | \`ctx.on()\` \u2014 subscribe to events |
462
+ | \`events:emit\` | \`ctx.emit()\` \u2014 emit custom events (must prefix with plugin name) |
463
+ | \`services:register\` | \`ctx.registerService()\` \u2014 provide services to other plugins |
464
+ | \`services:use\` | \`ctx.getService()\`, \`ctx.sendMessage()\` \u2014 consume services |
465
+ | \`middleware:register\` | \`ctx.registerMiddleware()\` \u2014 intercept and modify flows |
466
+ | \`commands:register\` | \`ctx.registerCommand()\` \u2014 add chat commands |
467
+ | \`storage:read\` | \`ctx.storage.get()\`, \`ctx.storage.list()\` |
468
+ | \`storage:write\` | \`ctx.storage.set()\`, \`ctx.storage.delete()\` |
469
+ | \`kernel:access\` | \`ctx.sessions\`, \`ctx.config\`, \`ctx.eventBus\`, \`ctx.core\` |
470
+
471
+ Calling a method without the required permission throws \`PluginPermissionError\`.
472
+
473
+ ## Middleware Hooks (18 total)
474
+
475
+ Register with \`ctx.registerMiddleware(hook, { priority?, handler })\`. Return \`null\` to block the flow, call \`next()\` to continue.
476
+
477
+ ### Message flow
478
+ - \`message:incoming\` \u2014 incoming user message (channelId, threadId, userId, text, attachments)
479
+ - \`message:outgoing\` \u2014 outgoing message to user (sessionId, message)
480
+
481
+ ### Agent flow
482
+ - \`agent:beforePrompt\` \u2014 before prompt is sent to agent (sessionId, text, attachments)
483
+ - \`agent:beforeEvent\` \u2014 before agent event is processed (sessionId, event)
484
+ - \`agent:afterEvent\` \u2014 after agent event, before delivery (sessionId, event, outgoingMessage)
485
+
486
+ ### Turn lifecycle
487
+ - \`turn:start\` \u2014 agent turn begins (sessionId, promptText, promptNumber)
488
+ - \`turn:end\` \u2014 agent turn ends (sessionId, stopReason, durationMs)
489
+
490
+ ### File system
491
+ - \`fs:beforeRead\` \u2014 before file read (sessionId, path, line, limit)
492
+ - \`fs:beforeWrite\` \u2014 before file write (sessionId, path, content)
493
+
494
+ ### Terminal
495
+ - \`terminal:beforeCreate\` \u2014 before terminal process spawned (sessionId, command, args, env, cwd)
496
+ - \`terminal:afterExit\` \u2014 after terminal process exits (sessionId, terminalId, command, exitCode, durationMs)
497
+
498
+ ### Permission
499
+ - \`permission:beforeRequest\` \u2014 before permission prompt (sessionId, request, autoResolve)
500
+ - \`permission:afterResolve\` \u2014 after permission resolved (sessionId, requestId, decision, userId, durationMs)
501
+
502
+ ### Session
503
+ - \`session:beforeCreate\` \u2014 before session creation (agentName, workingDir, userId, channelId, threadId)
504
+ - \`session:afterDestroy\` \u2014 after session destroyed (sessionId, reason, durationMs, promptCount)
505
+
506
+ ### Control
507
+ - \`mode:beforeChange\` \u2014 before mode change (sessionId, fromMode, toMode)
508
+ - \`config:beforeChange\` \u2014 before config change (sessionId, configId, oldValue, newValue)
509
+ - \`model:beforeChange\` \u2014 before model change (sessionId, fromModel, toModel)
510
+ - \`agent:beforeCancel\` \u2014 before agent cancellation (sessionId, reason)
511
+
512
+ ## Plugin Events (subscribe with ctx.on)
513
+
514
+ ### System
515
+ - \`kernel:booted\`, \`system:ready\`, \`system:shutdown\`, \`system:commands-ready\`
516
+
517
+ ### Plugin lifecycle
518
+ - \`plugin:loaded\`, \`plugin:failed\`, \`plugin:disabled\`, \`plugin:unloaded\`
519
+
520
+ ### Session
521
+ - \`session:created\`, \`session:ended\`, \`session:named\`, \`session:updated\`
522
+
523
+ ### Agent
524
+ - \`agent:event\`, \`agent:prompt\`
525
+
526
+ ### Permission
527
+ - \`permission:request\`, \`permission:resolved\`
528
+
529
+ ## Testing
530
+
531
+ Use \`@openacp/plugin-sdk/testing\`:
532
+
533
+ \`\`\`typescript
534
+ import { createTestContext, createTestInstallContext, mockServices } from '@openacp/plugin-sdk/testing'
535
+ \`\`\`
536
+
537
+ ### createTestContext(opts)
538
+
539
+ Creates a test \`PluginContext\`. All state is in-memory.
540
+
541
+ \`\`\`typescript
542
+ const ctx = createTestContext({
543
+ pluginName: '${params.pluginName}',
544
+ pluginConfig: { enabled: true },
545
+ permissions: plugin.permissions,
546
+ services: { security: mockServices.security() },
547
+ })
548
+ await plugin.setup(ctx)
549
+ expect(ctx.registeredCommands.has('mycommand')).toBe(true)
550
+ const response = await ctx.executeCommand('mycommand', { raw: 'test' })
551
+ \`\`\`
552
+
553
+ Inspection properties: \`registeredServices\`, \`registeredCommands\`, \`registeredMiddleware\`, \`emittedEvents\`, \`sentMessages\`, \`executeCommand()\`.
554
+
555
+ ### createTestInstallContext(opts)
556
+
557
+ Creates a test \`InstallContext\`. Terminal prompts auto-answered from \`terminalResponses\`.
558
+
559
+ \`\`\`typescript
560
+ const ctx = createTestInstallContext({
561
+ pluginName: '${params.pluginName}',
562
+ terminalResponses: { password: ['sk-test-key'], select: ['en'] },
563
+ })
564
+ await plugin.install!(ctx)
565
+ expect(ctx.settingsData.get('apiKey')).toBe('sk-test-key')
566
+ \`\`\`
567
+
568
+ ### mockServices
569
+
570
+ Factory functions for mock service implementations:
571
+
572
+ \`\`\`typescript
573
+ mockServices.security(overrides?) // checkAccess, checkSessionLimit, getUserRole
574
+ mockServices.fileService(overrides?) // saveFile, resolveFile, readTextFileWithRange
575
+ mockServices.notifications(overrides?) // notify, notifyAll
576
+ mockServices.usage(overrides?) // trackUsage, checkBudget, getSummary
577
+ mockServices.speech(overrides?) // textToSpeech, speechToText, register*
578
+ mockServices.tunnel(overrides?) // getPublicUrl, start, stop, getStore, fileUrl, diffUrl
579
+ mockServices.context(overrides?) // buildContext, registerProvider
580
+ \`\`\`
581
+
582
+ ## Conventions
583
+
584
+ - **ESM-only**: \`"type": "module"\` in package.json
585
+ - **Import extensions**: All imports must use \`.js\` extension (e.g., \`import x from './util.js'\`)
586
+ - **TypeScript strict mode**: \`strict: true\` in tsconfig.json
587
+ - **Target**: ES2022, module NodeNext
588
+ - **Test framework**: Vitest
589
+ - **Test files**: \`src/**/__tests__/*.test.ts\`
590
+
591
+ ## How to Add a Command
592
+
593
+ \`\`\`typescript
594
+ // In setup():
595
+ ctx.registerCommand({
596
+ name: 'mycommand',
597
+ description: 'Does something useful',
598
+ usage: '<arg>',
599
+ category: 'plugin',
600
+ async handler(args) {
601
+ const input = args.raw.trim()
602
+ if (!input) return { type: 'error', message: 'Usage: /mycommand <arg>' }
603
+ return { type: 'text', text: \\\`Result: \\\${input}\\\` }
604
+ },
605
+ })
606
+ \`\`\`
607
+
608
+ Requires \`commands:register\` permission. Available as \`/mycommand\` (if no conflict) and \`/pluginscope:mycommand\` (always).
609
+
610
+ ## How to Add a Service
611
+
612
+ \`\`\`typescript
613
+ // In setup():
614
+ const myService = new MyService()
615
+ ctx.registerService('my-service', myService)
616
+ \`\`\`
617
+
618
+ Requires \`services:register\` permission. Other plugins consume with \`ctx.getService<MyService>('my-service')\`.
619
+
620
+ ## How to Add Middleware
621
+
622
+ \`\`\`typescript
623
+ // In setup():
624
+ ctx.registerMiddleware('message:outgoing', {
625
+ priority: 50, // lower = earlier execution
626
+ handler: async (payload, next) => {
627
+ payload.message.text = modifyText(payload.message.text)
628
+ return next() // continue chain; return null to block
629
+ },
630
+ })
631
+ \`\`\`
632
+
633
+ Requires \`middleware:register\` permission.
634
+
635
+ ## How Settings Work
636
+
637
+ 1. Define \`settingsSchema\` (Zod) on the plugin object
638
+ 2. In \`install()\`: use \`ctx.terminal\` for interactive prompts, save with \`ctx.settings.set()\`
639
+ 3. In \`configure()\`: re-run prompts with current values pre-filled
640
+ 4. In \`setup()\`: read settings from \`ctx.pluginConfig\`
641
+ 5. In \`migrate()\`: transform old settings to new format on version change
642
+
643
+ ## Version Compatibility
644
+
645
+ The \`engines.openacp\` field in package.json declares the minimum CLI version. OpenACP checks this on install and warns if incompatible.
646
+ `;
647
+ }
648
+
649
+ // src/cli/plugin-template/plugin-guide.ts
650
+ function generatePluginGuide(params) {
651
+ return `# Plugin Developer Guide
652
+
653
+ ## Overview
654
+
655
+ **${params.pluginName}** is an OpenACP plugin.
656
+
657
+ > TODO: Describe what this plugin does.
658
+
659
+ ## Project Structure
660
+
661
+ \`\`\`
662
+ src/
663
+ index.ts \u2014 Plugin entry point (exports OpenACPPlugin object)
664
+ __tests__/
665
+ index.test.ts \u2014 Tests using Vitest + @openacp/plugin-sdk/testing
666
+ package.json \u2014 npm package config with engines.openacp constraint
667
+ tsconfig.json \u2014 TypeScript strict mode, ES2022, NodeNext
668
+ CLAUDE.md \u2014 Full technical reference for AI coding agents
669
+ PLUGIN_GUIDE.md \u2014 This file
670
+ \`\`\`
671
+
672
+ ## Development Workflow
673
+
674
+ 1. **Edit** \`src/index.ts\` \u2014 implement your plugin logic
675
+ 2. **Dev mode**: \`openacp dev .\` \u2014 compiles, watches, and hot-reloads your plugin
676
+ 3. **Test**: \`npm test\` \u2014 runs Vitest with SDK testing utilities
677
+ 4. **Build**: \`npm run build\` \u2014 compiles TypeScript to \`dist/\`
678
+
679
+ \`\`\`bash
680
+ npm install
681
+ openacp dev . # start developing with hot-reload
682
+ npm test # run tests
683
+ npm run build # compile for publishing
684
+ \`\`\`
685
+
686
+ ## Adding a Command
687
+
688
+ Register commands in your \`setup()\` function. Requires \`commands:register\` permission.
689
+
690
+ \`\`\`typescript
691
+ async setup(ctx: PluginContext) {
692
+ ctx.registerCommand({
693
+ name: 'greet',
694
+ description: 'Send a greeting',
695
+ usage: '[name]',
696
+ category: 'plugin',
697
+ async handler(args) {
698
+ const name = args.raw.trim() || 'World'
699
+ return { type: 'text', text: \\\`Hello, \\\${name}!\\\` }
700
+ },
701
+ })
702
+ }
703
+ \`\`\`
704
+
705
+ The command will be available as \`/greet\` in all messaging platforms.
706
+
707
+ ## Adding a Service
708
+
709
+ Provide a service that other plugins can consume. Requires \`services:register\` permission.
710
+
711
+ \`\`\`typescript
712
+ async setup(ctx: PluginContext) {
713
+ const myService = {
714
+ doSomething(input: string): string {
715
+ return input.toUpperCase()
716
+ },
717
+ }
718
+ ctx.registerService('my-service', myService)
719
+ }
720
+ \`\`\`
721
+
722
+ Other plugins access it with \`ctx.getService<MyServiceType>('my-service')\`.
723
+
724
+ ## Adding Middleware
725
+
726
+ Intercept and modify message flows. Requires \`middleware:register\` permission.
727
+
728
+ \`\`\`typescript
729
+ async setup(ctx: PluginContext) {
730
+ ctx.registerMiddleware('message:outgoing', {
731
+ priority: 50,
732
+ handler: async (payload, next) => {
733
+ // Modify the message before delivery
734
+ payload.message.text += '\\n-- sent via ${params.pluginName}'
735
+ return next() // continue the chain
736
+ // return null to block the message entirely
737
+ },
738
+ })
739
+ }
740
+ \`\`\`
741
+
742
+ ## Handling Settings
743
+
744
+ ### Install flow (first-time setup)
745
+
746
+ \`\`\`typescript
747
+ async install(ctx: InstallContext) {
748
+ const apiKey = await ctx.terminal.password({
749
+ message: 'Enter your API key:',
750
+ validate: (v) => v.length > 0 ? undefined : 'Required',
751
+ })
752
+ await ctx.settings.set('apiKey', apiKey)
753
+ ctx.terminal.log.success('Configured!')
754
+ }
755
+ \`\`\`
756
+
757
+ ### Configure flow (reconfiguration)
758
+
759
+ \`\`\`typescript
760
+ async configure(ctx: InstallContext) {
761
+ const current = await ctx.settings.getAll()
762
+ const apiKey = await ctx.terminal.password({
763
+ message: \\\`API key (current: \\\${current.apiKey ? '***' : 'not set'}):\\\`,
764
+ })
765
+ if (apiKey) await ctx.settings.set('apiKey', apiKey)
766
+ ctx.terminal.log.success('Updated!')
767
+ }
768
+ \`\`\`
769
+
770
+ ### Reading settings at runtime
771
+
772
+ \`\`\`typescript
773
+ async setup(ctx: PluginContext) {
774
+ const apiKey = ctx.pluginConfig.apiKey as string
775
+ if (!apiKey) {
776
+ ctx.log.warn('Not configured \u2014 run: openacp plugin configure ${params.pluginName}')
777
+ return
778
+ }
779
+ // Use apiKey...
780
+ }
781
+ \`\`\`
782
+
783
+ ## Testing
784
+
785
+ Tests use Vitest and \`@openacp/plugin-sdk/testing\`.
786
+
787
+ \`\`\`typescript
788
+ import { describe, it, expect } from 'vitest'
789
+ import { createTestContext, createTestInstallContext, mockServices } from '@openacp/plugin-sdk/testing'
790
+ import plugin from '../index.js'
791
+
792
+ describe('${params.pluginName}', () => {
793
+ it('registers commands on setup', async () => {
794
+ const ctx = createTestContext({ pluginName: '${params.pluginName}' })
795
+ await plugin.setup(ctx)
796
+ expect(ctx.registeredCommands.has('greet')).toBe(true)
797
+ })
798
+
799
+ it('command returns expected response', async () => {
800
+ const ctx = createTestContext({ pluginName: '${params.pluginName}' })
801
+ await plugin.setup(ctx)
802
+ const res = await ctx.executeCommand('greet', { raw: 'Alice' })
803
+ expect(res).toEqual({ type: 'text', text: 'Hello, Alice!' })
804
+ })
805
+
806
+ it('install saves settings', async () => {
807
+ const ctx = createTestInstallContext({
808
+ pluginName: '${params.pluginName}',
809
+ terminalResponses: { password: ['sk-test-key'] },
810
+ })
811
+ await plugin.install!(ctx)
812
+ expect(ctx.settingsData.get('apiKey')).toBe('sk-test-key')
813
+ })
814
+ })
815
+ \`\`\`
816
+
817
+ ### Available mock services
818
+
819
+ \`\`\`typescript
820
+ const ctx = createTestContext({
821
+ pluginName: '${params.pluginName}',
822
+ services: {
823
+ security: mockServices.security(),
824
+ usage: mockServices.usage({ async checkBudget() { return { ok: false, percent: 100 } } }),
825
+ },
826
+ })
827
+ \`\`\`
828
+
829
+ ## Publishing
830
+
831
+ 1. Update \`version\` in both \`package.json\` and \`src/index.ts\`
832
+ 2. Build and test:
833
+ \`\`\`bash
834
+ npm run build
835
+ npm test
836
+ \`\`\`
837
+ 3. Publish:
838
+ \`\`\`bash
839
+ npm publish --access public
840
+ \`\`\`
841
+ 4. Users install with:
842
+ \`\`\`bash
843
+ openacp plugin install ${params.pluginName}
844
+ \`\`\`
845
+ 5. Submit to the [OpenACP Plugin Registry](https://github.com/Open-ACP/plugin-registry) for discoverability.
846
+
847
+ ## Useful Links
848
+
849
+ - [Architecture: Plugin System](https://docs.openacp.dev/architecture/plugin-system)
850
+ - [Architecture: Writing Plugins](https://docs.openacp.dev/architecture/writing-plugins)
851
+ - [Architecture: Command System](https://docs.openacp.dev/architecture/command-system)
852
+ - [Plugin SDK Reference](https://docs.openacp.dev/extending/plugin-sdk-reference)
853
+ - [Getting Started: Your First Plugin](https://docs.openacp.dev/extending/getting-started-plugin)
854
+ - [Dev Mode](https://docs.openacp.dev/extending/dev-mode)
855
+ - [Contributing](https://github.com/Open-ACP/OpenACP/blob/main/CONTRIBUTING.md)
856
+ `;
857
+ }
858
+
859
+ // src/cli/commands/plugin-create.ts
860
+ async function cmdPluginCreate() {
861
+ p.intro("Create a new OpenACP plugin");
862
+ const result = await p.group(
863
+ {
864
+ name: () => p.text({
865
+ message: "Plugin name (e.g., @myorg/adapter-matrix)",
866
+ placeholder: "@myorg/my-plugin",
867
+ validate: (value) => {
868
+ if (!value || !value.trim()) return "Plugin name is required";
869
+ if (!/^(@[a-z0-9-]+\/)?[a-z0-9-]+$/.test(value.trim())) {
870
+ return "Must be a valid npm package name (lowercase, hyphens, optional @scope/)";
871
+ }
872
+ return void 0;
873
+ }
874
+ }),
875
+ description: () => p.text({
876
+ message: "Description",
877
+ placeholder: "A short description of your plugin"
878
+ }),
879
+ author: () => p.text({
880
+ message: "Author",
881
+ placeholder: "Your Name <email@example.com>"
882
+ }),
883
+ license: () => p.select({
884
+ message: "License",
885
+ options: [
886
+ { value: "MIT", label: "MIT" },
887
+ { value: "Apache-2.0", label: "Apache 2.0" },
888
+ { value: "ISC", label: "ISC" },
889
+ { value: "UNLICENSED", label: "Unlicensed (private)" }
890
+ ]
891
+ })
892
+ },
893
+ {
894
+ onCancel: () => {
895
+ p.cancel("Plugin creation cancelled.");
896
+ process.exit(0);
897
+ }
898
+ }
899
+ );
900
+ const pluginName = result.name.trim();
901
+ const dirName = pluginName.replace(/^@[^/]+\//, "");
902
+ const targetDir = path.resolve(process.cwd(), dirName);
903
+ if (fs.existsSync(targetDir)) {
904
+ p.cancel(`Directory "${dirName}" already exists.`);
905
+ process.exit(1);
906
+ }
907
+ const spinner2 = p.spinner();
908
+ spinner2.start("Scaffolding plugin...");
909
+ fs.mkdirSync(path.join(targetDir, "src", "__tests__"), { recursive: true });
910
+ const params = {
911
+ pluginName,
912
+ description: result.description || "",
913
+ author: result.author || "",
914
+ license: result.license,
915
+ cliVersion: getCurrentVersion()
916
+ };
917
+ const files = [
918
+ { relativePath: "package.json", content: generatePackageJson(params) },
919
+ { relativePath: "tsconfig.json", content: generateTsconfig() },
920
+ { relativePath: ".gitignore", content: generateGitignore() },
921
+ { relativePath: ".npmignore", content: generateNpmignore() },
922
+ { relativePath: ".editorconfig", content: generateEditorconfig() },
923
+ { relativePath: "README.md", content: generateReadme(params) },
924
+ { relativePath: "CLAUDE.md", content: generateClaudeMd(params) },
925
+ { relativePath: "PLUGIN_GUIDE.md", content: generatePluginGuide(params) },
926
+ { relativePath: path.join("src", "index.ts"), content: generatePluginSource(params) },
927
+ { relativePath: path.join("src", "__tests__", "index.test.ts"), content: generatePluginTest(params) }
928
+ ];
929
+ for (const file of files) {
930
+ fs.writeFileSync(path.join(targetDir, file.relativePath), file.content);
931
+ }
932
+ spinner2.stop("Plugin scaffolded!");
933
+ p.note(
934
+ [
935
+ `cd ${dirName}`,
936
+ "npm install",
937
+ "npm run build",
938
+ "npm test",
939
+ "",
940
+ "# Start development with hot-reload:",
941
+ `openacp dev .`
942
+ ].join("\n"),
943
+ "Next steps"
944
+ );
945
+ p.outro(`Plugin ${pluginName} created in ./${dirName}`);
946
+ }
947
+ export {
948
+ cmdPluginCreate
949
+ };
950
+ //# sourceMappingURL=plugin-create-LCXXNDK6.js.map