firekeep-client 1.2.0__tar.gz → 1.3.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.
- {firekeep_client-1.2.0/firekeep_client.egg-info → firekeep_client-1.3.0}/PKG-INFO +1 -1
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/__init__.py +1 -1
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/__init__.py +4 -1
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/base.py +5 -0
- firekeep_client-1.3.0/firekeep_client/adapters/claude_desktop.py +159 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/cli.py +54 -7
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/contract/matrix.py +17 -4
- {firekeep_client-1.2.0 → firekeep_client-1.3.0/firekeep_client.egg-info}/PKG-INFO +1 -1
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client.egg-info/SOURCES.txt +1 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/pyproject.toml +1 -1
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_package.py +1 -1
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/LICENSE +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/NOTICE +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/README.md +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/claude.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/codex.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/generic.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/kiro.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/opencode.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/autoupdate.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/backups.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/config_write.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/connect.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/contract/__init__.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/decision/__init__.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/decision/board.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/decision/server.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/dexes.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/docdexsync.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/gateway.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooklog.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/__init__.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/__main__.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/_git.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/_mcp.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/destructive.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/post_tool.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/pre_tool.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/precompact.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/prompt.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/runbooks.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/session_end.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/session_start.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/stop.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/join.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/joincode.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/maildexsync.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/migrate.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/nightshift.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/pathenv.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/promptrecall.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/resolver.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/serverinit.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/shim.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/sidecar.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/signing.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/state.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/stdio.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/symdexindex.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/transport.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/updater.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/wizard.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/worktree_snapshot.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client.egg-info/dependency_links.txt +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client.egg-info/entry_points.txt +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client.egg-info/requires.txt +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client.egg-info/top_level.txt +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/setup.cfg +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_attribution_headers.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_autoupdate.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bash_layer_retired.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_docdex.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_maildex.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_ps1.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_reinstall.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_sh.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_symdex.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_venv_provisioning.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_ci_publishes_symdex.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_backup.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_dex.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_docdex.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_doctor.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_doctor_generic.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_entry.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_generic_install.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_install.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_maildex.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_onboarding.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_personal.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_uninstall.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_uninstall_generic.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_update.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_config_migration.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_connect.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_decision_board_html.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_decision_bypass.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_decision_registration.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_decision_server.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_dexes.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_docdexsync.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_docs_reference_client_kit.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_doctor_credential_expiry.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_doctor_embeddings.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_doctor_no_server.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_e2e_bootstrap.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_gateway.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_generic_config.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_hooklog.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_import_boundary.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_init_self_enroll.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_init_stale_bundle.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_join.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_join_config_write.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_joincode.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_kit_smoke.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_maildexsync.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_make_release.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_memory_instructions.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_migration_entrypoints.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_nightshift.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_pathenv.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_promptrecall.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_resolver.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_resolver_bypass.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_resolver_single_connection.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_runbook_match_parity.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_runbook_state.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_scratch_ttl.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_serverinit.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_shim_bridge.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_shim_bypass.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_shim_fail_loud.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_shim_identity.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_shim_recovery.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_shim_skeleton.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_sidecar.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_sidecar_robustness.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_signing.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_state.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_stdio_encoding.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_symdexindex.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_transport.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_transport_sse.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_twin_removed.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_updater.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_wizard.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_wizard_generic.py +0 -0
- {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_worktree_snapshot.py +0 -0
|
@@ -19,6 +19,9 @@ def get_adapter(name: str) -> Adapter:
|
|
|
19
19
|
if name == "opencode":
|
|
20
20
|
from firekeep_client.adapters.opencode import OpencodeAdapter
|
|
21
21
|
return OpencodeAdapter()
|
|
22
|
+
if name == "claude-desktop":
|
|
23
|
+
from firekeep_client.adapters.claude_desktop import ClaudeDesktopAdapter
|
|
24
|
+
return ClaudeDesktopAdapter()
|
|
22
25
|
if name == "generic":
|
|
23
26
|
# The target file (if any) lives in the kit config, not in the render
|
|
24
27
|
# loop's signature — resolver, never cli: adapters -> cli would be a cycle.
|
|
@@ -26,4 +29,4 @@ def get_adapter(name: str) -> Adapter:
|
|
|
26
29
|
from firekeep_client.resolver import generic_agents_md
|
|
27
30
|
return GenericAdapter(agents_md=generic_agents_md())
|
|
28
31
|
raise ValueError(
|
|
29
|
-
f"unknown adapter: {name!r} (expected claude|codex|kiro|opencode|generic)")
|
|
32
|
+
f"unknown adapter: {name!r} (expected claude|codex|kiro|opencode|claude-desktop|generic)")
|
|
@@ -374,6 +374,11 @@ the session.
|
|
|
374
374
|
- Operational or repeated-failure task → also `skill_recall(task)`. The
|
|
375
375
|
session-start briefing only ever matched your ORIGINAL goal, never what the user
|
|
376
376
|
asked on turn 7.
|
|
377
|
+
- About to BUILD something substantial (a feature, a module, a big function) →
|
|
378
|
+
check prior art first: `memory_recall` for what the team already built, and
|
|
379
|
+
`get_similar_symbols`/`search_symbols` for code that already does it. Ten
|
|
380
|
+
seconds of checking beats a day of parallel reinvention; `ctx_start_session`'s
|
|
381
|
+
`prior_art` block is the same check done for you at the moment you declare a goal.
|
|
377
382
|
- Your own earlier plan or decisions missing from context (after compaction) →
|
|
378
383
|
`ctx_get_shadow` before asking the user to repeat themselves. Pass
|
|
379
384
|
`since=<shadow_cursor>` ONLY if the earlier shadow is still visible in your
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Claude Desktop adapter: the `firekeep` entry in `claude_desktop_config.json`.
|
|
2
|
+
|
|
3
|
+
Claude Desktop is the first NON-CODING host the kit ships a bespoke adapter for
|
|
4
|
+
— the consumer chat app, not an agentic IDE. It runs local stdio MCP servers
|
|
5
|
+
from one JSON config file, so this adapter delivers exactly the generic tier
|
|
6
|
+
with the friction removed: the same gateway entry the generic adapter PRINTS
|
|
7
|
+
for pasting is WRITTEN here, because unlike a client we know nothing about,
|
|
8
|
+
Claude Desktop's config path is documented and stable per platform.
|
|
9
|
+
|
|
10
|
+
What it deliberately does NOT do:
|
|
11
|
+
- No hooks. Claude Desktop exposes no hook surface — no session lifecycle, no
|
|
12
|
+
prompt interception, no pre-edit gate. Its column in contract/matrix.py is
|
|
13
|
+
the generic floor, and claiming more would be the lie the matrix exists to
|
|
14
|
+
prevent.
|
|
15
|
+
- No instruction file. Claude Desktop reads no rules file the kit could safely
|
|
16
|
+
own; the cognitive protocol arrives through the MCP `initialize` handshake
|
|
17
|
+
(GATEWAY_INSTRUCTIONS), the same second channel every runtime gets.
|
|
18
|
+
|
|
19
|
+
The config is JSON, which has no comments and no partial-edit syntax, so the
|
|
20
|
+
file is parsed and re-serialized rather than marker-blocked. Only the
|
|
21
|
+
`mcpServers.firekeep` key is ever set or removed; every other key — other
|
|
22
|
+
servers, app settings — survives byte-for-byte at the VALUE level (formatting
|
|
23
|
+
is normalized to indent=2, which Claude Desktop itself also does on save).
|
|
24
|
+
A file that does not parse is REFUSED loudly and left exactly as it is:
|
|
25
|
+
clobbering a consumer app's config because we could not read it would be the
|
|
26
|
+
worst possible outcome of an install, and the render loop has no per-runtime
|
|
27
|
+
catch — so the refusal must not raise.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import sys
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from firekeep_client.adapters.base import Adapter, shim_servers, write_text_if_changed
|
|
37
|
+
|
|
38
|
+
_CONFIG_NAME = "claude_desktop_config.json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _config_dir() -> Path:
|
|
42
|
+
"""Claude Desktop's config directory for the current platform.
|
|
43
|
+
|
|
44
|
+
Resolved on every call, never cached — tests steer it via APPDATA /
|
|
45
|
+
XDG_CONFIG_HOME / HOME, and the opencode adapter set the precedent that
|
|
46
|
+
an env-following path must follow the env at call time."""
|
|
47
|
+
if sys.platform == "win32":
|
|
48
|
+
appdata = os.environ.get("APPDATA")
|
|
49
|
+
base = Path(appdata) if appdata else Path.home() / "AppData" / "Roaming"
|
|
50
|
+
return base / "Claude"
|
|
51
|
+
if sys.platform == "darwin":
|
|
52
|
+
return Path.home() / "Library" / "Application Support" / "Claude"
|
|
53
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
54
|
+
return (Path(xdg) if xdg else Path.home() / ".config") / "Claude"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def config_path() -> Path:
|
|
58
|
+
return _config_dir() / _CONFIG_NAME
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def app_present() -> bool:
|
|
62
|
+
"""Whether Claude Desktop appears installed on this machine.
|
|
63
|
+
|
|
64
|
+
The config directory is the evidence: the app creates it on first run.
|
|
65
|
+
This is the auto-render gate — `firekeep install` (no --runtime) mounts
|
|
66
|
+
the gateway here only when the app exists, because writing another
|
|
67
|
+
vendor's consumer-app config onto machines that never ran the app would
|
|
68
|
+
scatter orphan files for zero benefit. Explicit
|
|
69
|
+
`--runtime claude-desktop` bypasses the gate (creating the directory),
|
|
70
|
+
for a user installing Firekeep ahead of the app."""
|
|
71
|
+
return _config_dir().is_dir()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _expected_entry(venv_bin: Path) -> dict:
|
|
75
|
+
command, args = shim_servers(venv_bin, ClaudeDesktopAdapter.name)["firekeep"]
|
|
76
|
+
return {"command": command, "args": list(args)}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _load(path: Path) -> dict | None:
|
|
80
|
+
"""Parse the config, or None when it cannot be safely rewritten.
|
|
81
|
+
|
|
82
|
+
Missing file -> {} (we will create it). Unreadable or unparseable or
|
|
83
|
+
non-object -> None, and the caller warns and leaves the file alone."""
|
|
84
|
+
try:
|
|
85
|
+
raw = path.read_text(encoding="utf-8")
|
|
86
|
+
except FileNotFoundError:
|
|
87
|
+
return {}
|
|
88
|
+
except (OSError, UnicodeError):
|
|
89
|
+
return None
|
|
90
|
+
if not raw.strip():
|
|
91
|
+
return {}
|
|
92
|
+
try:
|
|
93
|
+
parsed = json.loads(raw)
|
|
94
|
+
except json.JSONDecodeError:
|
|
95
|
+
return None
|
|
96
|
+
return parsed if isinstance(parsed, dict) else None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def mcp_entry_is_current(text: str, venv_bin: Path) -> bool:
|
|
100
|
+
"""Whether `text` carries the exact gateway entry this adapter would render."""
|
|
101
|
+
try:
|
|
102
|
+
parsed = json.loads(text)
|
|
103
|
+
except json.JSONDecodeError:
|
|
104
|
+
return False
|
|
105
|
+
if not isinstance(parsed, dict):
|
|
106
|
+
return False
|
|
107
|
+
servers = parsed.get("mcpServers")
|
|
108
|
+
if not isinstance(servers, dict):
|
|
109
|
+
return False
|
|
110
|
+
return servers.get("firekeep") == _expected_entry(venv_bin)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ClaudeDesktopAdapter(Adapter):
|
|
114
|
+
name = "claude-desktop"
|
|
115
|
+
|
|
116
|
+
def render(self, *, venv_bin: Path) -> None:
|
|
117
|
+
path = config_path()
|
|
118
|
+
data = _load(path)
|
|
119
|
+
if data is None:
|
|
120
|
+
print(
|
|
121
|
+
f"firekeep: WARNING — {path} exists but is not valid JSON; "
|
|
122
|
+
"left untouched. Fix the file, then run "
|
|
123
|
+
"`firekeep install --runtime claude-desktop`.",
|
|
124
|
+
file=sys.stderr,
|
|
125
|
+
)
|
|
126
|
+
return
|
|
127
|
+
servers = data.setdefault("mcpServers", {})
|
|
128
|
+
if not isinstance(servers, dict):
|
|
129
|
+
print(
|
|
130
|
+
f"firekeep: WARNING — {path} has a non-object mcpServers; "
|
|
131
|
+
"left untouched.",
|
|
132
|
+
file=sys.stderr,
|
|
133
|
+
)
|
|
134
|
+
return
|
|
135
|
+
servers["firekeep"] = _expected_entry(venv_bin)
|
|
136
|
+
if write_text_if_changed(path, json.dumps(data, indent=2) + "\n"):
|
|
137
|
+
# Only on a real change: this render re-runs on every
|
|
138
|
+
# `firekeep update`, and a restart nag for a byte-identical
|
|
139
|
+
# config would train users to ignore it.
|
|
140
|
+
print("firekeep: Claude Desktop configured — restart the app to load Firekeep.")
|
|
141
|
+
|
|
142
|
+
def unrender(self) -> None:
|
|
143
|
+
path = config_path()
|
|
144
|
+
if not path.exists():
|
|
145
|
+
return
|
|
146
|
+
data = _load(path)
|
|
147
|
+
if data is None:
|
|
148
|
+
print(
|
|
149
|
+
f"firekeep: WARNING — could not parse {path}; remove the "
|
|
150
|
+
"'firekeep' entry from mcpServers yourself.",
|
|
151
|
+
file=sys.stderr,
|
|
152
|
+
)
|
|
153
|
+
return
|
|
154
|
+
servers = data.get("mcpServers")
|
|
155
|
+
if not isinstance(servers, dict) or "firekeep" not in servers:
|
|
156
|
+
return
|
|
157
|
+
del servers["firekeep"]
|
|
158
|
+
# The file itself stays — it belongs to Claude Desktop, not to us.
|
|
159
|
+
write_text_if_changed(path, json.dumps(data, indent=2) + "\n")
|
|
@@ -291,14 +291,30 @@ def _generic_is_configured() -> bool:
|
|
|
291
291
|
return resolver.generic_agents_md() is not None
|
|
292
292
|
|
|
293
293
|
|
|
294
|
-
def
|
|
294
|
+
def _claude_desktop_present() -> bool:
|
|
295
|
+
"""Whether Claude Desktop appears installed (its config dir exists).
|
|
296
|
+
|
|
297
|
+
Gates the "all" fan-out for install AND uninstall: install must not write
|
|
298
|
+
another vendor's consumer-app config onto machines that never ran the app,
|
|
299
|
+
and uninstall has nothing to clean where nothing was written. Lazy import —
|
|
300
|
+
cli must stay importable without every adapter module."""
|
|
301
|
+
from firekeep_client.adapters.claude_desktop import app_present
|
|
302
|
+
return app_present()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _selected_runtimes(runtime: str, *, include_generic: bool = False,
|
|
306
|
+
include_claude_desktop: bool = False) -> list[str]:
|
|
295
307
|
"""PURE: a function of its arguments only, never of the config on disk.
|
|
296
308
|
|
|
297
309
|
`generic` joins the "all" fan-out only when the caller says so, so an
|
|
298
310
|
unconfigured user gets exactly the four — the invariant test_cli_install and
|
|
299
|
-
test_cli_uninstall pin by count.
|
|
311
|
+
test_cli_uninstall pin by count. `claude-desktop` likewise: its flag is
|
|
312
|
+
computed at the call site from claude_desktop.app_present(), so a machine
|
|
313
|
+
that never ran Claude Desktop gets no config written for it."""
|
|
300
314
|
if runtime == "all":
|
|
301
|
-
return ["claude", "codex", "kiro", "opencode"]
|
|
315
|
+
return (["claude", "codex", "kiro", "opencode"]
|
|
316
|
+
+ (["claude-desktop"] if include_claude_desktop else [])
|
|
317
|
+
+ (["generic"] if include_generic else []))
|
|
302
318
|
return [runtime]
|
|
303
319
|
|
|
304
320
|
|
|
@@ -523,7 +539,9 @@ def cmd_install(args) -> int:
|
|
|
523
539
|
# embedded paths stay literally identical across flips) and keeps
|
|
524
540
|
# runtime configs from pinning a venv that GC will remove.
|
|
525
541
|
venv_bin = _venv_bin(_venv_root(home))
|
|
526
|
-
for name in _selected_runtimes(args.runtime,
|
|
542
|
+
for name in _selected_runtimes(args.runtime,
|
|
543
|
+
include_generic=_generic_is_configured(),
|
|
544
|
+
include_claude_desktop=_claude_desktop_present()):
|
|
527
545
|
step = f"render {name} adapter"
|
|
528
546
|
get_adapter(name).render(venv_bin=venv_bin)
|
|
529
547
|
|
|
@@ -747,7 +765,8 @@ def cmd_uninstall(args) -> int:
|
|
|
747
765
|
# config we could not parse leaves the block stranded with no way to find it.
|
|
748
766
|
generic_target = resolver.generic_agents_md()
|
|
749
767
|
generic_orphan = _generic_orphan_warning(home) if generic_target is None else None
|
|
750
|
-
runtimes = _selected_runtimes("all", include_generic=generic_target is not None
|
|
768
|
+
runtimes = _selected_runtimes("all", include_generic=generic_target is not None,
|
|
769
|
+
include_claude_desktop=_claude_desktop_present())
|
|
751
770
|
|
|
752
771
|
# Say exactly what will be removed BEFORE touching anything.
|
|
753
772
|
print("firekeep uninstall will remove:")
|
|
@@ -1361,9 +1380,36 @@ def _check_codex_adapter(venv: Path) -> list[tuple[str, str, str]]:
|
|
|
1361
1380
|
return [("codex-mcp", "ok", str(config))]
|
|
1362
1381
|
|
|
1363
1382
|
|
|
1383
|
+
def _check_claude_desktop_adapter(venv: Path) -> list[tuple[str, str, str]]:
|
|
1384
|
+
"""claude-desktop-mcp: silent unless the app's config exists AND mentions us.
|
|
1385
|
+
|
|
1386
|
+
Mirrors _check_codex_adapter's philosophy — doctor must not warn a user
|
|
1387
|
+
about a runtime they never installed. A present-but-unmanaged config is a
|
|
1388
|
+
user who chose not to mount Firekeep there; also silence. No instructions
|
|
1389
|
+
row exists for this runtime: Claude Desktop reads no rules file, so the
|
|
1390
|
+
protocol's only channel is the gateway handshake."""
|
|
1391
|
+
from firekeep_client.adapters.claude_desktop import config_path, mcp_entry_is_current
|
|
1392
|
+
config = config_path()
|
|
1393
|
+
if not config.exists():
|
|
1394
|
+
return []
|
|
1395
|
+
try:
|
|
1396
|
+
text = config.read_text(encoding="utf-8")
|
|
1397
|
+
except OSError as exc:
|
|
1398
|
+
return [("claude-desktop-mcp", "fail", f"cannot read {config}: {exc}")]
|
|
1399
|
+
if '"firekeep"' not in text:
|
|
1400
|
+
return []
|
|
1401
|
+
repair = "run `firekeep install --runtime claude-desktop`"
|
|
1402
|
+
if not mcp_entry_is_current(text, _venv_bin(venv)):
|
|
1403
|
+
return [("claude-desktop-mcp", "fail",
|
|
1404
|
+
f"stale Firekeep gateway entry in {config}; {repair}")]
|
|
1405
|
+
return [("claude-desktop-mcp", "ok", str(config))]
|
|
1406
|
+
|
|
1407
|
+
|
|
1364
1408
|
# The shipped runtimes, in adapter order (`_selected_runtimes("all")`), plus
|
|
1365
1409
|
# generic — which contributes a row ONLY when the user configured it, so an
|
|
1366
|
-
# unconfigured user's doctor output is unchanged.
|
|
1410
|
+
# unconfigured user's doctor output is unchanged. claude-desktop is absent on
|
|
1411
|
+
# purpose: it renders no instruction file (rendered_instructions_path returns
|
|
1412
|
+
# None for it), so there is no block to hash-check.
|
|
1367
1413
|
_INSTRUCTION_RUNTIMES = ("claude", "codex", "kiro", "opencode", "generic")
|
|
1368
1414
|
|
|
1369
1415
|
|
|
@@ -1639,6 +1685,7 @@ def run_doctor(cfg=None) -> list[tuple[str, str, str]]:
|
|
|
1639
1685
|
results.append(_check_venv_scripts(_venv_root()))
|
|
1640
1686
|
results.extend(_check_dexes())
|
|
1641
1687
|
results.extend(_check_codex_adapter(_venv_root()))
|
|
1688
|
+
results.extend(_check_claude_desktop_adapter(_venv_root()))
|
|
1642
1689
|
results.extend(_check_instructions())
|
|
1643
1690
|
results.append(_check_config_perms(_config_path()))
|
|
1644
1691
|
ca = _check_ca_expiry(cfg)
|
|
@@ -2686,7 +2733,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
2686
2733
|
# bootstrap's FIREKEEP_RUNTIME) remains available for a targeted re-render.
|
|
2687
2734
|
inst.add_argument(
|
|
2688
2735
|
"--runtime",
|
|
2689
|
-
choices=["claude", "codex", "kiro", "opencode", "generic", "all"],
|
|
2736
|
+
choices=["claude", "codex", "kiro", "opencode", "claude-desktop", "generic", "all"],
|
|
2690
2737
|
default=None,
|
|
2691
2738
|
)
|
|
2692
2739
|
# `generic` is any MCP client the kit ships no bespoke adapter for: it prints
|
|
@@ -45,7 +45,13 @@ from firekeep_client.adapters.claude import CLAUDE_HOOKS
|
|
|
45
45
|
# and the instruction protocol, and nothing that rides a hook — a client we know
|
|
46
46
|
# nothing about exposes no hook surface to wire. It is listed last because it is
|
|
47
47
|
# what a runtime degrades TO, not a peer of the four.
|
|
48
|
-
|
|
48
|
+
#
|
|
49
|
+
# `claude-desktop` (the consumer chat app, not Claude Code) sits AT that floor
|
|
50
|
+
# on purpose: it has a bespoke adapter only because its config path is known —
|
|
51
|
+
# every capability cell is the generic value, because the app exposes no hook
|
|
52
|
+
# surface. If a cell here ever claims more than generic's, either Claude
|
|
53
|
+
# Desktop grew hooks or the cell is lying.
|
|
54
|
+
RUNTIMES = ("claude", "kiro", "codex", "opencode", "claude-desktop", "generic")
|
|
49
55
|
|
|
50
56
|
|
|
51
57
|
def _precompact_claude(hooks: tuple[tuple[str, str, str | None, int], ...]) -> str:
|
|
@@ -77,6 +83,7 @@ def _precompact_claude(hooks: tuple[tuple[str, str, str | None, int], ...]) -> s
|
|
|
77
83
|
MATRIX: dict[str, dict[str, str]] = {
|
|
78
84
|
"briefing": {"claude": "hook", "kiro": "agentSpawn hook", "codex": "manual/memory_recall",
|
|
79
85
|
"opencode": "plugin (first event, console log only)",
|
|
86
|
+
"claude-desktop": "none (MCP only)",
|
|
80
87
|
"generic": "none (MCP only)"},
|
|
81
88
|
# Proactive recall (firekeep_client.promptrecall): fires only where the runtime
|
|
82
89
|
# hands the prompt TEXT to a hook, because there is nothing to embed otherwise.
|
|
@@ -90,14 +97,17 @@ MATRIX: dict[str, dict[str, str]] = {
|
|
|
90
97
|
# briefing remains the only push.
|
|
91
98
|
"proactive_recall": {"claude": "per-prompt push", "kiro": "per-prompt push",
|
|
92
99
|
"codex": "none (no hooks)", "opencode": "none (no prompt text)",
|
|
100
|
+
"claude-desktop": "none (no hooks)",
|
|
93
101
|
"generic": "none (no hooks)"},
|
|
94
102
|
"presence": {"claude": "hook", "kiro": "hook", "codex": "sidecar (manual today)",
|
|
95
|
-
"opencode": "plugin hooks", "
|
|
103
|
+
"opencode": "plugin hooks", "claude-desktop": "sidecar (manual today)",
|
|
104
|
+
"generic": "sidecar (manual today)"},
|
|
96
105
|
# kiro (validated 2.12.1): the fs_write pre-edit hook FIRES (the agent-gateway before-call
|
|
97
106
|
# runs + records), but kiro does not enforce its own exit-2 block — so it is advisory, not
|
|
98
107
|
# a hard gate. See firekeep_client/adapters/kiro.py + docs/KIRO-VALIDATION.md.
|
|
99
108
|
"pre_edit_block": {"claude": "guaranteed", "kiro": "advisory (fires, non-blocking on 2.12.1)", "codex": "none",
|
|
100
109
|
"opencode": "guaranteed (plugin throw, validated 1.14.22)",
|
|
110
|
+
"claude-desktop": "none",
|
|
101
111
|
"generic": "none"},
|
|
102
112
|
# Only Claude exposes a compaction event; the other three runtimes have no
|
|
103
113
|
# such lifecycle hook to wire, so this degrades honestly rather than silently.
|
|
@@ -105,9 +115,11 @@ MATRIX: dict[str, dict[str, str]] = {
|
|
|
105
115
|
# _precompact_claude) so it cannot claim a hook the kit does not render; the
|
|
106
116
|
# other three are hand-authored because there is nothing to derive them from.
|
|
107
117
|
"precompact": {"claude": _precompact_claude(CLAUDE_HOOKS), "kiro": "none",
|
|
108
|
-
"codex": "none", "opencode": "none", "
|
|
118
|
+
"codex": "none", "opencode": "none", "claude-desktop": "none",
|
|
119
|
+
"generic": "none"},
|
|
109
120
|
"reconcile": {"claude": "hooks", "kiro": "kiro pre/post hooks", "codex": "self-reported",
|
|
110
|
-
"opencode": "plugin pre/post hooks", "
|
|
121
|
+
"opencode": "plugin pre/post hooks", "claude-desktop": "self-reported",
|
|
122
|
+
"generic": "self-reported"},
|
|
111
123
|
# Personal / bypass mode: the is_bypassed() gate (marker + FIREKEEP_BYPASS) works on
|
|
112
124
|
# every runtime; only the /personal slash command is claude-specific. kiro/codex
|
|
113
125
|
# toggle via the `firekeep personal` CLI (or `! firekeep personal`), or FIREKEEP_BYPASS at launch.
|
|
@@ -116,6 +128,7 @@ MATRIX: dict[str, dict[str, str]] = {
|
|
|
116
128
|
"kiro": "firekeep personal CLI + FIREKEEP_BYPASS (no /personal command)",
|
|
117
129
|
"codex": "firekeep personal CLI + FIREKEEP_BYPASS (sidecar honors the gate)",
|
|
118
130
|
"opencode": "firekeep personal CLI + FIREKEEP_BYPASS (no /personal command)",
|
|
131
|
+
"claude-desktop": "firekeep personal CLI + FIREKEEP_BYPASS (no /personal command)",
|
|
119
132
|
"generic": "firekeep personal CLI + FIREKEEP_BYPASS (no /personal command)",
|
|
120
133
|
},
|
|
121
134
|
}
|
|
@@ -40,6 +40,7 @@ firekeep_client.egg-info/top_level.txt
|
|
|
40
40
|
firekeep_client/adapters/__init__.py
|
|
41
41
|
firekeep_client/adapters/base.py
|
|
42
42
|
firekeep_client/adapters/claude.py
|
|
43
|
+
firekeep_client/adapters/claude_desktop.py
|
|
43
44
|
firekeep_client/adapters/codex.py
|
|
44
45
|
firekeep_client/adapters/generic.py
|
|
45
46
|
firekeep_client/adapters/kiro.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.3.0"
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|