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.
Files changed (150) hide show
  1. {firekeep_client-1.2.0/firekeep_client.egg-info → firekeep_client-1.3.0}/PKG-INFO +1 -1
  2. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/__init__.py +1 -1
  3. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/__init__.py +4 -1
  4. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/base.py +5 -0
  5. firekeep_client-1.3.0/firekeep_client/adapters/claude_desktop.py +159 -0
  6. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/cli.py +54 -7
  7. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/contract/matrix.py +17 -4
  8. {firekeep_client-1.2.0 → firekeep_client-1.3.0/firekeep_client.egg-info}/PKG-INFO +1 -1
  9. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client.egg-info/SOURCES.txt +1 -0
  10. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/pyproject.toml +1 -1
  11. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_package.py +1 -1
  12. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/LICENSE +0 -0
  13. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/NOTICE +0 -0
  14. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/README.md +0 -0
  15. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/claude.py +0 -0
  16. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/codex.py +0 -0
  17. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/generic.py +0 -0
  18. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/kiro.py +0 -0
  19. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/adapters/opencode.py +0 -0
  20. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/autoupdate.py +0 -0
  21. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/backups.py +0 -0
  22. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/config_write.py +0 -0
  23. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/connect.py +0 -0
  24. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/contract/__init__.py +0 -0
  25. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/decision/__init__.py +0 -0
  26. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/decision/board.py +0 -0
  27. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/decision/server.py +0 -0
  28. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/dexes.py +0 -0
  29. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/docdexsync.py +0 -0
  30. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/gateway.py +0 -0
  31. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooklog.py +0 -0
  32. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/__init__.py +0 -0
  33. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/__main__.py +0 -0
  34. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/_git.py +0 -0
  35. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/_mcp.py +0 -0
  36. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/destructive.py +0 -0
  37. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/post_tool.py +0 -0
  38. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/pre_tool.py +0 -0
  39. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/precompact.py +0 -0
  40. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/prompt.py +0 -0
  41. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/runbooks.py +0 -0
  42. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/session_end.py +0 -0
  43. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/session_start.py +0 -0
  44. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/hooks/stop.py +0 -0
  45. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/join.py +0 -0
  46. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/joincode.py +0 -0
  47. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/maildexsync.py +0 -0
  48. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/migrate.py +0 -0
  49. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/nightshift.py +0 -0
  50. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/pathenv.py +0 -0
  51. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/promptrecall.py +0 -0
  52. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/resolver.py +0 -0
  53. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/serverinit.py +0 -0
  54. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/shim.py +0 -0
  55. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/sidecar.py +0 -0
  56. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/signing.py +0 -0
  57. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/state.py +0 -0
  58. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/stdio.py +0 -0
  59. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/symdexindex.py +0 -0
  60. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/transport.py +0 -0
  61. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/updater.py +0 -0
  62. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/wizard.py +0 -0
  63. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client/worktree_snapshot.py +0 -0
  64. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client.egg-info/dependency_links.txt +0 -0
  65. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client.egg-info/entry_points.txt +0 -0
  66. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client.egg-info/requires.txt +0 -0
  67. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/firekeep_client.egg-info/top_level.txt +0 -0
  68. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/setup.cfg +0 -0
  69. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_attribution_headers.py +0 -0
  70. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_autoupdate.py +0 -0
  71. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bash_layer_retired.py +0 -0
  72. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_docdex.py +0 -0
  73. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_maildex.py +0 -0
  74. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_ps1.py +0 -0
  75. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_reinstall.py +0 -0
  76. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_sh.py +0 -0
  77. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_symdex.py +0 -0
  78. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_bootstrap_venv_provisioning.py +0 -0
  79. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_ci_publishes_symdex.py +0 -0
  80. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli.py +0 -0
  81. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_backup.py +0 -0
  82. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_dex.py +0 -0
  83. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_docdex.py +0 -0
  84. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_doctor.py +0 -0
  85. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_doctor_generic.py +0 -0
  86. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_entry.py +0 -0
  87. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_generic_install.py +0 -0
  88. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_install.py +0 -0
  89. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_maildex.py +0 -0
  90. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_onboarding.py +0 -0
  91. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_personal.py +0 -0
  92. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_uninstall.py +0 -0
  93. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_uninstall_generic.py +0 -0
  94. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_cli_update.py +0 -0
  95. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_config_migration.py +0 -0
  96. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_connect.py +0 -0
  97. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_decision_board_html.py +0 -0
  98. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_decision_bypass.py +0 -0
  99. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_decision_registration.py +0 -0
  100. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_decision_server.py +0 -0
  101. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_dexes.py +0 -0
  102. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_docdexsync.py +0 -0
  103. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_docs_reference_client_kit.py +0 -0
  104. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_doctor_credential_expiry.py +0 -0
  105. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_doctor_embeddings.py +0 -0
  106. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_doctor_no_server.py +0 -0
  107. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_e2e_bootstrap.py +0 -0
  108. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_gateway.py +0 -0
  109. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_generic_config.py +0 -0
  110. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_hooklog.py +0 -0
  111. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_import_boundary.py +0 -0
  112. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_init_self_enroll.py +0 -0
  113. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_init_stale_bundle.py +0 -0
  114. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_join.py +0 -0
  115. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_join_config_write.py +0 -0
  116. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_joincode.py +0 -0
  117. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_kit_smoke.py +0 -0
  118. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_maildexsync.py +0 -0
  119. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_make_release.py +0 -0
  120. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_memory_instructions.py +0 -0
  121. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_migration_entrypoints.py +0 -0
  122. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_nightshift.py +0 -0
  123. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_pathenv.py +0 -0
  124. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_promptrecall.py +0 -0
  125. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_resolver.py +0 -0
  126. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_resolver_bypass.py +0 -0
  127. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_resolver_single_connection.py +0 -0
  128. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_runbook_match_parity.py +0 -0
  129. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_runbook_state.py +0 -0
  130. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_scratch_ttl.py +0 -0
  131. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_serverinit.py +0 -0
  132. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_shim_bridge.py +0 -0
  133. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_shim_bypass.py +0 -0
  134. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_shim_fail_loud.py +0 -0
  135. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_shim_identity.py +0 -0
  136. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_shim_recovery.py +0 -0
  137. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_shim_skeleton.py +0 -0
  138. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_sidecar.py +0 -0
  139. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_sidecar_robustness.py +0 -0
  140. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_signing.py +0 -0
  141. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_state.py +0 -0
  142. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_stdio_encoding.py +0 -0
  143. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_symdexindex.py +0 -0
  144. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_transport.py +0 -0
  145. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_transport_sse.py +0 -0
  146. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_twin_removed.py +0 -0
  147. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_updater.py +0 -0
  148. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_wizard.py +0 -0
  149. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_wizard_generic.py +0 -0
  150. {firekeep_client-1.2.0 → firekeep_client-1.3.0}/tests/test_worktree_snapshot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firekeep-client
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Client for Firekeep, the self-hosted operating layer for connected AI agents.
5
5
  License-Expression: LicenseRef-Firekeep-BUSL-1.1
6
6
  Project-URL: Homepage, https://firekeep.ai
@@ -9,4 +9,4 @@ versions without a verdict (`_check_versions`) and judges staleness against the
9
9
  release manifest instead (`_check_client_version`).
10
10
  """
11
11
 
12
- __version__ = "1.2.0"
12
+ __version__ = "1.3.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 _selected_runtimes(runtime: str, *, include_generic: bool = False) -> list[str]:
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"] + (["generic"] if include_generic else [])
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, include_generic=_generic_is_configured()):
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
- RUNTIMES = ("claude", "kiro", "codex", "opencode", "generic")
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", "generic": "sidecar (manual today)"},
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", "generic": "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", "generic": "self-reported"},
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
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firekeep-client
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Client for Firekeep, the self-hosted operating layer for connected AI agents.
5
5
  License-Expression: LicenseRef-Firekeep-BUSL-1.1
6
6
  Project-URL: Homepage, https://firekeep.ai
@@ -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.2.0"
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"
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  def test_package_imports_and_exposes_frozen_version():
8
8
  import firekeep_client
9
9
 
10
- assert firekeep_client.__version__ == "1.2.0"
10
+ assert firekeep_client.__version__ == "1.3.0"
11
11
 
12
12
 
13
13
  def test_frozen_module_layout_is_present():
File without changes
File without changes