From 91469a525a07efd5b9109bc6ab8b7cad7fae7a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 19 Jul 2026 02:34:40 -0400 Subject: [PATCH] feat: add canonical integration catalog --- .github/workflows/ci.yml | 10 + integrations/README.md | 20 ++ integrations/catalog.json | 482 ++++++++++++++++++++++++++++++ integrations/catalog.schema.json | 214 +++++++++++++ package.json | 1 + test/integration-catalog.test.mjs | 159 ++++++++++ 6 files changed, 886 insertions(+) create mode 100644 integrations/README.md create mode 100644 integrations/catalog.json create mode 100644 integrations/catalog.schema.json create mode 100644 test/integration-catalog.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9fb9d0..fac7c4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,16 @@ on: branches: [main] jobs: + integration-catalog: + name: Validate Integration Catalog + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: npm run validate:integrations + typecheck-server: name: Typecheck Server runs-on: ubuntu-latest diff --git a/integrations/README.md b/integrations/README.md new file mode 100644 index 0000000..11db945 --- /dev/null +++ b/integrations/README.md @@ -0,0 +1,20 @@ +# Integration Catalog + +[`catalog.json`](catalog.json) is the canonical inventory for agent integration IDs, official hook contracts, blocking behavior, and SDK delivery state. Backend validation, Console controls, Docs, and SDK registries derive their integration facts from this catalog. + +`delivery_state` is computed from the `node`, `python`, and `go` adapter flags: + +| State | Meaning | +| --- | --- | +| `available` | All three SDK adapters exist | +| `partial` | One or two SDK adapters exist | +| `planned` | The provider contract is researched and adapter delivery is pending | + +Update workflow: + +1. Verify the provider contract against its official `source_url`. +2. Update `verified_on`, hook fields, blocking semantics, and adapter flags. +3. Run `npm run validate:integrations` from the repository root. +4. Synchronize generated consumers in Backend, Console, Docs, and standalone SDK repositories. + +[`catalog.schema.json`](catalog.schema.json) freezes the machine-readable contract. [`integration-catalog.test.mjs`](../test/integration-catalog.test.mjs) enforces provider completeness, stable IDs, event fields, delivery-state derivation, and schema enums in CI. diff --git a/integrations/catalog.json b/integrations/catalog.json new file mode 100644 index 0000000..903ec3a --- /dev/null +++ b/integrations/catalog.json @@ -0,0 +1,482 @@ +{ + "$schema": "./catalog.schema.json", + "schema_version": 1, + "verified_on": "2026-07-19", + "providers": [ + { + "id": "augment", + "name": "Augment Code CLI", + "vendor": "Augment Code", + "surface": "cli", + "integration_mode": "command_hooks", + "config_format": "json", + "config_paths": [ + "~/.augment/settings.json", + ".augment/settings.json", + ".augment/settings.local.json" + ], + "events": { + "before_tool": "PreToolUse", + "after_tool": "PostToolUse" + }, + "event_fields": { + "name": "tool_name", + "input": "tool_input", + "session": "conversation_id" + }, + "blocking": { + "mechanism": "exit_code_2", + "failure_mode": "fail_open" + }, + "adapters": { + "node": false, + "python": false, + "go": false + }, + "delivery_state": "planned", + "source_url": "https://docs.augmentcode.com/cli/hooks" + }, + { + "id": "claudecode", + "name": "Claude Code", + "vendor": "Anthropic", + "surface": "cli", + "integration_mode": "command_hooks", + "config_format": "json", + "config_paths": [ + "~/.claude/settings.json", + ".claude/settings.json", + ".claude/settings.local.json" + ], + "events": { + "before_tool": "PreToolUse", + "after_tool": "PostToolUse" + }, + "event_fields": { + "name": "tool_name", + "input": "tool_input", + "session": "session_id" + }, + "blocking": { + "mechanism": "exit_code_2", + "failure_mode": "fail_open" + }, + "adapters": { + "node": true, + "python": true, + "go": true + }, + "delivery_state": "available", + "source_url": "https://code.claude.com/docs/en/hooks" + }, + { + "id": "cline", + "name": "Cline", + "vendor": "Cline", + "surface": "cli_and_ide", + "integration_mode": "plugin_api", + "config_format": "plugin", + "config_paths": [], + "events": { + "before_tool": "beforeTool", + "after_tool": "afterTool" + }, + "blocking": { + "mechanism": "hook_policy", + "failure_mode": "configurable" + }, + "adapters": { + "node": false, + "python": false, + "go": false + }, + "delivery_state": "planned", + "source_url": "https://docs.cline.bot/sdk/plugins" + }, + { + "id": "codex", + "name": "OpenAI Codex", + "vendor": "OpenAI", + "surface": "cli", + "integration_mode": "command_hooks", + "config_format": "json", + "config_paths": [ + "~/.codex/hooks.json", + ".codex/hooks.json" + ], + "events": { + "before_tool": "PreToolUse", + "after_tool": "PostToolUse" + }, + "event_fields": { + "name": "tool_name", + "input": "tool_input", + "session": "session_id" + }, + "blocking": { + "mechanism": "exit_code_2", + "failure_mode": "fail_open" + }, + "adapters": { + "node": true, + "python": false, + "go": false + }, + "delivery_state": "partial", + "source_url": "https://learn.chatgpt.com/docs/hooks" + }, + { + "id": "copilot", + "name": "GitHub Copilot", + "vendor": "GitHub", + "surface": "cli", + "integration_mode": "command_hooks", + "config_format": "json", + "config_paths": [ + "~/.copilot/hooks/*.json", + ".github/hooks/*.json" + ], + "events": { + "before_tool": "preToolUse", + "after_tool": "postToolUse" + }, + "blocking": { + "mechanism": "exit_code_2", + "failure_mode": "fail_open" + }, + "adapters": { + "node": true, + "python": true, + "go": true + }, + "delivery_state": "available", + "source_url": "https://docs.github.com/en/copilot/reference/hooks-reference" + }, + { + "id": "cursor", + "name": "Cursor", + "vendor": "Anysphere", + "surface": "ide", + "integration_mode": "command_hooks", + "config_format": "json", + "config_paths": [ + "~/.cursor/hooks.json", + ".cursor/hooks.json" + ], + "events": { + "before_tool": "preToolUse", + "after_tool": "postToolUse" + }, + "blocking": { + "mechanism": "exit_code_2", + "failure_mode": "fail_open" + }, + "adapters": { + "node": true, + "python": true, + "go": true + }, + "delivery_state": "available", + "source_url": "https://cursor.com/docs/hooks" + }, + { + "id": "droid", + "name": "Factory Droid", + "vendor": "Factory", + "surface": "cli", + "integration_mode": "command_hooks", + "config_format": "json", + "config_paths": [ + "~/.factory/hooks.json", + ".factory/hooks.json" + ], + "events": { + "before_tool": "PreToolUse", + "after_tool": "PostToolUse" + }, + "event_fields": { + "name": "tool_name", + "input": "tool_input", + "session": "session_id" + }, + "blocking": { + "mechanism": "exit_code_2", + "failure_mode": "fail_open" + }, + "adapters": { + "node": false, + "python": false, + "go": false + }, + "delivery_state": "planned", + "source_url": "https://docs.factory.ai/reference/hooks-reference" + }, + { + "id": "gemini", + "name": "Gemini CLI", + "vendor": "Google", + "surface": "cli", + "integration_mode": "command_hooks", + "config_format": "json", + "config_paths": [ + "~/.gemini/settings.json", + ".gemini/settings.json" + ], + "events": { + "before_tool": "BeforeTool", + "after_tool": "AfterTool" + }, + "event_fields": { + "name": "tool_name", + "input": "tool_input", + "session": "session_id" + }, + "blocking": { + "mechanism": "exit_code_2", + "failure_mode": "fail_open" + }, + "adapters": { + "node": true, + "python": true, + "go": true + }, + "delivery_state": "available", + "source_url": "https://geminicli.com/docs/hooks/reference/" + }, + { + "id": "grok", + "name": "Grok Build", + "vendor": "xAI", + "surface": "cli", + "integration_mode": "command_hooks", + "config_format": "json", + "config_paths": [ + "~/.grok/hooks/*.json", + ".grok/hooks/*.json" + ], + "events": { + "before_tool": "PreToolUse", + "after_tool": "PostToolUse" + }, + "event_fields": { + "name": "toolName", + "input": "toolInput", + "session": "sessionId" + }, + "blocking": { + "mechanism": "exit_code_2", + "failure_mode": "fail_open" + }, + "adapters": { + "node": false, + "python": false, + "go": false + }, + "delivery_state": "planned", + "source_url": "https://docs.x.ai/build/features/hooks" + }, + { + "id": "kimi", + "name": "Kimi Code", + "vendor": "Moonshot AI", + "surface": "cli", + "integration_mode": "command_hooks", + "config_format": "toml", + "config_paths": [ + "~/.kimi-code/config.toml" + ], + "events": { + "before_tool": "PreToolUse", + "after_tool": "PostToolUse" + }, + "event_fields": { + "name": "tool_name", + "input": "tool_input", + "session": "session_id" + }, + "blocking": { + "mechanism": "exit_code_2", + "failure_mode": "fail_open" + }, + "adapters": { + "node": false, + "python": false, + "go": false + }, + "delivery_state": "planned", + "source_url": "https://moonshotai.github.io/kimi-code/en/customization/hooks" + }, + { + "id": "kirocli", + "name": "Kiro CLI", + "vendor": "AWS", + "surface": "cli", + "integration_mode": "command_hooks", + "config_format": "json", + "config_paths": [ + "~/.kiro/agents/*.json", + ".kiro/agents/*.json" + ], + "events": { + "before_tool": "preToolUse", + "after_tool": "postToolUse" + }, + "event_fields": { + "name": "tool_name", + "input": "tool_input", + "session": "session_id" + }, + "blocking": { + "mechanism": "exit_code_2", + "failure_mode": "fail_open" + }, + "adapters": { + "node": true, + "python": true, + "go": true + }, + "delivery_state": "available", + "source_url": "https://kiro.dev/docs/cli/hooks/" + }, + { + "id": "kiroide", + "name": "Kiro IDE", + "vendor": "AWS", + "surface": "ide", + "integration_mode": "command_hooks", + "config_format": "json", + "config_paths": [ + ".kiro/hooks/*.json" + ], + "events": { + "before_tool": "PreToolUse", + "after_tool": "PostToolUse" + }, + "blocking": { + "mechanism": "exit_code_2", + "failure_mode": "fail_open" + }, + "adapters": { + "node": true, + "python": true, + "go": true + }, + "delivery_state": "available", + "source_url": "https://kiro.dev/docs/hooks/" + }, + { + "id": "letta", + "name": "Letta Code", + "vendor": "Letta", + "surface": "cli", + "integration_mode": "command_hooks", + "config_format": "json", + "config_paths": [ + "~/.letta/settings.json", + ".letta/settings.json", + ".letta/settings.local.json" + ], + "events": { + "before_tool": "PreToolUse", + "after_tool": "PostToolUse" + }, + "blocking": { + "mechanism": "exit_code_2", + "failure_mode": "fail_open" + }, + "adapters": { + "node": true, + "python": true, + "go": true + }, + "delivery_state": "available", + "source_url": "https://docs.letta.com/letta-agent/hooks" + }, + { + "id": "opencode", + "name": "OpenCode", + "vendor": "SST", + "surface": "cli_and_ide", + "integration_mode": "plugin_api", + "config_format": "plugin", + "config_paths": [ + "~/.config/opencode/plugins/*.{js,ts,mjs}", + ".opencode/plugins/*.{js,ts,mjs}" + ], + "events": { + "before_tool": "tool.execute.before", + "after_tool": "tool.execute.after" + }, + "event_fields": { + "name": "tool", + "input": "args", + "session": "sessionID" + }, + "blocking": { + "mechanism": "exception", + "failure_mode": "fail_closed" + }, + "adapters": { + "node": true, + "python": true, + "go": true + }, + "delivery_state": "available", + "source_url": "https://opencode.ai/docs/plugins/" + }, + { + "id": "qwen", + "name": "Qwen Code", + "vendor": "Alibaba Cloud", + "surface": "cli", + "integration_mode": "command_hooks", + "config_format": "json", + "config_paths": [ + "~/.qwen/settings.json", + ".qwen/settings.json" + ], + "events": { + "before_tool": "PreToolUse", + "after_tool": "PostToolUse" + }, + "event_fields": { + "name": "tool_name", + "input": "tool_input", + "session": "session_id" + }, + "blocking": { + "mechanism": "exit_code_2", + "failure_mode": "fail_open" + }, + "adapters": { + "node": false, + "python": false, + "go": false + }, + "delivery_state": "planned", + "source_url": "https://qwenlm.github.io/qwen-code-docs/en/users/features/hooks/" + } + ], + "custom_integrations": [ + { + "id": "enterprise", + "name": "Enterprise", + "description": "Managed enterprise integration" + }, + { + "id": "gui", + "name": "GUI Agent", + "description": "Desktop or browser automation agent" + }, + { + "id": "other", + "name": "Other", + "description": "Custom integration outside the provider catalog" + }, + { + "id": "sdk", + "name": "SDK", + "description": "Direct Elydora SDK integration" + } + ] +} diff --git a/integrations/catalog.schema.json b/integrations/catalog.schema.json new file mode 100644 index 0000000..dace43c --- /dev/null +++ b/integrations/catalog.schema.json @@ -0,0 +1,214 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://elydora.com/schemas/integration-catalog.schema.json", + "title": "Elydora Integration Catalog", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "verified_on", + "providers", + "custom_integrations" + ], + "properties": { + "$schema": { + "type": "string" + }, + "schema_version": { + "const": 1 + }, + "verified_on": { + "type": "string", + "format": "date" + }, + "providers": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "name", + "vendor", + "surface", + "integration_mode", + "config_format", + "config_paths", + "events", + "blocking", + "adapters", + "delivery_state", + "source_url" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9]*$" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "vendor": { + "type": "string", + "minLength": 1 + }, + "surface": { + "enum": [ + "cli", + "ide", + "cli_and_ide" + ] + }, + "integration_mode": { + "enum": [ + "command_hooks", + "plugin_api" + ] + }, + "config_format": { + "enum": [ + "json", + "toml", + "plugin" + ] + }, + "config_paths": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "events": { + "type": "object", + "additionalProperties": false, + "required": [ + "before_tool", + "after_tool" + ], + "properties": { + "before_tool": { + "type": "string", + "minLength": 1 + }, + "after_tool": { + "type": "string", + "minLength": 1 + } + } + }, + "event_fields": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "input", + "session" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "input": { + "type": "string", + "minLength": 1 + }, + "session": { + "type": "string", + "minLength": 1 + } + } + }, + "blocking": { + "type": "object", + "additionalProperties": false, + "required": [ + "mechanism", + "failure_mode" + ], + "properties": { + "mechanism": { + "enum": [ + "any_nonzero_exit", + "exception", + "exit_code_2", + "hook_policy" + ] + }, + "failure_mode": { + "enum": [ + "adapter_controlled", + "configurable", + "fail_closed", + "fail_open" + ] + } + } + }, + "adapters": { + "type": "object", + "additionalProperties": false, + "required": [ + "node", + "python", + "go" + ], + "properties": { + "node": { + "type": "boolean" + }, + "python": { + "type": "boolean" + }, + "go": { + "type": "boolean" + } + } + }, + "delivery_state": { + "enum": [ + "available", + "partial", + "planned" + ] + }, + "source_url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + } + } + } + }, + "custom_integrations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "name", + "description" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9]*$" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + } + } + } + } + } +} diff --git a/package.json b/package.json index db359b3..262081d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ }, "license": "Apache-2.0", "scripts": { + "validate:integrations": "node --test test/integration-catalog.test.mjs", "build:server": "cd packages/server && npm run build", "build:console": "cd packages/console && npm run build", "build": "npm run build:server && npm run build:console", diff --git a/test/integration-catalog.test.mjs b/test/integration-catalog.test.mjs new file mode 100644 index 0000000..b60b545 --- /dev/null +++ b/test/integration-catalog.test.mjs @@ -0,0 +1,159 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +const root = new URL('../', import.meta.url); + +const expectedProviderIds = [ + 'augment', + 'claudecode', + 'cline', + 'codex', + 'copilot', + 'cursor', + 'droid', + 'gemini', + 'grok', + 'kimi', + 'kirocli', + 'kiroide', + 'letta', + 'opencode', + 'qwen', +]; + +const expectedCustomIntegrationIds = ['enterprise', 'gui', 'other', 'sdk']; +const expectedAdapterKeys = ['go', 'node', 'python']; +const requiredProviderFields = [ + 'id', + 'name', + 'vendor', + 'surface', + 'integration_mode', + 'config_format', + 'config_paths', + 'events', + 'blocking', + 'adapters', + 'delivery_state', + 'source_url', +]; + +async function readJson(path) { + return JSON.parse(await readFile(new URL(path, root), 'utf8')); +} + +function deliveryState(adapters) { + const availableCount = Object.values(adapters).filter(Boolean).length; + if (availableCount === expectedAdapterKeys.length) return 'available'; + if (availableCount === 0) return 'planned'; + return 'partial'; +} + +test('catalog contains the complete, ordered integration inventory', async () => { + const catalog = await readJson('integrations/catalog.json'); + + assert.equal(catalog.schema_version, 1); + assert.match(catalog.verified_on, /^\d{4}-\d{2}-\d{2}$/); + assert.deepEqual( + catalog.providers.map(({ id }) => id), + expectedProviderIds, + ); + assert.deepEqual( + catalog.custom_integrations.map(({ id }) => id), + expectedCustomIntegrationIds, + ); +}); + +test('every provider exposes a complete, machine-readable hook contract', async () => { + const catalog = await readJson('integrations/catalog.json'); + + for (const provider of catalog.providers) { + for (const field of requiredProviderFields) { + assert.ok(field in provider, `${provider.id} is missing ${field}`); + } + + assert.match(provider.id, /^[a-z][a-z0-9]*$/); + assert.equal(typeof provider.name, 'string'); + assert.equal(typeof provider.vendor, 'string'); + assert.ok(['cli', 'ide', 'cli_and_ide'].includes(provider.surface)); + assert.ok(['command_hooks', 'plugin_api'].includes(provider.integration_mode)); + assert.ok(['json', 'toml', 'plugin'].includes(provider.config_format)); + assert.ok(Array.isArray(provider.config_paths)); + assert.equal(typeof provider.events.before_tool, 'string'); + assert.equal(typeof provider.events.after_tool, 'string'); + assert.ok([ + 'any_nonzero_exit', + 'exception', + 'exit_code_2', + 'hook_policy', + ].includes(provider.blocking.mechanism)); + assert.ok([ + 'adapter_controlled', + 'configurable', + 'fail_closed', + 'fail_open', + ].includes(provider.blocking.failure_mode)); + assert.deepEqual(Object.keys(provider.adapters).sort(), expectedAdapterKeys); + assert.ok(Object.values(provider.adapters).every((value) => typeof value === 'boolean')); + assert.equal(provider.delivery_state, deliveryState(provider.adapters)); + assert.match(provider.source_url, /^https:\/\//); + + if (provider.integration_mode === 'command_hooks') { + assert.ok(provider.config_paths.length > 0, `${provider.id} requires config paths`); + } + + if (provider.event_fields) { + assert.equal(typeof provider.event_fields.name, 'string'); + assert.equal(typeof provider.event_fields.input, 'string'); + assert.equal(typeof provider.event_fields.session, 'string'); + } + } +}); + +test('schema freezes the provider contract and supported enums', async () => { + const schema = await readJson('integrations/catalog.schema.json'); + const providerSchema = schema.properties.providers.items; + + assert.equal(schema.$schema, 'https://json-schema.org/draft/2020-12/schema'); + assert.equal(schema.additionalProperties, false); + assert.deepEqual([...providerSchema.required].sort(), [...requiredProviderFields].sort()); + assert.deepEqual(providerSchema.properties.surface.enum, ['cli', 'ide', 'cli_and_ide']); + assert.deepEqual(providerSchema.properties.integration_mode.enum, ['command_hooks', 'plugin_api']); + assert.deepEqual(providerSchema.properties.delivery_state.enum, ['available', 'partial', 'planned']); +}); + +test('high-drift providers retain their verified hook contracts', async () => { + const catalog = await readJson('integrations/catalog.json'); + const providers = new Map(catalog.providers.map((provider) => [provider.id, provider])); + + assert.deepEqual(providers.get('codex').config_paths, [ + '~/.codex/hooks.json', + '.codex/hooks.json', + ]); + assert.deepEqual(providers.get('kimi').events, { + before_tool: 'PreToolUse', + after_tool: 'PostToolUse', + }); + assert.equal(providers.get('kimi').config_format, 'toml'); + assert.deepEqual(providers.get('grok').event_fields, { + name: 'toolName', + input: 'toolInput', + session: 'sessionId', + }); + assert.deepEqual(providers.get('kirocli').config_paths, [ + '~/.kiro/agents/*.json', + '.kiro/agents/*.json', + ]); + assert.deepEqual(providers.get('kiroide').config_paths, [ + '.kiro/hooks/*.json', + ]); + assert.deepEqual(providers.get('kiroide').events, { + before_tool: 'PreToolUse', + after_tool: 'PostToolUse', + }); + assert.deepEqual(providers.get('kiroide').blocking, { + mechanism: 'exit_code_2', + failure_mode: 'fail_open', + }); +});