@meridiona/meridian-darwin-arm64 1.22.0 → 1.23.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.
- package/VERSION +1 -1
- package/bin/meridian +0 -0
- package/package.json +1 -1
- package/scripts/install-from-bundle.sh +5 -1
- package/scripts/meridian-cli.sh +1 -1
- package/services/agents/llm_selector.py +26 -9
- package/services/agents/run_task_linker_mlx.py +95 -20
- package/services/agents/server.py +15 -8
- package/services/pyproject.toml +4 -2
- package/services/scripts/install-claude-hook.sh +12 -16
- package/ui.tar.gz +0 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.23.0
|
package/bin/meridian
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meridiona/meridian-darwin-arm64",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "Prebuilt Meridian app for macOS arm64 (daemon binary + dashboard + Python services). Installed via @meridiona/meridian.",
|
|
5
5
|
"homepage": "https://github.com/Meridiona/meridian",
|
|
6
6
|
"repository": {
|
|
@@ -319,10 +319,14 @@ if [[ -f "${VENV_TARBALL}" ]]; then
|
|
|
319
319
|
fi
|
|
320
320
|
else
|
|
321
321
|
# Dev / source install — no pre-built tarball. Resolve from uv.lock.
|
|
322
|
-
|
|
322
|
+
# Both extras: mlx (classifier + server) AND pm_worklog_update (agno) — the
|
|
323
|
+
# one MLX server process serves /classify_sessions AND /synthesise_worklog,
|
|
324
|
+
# so the venv needs agno or worklog synthesis 500s with ModuleNotFoundError.
|
|
325
|
+
info "Installing Python + MLX deps (mlx-lm/outlines/fastapi/agno; first run may download a few hundred MB)…"
|
|
323
326
|
if "${UV_BIN}" sync \
|
|
324
327
|
--project "${APP_ROOT}/services" \
|
|
325
328
|
--extra mlx \
|
|
329
|
+
--extra pm_worklog_update \
|
|
326
330
|
--frozen \
|
|
327
331
|
--python "${PYTHON_BIN}"; then
|
|
328
332
|
ok "Python services ready ($(${VENV}/bin/python --version 2>&1))"
|
package/scripts/meridian-cli.sh
CHANGED
|
@@ -483,7 +483,7 @@ case "$CMD" in
|
|
|
483
483
|
uninstall) cmd_uninstall ;;
|
|
484
484
|
permissions) cmd_permissions ;;
|
|
485
485
|
version|--version|-v) cat "${REPO_ROOT}/VERSION" 2>/dev/null || echo "unknown" ;;
|
|
486
|
-
worklog-status|pm-worklog) cmd_daemon_passthrough "$CMD" "$@" ;;
|
|
486
|
+
worklog-status|pm-worklog|coding-agent-hook|coding-agent-summarise|coding-agent-classify) cmd_daemon_passthrough "$CMD" "$@" ;;
|
|
487
487
|
--help|-h|help|"") cmd_help ;;
|
|
488
488
|
*) err "unknown command: ${CMD}"; echo; cmd_help; exit 1 ;;
|
|
489
489
|
esac
|
|
@@ -274,6 +274,9 @@ class LocalModelEndpoint:
|
|
|
274
274
|
_MANAGED_SERVER_PORT = 8765
|
|
275
275
|
_MANAGED_SERVER_PID_FILE = Path.home() / ".meridian" / "mlx_lm_server.pid"
|
|
276
276
|
|
|
277
|
+
# Sentinel returned by select_mlx_model_id() when Apple Intelligence is chosen.
|
|
278
|
+
APPLE_INTELLIGENCE_ID = "apple-intelligence"
|
|
279
|
+
|
|
277
280
|
|
|
278
281
|
def _metal_headroom_gb() -> tuple[float, str]:
|
|
279
282
|
"""Primary memory signal — headroom within Metal's recommended working set.
|
|
@@ -890,6 +893,9 @@ def select_mlx_model_id(
|
|
|
890
893
|
span.set_attribute("llm.selected_model", preferred_hf_id or "")
|
|
891
894
|
return preferred_hf_id
|
|
892
895
|
|
|
896
|
+
macos_major = int(platform.mac_ver()[0].split(".")[0] or "0")
|
|
897
|
+
apple_intelligence = macos_major >= 26
|
|
898
|
+
|
|
893
899
|
try:
|
|
894
900
|
snap = probe_compute()
|
|
895
901
|
except Exception as exc: # noqa: BLE001
|
|
@@ -928,13 +934,24 @@ def select_mlx_model_id(
|
|
|
928
934
|
return preferred_hf_id
|
|
929
935
|
|
|
930
936
|
# 2. Largest catalog model that BOTH fits the budget AND is already in
|
|
931
|
-
# the HF cache.
|
|
932
|
-
#
|
|
933
|
-
#
|
|
934
|
-
#
|
|
935
|
-
# here is already thermal-capped
|
|
937
|
+
# the HF cache. Apple Intelligence (apple_fm, min_ram=0) is always
|
|
938
|
+
# "available" on supported machines — no HF cache check needed.
|
|
939
|
+
# Gating MLX entries on the cache keeps "dynamic" meaning "best
|
|
940
|
+
# among what's present" — never a surprise multi-GB download on
|
|
941
|
+
# constrained machines. The `budget` here is already thermal-capped.
|
|
936
942
|
for model_id, backend, min_ram, quality, hf_id in _MODELS:
|
|
937
|
-
if backend
|
|
943
|
+
if backend == "apple_fm":
|
|
944
|
+
if apple_intelligence:
|
|
945
|
+
span.set_attribute("llm.reason", "apple_intelligence_catalog")
|
|
946
|
+
span.set_attribute("llm.selected_model", APPLE_INTELLIGENCE_ID)
|
|
947
|
+
log.info(
|
|
948
|
+
"llm_selector: MLX in-process fallback=Apple Intelligence "
|
|
949
|
+
"(no cached MLX model fits budget=%.1f GB)",
|
|
950
|
+
budget,
|
|
951
|
+
)
|
|
952
|
+
return APPLE_INTELLIGENCE_ID
|
|
953
|
+
continue
|
|
954
|
+
if min_ram > budget:
|
|
938
955
|
continue
|
|
939
956
|
if not _hf_model_cached(hf_id):
|
|
940
957
|
log.debug(
|
|
@@ -951,9 +968,9 @@ def select_mlx_model_id(
|
|
|
951
968
|
)
|
|
952
969
|
return hf_id
|
|
953
970
|
|
|
954
|
-
# 3. Nothing cached fits
|
|
955
|
-
#
|
|
956
|
-
# behaviour
|
|
971
|
+
# 3. Nothing cached fits and Apple Intelligence is unavailable (macOS < 26) —
|
|
972
|
+
# best effort with the preferred id. (This preserves the pre-existing
|
|
973
|
+
# single-model behaviour on older macOS; the load may trigger a download.)
|
|
957
974
|
span.set_attribute("llm.reason", "nothing_cached_fits_use_preferred")
|
|
958
975
|
span.set_attribute("llm.selected_model", preferred_hf_id or "")
|
|
959
976
|
log.warning(
|
|
@@ -87,15 +87,20 @@ def _resolve_model_id() -> str:
|
|
|
87
87
|
return _MLX_MODEL_ID
|
|
88
88
|
|
|
89
89
|
try:
|
|
90
|
-
from agents.llm_selector import
|
|
90
|
+
from agents.llm_selector import (
|
|
91
|
+
APPLE_INTELLIGENCE_ID, resolve_model, select_mlx_model_id,
|
|
92
|
+
)
|
|
91
93
|
entry = resolve_model(_DEFAULT_MLX_MODEL_ID)
|
|
92
94
|
preferred_min_ram = (
|
|
93
95
|
entry["min_ram_gb"] if entry else _DEFAULT_MLX_MODEL_MIN_RAM_GB
|
|
94
96
|
)
|
|
95
|
-
|
|
97
|
+
selected = select_mlx_model_id(
|
|
96
98
|
preferred_hf_id=_DEFAULT_MLX_MODEL_ID,
|
|
97
99
|
preferred_min_ram_gb=preferred_min_ram,
|
|
98
|
-
)
|
|
100
|
+
)
|
|
101
|
+
# Propagate the Apple Intelligence sentinel as-is; fall back to the
|
|
102
|
+
# default MLX model only when nothing at all was selected (None).
|
|
103
|
+
_MLX_MODEL_ID = selected if selected is not None else _DEFAULT_MLX_MODEL_ID
|
|
99
104
|
except Exception as exc: # noqa: BLE001
|
|
100
105
|
log.warning(
|
|
101
106
|
"run_task_linker_mlx: dynamic model selection failed (%s) — "
|
|
@@ -267,6 +272,44 @@ def _get_model() -> Any:
|
|
|
267
272
|
return outlines_model
|
|
268
273
|
|
|
269
274
|
|
|
275
|
+
def _classify_apple_fm(messages: list[dict[str, str]]) -> "SessionClassification":
|
|
276
|
+
"""Classify via Apple Foundation Models (non-FSM, JSON parsing with one retry)."""
|
|
277
|
+
import asyncio
|
|
278
|
+
|
|
279
|
+
from apple_fm_sdk import LanguageModelSession # type: ignore[import]
|
|
280
|
+
|
|
281
|
+
system = next((m["content"] for m in messages if m["role"] == "system"), "")
|
|
282
|
+
user = next((m["content"] for m in messages if m["role"] == "user"), "")
|
|
283
|
+
user_with_hint = (
|
|
284
|
+
user
|
|
285
|
+
+ "\n\nRespond with a JSON object matching the SessionClassification schema. "
|
|
286
|
+
"Output only valid JSON — no markdown fences, no extra text."
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
async def _run(prompt: str) -> str:
|
|
290
|
+
session = LanguageModelSession(instructions=system)
|
|
291
|
+
r = await session.respond(prompt)
|
|
292
|
+
return getattr(r, "content", r)
|
|
293
|
+
|
|
294
|
+
raw = asyncio.run(_run(user_with_hint))
|
|
295
|
+
try:
|
|
296
|
+
text = raw.strip()
|
|
297
|
+
if text.startswith("```"):
|
|
298
|
+
text = text.split("\n", 1)[1].rsplit("```", 1)[0].strip()
|
|
299
|
+
return SessionClassification.model_validate_json(text)
|
|
300
|
+
except Exception:
|
|
301
|
+
# One retry: ask the model to fix the JSON it produced.
|
|
302
|
+
fix_prompt = (
|
|
303
|
+
"The JSON you produced was invalid. Fix it and return only valid JSON:\n"
|
|
304
|
+
+ raw
|
|
305
|
+
)
|
|
306
|
+
raw2 = asyncio.run(_run(fix_prompt))
|
|
307
|
+
text2 = raw2.strip()
|
|
308
|
+
if text2.startswith("```"):
|
|
309
|
+
text2 = text2.split("\n", 1)[1].rsplit("```", 1)[0].strip()
|
|
310
|
+
return SessionClassification.model_validate_json(text2)
|
|
311
|
+
|
|
312
|
+
|
|
270
313
|
# ---------------------------------------------------------------------------
|
|
271
314
|
# DB helpers
|
|
272
315
|
# ---------------------------------------------------------------------------
|
|
@@ -424,25 +467,39 @@ def _classify_one(
|
|
|
424
467
|
# ── llm_inference ─────────────────────────────────────────────────────────
|
|
425
468
|
t0 = time.time()
|
|
426
469
|
with tracer.start_as_current_span("llm_inference") as llm_span:
|
|
427
|
-
|
|
470
|
+
model_id = _resolve_model_id()
|
|
471
|
+
llm_span.set_attribute("model", model_id)
|
|
428
472
|
llm_span.set_attribute("max_tokens", _MAX_TOKENS)
|
|
429
473
|
llm_span.set_attribute("temperature", _TEMPERATURE)
|
|
430
474
|
llm_span.add_event("inference_started", {"session_id": session_id})
|
|
475
|
+
|
|
476
|
+
# Apple Intelligence path — no in-process MLX model; JSON parsing with retry.
|
|
431
477
|
try:
|
|
432
|
-
from
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
478
|
+
from agents.llm_selector import APPLE_INTELLIGENCE_ID
|
|
479
|
+
_use_apple_fm = model_id == APPLE_INTELLIGENCE_ID
|
|
480
|
+
except Exception:
|
|
481
|
+
_use_apple_fm = False
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
if _use_apple_fm:
|
|
485
|
+
result = _classify_apple_fm(messages)
|
|
486
|
+
raw = result.model_dump_json()
|
|
487
|
+
else:
|
|
488
|
+
from mlx_lm.sample_utils import make_sampler
|
|
489
|
+
from outlines.inputs import Chat
|
|
490
|
+
|
|
491
|
+
model = _get_model()
|
|
492
|
+
raw = model(
|
|
493
|
+
Chat(messages),
|
|
494
|
+
output_type=SessionClassification,
|
|
495
|
+
max_tokens=_MAX_TOKENS,
|
|
496
|
+
sampler=make_sampler(temp=_TEMPERATURE),
|
|
497
|
+
verbose=False,
|
|
498
|
+
)
|
|
443
499
|
except Exception as exc:
|
|
444
500
|
elapsed = time.time() - t0
|
|
445
|
-
|
|
501
|
+
outcome = "apple_fm_error" if _use_apple_fm else "mlx_error"
|
|
502
|
+
llm_span.set_attribute("outcome", outcome)
|
|
446
503
|
llm_span.set_attribute("elapsed_s", elapsed)
|
|
447
504
|
llm_span.set_status(StatusCode.ERROR, str(exc))
|
|
448
505
|
llm_span.add_event("inference_error", {
|
|
@@ -455,11 +512,12 @@ def _classify_one(
|
|
|
455
512
|
session_id, exc,
|
|
456
513
|
)
|
|
457
514
|
return _error_result(
|
|
458
|
-
session_id, f"
|
|
515
|
+
session_id, f"inference error: {exc}", elapsed, outcome
|
|
459
516
|
)
|
|
460
517
|
|
|
461
518
|
elapsed = time.time() - t0
|
|
462
|
-
|
|
519
|
+
outcome = "apple_fm" if _use_apple_fm else "mlx_direct"
|
|
520
|
+
llm_span.set_attribute("outcome", outcome)
|
|
463
521
|
llm_span.set_attribute("elapsed_s", elapsed)
|
|
464
522
|
llm_span.set_attribute("response_chars", len(raw))
|
|
465
523
|
llm_span.add_event("inference_complete", {
|
|
@@ -480,7 +538,9 @@ def _classify_one(
|
|
|
480
538
|
"preview": raw[:500],
|
|
481
539
|
})
|
|
482
540
|
|
|
483
|
-
#
|
|
541
|
+
# Both paths converge on a JSON string in `raw`; parse to SessionClassification.
|
|
542
|
+
# Apple FM already validated once inside _classify_apple_fm; re-parsing from
|
|
543
|
+
# model_dump_json() is a no-op that keeps the two paths uniform.
|
|
484
544
|
try:
|
|
485
545
|
result = SessionClassification.model_validate_json(raw)
|
|
486
546
|
except Exception as exc:
|
|
@@ -540,7 +600,7 @@ def _classify_one(
|
|
|
540
600
|
"category_explanation": result.category_explanation,
|
|
541
601
|
"session_type": result.session_type,
|
|
542
602
|
"reasoning": result.reasoning,
|
|
543
|
-
"method":
|
|
603
|
+
"method": outcome,
|
|
544
604
|
"dimensions": result.dimensions,
|
|
545
605
|
"session_summary": result.session_summary,
|
|
546
606
|
"elapsed_s": elapsed,
|
|
@@ -632,6 +692,21 @@ def main() -> None:
|
|
|
632
692
|
log.error("run_task_linker_mlx: meridian_db path is empty")
|
|
633
693
|
sys.exit(1)
|
|
634
694
|
|
|
695
|
+
# Canonicalize and restrict to ~/.meridian/ to prevent path traversal.
|
|
696
|
+
try:
|
|
697
|
+
canonical = Path(db_path).expanduser().resolve()
|
|
698
|
+
except (OSError, ValueError) as exc:
|
|
699
|
+
log.error("run_task_linker_mlx: invalid db path: %s", exc)
|
|
700
|
+
sys.exit(1)
|
|
701
|
+
allowed_root = Path.home() / ".meridian"
|
|
702
|
+
if not str(canonical).startswith(str(allowed_root) + "/") and canonical != allowed_root:
|
|
703
|
+
log.error(
|
|
704
|
+
"run_task_linker_mlx: db path %s is outside allowed directory %s",
|
|
705
|
+
canonical, allowed_root,
|
|
706
|
+
)
|
|
707
|
+
sys.exit(1)
|
|
708
|
+
db_path = str(canonical)
|
|
709
|
+
|
|
635
710
|
if not Path(db_path).exists():
|
|
636
711
|
log.error("run_task_linker_mlx: db file does not exist: %s", db_path)
|
|
637
712
|
sys.exit(1)
|
|
@@ -40,7 +40,7 @@ os.environ.setdefault("HERMES_HOME", str(_SERVICES_DIR / ".hermes"))
|
|
|
40
40
|
import opentelemetry.context as _otel_context
|
|
41
41
|
from fastapi import FastAPI, HTTPException
|
|
42
42
|
from opentelemetry import trace
|
|
43
|
-
from pydantic import BaseModel
|
|
43
|
+
from pydantic import BaseModel, Field
|
|
44
44
|
|
|
45
45
|
from agents import observability
|
|
46
46
|
|
|
@@ -59,12 +59,16 @@ _app_state: dict[str, Any] = {}
|
|
|
59
59
|
async def _lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
60
60
|
if _app_state.get("backend") == "mlx":
|
|
61
61
|
import datetime
|
|
62
|
-
log.info("server: loading MLX model at startup…")
|
|
63
62
|
import agents.run_task_linker_mlx as _mlx
|
|
64
|
-
_mlx._get_model()
|
|
65
63
|
_app_state["mlx_module"] = _mlx
|
|
66
64
|
_app_state["loaded_at"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
67
|
-
|
|
65
|
+
from agents.llm_selector import APPLE_INTELLIGENCE_ID
|
|
66
|
+
if _mlx._resolve_model_id() == APPLE_INTELLIGENCE_ID:
|
|
67
|
+
log.info("server: 8 GB machine — Apple Intelligence backend, no MLX model to pre-load")
|
|
68
|
+
else:
|
|
69
|
+
log.info("server: loading MLX model at startup…")
|
|
70
|
+
_mlx._get_model()
|
|
71
|
+
log.info("server: MLX model ready")
|
|
68
72
|
yield
|
|
69
73
|
|
|
70
74
|
|
|
@@ -148,8 +152,11 @@ async def chat(req: ChatRequest) -> ChatResponse:
|
|
|
148
152
|
# MLX backend — direct in-process inference, model pre-loaded at startup
|
|
149
153
|
# ---------------------------------------------------------------------------
|
|
150
154
|
|
|
155
|
+
_MAX_INPUT_CHARS = 128_000 # ~32k tokens; hard ceiling to prevent resource exhaustion
|
|
156
|
+
|
|
157
|
+
|
|
151
158
|
class ClassifyRequest(BaseModel):
|
|
152
|
-
input: str # fully-formatted user_message string
|
|
159
|
+
input: str = Field(..., max_length=_MAX_INPUT_CHARS) # fully-formatted user_message string
|
|
153
160
|
|
|
154
161
|
|
|
155
162
|
class ClassifyResponse(BaseModel):
|
|
@@ -341,7 +348,7 @@ class _OAIChatRequest(BaseModel):
|
|
|
341
348
|
model: str | None = None
|
|
342
349
|
messages: list[_OAIMessage]
|
|
343
350
|
temperature: float | None = None
|
|
344
|
-
max_tokens: int | None = None
|
|
351
|
+
max_tokens: int | None = Field(None, ge=1, le=8192)
|
|
345
352
|
top_p: float | None = None
|
|
346
353
|
stop: list[str] | str | None = None
|
|
347
354
|
stream: bool = False
|
|
@@ -452,9 +459,9 @@ async def openai_chat_completions(req: _OAIChatRequest) -> dict:
|
|
|
452
459
|
|
|
453
460
|
|
|
454
461
|
class _SummariseRequest(BaseModel):
|
|
455
|
-
transcript: str
|
|
462
|
+
transcript: str = Field(..., max_length=_MAX_INPUT_CHARS)
|
|
456
463
|
system: str | None = None
|
|
457
|
-
max_tokens: int = 2048
|
|
464
|
+
max_tokens: int = Field(2048, ge=1, le=8192)
|
|
458
465
|
temperature: float = 0.2
|
|
459
466
|
|
|
460
467
|
|
package/services/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "meridian-agents"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.23.0"
|
|
8
8
|
description = "Meridian agents — hermes task linking and Jira progress updates for meridian.db"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
authors = [{ name = "Meridiona" }]
|
|
@@ -53,7 +53,9 @@ meridian-server = "agents.server:main"
|
|
|
53
53
|
|
|
54
54
|
[tool.uv]
|
|
55
55
|
# Lock all extras so `uv lock` produces a complete, reproducible uv.lock.
|
|
56
|
-
#
|
|
56
|
+
# The shipped venv installs BOTH server-side extras — `uv sync --extra mlx
|
|
57
|
+
# --extra pm_worklog_update` — because the one MLX server serves
|
|
58
|
+
# /classify_sessions (mlx) AND /synthesise_worklog (agno, pm_worklog_update).
|
|
57
59
|
constraint-dependencies = []
|
|
58
60
|
|
|
59
61
|
[tool.setuptools.packages.find]
|
|
@@ -66,36 +66,32 @@ with open(settings_path) as f:
|
|
|
66
66
|
hooks = settings.setdefault("hooks", {})
|
|
67
67
|
session_end = hooks.setdefault("SessionEnd", [])
|
|
68
68
|
|
|
69
|
-
# SessionEnd doesn't support `matcher` — each entry is just a list of
|
|
70
|
-
# hooks. We use an unmatched group whose first command carries our
|
|
71
|
-
# marker as a comment so we can find + replace it later.
|
|
72
69
|
new_entry = {
|
|
73
70
|
"hooks": [
|
|
74
71
|
{
|
|
75
72
|
"type": "command",
|
|
76
73
|
"command": hook_cmd,
|
|
77
|
-
# Claude Code reads `timeout` in milliseconds
|
|
78
|
-
# existing entries already in this settings.json). 30s ceiling
|
|
74
|
+
# Claude Code reads `timeout` in milliseconds. 30s ceiling
|
|
79
75
|
# — the hook itself returns in <100 ms.
|
|
80
76
|
"timeout": 30000,
|
|
81
|
-
# Comment field that Claude Code preserves but doesn't act on
|
|
82
|
-
# — gives us a robust idempotency / uninstall handle.
|
|
83
|
-
"_meridian": marker,
|
|
84
77
|
}
|
|
85
78
|
]
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
# Replace any prior meridian entry
|
|
81
|
+
# Replace any prior meridian entry by matching on the command substring
|
|
82
|
+
# "coding-agent-hook". Claude Code strips unknown JSON fields (like the
|
|
83
|
+
# former "_meridian" marker) on every save, so command-string matching
|
|
84
|
+
# is the only reliable idempotency mechanism.
|
|
89
85
|
filtered = []
|
|
90
86
|
removed = 0
|
|
91
87
|
for group in session_end:
|
|
92
|
-
is_ours =
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
is_ours = any(
|
|
89
|
+
"coding-agent-hook" in h.get("command", "")
|
|
90
|
+
for h in group.get("hooks", [])
|
|
91
|
+
)
|
|
92
|
+
if is_ours:
|
|
93
|
+
removed += 1
|
|
94
|
+
else:
|
|
99
95
|
filtered.append(group)
|
|
100
96
|
|
|
101
97
|
filtered.append(new_entry)
|
package/ui.tar.gz
CHANGED
|
Binary file
|