firekeep-client 1.4.2__tar.gz → 1.5.2__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.
- {firekeep_client-1.4.2/firekeep_client.egg-info → firekeep_client-1.5.2}/PKG-INFO +1 -1
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/__init__.py +1 -1
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/cli.py +59 -1
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/contract/matrix.py +41 -5
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/decision/server.py +17 -1
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/gateway.py +59 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooks/__main__.py +80 -4
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooks/prompt.py +9 -2
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/promptrecall.py +22 -5
- {firekeep_client-1.4.2 → firekeep_client-1.5.2/firekeep_client.egg-info}/PKG-INFO +1 -1
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client.egg-info/SOURCES.txt +5 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/pyproject.toml +1 -1
- firekeep_client-1.5.2/tests/test_cli_doctor_report.py +216 -0
- firekeep_client-1.5.2/tests/test_decision_poll_ceiling.py +105 -0
- firekeep_client-1.5.2/tests/test_gateway_schema_slimming.py +194 -0
- firekeep_client-1.5.2/tests/test_install_skill_contract.py +344 -0
- firekeep_client-1.5.2/tests/test_instruction_budget.py +113 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_package.py +21 -1
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/LICENSE +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/NOTICE +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/README.md +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/adapters/__init__.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/adapters/base.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/adapters/claude.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/adapters/claude_desktop.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/adapters/codex.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/adapters/generic.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/adapters/kiro.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/adapters/opencode.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/autoupdate.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/backups.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/config_write.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/connect.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/contract/__init__.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/decision/__init__.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/decision/board.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/dexes.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/docdexsync.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooklog.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooks/__init__.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooks/_git.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooks/_mcp.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooks/destructive.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooks/post_tool.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooks/pre_tool.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooks/precompact.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooks/runbooks.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooks/session_end.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooks/session_start.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/hooks/stop.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/join.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/joincode.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/maildexsync.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/migrate.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/nightshift.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/pathenv.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/resolver.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/serverinit.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/shim.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/sidecar.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/signing.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/state.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/stdio.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/symdexindex.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/transport.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/updater.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/wizard.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client/worktree_snapshot.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client.egg-info/dependency_links.txt +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client.egg-info/entry_points.txt +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client.egg-info/requires.txt +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/firekeep_client.egg-info/top_level.txt +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/setup.cfg +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_attribution_headers.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_autoupdate.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_bash_layer_retired.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_bootstrap_docdex.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_bootstrap_maildex.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_bootstrap_ps1.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_bootstrap_reinstall.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_bootstrap_sh.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_bootstrap_symdex.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_bootstrap_venv_provisioning.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_ci_publishes_symdex.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_backup.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_dex.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_docdex.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_doctor.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_doctor_generic.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_entry.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_generic_install.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_install.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_maildex.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_onboarding.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_personal.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_uninstall.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_uninstall_generic.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_cli_update.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_config_migration.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_connect.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_decision_board_html.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_decision_bypass.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_decision_registration.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_decision_server.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_dexes.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_docdexsync.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_docs_reference_client_kit.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_doctor_credential_expiry.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_doctor_embeddings.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_doctor_no_server.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_e2e_bootstrap.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_gateway.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_gateway_toolset.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_generic_config.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_hooklog.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_import_boundary.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_init_self_enroll.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_init_stale_bundle.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_join.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_join_config_write.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_joincode.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_kit_smoke.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_maildexsync.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_make_release.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_memory_instructions.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_migration_entrypoints.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_nightshift.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_pathenv.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_promptrecall.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_resolver.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_resolver_bypass.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_resolver_single_connection.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_runbook_match_parity.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_runbook_state.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_scratch_ttl.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_serverinit.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_shim_bridge.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_shim_bypass.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_shim_fail_loud.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_shim_identity.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_shim_recovery.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_shim_skeleton.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_sidecar.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_sidecar_robustness.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_signing.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_state.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_stdio_encoding.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_symdexindex.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_transport.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_transport_sse.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_twin_removed.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_updater.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_wizard.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_wizard_generic.py +0 -0
- {firekeep_client-1.4.2 → firekeep_client-1.5.2}/tests/test_worktree_snapshot.py +0 -0
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import argparse
|
|
9
9
|
import contextlib
|
|
10
|
+
import http.client
|
|
10
11
|
import os
|
|
11
12
|
import re
|
|
12
13
|
import shutil
|
|
@@ -41,7 +42,7 @@ from firekeep_client.adapters.base import (
|
|
|
41
42
|
rendered_instructions_path,
|
|
42
43
|
)
|
|
43
44
|
from firekeep_client.adapters.codex import mcp_block_is_current
|
|
44
|
-
from firekeep_client.transport import get_json, TransportError
|
|
45
|
+
from firekeep_client.transport import get_json, post_json, TransportError
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def _config_path() -> Path:
|
|
@@ -1701,6 +1702,51 @@ def run_doctor(cfg=None) -> list[tuple[str, str, str]]:
|
|
|
1701
1702
|
return results
|
|
1702
1703
|
|
|
1703
1704
|
|
|
1705
|
+
# --- doctor --report: opt-in, per-invocation, redacted -----------------------
|
|
1706
|
+
#
|
|
1707
|
+
# Design record: firekeep.ai/privacy.html discloses this exact mechanism.
|
|
1708
|
+
# There is deliberately NO persisted config toggle (no [telemetry] section) —
|
|
1709
|
+
# every send is one explicit act (typing --report on this one command), never
|
|
1710
|
+
# a standing "always send" setting that could be flipped once and forgotten.
|
|
1711
|
+
# Nothing about doctor's automatic behavior changes; without the flag this
|
|
1712
|
+
# code path is never reached and no network call to firekeep.ai happens.
|
|
1713
|
+
DOCTOR_REPORT_URL = "https://firekeep.ai/doctor-report.php"
|
|
1714
|
+
|
|
1715
|
+
|
|
1716
|
+
def _redact_for_report(results: list[tuple[str, str, str]]) -> dict:
|
|
1717
|
+
"""Check name + status ONLY. The third tuple element — `detail`, the
|
|
1718
|
+
human-readable message doctor prints locally — is dropped entirely and
|
|
1719
|
+
never touches this function's return value. `detail` is where paths,
|
|
1720
|
+
hostnames, and config contents live (see e.g. `_check_health`'s
|
|
1721
|
+
`f"{_ep_url(svc, cfg)}: {exc}"` and `_check_config_perms`'s literal path),
|
|
1722
|
+
so the redaction is structural — no field to forget to strip — rather
|
|
1723
|
+
than a scrub applied after the fact."""
|
|
1724
|
+
return {
|
|
1725
|
+
"client_version": __version__,
|
|
1726
|
+
"checks": [{"id": name, "status": status} for name, status, _detail in results],
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
def _send_doctor_report(results: list[tuple[str, str, str]]) -> str:
|
|
1731
|
+
"""POST the redacted summary; return one line describing what happened.
|
|
1732
|
+
Never raises — a failed report must never affect doctor's own exit code
|
|
1733
|
+
or hide the check results already printed above it."""
|
|
1734
|
+
body = _redact_for_report(results)
|
|
1735
|
+
try:
|
|
1736
|
+
# No auth headers: this is an anonymous, unauthenticated report to a
|
|
1737
|
+
# public collection endpoint, not a call to the user's own server.
|
|
1738
|
+
post_json(DOCTOR_REPORT_URL, body, headers={})
|
|
1739
|
+
return "firekeep: anonymous report sent (check names + status only — see firekeep.ai/privacy.html)"
|
|
1740
|
+
except (TransportError, OSError, http.client.HTTPException) as exc:
|
|
1741
|
+
# http.client.HTTPException (IncompleteRead, BadStatusLine, ...) is
|
|
1742
|
+
# NOT an OSError -- it's raised deep inside urlopen()'s response read
|
|
1743
|
+
# (transport._request), and without this clause a truncated response
|
|
1744
|
+
# from a hostile proxy or captive portal would escape as an
|
|
1745
|
+
# unhandled traceback AFTER doctor's own results already printed,
|
|
1746
|
+
# discarding the exit code the fail rows above earned.
|
|
1747
|
+
return f"firekeep: report NOT sent ({exc}) — doctor results above are unaffected"
|
|
1748
|
+
|
|
1749
|
+
|
|
1704
1750
|
def cmd_restore(args) -> int:
|
|
1705
1751
|
"""Browse or restore local snapshots of uncommitted work.
|
|
1706
1752
|
|
|
@@ -2480,6 +2526,8 @@ def cmd_doctor(args) -> int:
|
|
|
2480
2526
|
hint = _generic_hint()
|
|
2481
2527
|
if hint is not None:
|
|
2482
2528
|
print(hint)
|
|
2529
|
+
if getattr(args, "report", False):
|
|
2530
|
+
print(_send_doctor_report(results))
|
|
2483
2531
|
return rc
|
|
2484
2532
|
|
|
2485
2533
|
|
|
@@ -2909,6 +2957,16 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
2909
2957
|
"doctor", aliases=["status"],
|
|
2910
2958
|
help="preflight health / skew / perm checks",
|
|
2911
2959
|
)
|
|
2960
|
+
# Opt-in, per-invocation, no persisted setting — see cmd_doctor and
|
|
2961
|
+
# _redact_for_report. Off unless this exact flag is typed; doctor's own
|
|
2962
|
+
# output and exit code are unaffected either way.
|
|
2963
|
+
doc.add_argument(
|
|
2964
|
+
"--report", action="store_true",
|
|
2965
|
+
help="also send an ANONYMOUS report (check names + pass/warn/fail "
|
|
2966
|
+
"status and your client version only — never messages, paths, "
|
|
2967
|
+
"or hostnames) to firekeep.ai. Off by default; see "
|
|
2968
|
+
"firekeep.ai/privacy.html.",
|
|
2969
|
+
)
|
|
2912
2970
|
doc.set_defaults(func=cmd_doctor)
|
|
2913
2971
|
|
|
2914
2972
|
shift = sub.add_parser(
|
|
@@ -76,12 +76,47 @@ def _precompact_claude(hooks: tuple[tuple[str, str, str | None, int], ...]) -> s
|
|
|
76
76
|
# (firekeep_client/adapters/opencode.py). VALIDATED live on opencode 1.14.22
|
|
77
77
|
# (2026-07-18, docs/OPENCODE-VALIDATION.md): pre-edit block is a HARD gate (the
|
|
78
78
|
# write tool aborted with the policy reason), prompt-core inbox surfaced, stop
|
|
79
|
-
# fired on session.deleted.
|
|
79
|
+
# fired on session.deleted. Caveat: session.created publishes before plugins
|
|
80
80
|
# subscribe in `run` mode (bridge fires session_start from its first hook
|
|
81
|
-
# instead)
|
|
82
|
-
#
|
|
81
|
+
# instead).
|
|
82
|
+
#
|
|
83
|
+
# CORRECTION (2026-08-21) — this comment used to say opencode's briefing lands
|
|
84
|
+
# in the console "NOT the model context" BECAUSE it has no systemMessage
|
|
85
|
+
# channel. That reasoning was wrong, and the rows below inherited the error.
|
|
86
|
+
#
|
|
87
|
+
# Firekeep's dict hook cores all return {"systemMessage": ...}. On Claude Code
|
|
88
|
+
# `systemMessage` is shown to the HUMAN; the channel that reaches the model is
|
|
89
|
+
# `hookSpecificOutput.additionalContext`, which appears nowhere in this client.
|
|
90
|
+
# Measured in one session by comparing every SessionStart hook attachment: the
|
|
91
|
+
# three emitting `additionalContext` were verbatim in the model's context, the
|
|
92
|
+
# two emitting `systemMessage` (Firekeep's pre-flight briefing, the symdex
|
|
93
|
+
# banner) were absent. Claude Code's own docs are explicit — "To surface a
|
|
94
|
+
# message to the user on any platform, return systemMessage" — while for
|
|
95
|
+
# SessionStart and UserPromptSubmit "Claude Code adds plain-text stdout as
|
|
96
|
+
# context that Claude can see and act on".
|
|
97
|
+
#
|
|
98
|
+
# FIXED in the same series: hooks/__main__.py now emits BOTH channels for the
|
|
99
|
+
# two events Claude Code documents as accepting model-facing context —
|
|
100
|
+
# session_start (SessionStart) and prompt (UserPromptSubmit). Verified on the
|
|
101
|
+
# real rendered command line: hookEventName SessionStart, 2,265 characters of
|
|
102
|
+
# briefing in additionalContext, systemMessage retained so the human still sees
|
|
103
|
+
# it. The claude cells below are therefore true again — but they were wrong for
|
|
104
|
+
# as long as this comment's first half describes, which is why the history
|
|
105
|
+
# stays here rather than being tidied away.
|
|
106
|
+
#
|
|
107
|
+
# Still human-only, deliberately: stop, precompact and session_end. Claude Code
|
|
108
|
+
# does not document a model-facing channel for those events and nobody has
|
|
109
|
+
# measured one; emitting a plausible-looking shape at an event that ignores it
|
|
110
|
+
# would put the text back where it started while looking fixed.
|
|
111
|
+
#
|
|
112
|
+
# kiro stays marked unverified: its channel is a different mechanism
|
|
113
|
+
# (agentSpawn), nobody has run the same measurement on it, and the fix above is
|
|
114
|
+
# scoped to the claude runtime for exactly that reason. Do not assume it shares
|
|
115
|
+
# Claude Code's semantics; measure before changing the cell.
|
|
83
116
|
MATRIX: dict[str, dict[str, str]] = {
|
|
84
|
-
"briefing": {"claude": "hook",
|
|
117
|
+
"briefing": {"claude": "hook",
|
|
118
|
+
"kiro": "agentSpawn hook (delivery unverified)",
|
|
119
|
+
"codex": "manual/memory_recall",
|
|
85
120
|
"opencode": "plugin (first event, console log only)",
|
|
86
121
|
"claude-desktop": "none (MCP only)",
|
|
87
122
|
"generic": "none (MCP only)"},
|
|
@@ -95,7 +130,8 @@ MATRIX: dict[str, dict[str, str]] = {
|
|
|
95
130
|
# deciding whether opencode support is a wiring job or a protocol limit gets the
|
|
96
131
|
# answer from the cell. For all four non-claude/kiro runtimes the session-start
|
|
97
132
|
# briefing remains the only push.
|
|
98
|
-
"proactive_recall": {"claude": "per-prompt push",
|
|
133
|
+
"proactive_recall": {"claude": "per-prompt push",
|
|
134
|
+
"kiro": "per-prompt push (delivery unverified)",
|
|
99
135
|
"codex": "none (no hooks)", "opencode": "none (no prompt text)",
|
|
100
136
|
"claude-desktop": "none (no hooks)",
|
|
101
137
|
"generic": "none (no hooks)"},
|
|
@@ -56,7 +56,23 @@ _DEFAULT_SYNTH_TIMEOUT = 30.0
|
|
|
56
56
|
_INGEST_TIMEOUT_HEADROOM = 15.0
|
|
57
57
|
# Bounded long-poll ceiling — kept under the MCP tool-call ceiling so a single
|
|
58
58
|
# decision_board / decision_board_check call returns before the runtime times out.
|
|
59
|
-
|
|
59
|
+
#
|
|
60
|
+
# This is a TOKEN-COST setting as much as a latency one. Every expiry costs the
|
|
61
|
+
# agent one more model turn, and a model turn re-sends the whole conversation:
|
|
62
|
+
# measured 2026-08-21 across 5 sessions and 13 boards, 197 poll-only turns cost
|
|
63
|
+
# 93.5M raw token units / 9.8M price-weighted — ~49,800 weighted tokens per
|
|
64
|
+
# poll. Raising the ceiling is free when the human is responsive, because the
|
|
65
|
+
# loop below returns the instant an answer lands; it only ever bounds the
|
|
66
|
+
# UNANSWERED case.
|
|
67
|
+
#
|
|
68
|
+
# 50s, not more, because the ceiling has to survive the most constrained host
|
|
69
|
+
# rather than the one we happen to run on. Claude Code resolves a per-call
|
|
70
|
+
# timeout of ~27.8h for STDIO servers (the gateway's path) but 60s for REMOTE
|
|
71
|
+
# HTTP/SSE servers — which is the path deploy/chatgpt-tunnel/ takes. 60s is the
|
|
72
|
+
# tightest ceiling we can actually name, so this sits 10s under it.
|
|
73
|
+
# DECISION_POLL_SECONDS overrides for an operator who knows their host.
|
|
74
|
+
# Guarded by tests/test_decision_poll_ceiling.py.
|
|
75
|
+
_DEFAULT_POLL_SECONDS = 50.0
|
|
60
76
|
# Poll granularity — MUST be a real ``await anyio.sleep`` (never a blocking wait),
|
|
61
77
|
# so the event loop stays responsive.
|
|
62
78
|
_POLL_INTERVAL = 0.5
|
|
@@ -98,6 +98,60 @@ def _active_toolset() -> tuple[str | None, frozenset[str] | None]:
|
|
|
98
98
|
return preset, TOOLSET_PRESETS[preset]
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
def _slim_schema(schema: Any) -> Any:
|
|
102
|
+
"""Collapse Pydantic's ``X | None`` rendering into JSON Schema's type array.
|
|
103
|
+
|
|
104
|
+
Pydantic emits an optional field as
|
|
105
|
+
``{"anyOf": [{"type": "X"}, {"type": "null"}]}`` — 43 characters where
|
|
106
|
+
``{"type": ["X", "null"]}`` is 24, for the same meaning. Measured on the
|
|
107
|
+
live gateway (2026-08-21): 66 such fields across 98 tools. Tool definitions
|
|
108
|
+
sit in the cached prompt prefix, so on a runtime WITHOUT tool deferral that
|
|
109
|
+
is bytes re-sent every turn for the life of the session.
|
|
110
|
+
|
|
111
|
+
Only the exact two-branch ``[T, null]`` shape collapses, and only into the
|
|
112
|
+
type-array form. Dropping the null branch outright would be shorter and is
|
|
113
|
+
deliberately NOT done: it would make an explicit ``null`` invalid where the
|
|
114
|
+
server accepts it, which is a behaviour change wearing a token saving's
|
|
115
|
+
clothes. ``tests/test_gateway_schema_slimming.py`` proves the equivalence
|
|
116
|
+
against a real validator rather than asserting it.
|
|
117
|
+
|
|
118
|
+
Never raises. A backend may serve any shape it likes, and a schema this
|
|
119
|
+
cannot parse must reach the model unmodified rather than take the surface
|
|
120
|
+
down — the same failure isolation the rest of the gateway is built on.
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
return _slim(schema)
|
|
124
|
+
except Exception: # noqa: BLE001 - pass-through beats a broken tool surface
|
|
125
|
+
return schema
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _slim(node: Any) -> Any:
|
|
129
|
+
if isinstance(node, list):
|
|
130
|
+
return [_slim(item) for item in node]
|
|
131
|
+
if not isinstance(node, dict):
|
|
132
|
+
return node
|
|
133
|
+
|
|
134
|
+
branches = node.get("anyOf")
|
|
135
|
+
if isinstance(branches, list) and len(branches) == 2:
|
|
136
|
+
nulls = [b for b in branches if b == {"type": "null"}]
|
|
137
|
+
others = [b for b in branches if isinstance(b, dict) and b != {"type": "null"}]
|
|
138
|
+
# Exactly one null branch and one non-null branch whose own `type` is a
|
|
139
|
+
# plain string. Anything richer (a nested anyOf, a branch with no type,
|
|
140
|
+
# a $ref) is left alone: this collapse is only provably lossless for
|
|
141
|
+
# the simple shape, and "left alone" costs bytes, not correctness.
|
|
142
|
+
if len(nulls) == 1 and len(others) == 1 and isinstance(others[0].get("type"), str):
|
|
143
|
+
collapsed = {k: v for k, v in node.items() if k != "anyOf"}
|
|
144
|
+
# The branch's own keywords (items, additionalProperties, ...) come
|
|
145
|
+
# with it; the field's siblings (default, description) stay put.
|
|
146
|
+
for key, value in others[0].items():
|
|
147
|
+
if key != "type":
|
|
148
|
+
collapsed.setdefault(key, value)
|
|
149
|
+
collapsed["type"] = [others[0]["type"], "null"]
|
|
150
|
+
return {k: _slim(v) for k, v in collapsed.items()}
|
|
151
|
+
|
|
152
|
+
return {k: _slim(v) for k, v in node.items()}
|
|
153
|
+
|
|
154
|
+
|
|
101
155
|
def _console_script(name: str) -> str:
|
|
102
156
|
suffix = ".exe" if os.name == "nt" else ""
|
|
103
157
|
filename = f"{name}{suffix}"
|
|
@@ -288,6 +342,11 @@ class Gateway:
|
|
|
288
342
|
self.tools_filtered += 1
|
|
289
343
|
continue
|
|
290
344
|
self.routes[name] = backend
|
|
345
|
+
# Slim the ADVERTISED schema only. Routing, calls and the
|
|
346
|
+
# upstream's own validation are untouched: _slim_schema returns
|
|
347
|
+
# new objects and the backend's tool dict is left as served.
|
|
348
|
+
if isinstance(tool.get("inputSchema"), dict):
|
|
349
|
+
tool = {**tool, "inputSchema": _slim_schema(tool["inputSchema"])}
|
|
291
350
|
tools.append(tool)
|
|
292
351
|
return tools
|
|
293
352
|
|
|
@@ -70,6 +70,71 @@ _CORE_MODULES = {
|
|
|
70
70
|
_INT_CORES = frozenset({"pre_tool", "post_tool"})
|
|
71
71
|
_DICT_CORES = frozenset({"session_start", "stop", "session_end", "prompt", "precompact"})
|
|
72
72
|
|
|
73
|
+
# Cores whose text must reach the MODEL, and the Claude Code event name that
|
|
74
|
+
# carries it.
|
|
75
|
+
#
|
|
76
|
+
# `systemMessage` is shown to the human; the channel that reaches the model is
|
|
77
|
+
# `hookSpecificOutput.additionalContext`. Measured 2026-08-21 by comparing every
|
|
78
|
+
# SessionStart hook attachment in one session: the three hooks emitting
|
|
79
|
+
# additionalContext were verbatim in the model's context, the two emitting
|
|
80
|
+
# systemMessage (Firekeep's pre-flight briefing, the symdex banner) were absent.
|
|
81
|
+
# Claude Code's docs say the same thing from the other side — "To surface a
|
|
82
|
+
# message to the user on any platform, return systemMessage", against "for
|
|
83
|
+
# SessionStart and UserPromptSubmit Claude Code adds plain-text stdout as
|
|
84
|
+
# context that Claude can see and act on".
|
|
85
|
+
#
|
|
86
|
+
# The text was always written for the model: the briefing opens "You are
|
|
87
|
+
# agent-...", proactive recall pushes memories for the agent to act on, and the
|
|
88
|
+
# bypass notice says "you should NOT use firekeep_* tools" — an instruction a
|
|
89
|
+
# human reader cannot follow. All of it was landing in the terminal.
|
|
90
|
+
#
|
|
91
|
+
# ONLY these two events. `stop`, `precompact` and `session_end` are absent
|
|
92
|
+
# because nobody has verified those events accept model-facing context, and
|
|
93
|
+
# emitting a plausible-looking shape at an event that ignores it would leave the
|
|
94
|
+
# text exactly where it started while looking fixed — which is how this bug
|
|
95
|
+
# happened the first time.
|
|
96
|
+
_MODEL_CONTEXT_EVENTS = {
|
|
97
|
+
"session_start": "SessionStart",
|
|
98
|
+
"prompt": "UserPromptSubmit",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# ONLY the runtime whose channel has actually been measured. kiro, opencode and
|
|
102
|
+
# the rest use different mechanisms; handing them a Claude Code-shaped payload
|
|
103
|
+
# would be the same guess in a new place. An OLD rendered hook command carries
|
|
104
|
+
# no `--runtime` at all and must keep working unchanged.
|
|
105
|
+
_MODEL_CONTEXT_RUNTIMES = frozenset({"claude"})
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _with_model_context(result: dict, core_name: str, runtime: str | None) -> dict:
|
|
109
|
+
"""Add the model-facing channel alongside the human-facing one.
|
|
110
|
+
|
|
111
|
+
Dual channel on purpose: only `additionalContext` enters the context window,
|
|
112
|
+
so keeping `systemMessage` costs no model tokens and is what lets the human
|
|
113
|
+
see what their agent was just handed.
|
|
114
|
+
|
|
115
|
+
Never raises — a hook that cannot decorate its output must still emit it.
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
event = _MODEL_CONTEXT_EVENTS.get(core_name)
|
|
119
|
+
if not event or runtime not in _MODEL_CONTEXT_RUNTIMES:
|
|
120
|
+
return result
|
|
121
|
+
if not isinstance(result, dict) or result.get("hookSpecificOutput"):
|
|
122
|
+
return result
|
|
123
|
+
text = result.get("systemMessage")
|
|
124
|
+
# Only promote text destined for a reader. A core returning control
|
|
125
|
+
# fields (decision/reason) is not describing something to inject.
|
|
126
|
+
if not isinstance(text, str) or not text.strip():
|
|
127
|
+
return result
|
|
128
|
+
return {
|
|
129
|
+
**result,
|
|
130
|
+
"hookSpecificOutput": {
|
|
131
|
+
"hookEventName": event,
|
|
132
|
+
"additionalContext": text,
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
except Exception: # noqa: BLE001 — decoration must never break a hook
|
|
136
|
+
return result
|
|
137
|
+
|
|
73
138
|
# Cores that must run even while personal mode is ON, because they self-handle
|
|
74
139
|
# bypass and own end-of-session cleanup: `stop` clears the personal marker
|
|
75
140
|
# itself, and `session_end` must be free to decline comms without the dispatcher
|
|
@@ -191,6 +256,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
191
256
|
# the resolver seam carries the X-Firekeep-* attribution headers
|
|
192
257
|
# (resolver._runtime_attribution). Best-effort — attribution must never be
|
|
193
258
|
# able to break a hook.
|
|
259
|
+
runtime: str | None = None
|
|
194
260
|
try:
|
|
195
261
|
runtime = _parse_runtime(argv[1:])
|
|
196
262
|
if runtime:
|
|
@@ -198,6 +264,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
198
264
|
except Exception as e: # noqa: BLE001
|
|
199
265
|
hooklog.log_failure(core_name, f"runtime flag parse failed: {e!r}")
|
|
200
266
|
|
|
267
|
+
def _emit(result: dict) -> None:
|
|
268
|
+
"""The ONE place a dict core's output reaches stdout.
|
|
269
|
+
|
|
270
|
+
Routed through _with_model_context so every message a core produces —
|
|
271
|
+
briefing, recall push, personal-mode notice, migration refusal — takes
|
|
272
|
+
the same channel decision. Four separate print sites is how one of them
|
|
273
|
+
silently keeps the old behaviour.
|
|
274
|
+
"""
|
|
275
|
+
print(json.dumps(_with_model_context(result, core_name, runtime)))
|
|
276
|
+
|
|
201
277
|
# `/personal` as chat text: intercepted BEFORE the bypass gate below — while
|
|
202
278
|
# personal mode is ON the prompt core is short-circuited, so an in-core
|
|
203
279
|
# intercept could never toggle OFF again.
|
|
@@ -206,7 +282,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
206
282
|
payload = _read_payload(core_name)
|
|
207
283
|
personal_msg = _personal_text_command(payload)
|
|
208
284
|
if personal_msg is not None:
|
|
209
|
-
|
|
285
|
+
_emit({"systemMessage": personal_msg})
|
|
210
286
|
return 0
|
|
211
287
|
|
|
212
288
|
# Personal / bypass mode: Firekeep goes dormant for this session. Checked LIVE
|
|
@@ -219,7 +295,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
219
295
|
if core_name not in _BYPASS_EXEMPT and resolver.is_bypassed():
|
|
220
296
|
if core_name in _INT_CORES:
|
|
221
297
|
return 0 # allow the edit; no policy/gateway call reaches the server
|
|
222
|
-
|
|
298
|
+
_emit({"systemMessage": _BYPASS_MSG}) # session_start / prompt
|
|
223
299
|
return 0
|
|
224
300
|
except Exception as e: # noqa: BLE001 — a gate failure must never break the session
|
|
225
301
|
hooklog.log_failure(core_name, f"bypass check failed: {e!r}")
|
|
@@ -238,11 +314,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
238
314
|
|
|
239
315
|
# dict core (session_start/stop/prompt).
|
|
240
316
|
if result:
|
|
241
|
-
|
|
317
|
+
_emit(result)
|
|
242
318
|
return 0
|
|
243
319
|
except resolver.ConfigMigrationConflict as e:
|
|
244
320
|
hooklog.log_failure(core_name, f"config migration refused: {e}")
|
|
245
|
-
|
|
321
|
+
_emit({"systemMessage": str(e)})
|
|
246
322
|
return 0
|
|
247
323
|
except Exception as e: # noqa: BLE001 — the dispatcher itself must never raise.
|
|
248
324
|
hooklog.log_failure(core_name, f"dispatcher crashed: {e!r}")
|
|
@@ -57,7 +57,13 @@ def _dedup_lines(items: list[str]) -> list[str]:
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def _task_line(task: dict) -> str:
|
|
60
|
-
|
|
60
|
+
# The title is server-supplied free text with no length contract, so it is
|
|
61
|
+
# trimmed to promptrecall's shared line budget. The id and creator stay
|
|
62
|
+
# OUTSIDE the trim: a title over 200 chars is already a description, and the
|
|
63
|
+
# id is how the agent asks relay_task_list for the rest. Pointer, not payload.
|
|
64
|
+
title = promptrecall.trim_line(
|
|
65
|
+
task.get("title") or task.get("description") or task.get("id") or "task"
|
|
66
|
+
)
|
|
61
67
|
tid = str(task.get("id") or "")
|
|
62
68
|
creator = str(task.get("creator") or task.get("created_by") or "")
|
|
63
69
|
line = f"- {title}"
|
|
@@ -122,7 +128,8 @@ def run(payload: dict) -> dict:
|
|
|
122
128
|
if ts <= last_seen or str(m.get("sender") or "") == agent:
|
|
123
129
|
continue
|
|
124
130
|
sender = str(m.get("sender") or "?")
|
|
125
|
-
|
|
131
|
+
# Same contract as _task_line: trim the body, keep the sender.
|
|
132
|
+
content = promptrecall.trim_line(m.get("content"))
|
|
126
133
|
fresh.append(f"- {content} — {sender}")
|
|
127
134
|
if newest > last_seen:
|
|
128
135
|
state.write_scratch(seen_key, repr(newest))
|
|
@@ -181,15 +181,32 @@ def _source_id(source: dict) -> str:
|
|
|
181
181
|
return "sha:" + hashlib.sha256(text.encode("utf-8", "replace")).hexdigest()[:16]
|
|
182
182
|
|
|
183
183
|
|
|
184
|
+
def trim_line(text: object) -> str:
|
|
185
|
+
"""Collapse whitespace and cap at MAX_LINE_CHARS with a visible ellipsis.
|
|
186
|
+
|
|
187
|
+
Exported because the prompt hook's two older neighbours — relay task titles
|
|
188
|
+
and channel message bodies — render server-supplied strings into the SAME
|
|
189
|
+
systemMessage and had no cap at all: a hostile fixture drove that hook to
|
|
190
|
+
28,418 characters from one task and one message (measured 2026-08-21). They
|
|
191
|
+
share this function rather than re-declaring 200, so the budget stays one
|
|
192
|
+
number in one place.
|
|
193
|
+
|
|
194
|
+
Callers keep identifiers (task id, sender) OUTSIDE the trim: the point is to
|
|
195
|
+
hand over a pointer, not to lose the handle that fetches the full text.
|
|
196
|
+
"""
|
|
197
|
+
if not isinstance(text, str):
|
|
198
|
+
return ""
|
|
199
|
+
collapsed = " ".join(text.split())
|
|
200
|
+
if len(collapsed) > MAX_LINE_CHARS:
|
|
201
|
+
return collapsed[:MAX_LINE_CHARS - 3] + "..."
|
|
202
|
+
return collapsed
|
|
203
|
+
|
|
204
|
+
|
|
184
205
|
def _line(source: dict, score: float) -> str:
|
|
185
206
|
"""One collapsed, trimmed line. The score shown is the one that was actually
|
|
186
207
|
thresholded (`_relevance`), never the normalized rank — a displayed number the
|
|
187
208
|
filter did not use would misdescribe why the memory is on screen."""
|
|
188
|
-
|
|
189
|
-
text = " ".join(content.split()) if isinstance(content, str) else ""
|
|
190
|
-
if len(text) > MAX_LINE_CHARS:
|
|
191
|
-
text = text[:MAX_LINE_CHARS - 3] + "..."
|
|
192
|
-
return f"- {text} (score {score:.2f})"
|
|
209
|
+
return f"- {trim_line(source.get('content'))} (score {score:.2f})"
|
|
193
210
|
|
|
194
211
|
|
|
195
212
|
def select(sources, *, seen, floor: float) -> list[tuple[str, str]]:
|
|
@@ -80,6 +80,7 @@ tests/test_cli_dex.py
|
|
|
80
80
|
tests/test_cli_docdex.py
|
|
81
81
|
tests/test_cli_doctor.py
|
|
82
82
|
tests/test_cli_doctor_generic.py
|
|
83
|
+
tests/test_cli_doctor_report.py
|
|
83
84
|
tests/test_cli_entry.py
|
|
84
85
|
tests/test_cli_generic_install.py
|
|
85
86
|
tests/test_cli_install.py
|
|
@@ -93,6 +94,7 @@ tests/test_config_migration.py
|
|
|
93
94
|
tests/test_connect.py
|
|
94
95
|
tests/test_decision_board_html.py
|
|
95
96
|
tests/test_decision_bypass.py
|
|
97
|
+
tests/test_decision_poll_ceiling.py
|
|
96
98
|
tests/test_decision_registration.py
|
|
97
99
|
tests/test_decision_server.py
|
|
98
100
|
tests/test_dexes.py
|
|
@@ -103,12 +105,15 @@ tests/test_doctor_embeddings.py
|
|
|
103
105
|
tests/test_doctor_no_server.py
|
|
104
106
|
tests/test_e2e_bootstrap.py
|
|
105
107
|
tests/test_gateway.py
|
|
108
|
+
tests/test_gateway_schema_slimming.py
|
|
106
109
|
tests/test_gateway_toolset.py
|
|
107
110
|
tests/test_generic_config.py
|
|
108
111
|
tests/test_hooklog.py
|
|
109
112
|
tests/test_import_boundary.py
|
|
110
113
|
tests/test_init_self_enroll.py
|
|
111
114
|
tests/test_init_stale_bundle.py
|
|
115
|
+
tests/test_install_skill_contract.py
|
|
116
|
+
tests/test_instruction_budget.py
|
|
112
117
|
tests/test_join.py
|
|
113
118
|
tests/test_join_config_write.py
|
|
114
119
|
tests/test_joincode.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "firekeep-client"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.5.2"
|
|
8
8
|
description = "Client for Firekeep, the self-hosted operating layer for connected AI agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|