diff --git a/AGENTS.md b/AGENTS.md index 4650641..423c97e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,7 @@ This repository publishes the open-source server, console, integration catalog, - Write Grok Build integrations to a native user hook file under non-empty `GROK_HOME` or `~/.grok`; keep Claude Code and Cursor compatibility sources read-only. - Write Auggie hooks only to `~/.augment/settings.json`; keep system and workspace settings read-only. Generate `.cmd` wrappers on Windows and `.sh` wrappers on Unix because Auggie dispatches supported script paths, and express hook timeouts in milliseconds. - Validate Auggie matcher syntax during installation with Node.js `new RegExp`; keep status and uninstall independent from the JavaScript validator so recovery remains available offline. +- Write Cline file hooks to `$CLINE_DIR/hooks` with `~/.cline/hooks` as the default; keep Documents and workspace hook roots read-only. Use supported event filenames and return JSON stdout with `cancel: true` for blocking. Treat hook errors, timeouts, and invalid control JSON as fail-open. ## Mirror Workflow diff --git a/integrations/catalog.json b/integrations/catalog.json index 9f17b82..0727b04 100644 --- a/integrations/catalog.json +++ b/integrations/catalog.json @@ -64,20 +64,30 @@ "name": "Cline", "vendor": "Cline", "surface": "cli_and_ide", - "integration_mode": "plugin_api", - "config_format": "plugin", - "config_paths": [], + "integration_mode": "command_hooks", + "config_format": "script", + "config_paths": [ + "~/Documents/Cline/Hooks/", + "$CLINE_DIR/hooks/", + ".clinerules/hooks/", + ".cline/hooks/" + ], "events": { - "before_tool": "beforeTool", - "after_tool": "afterTool" + "before_tool": "PreToolUse", + "after_tool": "PostToolUse" + }, + "event_fields": { + "name": "tool_call.name/tool_result.name", + "input": "tool_call.input/tool_result.input", + "session": "taskId" }, "blocking": { - "mechanism": "hook_policy", - "failure_mode": "configurable" + "mechanism": "json_stdout_cancel", + "failure_mode": "fail_open" }, "adapters": { "node": false, "python": false, "go": false }, "delivery_state": "planned", - "source_url": "https://docs.cline.bot/sdk/plugins" + "source_url": "https://github.com/cline/cline/blob/main/sdk/examples/hooks/README.md" }, { "id": "codex", diff --git a/integrations/catalog.schema.json b/integrations/catalog.schema.json index 6b71a7c..0f83a15 100644 --- a/integrations/catalog.schema.json +++ b/integrations/catalog.schema.json @@ -71,7 +71,8 @@ "enum": [ "json", "toml", - "plugin" + "plugin", + "script" ] }, "config_paths": { @@ -136,7 +137,8 @@ "any_nonzero_exit", "exception", "exit_code_2", - "hook_policy" + "hook_policy", + "json_stdout_cancel" ] }, "failure_mode": { @@ -252,7 +254,8 @@ "enum": [ "json", "toml", - "plugin" + "plugin", + "script" ] }, "config_paths": { @@ -318,7 +321,8 @@ "any_nonzero_exit", "exception", "exit_code_2", - "hook_policy" + "hook_policy", + "json_stdout_cancel" ] }, "failure_mode": { diff --git a/test/integration-catalog.test.mjs b/test/integration-catalog.test.mjs index 1385e8a..cc9095e 100644 --- a/test/integration-catalog.test.mjs +++ b/test/integration-catalog.test.mjs @@ -93,7 +93,7 @@ test('every provider exposes a complete, machine-readable hook contract', async 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(['json', 'toml', 'plugin', 'script'].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'); @@ -102,6 +102,7 @@ test('every provider exposes a complete, machine-readable hook contract', async 'exception', 'exit_code_2', 'hook_policy', + 'json_stdout_cancel', ].includes(provider.blocking.mechanism)); assert.ok([ 'adapter_controlled', @@ -128,7 +129,7 @@ test('every provider exposes a complete, machine-readable hook contract', async assert.match(variant.id, /^[a-z][a-z0-9]*$/); assert.ok(['stable', 'early_access', 'legacy'].includes(variant.release_channel)); assert.equal(typeof variant.activation, 'string'); - assert.ok(['json', 'toml', 'plugin'].includes(variant.config_format)); + assert.ok(['json', 'toml', 'plugin', 'script'].includes(variant.config_format)); assert.ok(Array.isArray(variant.config_paths) && variant.config_paths.length > 0); assert.equal(typeof variant.events.before_tool, 'string'); assert.equal(typeof variant.events.after_tool, 'string'); @@ -148,6 +149,17 @@ test('schema freezes the provider contract and supported enums', async () => { 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.config_format.enum, [ + 'json', + 'toml', + 'plugin', + 'script', + ]); + assert.ok( + providerSchema.properties.blocking.properties.mechanism.enum.includes( + 'json_stdout_cancel', + ), + ); assert.deepEqual(providerSchema.properties.delivery_state.enum, ['available', 'partial', 'planned']); assert.equal(providerSchema.properties.contract_variants.items.$ref, '#/$defs/contractVariant'); assert.deepEqual(schema.$defs.contractVariant.properties.release_channel.enum, [ @@ -202,6 +214,27 @@ test('high-drift providers retain their verified hook contracts', async () => { '~/.codex/hooks.json', '.codex/hooks.json', ]); + assert.equal(providers.get('cline').integration_mode, 'command_hooks'); + assert.equal(providers.get('cline').config_format, 'script'); + assert.deepEqual(providers.get('cline').config_paths, [ + '~/Documents/Cline/Hooks/', + '$CLINE_DIR/hooks/', + '.clinerules/hooks/', + '.cline/hooks/', + ]); + assert.deepEqual(providers.get('cline').events, { + before_tool: 'PreToolUse', + after_tool: 'PostToolUse', + }); + assert.deepEqual(providers.get('cline').event_fields, { + name: 'tool_call.name/tool_result.name', + input: 'tool_call.input/tool_result.input', + session: 'taskId', + }); + assert.deepEqual(providers.get('cline').blocking, { + mechanism: 'json_stdout_cancel', + failure_mode: 'fail_open', + }); assert.deepEqual(providers.get('kimi').events, { before_tool: 'PreToolUse', after_tool: 'PostToolUse',