@openacp/plugin-sdk 2026.41.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.
@@ -0,0 +1,2 @@
1
+ export type { ConfigFieldDef, DoctorReport, PendingFix } from '@openacp/cli';
2
+ export { DoctorEngine, getSafeFields, resolveOptions, getConfigValue, isHotReloadable } from '@openacp/cli';
package/dist/config.js ADDED
@@ -0,0 +1 @@
1
+ export { DoctorEngine, getSafeFields, resolveOptions, getConfigValue, isHotReloadable } from '@openacp/cli';
@@ -0,0 +1,2 @@
1
+ export type { DisplayVerbosity, ToolCallMeta, ToolUpdateMeta, ViewerLinks } from '@openacp/cli';
2
+ export { STATUS_ICONS, KIND_ICONS, progressBar, formatTokens, truncateContent, stripCodeFences, splitMessage, extractContentText, formatToolSummary, formatToolTitle, resolveToolIcon, } from '@openacp/cli';
@@ -0,0 +1 @@
1
+ export { STATUS_ICONS, KIND_ICONS, progressBar, formatTokens, truncateContent, stripCodeFences, splitMessage, extractContentText, formatToolSummary, formatToolTitle, resolveToolIcon, } from '@openacp/cli';
@@ -0,0 +1,16 @@
1
+ export type { OpenACPPlugin, PluginContext, PluginPermission, PluginStorage, InstallContext, MigrateContext, TerminalIO, SettingsAPI, } from '@openacp/cli';
2
+ export type { CommandDef, CommandArgs, CommandResponse, MenuOption, ListItem, } from '@openacp/cli';
3
+ export type { SecurityService, FileServiceInterface, NotificationService, UsageService, TunnelServiceInterface, ContextService, } from '@openacp/cli';
4
+ export type { TTSProvider, TTSOptions, TTSResult, STTProvider, STTOptions, STTResult, SpeechServiceInterface, } from './speech-types.js';
5
+ export type { IChannelAdapter, AdapterCapabilities, OutgoingMessage, PermissionRequest, PermissionOption, NotificationMessage, AgentCommand, MessagingAdapterConfig, IRenderer, RenderedMessage, } from '@openacp/cli';
6
+ export { MessagingAdapter, StreamAdapter, BaseRenderer } from '@openacp/cli';
7
+ export { SendQueue, DraftManager, ToolCallTracker, ActivityTracker } from '@openacp/cli';
8
+ export { ToolStateMap, ThoughtBuffer } from '@openacp/cli';
9
+ export { DisplaySpecBuilder } from '@openacp/cli';
10
+ export { OutputModeResolver } from '@openacp/cli';
11
+ export { ToolCardState } from '@openacp/cli';
12
+ export type { OpenACPCore, Session, SessionEvents, SessionManager, CommandRegistry, Attachment, PlanEntry, StopReason, SessionStatus, ConfigOption, UsageRecord, InstallProgress, DisplayVerbosity, ToolCallMeta, ToolUpdateMeta, ViewerLinks, TelegramPlatformData, } from '@openacp/cli';
13
+ export type { ToolDisplaySpec, ThoughtDisplaySpec, ToolEntry, OutputMode, ToolCardSnapshot, ToolCardStateConfig, } from '@openacp/cli';
14
+ export { log, createChildLogger } from '@openacp/cli';
15
+ export { PRODUCT_GUIDE } from '@openacp/cli';
16
+ export type { ConfigFieldDef, DoctorReport, PendingFix } from './config.js';
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ // ============================================================
2
+ // @openacp/plugin-sdk — main entry point
3
+ //
4
+ // Sub-path imports available:
5
+ // @openacp/plugin-sdk/formatting — format utils, icons
6
+ // @openacp/plugin-sdk/config — config utils, doctor engine
7
+ // @openacp/plugin-sdk/testing — test helpers, conformance tests
8
+ // ============================================================
9
+ // --- Adapter base classes (runtime) ---
10
+ export { MessagingAdapter, StreamAdapter, BaseRenderer } from '@openacp/cli';
11
+ // --- Adapter primitives (runtime) ---
12
+ export { SendQueue, DraftManager, ToolCallTracker, ActivityTracker } from '@openacp/cli';
13
+ export { ToolStateMap, ThoughtBuffer } from '@openacp/cli';
14
+ export { DisplaySpecBuilder } from '@openacp/cli';
15
+ export { OutputModeResolver } from '@openacp/cli';
16
+ export { ToolCardState } from '@openacp/cli';
17
+ // --- Logging (runtime) ---
18
+ export { log, createChildLogger } from '@openacp/cli';
19
+ // --- Data (runtime) ---
20
+ export { PRODUCT_GUIDE } from '@openacp/cli';
@@ -0,0 +1,36 @@
1
+ export interface TTSOptions {
2
+ language?: string;
3
+ voice?: string;
4
+ model?: string;
5
+ }
6
+ export interface TTSResult {
7
+ audioBuffer: Buffer;
8
+ mimeType: string;
9
+ }
10
+ export interface STTOptions {
11
+ language?: string;
12
+ model?: string;
13
+ }
14
+ export interface STTResult {
15
+ text: string;
16
+ language?: string;
17
+ duration?: number;
18
+ }
19
+ export interface TTSProvider {
20
+ readonly name: string;
21
+ synthesize(text: string, options?: TTSOptions): Promise<TTSResult>;
22
+ }
23
+ export interface STTProvider {
24
+ readonly name: string;
25
+ transcribe(audioBuffer: Buffer, mimeType: string, options?: STTOptions): Promise<STTResult>;
26
+ }
27
+ export interface SpeechServiceInterface {
28
+ registerTTSProvider(name: string, provider: TTSProvider): void;
29
+ unregisterTTSProvider(name: string): void;
30
+ registerSTTProvider(name: string, provider: STTProvider): void;
31
+ unregisterSTTProvider?(name: string): void;
32
+ isTTSAvailable(): boolean;
33
+ isSTTAvailable(): boolean;
34
+ synthesize(text: string, options?: TTSOptions): Promise<TTSResult>;
35
+ transcribe(audioBuffer: Buffer, mimeType: string, options?: STTOptions): Promise<STTResult>;
36
+ }
@@ -0,0 +1,4 @@
1
+ // Speech provider types for use by OpenACP plugins.
2
+ // These are stable interfaces that plugins can rely on without depending
3
+ // on @openacp/cli internals.
4
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { IChannelAdapter } from '@openacp/cli';
2
+ export declare function runAdapterConformanceTests(createAdapter: () => IChannelAdapter | Promise<IChannelAdapter>, cleanup?: () => Promise<void>): void;
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ export function runAdapterConformanceTests(createAdapter, cleanup) {
3
+ let adapter;
4
+ afterEach(async () => {
5
+ await cleanup?.();
6
+ });
7
+ describe('IChannelAdapter conformance', () => {
8
+ it('has a name', async () => {
9
+ adapter = await createAdapter();
10
+ expect(typeof adapter.name).toBe('string');
11
+ expect(adapter.name.length).toBeGreaterThan(0);
12
+ });
13
+ it('declares capabilities correctly', async () => {
14
+ adapter = await createAdapter();
15
+ const caps = adapter.capabilities;
16
+ expect(typeof caps.streaming).toBe('boolean');
17
+ expect(typeof caps.richFormatting).toBe('boolean');
18
+ expect(typeof caps.threads).toBe('boolean');
19
+ expect(typeof caps.reactions).toBe('boolean');
20
+ expect(typeof caps.fileUpload).toBe('boolean');
21
+ expect(typeof caps.voice).toBe('boolean');
22
+ });
23
+ it('sends text messages without error', async () => {
24
+ adapter = await createAdapter();
25
+ await expect(adapter.sendMessage('test-session', { type: 'text', text: 'hello' })).resolves.not.toThrow();
26
+ });
27
+ it('sends tool_call messages without error', async () => {
28
+ adapter = await createAdapter();
29
+ await expect(adapter.sendMessage('test-session', {
30
+ type: 'tool_call',
31
+ text: 'Read',
32
+ metadata: { id: 't1', name: 'Read', kind: 'read' },
33
+ })).resolves.not.toThrow();
34
+ });
35
+ it('sends usage messages without error', async () => {
36
+ adapter = await createAdapter();
37
+ await expect(adapter.sendMessage('test-session', {
38
+ type: 'usage',
39
+ text: '',
40
+ metadata: { tokensUsed: 1000, contextSize: 200000 },
41
+ })).resolves.not.toThrow();
42
+ });
43
+ it('sends error messages without error', async () => {
44
+ adapter = await createAdapter();
45
+ await expect(adapter.sendMessage('test-session', { type: 'error', text: 'something failed' })).resolves.not.toThrow();
46
+ });
47
+ it('handles session_end without error', async () => {
48
+ adapter = await createAdapter();
49
+ await expect(adapter.sendMessage('test-session', { type: 'session_end', text: 'finished' })).resolves.not.toThrow();
50
+ });
51
+ it('handles unknown message types gracefully', async () => {
52
+ adapter = await createAdapter();
53
+ await expect(adapter.sendMessage('test-session', { type: 'unknown_type', text: '' })).resolves.not.toThrow();
54
+ });
55
+ it('sendNotification does not throw', async () => {
56
+ adapter = await createAdapter();
57
+ await expect(adapter.sendNotification({
58
+ sessionId: 'test',
59
+ type: 'completed',
60
+ summary: 'done',
61
+ })).resolves.not.toThrow();
62
+ });
63
+ });
64
+ }
@@ -0,0 +1,14 @@
1
+ import type { SecurityService, FileServiceInterface, NotificationService, UsageService, SpeechServiceInterface, TunnelServiceInterface, ContextService } from '@openacp/cli';
2
+ /**
3
+ * Factory functions that create mock implementations of OpenACP service interfaces.
4
+ * Each returns an object matching the service contract with sensible defaults.
5
+ */
6
+ export declare const mockServices: {
7
+ security(overrides?: Partial<SecurityService>): SecurityService;
8
+ fileService(overrides?: Partial<FileServiceInterface>): FileServiceInterface;
9
+ notifications(overrides?: Partial<NotificationService>): NotificationService;
10
+ usage(overrides?: Partial<UsageService>): UsageService;
11
+ speech(overrides?: Partial<SpeechServiceInterface>): SpeechServiceInterface;
12
+ tunnel(overrides?: Partial<TunnelServiceInterface>): TunnelServiceInterface;
13
+ context(overrides?: Partial<ContextService>): ContextService;
14
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Factory functions that create mock implementations of OpenACP service interfaces.
3
+ * Each returns an object matching the service contract with sensible defaults.
4
+ */
5
+ export const mockServices = {
6
+ security(overrides) {
7
+ return {
8
+ async checkAccess() { return { allowed: true }; },
9
+ async checkSessionLimit() { return { allowed: true }; },
10
+ async getUserRole() { return 'user'; },
11
+ ...overrides,
12
+ };
13
+ },
14
+ fileService(overrides) {
15
+ return {
16
+ async saveFile(_sessionId, fileName, _data, mimeType) {
17
+ return { type: 'file', filePath: `/tmp/${fileName}`, fileName, mimeType, size: 0 };
18
+ },
19
+ async resolveFile() { return null; },
20
+ async readTextFileWithRange() { return ''; },
21
+ extensionFromMime() { return '.bin'; },
22
+ async convertOggToWav(data) { return data; },
23
+ ...overrides,
24
+ };
25
+ },
26
+ notifications(overrides) {
27
+ return {
28
+ async notify() { },
29
+ async notifyAll() { },
30
+ ...overrides,
31
+ };
32
+ },
33
+ usage(overrides) {
34
+ return {
35
+ async trackUsage() { },
36
+ async checkBudget() { return { ok: true, percent: 0 }; },
37
+ ...overrides,
38
+ };
39
+ },
40
+ speech(overrides) {
41
+ return {
42
+ async textToSpeech() { return Buffer.alloc(0); },
43
+ async speechToText() { return ''; },
44
+ registerTTSProvider() { },
45
+ registerSTTProvider() { },
46
+ ...overrides,
47
+ };
48
+ },
49
+ tunnel(overrides) {
50
+ return {
51
+ getPublicUrl() { return 'http://localhost:0'; },
52
+ async start() { return 'http://localhost:0'; },
53
+ async stop() { },
54
+ getStore() {
55
+ return {
56
+ storeFile() { return null; },
57
+ storeDiff() { return null; },
58
+ storeOutput() { return null; },
59
+ };
60
+ },
61
+ fileUrl(id) { return `http://localhost:0/file/${id}`; },
62
+ diffUrl(id) { return `http://localhost:0/diff/${id}`; },
63
+ outputUrl(id) { return `http://localhost:0/output/${id}`; },
64
+ ...overrides,
65
+ };
66
+ },
67
+ context(overrides) {
68
+ return {
69
+ async buildContext() { return ''; },
70
+ registerProvider() { },
71
+ ...overrides,
72
+ };
73
+ },
74
+ };
@@ -0,0 +1,36 @@
1
+ import type { PluginContext, CommandDef, CommandResponse, OutgoingMessage } from '@openacp/cli';
2
+ export interface TestContextOpts {
3
+ pluginName: string;
4
+ pluginConfig?: Record<string, unknown>;
5
+ permissions?: string[];
6
+ services?: Record<string, unknown>;
7
+ instanceRoot?: string;
8
+ }
9
+ export interface TestPluginContext extends PluginContext {
10
+ /** Services registered via registerService() */
11
+ registeredServices: Map<string, unknown>;
12
+ /** Commands registered via registerCommand() */
13
+ registeredCommands: Map<string, CommandDef>;
14
+ /** Middleware registered via registerMiddleware() */
15
+ registeredMiddleware: Array<{
16
+ hook: string;
17
+ opts: unknown;
18
+ }>;
19
+ /** Events emitted via emit() */
20
+ emittedEvents: Array<{
21
+ event: string;
22
+ payload: unknown;
23
+ }>;
24
+ /** Messages sent via sendMessage() */
25
+ sentMessages: Array<{
26
+ sessionId: string;
27
+ content: OutgoingMessage;
28
+ }>;
29
+ /** Dispatch a registered command by name */
30
+ executeCommand(name: string, args?: Partial<import('@openacp/cli').CommandArgs>): Promise<CommandResponse | void>;
31
+ }
32
+ /**
33
+ * Creates a test-friendly PluginContext for unit-testing plugins.
34
+ * All state is in-memory, logger is silent, services are pre-populated.
35
+ */
36
+ export declare function createTestContext(opts: TestContextOpts): TestPluginContext;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Creates a test-friendly PluginContext for unit-testing plugins.
3
+ * All state is in-memory, logger is silent, services are pre-populated.
4
+ */
5
+ export function createTestContext(opts) {
6
+ const storageData = new Map();
7
+ const eventHandlers = new Map();
8
+ const registeredServices = new Map();
9
+ const registeredCommands = new Map();
10
+ const registeredMiddleware = [];
11
+ const emittedEvents = [];
12
+ const sentMessages = [];
13
+ // Pre-populate services from opts
14
+ if (opts.services) {
15
+ for (const [name, impl] of Object.entries(opts.services)) {
16
+ registeredServices.set(name, impl);
17
+ }
18
+ }
19
+ const storage = {
20
+ async get(key) {
21
+ return storageData.get(key);
22
+ },
23
+ async set(key, value) {
24
+ storageData.set(key, value);
25
+ },
26
+ async delete(key) {
27
+ storageData.delete(key);
28
+ },
29
+ async list() {
30
+ return Array.from(storageData.keys());
31
+ },
32
+ getDataDir() {
33
+ return '/tmp/openacp-test-data';
34
+ },
35
+ };
36
+ const silentLog = {
37
+ trace() { },
38
+ debug() { },
39
+ info() { },
40
+ warn() { },
41
+ error() { },
42
+ fatal() { },
43
+ child() { return silentLog; },
44
+ };
45
+ const ctx = {
46
+ pluginName: opts.pluginName,
47
+ pluginConfig: opts.pluginConfig ?? {},
48
+ instanceRoot: opts.instanceRoot ?? '/tmp/openacp-test',
49
+ // Events
50
+ on(event, handler) {
51
+ if (!eventHandlers.has(event)) {
52
+ eventHandlers.set(event, new Set());
53
+ }
54
+ eventHandlers.get(event).add(handler);
55
+ },
56
+ off(event, handler) {
57
+ eventHandlers.get(event)?.delete(handler);
58
+ },
59
+ emit(event, payload) {
60
+ emittedEvents.push({ event, payload });
61
+ const handlers = eventHandlers.get(event);
62
+ if (handlers) {
63
+ for (const handler of handlers) {
64
+ handler(payload);
65
+ }
66
+ }
67
+ },
68
+ // Actions
69
+ registerMiddleware(hook, opts) {
70
+ registeredMiddleware.push({ hook, opts });
71
+ },
72
+ registerService(name, implementation) {
73
+ registeredServices.set(name, implementation);
74
+ },
75
+ getService(name) {
76
+ return registeredServices.get(name);
77
+ },
78
+ registerCommand(def) {
79
+ registeredCommands.set(def.name, def);
80
+ },
81
+ storage,
82
+ log: silentLog,
83
+ async sendMessage(sessionId, content) {
84
+ sentMessages.push({ sessionId, content });
85
+ },
86
+ // Kernel access stubs
87
+ sessions: {},
88
+ config: {},
89
+ eventBus: {},
90
+ core: {},
91
+ // Test-specific
92
+ registeredServices,
93
+ registeredCommands,
94
+ registeredMiddleware,
95
+ emittedEvents,
96
+ sentMessages,
97
+ async executeCommand(name, args) {
98
+ const cmd = registeredCommands.get(name);
99
+ if (!cmd) {
100
+ throw new Error(`Command not found: ${name}`);
101
+ }
102
+ const defaultArgs = {
103
+ raw: '',
104
+ sessionId: null,
105
+ channelId: 'test',
106
+ userId: 'test-user',
107
+ async reply() { },
108
+ ...args,
109
+ };
110
+ return cmd.handler(defaultArgs);
111
+ },
112
+ };
113
+ return ctx;
114
+ }
@@ -0,0 +1,20 @@
1
+ import type { InstallContext } from '@openacp/cli';
2
+ export interface TestInstallContextOpts {
3
+ pluginName: string;
4
+ legacyConfig?: Record<string, unknown>;
5
+ terminalResponses?: Record<string, unknown[]>;
6
+ }
7
+ interface TerminalCall {
8
+ method: string;
9
+ args: unknown;
10
+ }
11
+ /**
12
+ * Creates a test-friendly InstallContext for unit-testing plugin install/configure/uninstall.
13
+ * Terminal prompts are auto-answered from the provided responses map.
14
+ * Settings are stored in-memory.
15
+ */
16
+ export declare function createTestInstallContext(opts: TestInstallContextOpts): InstallContext & {
17
+ terminalCalls: TerminalCall[];
18
+ settingsData: Map<string, unknown>;
19
+ };
20
+ export {};
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Creates a test-friendly InstallContext for unit-testing plugin install/configure/uninstall.
3
+ * Terminal prompts are auto-answered from the provided responses map.
4
+ * Settings are stored in-memory.
5
+ */
6
+ export function createTestInstallContext(opts) {
7
+ const settingsData = new Map();
8
+ const terminalCalls = [];
9
+ const responseQueues = new Map();
10
+ // Deep-copy response queues so we don't mutate caller's arrays
11
+ if (opts.terminalResponses) {
12
+ for (const [method, responses] of Object.entries(opts.terminalResponses)) {
13
+ responseQueues.set(method, [...responses]);
14
+ }
15
+ }
16
+ function getNextResponse(method, args) {
17
+ terminalCalls.push({ method, args });
18
+ const queue = responseQueues.get(method);
19
+ if (queue && queue.length > 0) {
20
+ return queue.shift();
21
+ }
22
+ // Default responses by method type
23
+ switch (method) {
24
+ case 'text': return '';
25
+ case 'password': return '';
26
+ case 'confirm': return false;
27
+ case 'select': return undefined;
28
+ case 'multiselect': return [];
29
+ default: return undefined;
30
+ }
31
+ }
32
+ const terminal = {
33
+ async text(promptOpts) {
34
+ return getNextResponse('text', promptOpts);
35
+ },
36
+ async select(promptOpts) {
37
+ return getNextResponse('select', promptOpts);
38
+ },
39
+ async confirm(promptOpts) {
40
+ return getNextResponse('confirm', promptOpts);
41
+ },
42
+ async password(promptOpts) {
43
+ return getNextResponse('password', promptOpts);
44
+ },
45
+ async multiselect(promptOpts) {
46
+ return getNextResponse('multiselect', promptOpts);
47
+ },
48
+ log: {
49
+ info() { },
50
+ success() { },
51
+ warning() { },
52
+ error() { },
53
+ step() { },
54
+ },
55
+ spinner() {
56
+ return {
57
+ start() { },
58
+ stop() { },
59
+ fail() { },
60
+ };
61
+ },
62
+ note() { },
63
+ cancel() { },
64
+ };
65
+ const settings = {
66
+ async get(key) {
67
+ return settingsData.get(key);
68
+ },
69
+ async set(key, value) {
70
+ settingsData.set(key, value);
71
+ },
72
+ async getAll() {
73
+ return Object.fromEntries(settingsData);
74
+ },
75
+ async setAll(allSettings) {
76
+ settingsData.clear();
77
+ for (const [k, v] of Object.entries(allSettings)) {
78
+ settingsData.set(k, v);
79
+ }
80
+ },
81
+ async delete(key) {
82
+ settingsData.delete(key);
83
+ },
84
+ async clear() {
85
+ settingsData.clear();
86
+ },
87
+ async has(key) {
88
+ return settingsData.has(key);
89
+ },
90
+ };
91
+ const silentLog = {
92
+ trace() { },
93
+ debug() { },
94
+ info() { },
95
+ warn() { },
96
+ error() { },
97
+ fatal() { },
98
+ child() { return silentLog; },
99
+ };
100
+ return {
101
+ pluginName: opts.pluginName,
102
+ terminal,
103
+ settings,
104
+ legacyConfig: opts.legacyConfig,
105
+ dataDir: '/tmp/openacp-test-data',
106
+ log: silentLog,
107
+ // Test-specific
108
+ terminalCalls,
109
+ settingsData,
110
+ };
111
+ }
@@ -0,0 +1,6 @@
1
+ export { createTestContext } from './testing/test-context.js';
2
+ export type { TestPluginContext, TestContextOpts } from './testing/test-context.js';
3
+ export { createTestInstallContext } from './testing/test-install-context.js';
4
+ export type { TestInstallContextOpts } from './testing/test-install-context.js';
5
+ export { mockServices } from './testing/mock-services.js';
6
+ export { runAdapterConformanceTests } from '@openacp/cli/testing';
@@ -0,0 +1,4 @@
1
+ export { createTestContext } from './testing/test-context.js';
2
+ export { createTestInstallContext } from './testing/test-install-context.js';
3
+ export { mockServices } from './testing/mock-services.js';
4
+ export { runAdapterConformanceTests } from '@openacp/cli/testing';
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@openacp/plugin-sdk",
3
+ "version": "2026.41.1",
4
+ "description": "SDK for building OpenACP plugins — types, base classes, and testing utilities",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./formatting": {
12
+ "types": "./dist/formatting.d.ts",
13
+ "import": "./dist/formatting.js"
14
+ },
15
+ "./config": {
16
+ "types": "./dist/config.d.ts",
17
+ "import": "./dist/config.js"
18
+ },
19
+ "./testing": {
20
+ "types": "./dist/testing.d.ts",
21
+ "import": "./dist/testing.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist/"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "test": "vitest run"
30
+ },
31
+ "peerDependencies": {
32
+ "@openacp/cli": ">=0.6.0"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/Open-ACP/OpenACP",
37
+ "directory": "packages/plugin-sdk"
38
+ },
39
+ "homepage": "https://github.com/Open-ACP/OpenACP",
40
+ "keywords": [
41
+ "openacp",
42
+ "plugin",
43
+ "sdk",
44
+ "testing"
45
+ ],
46
+ "license": "MIT"
47
+ }