git-ai-control 0.2.0

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,241 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ PROJECT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
5
+ GIT_AI_ROOT=${GIT_AI_ROOT:-"${HOME}/.git-ai"}
6
+ LAUNCH_AGENTS_ROOT="${HOME}/Library/LaunchAgents"
7
+ CONTROL_PANEL_DIR="${GIT_AI_ROOT}/control-panel"
8
+ FILTER_DIR="${GIT_AI_ROOT}/filters"
9
+ GIT_AI_BINARY="${GIT_AI_ROOT}/bin/git-ai"
10
+ POLICY_PATH="${GIT_AI_ROOT}/filter_plugins.json"
11
+ METRICS_PATH="${GIT_AI_ROOT}/custom_metrics.json"
12
+ UPSTREAMS_PATH="${GIT_AI_ROOT}/upstream_metrics.json"
13
+ CUSTOM_METRICS_MARKER="${CONTROL_PANEL_DIR}/.custom-metrics-managed"
14
+ CONTROL_PANEL_PLIST="${LAUNCH_AGENTS_ROOT}/com.git-ai.control-panel.plist"
15
+ FILTER_PLIST="${LAUNCH_AGENTS_ROOT}/com.git-ai.skill-usage-filter.plist"
16
+ CONTROL_PANEL_LABEL="gui/$(id -u)/com.git-ai.control-panel"
17
+ FILTER_LABEL="gui/$(id -u)/com.git-ai.skill-usage-filter"
18
+
19
+ require_file() {
20
+ if [ ! -f "$1" ]; then
21
+ echo "缺少安装文件:$1" >&2
22
+ exit 1
23
+ fi
24
+ }
25
+
26
+ wait_for_port_release() {
27
+ port=$1
28
+ attempt=0
29
+ while [ "${attempt}" -lt 20 ]; do
30
+ if ! /usr/sbin/lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then
31
+ return 0
32
+ fi
33
+ attempt=$((attempt + 1))
34
+ sleep 0.25
35
+ done
36
+ echo "端口 ${port} 仍被占用,停止安装" >&2
37
+ exit 1
38
+ }
39
+
40
+ wait_for_http() {
41
+ url=$1
42
+ label=$2
43
+ attempt=0
44
+ while [ "${attempt}" -lt 20 ]; do
45
+ if curl -fsS "${url}" >/dev/null 2>&1; then
46
+ return 0
47
+ fi
48
+ attempt=$((attempt + 1))
49
+ sleep 0.5
50
+ done
51
+ echo "${label} 启动失败:${url}" >&2
52
+ exit 1
53
+ }
54
+
55
+ require_file "${PROJECT_ROOT}/server.py"
56
+ require_file "${PROJECT_ROOT}/static/index.html"
57
+ require_file "${PROJECT_ROOT}/plugins/upload-filter/plugin_filter_runtime.py"
58
+ require_file "${PROJECT_ROOT}/policy.example.json"
59
+ require_file "${PROJECT_ROOT}/launchagents/com.git-ai.control-panel.plist.in"
60
+ require_file "${PROJECT_ROOT}/launchagents/com.git-ai.skill-usage-filter.plist.in"
61
+ require_file "${GIT_AI_BINARY}"
62
+
63
+ mkdir -p "${CONTROL_PANEL_DIR}" "${FILTER_DIR}" "${LAUNCH_AGENTS_ROOT}"
64
+
65
+ if [ ! -f "${POLICY_PATH}" ]; then
66
+ install -m 600 "${PROJECT_ROOT}/policy.example.json" "${POLICY_PATH}"
67
+ fi
68
+
69
+ if /usr/bin/strings "${GIT_AI_BINARY}" | grep -Fq "custom_metrics.json"; then
70
+ /usr/bin/python3 - \
71
+ "${METRICS_PATH}" \
72
+ "${UPSTREAMS_PATH}" \
73
+ "${FILTER_DIR}/plugin_filter_runtime.py" <<'PY'
74
+ import ast
75
+ import json
76
+ import os
77
+ import sys
78
+ import tempfile
79
+ from pathlib import Path
80
+ from urllib.parse import urlsplit
81
+
82
+ path = Path(sys.argv[1])
83
+ upstreams_path = Path(sys.argv[2])
84
+ installed_filter_path = Path(sys.argv[3])
85
+
86
+
87
+ def is_external_http_url(value):
88
+ if not isinstance(value, str):
89
+ return False
90
+ parsed = urlsplit(value)
91
+ return (
92
+ parsed.scheme in {"http", "https"}
93
+ and bool(parsed.netloc)
94
+ and parsed.hostname not in {"127.0.0.1", "localhost", "::1"}
95
+ )
96
+
97
+
98
+ def write_json(destination, payload):
99
+ destination.parent.mkdir(parents=True, exist_ok=True)
100
+ descriptor, temporary_name = tempfile.mkstemp(
101
+ prefix=f".{destination.name}.",
102
+ suffix=".tmp",
103
+ dir=destination.parent,
104
+ )
105
+ try:
106
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
107
+ json.dump(payload, handle, ensure_ascii=False, indent=2)
108
+ handle.write("\n")
109
+ handle.flush()
110
+ os.fsync(handle.fileno())
111
+ os.chmod(temporary_name, 0o600)
112
+ os.replace(temporary_name, destination)
113
+ except Exception:
114
+ try:
115
+ os.unlink(temporary_name)
116
+ except FileNotFoundError:
117
+ pass
118
+ raise
119
+
120
+
121
+ try:
122
+ config = json.loads(path.read_text(encoding="utf-8"))
123
+ except FileNotFoundError:
124
+ config = {}
125
+
126
+ if upstreams_path.exists():
127
+ saved_upstreams = json.loads(upstreams_path.read_text(encoding="utf-8"))
128
+ saved_routes = saved_upstreams.get("routes", saved_upstreams)
129
+ if not isinstance(saved_routes, dict) or not any(
130
+ is_external_http_url(url) for url in saved_routes.values()
131
+ ):
132
+ raise SystemExit("原上报端点备份无效,已停止安装")
133
+ os.chmod(upstreams_path, 0o600)
134
+ else:
135
+ route_keys = {
136
+ "/legacy/commit": "commit_endpoint",
137
+ "/legacy/checkpoint": "checkpoint_endpoint",
138
+ "/legacy/token-usage": "token_usage_endpoint",
139
+ "/prompt-duration": "prompt_duration_endpoint",
140
+ "/commit": "commit_endpoint_v2",
141
+ "/checkpoint": "checkpoint_endpoint_v2",
142
+ "/token-usage": "token_usage_endpoint_v2",
143
+ "/skill-usage": "skill_usage_endpoint_v2",
144
+ "/agent-usage": "agent_usage_endpoint_v2",
145
+ "/prompt-report": "prompt_report_endpoint_v2",
146
+ }
147
+ routes = {
148
+ route: config[key]
149
+ for route, key in route_keys.items()
150
+ if is_external_http_url(config.get(key))
151
+ }
152
+ token_url = routes.get("/token-usage")
153
+ if token_url:
154
+ routes["/token-usage/batch"] = f"{token_url.rstrip('/')}/batch"
155
+ routes["/token-usage/stats"] = f"{token_url.rstrip('/')}/stats"
156
+
157
+ if not routes and installed_filter_path.exists():
158
+ try:
159
+ module = ast.parse(installed_filter_path.read_text(encoding="utf-8"))
160
+ for statement in module.body:
161
+ if not isinstance(statement, ast.Assign):
162
+ continue
163
+ if not any(
164
+ isinstance(target, ast.Name) and target.id == "UPSTREAMS"
165
+ for target in statement.targets
166
+ ):
167
+ continue
168
+ legacy_routes = ast.literal_eval(statement.value)
169
+ routes = {
170
+ route: url
171
+ for route, url in legacy_routes.items()
172
+ if is_external_http_url(url)
173
+ }
174
+ break
175
+ except (OSError, SyntaxError, ValueError):
176
+ routes = {}
177
+
178
+ if not routes:
179
+ raise SystemExit(
180
+ "无法从 custom_metrics.json 识别原上报端点;"
181
+ "为避免丢失或回环请求,已停止安装"
182
+ )
183
+ write_json(upstreams_path, {"version": 1, "routes": routes})
184
+
185
+ config.update(
186
+ {
187
+ "enable": True,
188
+ "commit_endpoint": "http://127.0.0.1:38741/legacy/commit",
189
+ "checkpoint_endpoint": "http://127.0.0.1:38741/legacy/checkpoint",
190
+ "token_usage_endpoint": "http://127.0.0.1:38741/legacy/token-usage",
191
+ "prompt_duration_endpoint": "http://127.0.0.1:38741/prompt-duration",
192
+ "commit_endpoint_v2": "http://127.0.0.1:38741/commit",
193
+ "checkpoint_endpoint_v2": "http://127.0.0.1:38741/checkpoint",
194
+ "token_usage_endpoint_v2": "http://127.0.0.1:38741/token-usage",
195
+ "skill_usage_endpoint_v2": "http://127.0.0.1:38741/skill-usage",
196
+ "agent_usage_endpoint_v2": "http://127.0.0.1:38741/agent-usage",
197
+ "prompt_report_endpoint_v2": "http://127.0.0.1:38741/prompt-report",
198
+ }
199
+ )
200
+
201
+ write_json(path, config)
202
+ PY
203
+ touch "${CUSTOM_METRICS_MARKER}"
204
+ echo "检测到 custom_metrics 定制版:已启用细粒度上报过滤"
205
+ else
206
+ rm -f "${CUSTOM_METRICS_MARKER}"
207
+ echo "检测到官方上游版:保留原生配置管理,不修改无效的 custom_metrics.json"
208
+ fi
209
+
210
+ install -m 700 "${PROJECT_ROOT}/server.py" "${CONTROL_PANEL_DIR}/server.py"
211
+ install -m 700 \
212
+ "${PROJECT_ROOT}/plugins/upload-filter/plugin_filter_runtime.py" \
213
+ "${FILTER_DIR}/plugin_filter_runtime.py"
214
+ /usr/bin/rsync -a --delete "${PROJECT_ROOT}/static/" "${CONTROL_PANEL_DIR}/static/"
215
+
216
+ sed "s#__GIT_AI_ROOT__#${GIT_AI_ROOT}#g" \
217
+ "${PROJECT_ROOT}/launchagents/com.git-ai.control-panel.plist.in" \
218
+ > "${CONTROL_PANEL_PLIST}"
219
+ sed "s#__GIT_AI_ROOT__#${GIT_AI_ROOT}#g" \
220
+ "${PROJECT_ROOT}/launchagents/com.git-ai.skill-usage-filter.plist.in" \
221
+ > "${FILTER_PLIST}"
222
+ chmod 600 "${CONTROL_PANEL_PLIST}" "${FILTER_PLIST}"
223
+
224
+ plutil -lint "${CONTROL_PANEL_PLIST}" "${FILTER_PLIST}"
225
+ /usr/bin/python3 -m py_compile \
226
+ "${CONTROL_PANEL_DIR}/server.py" \
227
+ "${FILTER_DIR}/plugin_filter_runtime.py"
228
+
229
+ launchctl bootout "${CONTROL_PANEL_LABEL}" >/dev/null 2>&1 || true
230
+ launchctl bootout "${FILTER_LABEL}" >/dev/null 2>&1 || true
231
+ wait_for_port_release 38741
232
+ wait_for_port_release 38742
233
+ launchctl bootstrap "gui/$(id -u)" "${FILTER_PLIST}"
234
+ launchctl bootstrap "gui/$(id -u)" "${CONTROL_PANEL_PLIST}"
235
+ wait_for_http "http://127.0.0.1:38741/health" "过滤服务"
236
+ wait_for_http "http://127.0.0.1:38742/api/status" "配置页面"
237
+
238
+ echo "安装完成:"
239
+ echo " 配置页面:http://127.0.0.1:38742"
240
+ echo " 过滤服务:http://127.0.0.1:38741/health"
241
+ echo " 用户配置:${GIT_AI_ROOT}"
@@ -0,0 +1,91 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ GIT_AI_ROOT=${GIT_AI_ROOT:-"${HOME}/.git-ai"}
5
+ LAUNCH_AGENTS_ROOT="${HOME}/Library/LaunchAgents"
6
+ CONTROL_PANEL_DIR="${GIT_AI_ROOT}/control-panel"
7
+ FILTER_SCRIPT="${GIT_AI_ROOT}/filters/plugin_filter_runtime.py"
8
+ METRICS_PATH="${GIT_AI_ROOT}/custom_metrics.json"
9
+ UPSTREAMS_PATH="${GIT_AI_ROOT}/upstream_metrics.json"
10
+ CUSTOM_METRICS_MARKER="${CONTROL_PANEL_DIR}/.custom-metrics-managed"
11
+ CONTROL_PANEL_PLIST="${LAUNCH_AGENTS_ROOT}/com.git-ai.control-panel.plist"
12
+ FILTER_PLIST="${LAUNCH_AGENTS_ROOT}/com.git-ai.skill-usage-filter.plist"
13
+ CONTROL_PANEL_LABEL="gui/$(id -u)/com.git-ai.control-panel"
14
+ FILTER_LABEL="gui/$(id -u)/com.git-ai.skill-usage-filter"
15
+
16
+ case "${GIT_AI_ROOT}" in
17
+ "${HOME}/.git-ai") ;;
18
+ *)
19
+ echo "拒绝卸载非默认目录:${GIT_AI_ROOT}" >&2
20
+ exit 1
21
+ ;;
22
+ esac
23
+
24
+ launchctl bootout "${CONTROL_PANEL_LABEL}" >/dev/null 2>&1 || true
25
+ launchctl bootout "${FILTER_LABEL}" >/dev/null 2>&1 || true
26
+
27
+ if [ -f "${CUSTOM_METRICS_MARKER}" ]; then
28
+ /usr/bin/python3 - "${METRICS_PATH}" "${UPSTREAMS_PATH}" <<'PY'
29
+ import json
30
+ import os
31
+ import sys
32
+ import tempfile
33
+ from pathlib import Path
34
+
35
+ path = Path(sys.argv[1])
36
+ upstreams_path = Path(sys.argv[2])
37
+ if not path.exists():
38
+ raise SystemExit(0)
39
+ if not upstreams_path.exists():
40
+ raise SystemExit("缺少原上报端点备份,已停止卸载以避免留下无效配置")
41
+
42
+ config = json.loads(path.read_text(encoding="utf-8"))
43
+ upstreams = json.loads(upstreams_path.read_text(encoding="utf-8"))
44
+ routes = upstreams.get("routes", upstreams)
45
+ route_keys = {
46
+ "/legacy/commit": "commit_endpoint",
47
+ "/legacy/checkpoint": "checkpoint_endpoint",
48
+ "/legacy/token-usage": "token_usage_endpoint",
49
+ "/prompt-duration": "prompt_duration_endpoint",
50
+ "/commit": "commit_endpoint_v2",
51
+ "/checkpoint": "checkpoint_endpoint_v2",
52
+ "/token-usage": "token_usage_endpoint_v2",
53
+ "/skill-usage": "skill_usage_endpoint_v2",
54
+ "/agent-usage": "agent_usage_endpoint_v2",
55
+ "/prompt-report": "prompt_report_endpoint_v2",
56
+ }
57
+ for route, key in route_keys.items():
58
+ url = routes.get(route)
59
+ if isinstance(url, str) and url:
60
+ config[key] = url
61
+
62
+ descriptor, temporary_name = tempfile.mkstemp(
63
+ prefix=f".{path.name}.",
64
+ suffix=".tmp",
65
+ dir=path.parent,
66
+ )
67
+ try:
68
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
69
+ json.dump(config, handle, ensure_ascii=False, indent=2)
70
+ handle.write("\n")
71
+ handle.flush()
72
+ os.fsync(handle.fileno())
73
+ os.chmod(temporary_name, 0o600)
74
+ os.replace(temporary_name, path)
75
+ except Exception:
76
+ try:
77
+ os.unlink(temporary_name)
78
+ except FileNotFoundError:
79
+ pass
80
+ raise
81
+ PY
82
+ fi
83
+
84
+ rm -f "${CONTROL_PANEL_PLIST}" "${FILTER_PLIST}" "${FILTER_SCRIPT}"
85
+ rm -rf "${CONTROL_PANEL_DIR}"
86
+
87
+ echo "卸载完成。保留了以下用户数据:"
88
+ echo " ${GIT_AI_ROOT}/config.json"
89
+ echo " ${GIT_AI_ROOT}/filter_plugins.json"
90
+ echo " ${GIT_AI_ROOT}/upstream_metrics.json"
91
+ echo " ${GIT_AI_ROOT}/filters/*.log"