cc-relay 0.1.0__tar.gz

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.
cc_relay-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: cc-relay
3
+ Version: 0.1.0
4
+ Summary: Intelligent interrupt layer for Claude — learns when to ask, when to proceed
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/solost23/relay
7
+ Project-URL: Repository, https://github.com/solost23/relay
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: anthropic>=0.102.0
12
+ Requires-Dist: mcp[cli]>=1.27.1
13
+ Requires-Dist: plyer>=2.1.0
14
+ Provides-Extra: macos
15
+ Requires-Dist: pyobjus>=1.2.4; extra == "macos"
16
+ Dynamic: license-file
17
+
18
+ # Relay
19
+
20
+ Relay 是一个 Claude Code 智能中断层。它通过 hook 拦截每次工具调用,结合历史审批记录和风险评估,自动决定哪些操作直接执行、哪些需要暂停等你确认,并在需要确认时发送桌面通知。
21
+
22
+ **核心价值:** 让 AI 任务在后台跑,只在真正需要你决策时才打断你。
23
+
24
+ [English](README.en.md) | [日本語](README.ja.md) | [한국어](README.ko.md)
25
+
26
+ ## 工作原理
27
+
28
+ ```
29
+ Claude 准备执行工具(Write、Bash、Edit 等)
30
+
31
+ PreToolUse hook 触发 → relay hook pre
32
+
33
+ 查询历史批准率 + 评估风险等级
34
+
35
+ allow → 工具直接执行,自动记录为已批准
36
+ ask → 工具暂停,发送桌面通知,Claude Code 弹出确认提示
37
+
38
+ 用户确认后 Claude 继续,PostToolUse hook 记录结果
39
+
40
+ 历史积累 → 下次同类操作判断更准确
41
+ ```
42
+
43
+ ## 决策逻辑
44
+
45
+ | 条件 | 结果 |
46
+ |---|---|
47
+ | 高风险操作(删文件、force push、drop 表、写系统路径) | 始终拦截 |
48
+ | 低风险 + 历史批准率 ≥ 90% | 始终直接执行 |
49
+ | 该类型操作首次出现(无历史) | 低风险直接执行,其他拦截一次建立基线 |
50
+ | 其他情况 | 历史批准率 < 80% 则拦截 |
51
+
52
+ 操作类型按路径和命令细分,各自独立积累批准率:
53
+
54
+ | 操作类型 | 说明 | 风险 |
55
+ |---|---|---|
56
+ | `file_write:system` | 写入 `/etc/`、`/usr/` 等系统路径 | 高 |
57
+ | `file_write:config` | 写入 `.env`、`.yaml`、`.toml` 等配置文件 | 中 |
58
+ | `file_write:code` | 写入普通代码文件 | 中 |
59
+ | `bash_write:git` | git commit / push / merge | 中 |
60
+ | `bash_write:package_manager` | pip / uv / npm 安装 | 中 |
61
+ | `bash_write:shell` | mv / cp / chmod 等 shell 操作 | 中 |
62
+ | `file_delete` | rm、drop table 等删除操作 | 高 |
63
+ | `bash_read` / `file_read` | 只读操作 | 低 |
64
+
65
+ ## 安装
66
+
67
+ **全局安装**(推荐)——将以下内容添加到 **`~/.claude.json`** 的 `mcpServers` 字段:
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "relay": {
73
+ "type": "stdio",
74
+ "command": "uvx",
75
+ "args": ["cc-relay"]
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ **项目级安装**——在项目根目录创建 **`.mcp.json`**:
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "relay": {
87
+ "type": "stdio",
88
+ "command": "uvx",
89
+ "args": ["cc-relay"]
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ 重启 Claude Code。Relay 会在首次启动时自动将 hook 注册到 `~/.claude/settings.json`,之后所有工具调用都会经过 relay 的决策层。
96
+
97
+ ## 卸载
98
+
99
+ ```bash
100
+ uvx cc-relay --uninstall
101
+ ```
102
+
103
+ ## 通知支持
104
+
105
+ 通知文字会根据系统语言自动切换,目前支持中文、英文、日文、韩文。
106
+
107
+ | 平台 | 实现 | 说明 |
108
+ |---|---|---|
109
+ | macOS | `osascript` | 系统内置,开箱即用 |
110
+ | Linux | `notify-send` | 需要桌面环境,Ubuntu/GNOME 默认已有 |
111
+ | Windows | `plyer` | 开箱即用 |
112
+
113
+ ## MCP 工具(可选)
114
+
115
+ 安装 hook 后 relay 已经自动工作,不需要额外配置。但如果你想查看统计数据,可以在 Claude Code 里调用 `relay__get_stats_tool` 查看审批统计。
116
+
117
+ ## 已知限制
118
+
119
+ Relay hook 在 `--dangerously-skip-permissions` 模式下不生效(该模式完全跳过 hook 机制)。
120
+
121
+ ## 本地开发
122
+
123
+ ```bash
124
+ git clone https://github.com/solost23/cc-relay
125
+ cd cc-relay
126
+ uv sync
127
+ uv run pytest
128
+ uv run mcp dev relay/server.py
129
+ ```
130
+
@@ -0,0 +1,113 @@
1
+ # Relay
2
+
3
+ Relay 是一个 Claude Code 智能中断层。它通过 hook 拦截每次工具调用,结合历史审批记录和风险评估,自动决定哪些操作直接执行、哪些需要暂停等你确认,并在需要确认时发送桌面通知。
4
+
5
+ **核心价值:** 让 AI 任务在后台跑,只在真正需要你决策时才打断你。
6
+
7
+ [English](README.en.md) | [日本語](README.ja.md) | [한국어](README.ko.md)
8
+
9
+ ## 工作原理
10
+
11
+ ```
12
+ Claude 准备执行工具(Write、Bash、Edit 等)
13
+
14
+ PreToolUse hook 触发 → relay hook pre
15
+
16
+ 查询历史批准率 + 评估风险等级
17
+
18
+ allow → 工具直接执行,自动记录为已批准
19
+ ask → 工具暂停,发送桌面通知,Claude Code 弹出确认提示
20
+
21
+ 用户确认后 Claude 继续,PostToolUse hook 记录结果
22
+
23
+ 历史积累 → 下次同类操作判断更准确
24
+ ```
25
+
26
+ ## 决策逻辑
27
+
28
+ | 条件 | 结果 |
29
+ |---|---|
30
+ | 高风险操作(删文件、force push、drop 表、写系统路径) | 始终拦截 |
31
+ | 低风险 + 历史批准率 ≥ 90% | 始终直接执行 |
32
+ | 该类型操作首次出现(无历史) | 低风险直接执行,其他拦截一次建立基线 |
33
+ | 其他情况 | 历史批准率 < 80% 则拦截 |
34
+
35
+ 操作类型按路径和命令细分,各自独立积累批准率:
36
+
37
+ | 操作类型 | 说明 | 风险 |
38
+ |---|---|---|
39
+ | `file_write:system` | 写入 `/etc/`、`/usr/` 等系统路径 | 高 |
40
+ | `file_write:config` | 写入 `.env`、`.yaml`、`.toml` 等配置文件 | 中 |
41
+ | `file_write:code` | 写入普通代码文件 | 中 |
42
+ | `bash_write:git` | git commit / push / merge | 中 |
43
+ | `bash_write:package_manager` | pip / uv / npm 安装 | 中 |
44
+ | `bash_write:shell` | mv / cp / chmod 等 shell 操作 | 中 |
45
+ | `file_delete` | rm、drop table 等删除操作 | 高 |
46
+ | `bash_read` / `file_read` | 只读操作 | 低 |
47
+
48
+ ## 安装
49
+
50
+ **全局安装**(推荐)——将以下内容添加到 **`~/.claude.json`** 的 `mcpServers` 字段:
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "relay": {
56
+ "type": "stdio",
57
+ "command": "uvx",
58
+ "args": ["cc-relay"]
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ **项目级安装**——在项目根目录创建 **`.mcp.json`**:
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "relay": {
70
+ "type": "stdio",
71
+ "command": "uvx",
72
+ "args": ["cc-relay"]
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ 重启 Claude Code。Relay 会在首次启动时自动将 hook 注册到 `~/.claude/settings.json`,之后所有工具调用都会经过 relay 的决策层。
79
+
80
+ ## 卸载
81
+
82
+ ```bash
83
+ uvx cc-relay --uninstall
84
+ ```
85
+
86
+ ## 通知支持
87
+
88
+ 通知文字会根据系统语言自动切换,目前支持中文、英文、日文、韩文。
89
+
90
+ | 平台 | 实现 | 说明 |
91
+ |---|---|---|
92
+ | macOS | `osascript` | 系统内置,开箱即用 |
93
+ | Linux | `notify-send` | 需要桌面环境,Ubuntu/GNOME 默认已有 |
94
+ | Windows | `plyer` | 开箱即用 |
95
+
96
+ ## MCP 工具(可选)
97
+
98
+ 安装 hook 后 relay 已经自动工作,不需要额外配置。但如果你想查看统计数据,可以在 Claude Code 里调用 `relay__get_stats_tool` 查看审批统计。
99
+
100
+ ## 已知限制
101
+
102
+ Relay hook 在 `--dangerously-skip-permissions` 模式下不生效(该模式完全跳过 hook 机制)。
103
+
104
+ ## 本地开发
105
+
106
+ ```bash
107
+ git clone https://github.com/solost23/cc-relay
108
+ cd cc-relay
109
+ uv sync
110
+ uv run pytest
111
+ uv run mcp dev relay/server.py
112
+ ```
113
+
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: cc-relay
3
+ Version: 0.1.0
4
+ Summary: Intelligent interrupt layer for Claude — learns when to ask, when to proceed
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/solost23/relay
7
+ Project-URL: Repository, https://github.com/solost23/relay
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: anthropic>=0.102.0
12
+ Requires-Dist: mcp[cli]>=1.27.1
13
+ Requires-Dist: plyer>=2.1.0
14
+ Provides-Extra: macos
15
+ Requires-Dist: pyobjus>=1.2.4; extra == "macos"
16
+ Dynamic: license-file
17
+
18
+ # Relay
19
+
20
+ Relay 是一个 Claude Code 智能中断层。它通过 hook 拦截每次工具调用,结合历史审批记录和风险评估,自动决定哪些操作直接执行、哪些需要暂停等你确认,并在需要确认时发送桌面通知。
21
+
22
+ **核心价值:** 让 AI 任务在后台跑,只在真正需要你决策时才打断你。
23
+
24
+ [English](README.en.md) | [日本語](README.ja.md) | [한국어](README.ko.md)
25
+
26
+ ## 工作原理
27
+
28
+ ```
29
+ Claude 准备执行工具(Write、Bash、Edit 等)
30
+
31
+ PreToolUse hook 触发 → relay hook pre
32
+
33
+ 查询历史批准率 + 评估风险等级
34
+
35
+ allow → 工具直接执行,自动记录为已批准
36
+ ask → 工具暂停,发送桌面通知,Claude Code 弹出确认提示
37
+
38
+ 用户确认后 Claude 继续,PostToolUse hook 记录结果
39
+
40
+ 历史积累 → 下次同类操作判断更准确
41
+ ```
42
+
43
+ ## 决策逻辑
44
+
45
+ | 条件 | 结果 |
46
+ |---|---|
47
+ | 高风险操作(删文件、force push、drop 表、写系统路径) | 始终拦截 |
48
+ | 低风险 + 历史批准率 ≥ 90% | 始终直接执行 |
49
+ | 该类型操作首次出现(无历史) | 低风险直接执行,其他拦截一次建立基线 |
50
+ | 其他情况 | 历史批准率 < 80% 则拦截 |
51
+
52
+ 操作类型按路径和命令细分,各自独立积累批准率:
53
+
54
+ | 操作类型 | 说明 | 风险 |
55
+ |---|---|---|
56
+ | `file_write:system` | 写入 `/etc/`、`/usr/` 等系统路径 | 高 |
57
+ | `file_write:config` | 写入 `.env`、`.yaml`、`.toml` 等配置文件 | 中 |
58
+ | `file_write:code` | 写入普通代码文件 | 中 |
59
+ | `bash_write:git` | git commit / push / merge | 中 |
60
+ | `bash_write:package_manager` | pip / uv / npm 安装 | 中 |
61
+ | `bash_write:shell` | mv / cp / chmod 等 shell 操作 | 中 |
62
+ | `file_delete` | rm、drop table 等删除操作 | 高 |
63
+ | `bash_read` / `file_read` | 只读操作 | 低 |
64
+
65
+ ## 安装
66
+
67
+ **全局安装**(推荐)——将以下内容添加到 **`~/.claude.json`** 的 `mcpServers` 字段:
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "relay": {
73
+ "type": "stdio",
74
+ "command": "uvx",
75
+ "args": ["cc-relay"]
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ **项目级安装**——在项目根目录创建 **`.mcp.json`**:
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "relay": {
87
+ "type": "stdio",
88
+ "command": "uvx",
89
+ "args": ["cc-relay"]
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ 重启 Claude Code。Relay 会在首次启动时自动将 hook 注册到 `~/.claude/settings.json`,之后所有工具调用都会经过 relay 的决策层。
96
+
97
+ ## 卸载
98
+
99
+ ```bash
100
+ uvx cc-relay --uninstall
101
+ ```
102
+
103
+ ## 通知支持
104
+
105
+ 通知文字会根据系统语言自动切换,目前支持中文、英文、日文、韩文。
106
+
107
+ | 平台 | 实现 | 说明 |
108
+ |---|---|---|
109
+ | macOS | `osascript` | 系统内置,开箱即用 |
110
+ | Linux | `notify-send` | 需要桌面环境,Ubuntu/GNOME 默认已有 |
111
+ | Windows | `plyer` | 开箱即用 |
112
+
113
+ ## MCP 工具(可选)
114
+
115
+ 安装 hook 后 relay 已经自动工作,不需要额外配置。但如果你想查看统计数据,可以在 Claude Code 里调用 `relay__get_stats_tool` 查看审批统计。
116
+
117
+ ## 已知限制
118
+
119
+ Relay hook 在 `--dangerously-skip-permissions` 模式下不生效(该模式完全跳过 hook 机制)。
120
+
121
+ ## 本地开发
122
+
123
+ ```bash
124
+ git clone https://github.com/solost23/cc-relay
125
+ cd cc-relay
126
+ uv sync
127
+ uv run pytest
128
+ uv run mcp dev relay/server.py
129
+ ```
130
+
@@ -0,0 +1,23 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ cc_relay.egg-info/PKG-INFO
5
+ cc_relay.egg-info/SOURCES.txt
6
+ cc_relay.egg-info/dependency_links.txt
7
+ cc_relay.egg-info/entry_points.txt
8
+ cc_relay.egg-info/requires.txt
9
+ cc_relay.egg-info/top_level.txt
10
+ relay/__init__.py
11
+ relay/__main__.py
12
+ relay/assessor.py
13
+ relay/db.py
14
+ relay/decision.py
15
+ relay/hook.py
16
+ relay/installer.py
17
+ relay/notifier.py
18
+ relay/server.py
19
+ tests/test_assessor.py
20
+ tests/test_db.py
21
+ tests/test_hook.py
22
+ tests/test_installer.py
23
+ tests/test_server.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ relay = relay.server:main
@@ -0,0 +1,6 @@
1
+ anthropic>=0.102.0
2
+ mcp[cli]>=1.27.1
3
+ plyer>=2.1.0
4
+
5
+ [macos]
6
+ pyobjus>=1.2.4
@@ -0,0 +1 @@
1
+ relay
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "cc-relay"
3
+ version = "0.1.0"
4
+ description = "Intelligent interrupt layer for Claude — learns when to ask, when to proceed"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ requires-python = ">=3.12"
8
+ dependencies = [
9
+ "anthropic>=0.102.0",
10
+ "mcp[cli]>=1.27.1",
11
+ "plyer>=2.1.0",
12
+ ]
13
+
14
+ [project.optional-dependencies]
15
+ macos = ["pyobjus>=1.2.4"]
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/solost23/relay"
19
+ Repository = "https://github.com/solost23/relay"
20
+
21
+ [project.scripts]
22
+ relay = "relay.server:main"
23
+
24
+ [dependency-groups]
25
+ dev = [
26
+ "pytest>=9.0.3",
27
+ ]
File without changes
@@ -0,0 +1,43 @@
1
+ import sys
2
+
3
+
4
+ def main() -> None:
5
+ args = sys.argv[1:]
6
+
7
+ if not args:
8
+ # Default: start MCP server
9
+ from relay.server import main as serve
10
+ serve()
11
+ return
12
+
13
+ cmd = args[0]
14
+
15
+ if cmd == "--install":
16
+ from relay.installer import install
17
+ install()
18
+
19
+ elif cmd == "--uninstall":
20
+ from relay.installer import uninstall
21
+ uninstall()
22
+
23
+ elif cmd == "hook" and len(args) >= 2:
24
+ from relay.db import init_db
25
+ init_db()
26
+ subcommand = args[1]
27
+ if subcommand == "pre":
28
+ from relay.hook import run_pre_tool_use
29
+ run_pre_tool_use()
30
+ elif subcommand == "post":
31
+ from relay.hook import run_post_tool_use
32
+ run_post_tool_use()
33
+ else:
34
+ print(f"Unknown hook subcommand: {subcommand}", file=sys.stderr)
35
+ sys.exit(1)
36
+
37
+ else:
38
+ print(f"Usage: relay [--install | --uninstall | hook pre | hook post]", file=sys.stderr)
39
+ sys.exit(1)
40
+
41
+
42
+ if __name__ == "__main__":
43
+ main()
@@ -0,0 +1,86 @@
1
+ _HIGH_RISK_TYPES = {
2
+ "file_delete",
3
+ "db_drop",
4
+ "git_reset",
5
+ "git_force_push",
6
+ "git_rebase",
7
+ "git_amend",
8
+ "rm_rf",
9
+ "process_kill",
10
+ "file_write:system",
11
+ "env_write",
12
+ "secret_write",
13
+ "permission_change",
14
+ "network_send",
15
+ "ci_cd_modify",
16
+ "cron_write",
17
+ }
18
+
19
+ _LOW_RISK_TYPES = {
20
+ "file_read",
21
+ "bash_read",
22
+ "git_log",
23
+ "git_status",
24
+ "git_diff",
25
+ "db_read",
26
+ "list_files",
27
+ }
28
+
29
+ _MEDIUM_RISK_TYPES = {
30
+ "file_write:code",
31
+ "file_write:config",
32
+ "file_create",
33
+ "db_write",
34
+ "db_update",
35
+ "bash_write:git",
36
+ "bash_write:package_manager",
37
+ "bash_write:shell",
38
+ "network_request",
39
+ # legacy — kept for existing DB records
40
+ "file_write",
41
+ "git_commit",
42
+ "git_push",
43
+ "bash_write",
44
+ }
45
+
46
+
47
+ def assess_risk(action_type: str, action_description: str) -> dict:
48
+ """Return risk_level, reversible flag, and reason for the given action."""
49
+ t = action_type.lower()
50
+
51
+ if t in _HIGH_RISK_TYPES:
52
+ return {
53
+ "risk_level": "high",
54
+ "reversible": False,
55
+ "reason": f"Action type '{action_type}' is destructive and not reversible.",
56
+ }
57
+
58
+ if t in _LOW_RISK_TYPES:
59
+ return {
60
+ "risk_level": "low",
61
+ "reversible": True,
62
+ "reason": f"Action type '{action_type}' is read-only and safe.",
63
+ }
64
+
65
+ if t in _MEDIUM_RISK_TYPES:
66
+ return {
67
+ "risk_level": "medium",
68
+ "reversible": True,
69
+ "reason": f"Action type '{action_type}' modifies state but is generally recoverable.",
70
+ }
71
+
72
+ # Heuristic fallback: scan description for danger words
73
+ danger_words = {"delete", "remove", "drop", "truncate", "force", "overwrite", "rm"}
74
+ desc_lower = action_description.lower()
75
+ if any(w in desc_lower for w in danger_words):
76
+ return {
77
+ "risk_level": "high",
78
+ "reversible": False,
79
+ "reason": "Description contains potentially destructive keywords.",
80
+ }
81
+
82
+ return {
83
+ "risk_level": "medium",
84
+ "reversible": True,
85
+ "reason": f"Unknown action type '{action_type}'; defaulting to medium risk.",
86
+ }
@@ -0,0 +1,96 @@
1
+ import sqlite3
2
+ from pathlib import Path
3
+
4
+ _DEFAULT_DB = Path.home() / ".relay" / "decisions.db"
5
+
6
+
7
+ def _db_path(path: Path | None) -> Path:
8
+ return path if path is not None else _DEFAULT_DB
9
+
10
+
11
+ def init_db(db_path: Path | None = None) -> Path:
12
+ p = _db_path(db_path)
13
+ p.parent.mkdir(parents=True, exist_ok=True)
14
+ with sqlite3.connect(p) as conn:
15
+ conn.execute("""
16
+ CREATE TABLE IF NOT EXISTS decisions (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ action_type TEXT NOT NULL,
19
+ action_description TEXT NOT NULL,
20
+ decision TEXT NOT NULL,
21
+ risk_level TEXT NOT NULL,
22
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
23
+ )
24
+ """)
25
+ return p
26
+
27
+
28
+ def record_decision(
29
+ action_type: str,
30
+ action_description: str,
31
+ decision: str,
32
+ risk_level: str,
33
+ db_path: Path | None = None,
34
+ ) -> None:
35
+ with sqlite3.connect(_db_path(db_path)) as conn:
36
+ conn.execute(
37
+ "INSERT INTO decisions (action_type, action_description, decision, risk_level) VALUES (?, ?, ?, ?)",
38
+ (action_type, action_description, decision, risk_level),
39
+ )
40
+
41
+
42
+ _APPROVAL_RATE_WINDOW = 50 # only consider the most recent N decisions per action type
43
+
44
+
45
+ def get_approval_rate(action_type: str, db_path: Path | None = None) -> float:
46
+ with sqlite3.connect(_db_path(db_path)) as conn:
47
+ rows = conn.execute(
48
+ "SELECT decision FROM decisions WHERE action_type = ? ORDER BY created_at DESC, id DESC LIMIT ?",
49
+ (action_type, _APPROVAL_RATE_WINDOW),
50
+ ).fetchall()
51
+ if not rows:
52
+ return 0.5
53
+ approved = sum(1 for (d,) in rows if d == "approved")
54
+ return approved / len(rows)
55
+
56
+
57
+ def get_stats(db_path: Path | None = None) -> dict:
58
+ """Return approval stats for all action types plus total decision count."""
59
+ with sqlite3.connect(_db_path(db_path)) as conn:
60
+ conn.row_factory = sqlite3.Row
61
+ total = conn.execute("SELECT COUNT(*) FROM decisions").fetchone()[0]
62
+ rows = conn.execute("""
63
+ SELECT
64
+ action_type,
65
+ COUNT(*) AS total,
66
+ SUM(CASE WHEN decision = 'approved' THEN 1 ELSE 0 END) AS approved
67
+ FROM decisions
68
+ GROUP BY action_type
69
+ ORDER BY total DESC
70
+ """).fetchall()
71
+ by_type = [
72
+ {
73
+ "action_type": r["action_type"],
74
+ "total": r["total"],
75
+ "approved": r["approved"],
76
+ "approval_rate": round(r["approved"] / r["total"], 3),
77
+ }
78
+ for r in rows
79
+ ]
80
+ return {"total_decisions": total, "by_action_type": by_type}
81
+
82
+
83
+ def get_recent_decisions(
84
+ action_type: str, limit: int = 20, db_path: Path | None = None
85
+ ) -> list[dict]:
86
+ with sqlite3.connect(_db_path(db_path)) as conn:
87
+ conn.row_factory = sqlite3.Row
88
+ rows = conn.execute(
89
+ """
90
+ SELECT action_type, action_description, decision, risk_level, created_at
91
+ FROM decisions WHERE action_type = ?
92
+ ORDER BY created_at DESC, id DESC LIMIT ?
93
+ """,
94
+ (action_type, limit),
95
+ ).fetchall()
96
+ return [dict(r) for r in rows]