dulus 0.2.39__tar.gz → 0.2.40__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 (182) hide show
  1. {dulus-0.2.39/dulus.egg-info → dulus-0.2.40}/PKG-INFO +1 -3
  2. {dulus-0.2.39 → dulus-0.2.40/dulus.egg-info}/PKG-INFO +1 -3
  3. {dulus-0.2.39 → dulus-0.2.40}/dulus.egg-info/SOURCES.txt +0 -2
  4. {dulus-0.2.39 → dulus-0.2.40}/pyproject.toml +1 -1
  5. dulus-0.2.39/license_manager.py +0 -187
  6. dulus-0.2.39/license_server.py +0 -63
  7. {dulus-0.2.39 → dulus-0.2.40}/LICENSE +0 -0
  8. {dulus-0.2.39 → dulus-0.2.40}/MANIFEST.in +0 -0
  9. {dulus-0.2.39 → dulus-0.2.40}/README.md +0 -0
  10. {dulus-0.2.39 → dulus-0.2.40}/agent.py +0 -0
  11. {dulus-0.2.39 → dulus-0.2.40}/backend/__init__.py +0 -0
  12. {dulus-0.2.39 → dulus-0.2.40}/backend/compressor.py +0 -0
  13. {dulus-0.2.39 → dulus-0.2.40}/backend/context.py +0 -0
  14. {dulus-0.2.39 → dulus-0.2.40}/backend/githook.py +0 -0
  15. {dulus-0.2.39 → dulus-0.2.40}/backend/marketplace.py +0 -0
  16. {dulus-0.2.39 → dulus-0.2.40}/backend/mempalace_bridge.py +0 -0
  17. {dulus-0.2.39 → dulus-0.2.40}/backend/personas.py +0 -0
  18. {dulus-0.2.39 → dulus-0.2.40}/backend/plugins.py +0 -0
  19. {dulus-0.2.39 → dulus-0.2.40}/backend/server.py +0 -0
  20. {dulus-0.2.39 → dulus-0.2.40}/backend/tasks.py +0 -0
  21. {dulus-0.2.39 → dulus-0.2.40}/batch_api.py +0 -0
  22. {dulus-0.2.39 → dulus-0.2.40}/checkpoint/__init__.py +0 -0
  23. {dulus-0.2.39 → dulus-0.2.40}/checkpoint/hooks.py +0 -0
  24. {dulus-0.2.39 → dulus-0.2.40}/checkpoint/store.py +0 -0
  25. {dulus-0.2.39 → dulus-0.2.40}/checkpoint/types.py +0 -0
  26. {dulus-0.2.39 → dulus-0.2.40}/claude_code_watcher.py +0 -0
  27. {dulus-0.2.39 → dulus-0.2.40}/clipboard_utils.py +0 -0
  28. {dulus-0.2.39 → dulus-0.2.40}/cloudsave.py +0 -0
  29. {dulus-0.2.39 → dulus-0.2.40}/common.py +0 -0
  30. {dulus-0.2.39 → dulus-0.2.40}/compaction.py +0 -0
  31. {dulus-0.2.39 → dulus-0.2.40}/config.py +0 -0
  32. {dulus-0.2.39 → dulus-0.2.40}/context.py +0 -0
  33. {dulus-0.2.39 → dulus-0.2.40}/data/__init__.py +0 -0
  34. {dulus-0.2.39 → dulus-0.2.40}/data/active_persona.json +0 -0
  35. {dulus-0.2.39 → dulus-0.2.40}/data/context.json +0 -0
  36. {dulus-0.2.39 → dulus-0.2.40}/data/marketplace.json +0 -0
  37. {dulus-0.2.39 → dulus-0.2.40}/data/personas.json +0 -0
  38. {dulus-0.2.39 → dulus-0.2.40}/data/plugins/__init__.py +0 -0
  39. {dulus-0.2.39 → dulus-0.2.40}/data/plugins/composio/__init__.py +0 -0
  40. {dulus-0.2.39 → dulus-0.2.40}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  41. {dulus-0.2.39 → dulus-0.2.40}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  42. {dulus-0.2.39 → dulus-0.2.40}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  43. {dulus-0.2.39 → dulus-0.2.40}/data/plugins/composio/plugin.json +0 -0
  44. {dulus-0.2.39 → dulus-0.2.40}/data/plugins/composio/plugin_tool.py +0 -0
  45. {dulus-0.2.39 → dulus-0.2.40}/data/tasks.json +0 -0
  46. {dulus-0.2.39 → dulus-0.2.40}/docs/README.md +0 -0
  47. {dulus-0.2.39 → dulus-0.2.40}/docs/__init__.py +0 -0
  48. {dulus-0.2.39 → dulus-0.2.40}/docs/api.html +0 -0
  49. {dulus-0.2.39 → dulus-0.2.40}/docs/architecture.md +0 -0
  50. {dulus-0.2.39 → dulus-0.2.40}/docs/azure-speech-template.json +0 -0
  51. {dulus-0.2.39 → dulus-0.2.40}/docs/dashboard/index.html +0 -0
  52. {dulus-0.2.39 → dulus-0.2.40}/docs/divider.svg +0 -0
  53. {dulus-0.2.39 → dulus-0.2.40}/docs/generate.py +0 -0
  54. {dulus-0.2.39 → dulus-0.2.40}/docs/hero.svg +0 -0
  55. {dulus-0.2.39 → dulus-0.2.40}/docs/index.html +0 -0
  56. {dulus-0.2.39 → dulus-0.2.40}/docs/news.md +0 -0
  57. {dulus-0.2.39 → dulus-0.2.40}/docs/nvidia-models.svg +0 -0
  58. {dulus-0.2.39 → dulus-0.2.40}/docs/particle-playground.html +0 -0
  59. {dulus-0.2.39 → dulus-0.2.40}/docs/personas/index.html +0 -0
  60. {dulus-0.2.39 → dulus-0.2.40}/docs/poetry-banner.png +0 -0
  61. {dulus-0.2.39 → dulus-0.2.40}/docs/preview.html +0 -0
  62. {dulus-0.2.39 → dulus-0.2.40}/docs/sec-agents.svg +0 -0
  63. {dulus-0.2.39 → dulus-0.2.40}/docs/sec-brainstorm.svg +0 -0
  64. {dulus-0.2.39 → dulus-0.2.40}/docs/sec-bridges.svg +0 -0
  65. {dulus-0.2.39 → dulus-0.2.40}/docs/sec-features.svg +0 -0
  66. {dulus-0.2.39 → dulus-0.2.40}/docs/sec-freetier.svg +0 -0
  67. {dulus-0.2.39 → dulus-0.2.40}/docs/sec-memory.svg +0 -0
  68. {dulus-0.2.39 → dulus-0.2.40}/docs/sec-models.svg +0 -0
  69. {dulus-0.2.39 → dulus-0.2.40}/docs/sec-perms.svg +0 -0
  70. {dulus-0.2.39 → dulus-0.2.40}/docs/sec-plugins.svg +0 -0
  71. {dulus-0.2.39 → dulus-0.2.40}/docs/sec-quickstart.svg +0 -0
  72. {dulus-0.2.39 → dulus-0.2.40}/docs/sec-ssj.svg +0 -0
  73. {dulus-0.2.39 → dulus-0.2.40}/docs/spinners.svg +0 -0
  74. {dulus-0.2.39 → dulus-0.2.40}/docs/split-pane.svg +0 -0
  75. {dulus-0.2.39 → dulus-0.2.40}/docs/terminal-boot.svg +0 -0
  76. {dulus-0.2.39 → dulus-0.2.40}/docs/uploads/particle-playground.html +0 -0
  77. {dulus-0.2.39 → dulus-0.2.40}/dulus.egg-info/dependency_links.txt +0 -0
  78. {dulus-0.2.39 → dulus-0.2.40}/dulus.egg-info/entry_points.txt +0 -0
  79. {dulus-0.2.39 → dulus-0.2.40}/dulus.egg-info/requires.txt +0 -0
  80. {dulus-0.2.39 → dulus-0.2.40}/dulus.egg-info/top_level.txt +0 -0
  81. {dulus-0.2.39 → dulus-0.2.40}/dulus.py +0 -0
  82. {dulus-0.2.39 → dulus-0.2.40}/dulus_gui.py +0 -0
  83. {dulus-0.2.39 → dulus-0.2.40}/dulus_mcp/__init__.py +0 -0
  84. {dulus-0.2.39 → dulus-0.2.40}/dulus_mcp/client.py +0 -0
  85. {dulus-0.2.39 → dulus-0.2.40}/dulus_mcp/config.py +0 -0
  86. {dulus-0.2.39 → dulus-0.2.40}/dulus_mcp/tools.py +0 -0
  87. {dulus-0.2.39 → dulus-0.2.40}/dulus_mcp/types.py +0 -0
  88. {dulus-0.2.39 → dulus-0.2.40}/gui/__init__.py +0 -0
  89. {dulus-0.2.39 → dulus-0.2.40}/gui/agent_bridge.py +0 -0
  90. {dulus-0.2.39 → dulus-0.2.40}/gui/chat_widget.py +0 -0
  91. {dulus-0.2.39 → dulus-0.2.40}/gui/main_window.py +0 -0
  92. {dulus-0.2.39 → dulus-0.2.40}/gui/personas.py +0 -0
  93. {dulus-0.2.39 → dulus-0.2.40}/gui/session_utils.py +0 -0
  94. {dulus-0.2.39 → dulus-0.2.40}/gui/settings_dialog.py +0 -0
  95. {dulus-0.2.39 → dulus-0.2.40}/gui/sidebar.py +0 -0
  96. {dulus-0.2.39 → dulus-0.2.40}/gui/tasks_view.py +0 -0
  97. {dulus-0.2.39 → dulus-0.2.40}/gui/themes.py +0 -0
  98. {dulus-0.2.39 → dulus-0.2.40}/gui/tool_panel.py +0 -0
  99. {dulus-0.2.39 → dulus-0.2.40}/input.py +0 -0
  100. {dulus-0.2.39 → dulus-0.2.40}/memory/__init__.py +0 -0
  101. {dulus-0.2.39 → dulus-0.2.40}/memory/audit.py +0 -0
  102. {dulus-0.2.39 → dulus-0.2.40}/memory/consolidator.py +0 -0
  103. {dulus-0.2.39 → dulus-0.2.40}/memory/context.py +0 -0
  104. {dulus-0.2.39 → dulus-0.2.40}/memory/offload.py +0 -0
  105. {dulus-0.2.39 → dulus-0.2.40}/memory/palace.py +0 -0
  106. {dulus-0.2.39 → dulus-0.2.40}/memory/scan.py +0 -0
  107. {dulus-0.2.39 → dulus-0.2.40}/memory/sessions.py +0 -0
  108. {dulus-0.2.39 → dulus-0.2.40}/memory/store.py +0 -0
  109. {dulus-0.2.39 → dulus-0.2.40}/memory/tools.py +0 -0
  110. {dulus-0.2.39 → dulus-0.2.40}/memory/types.py +0 -0
  111. {dulus-0.2.39 → dulus-0.2.40}/memory/vector_search.py +0 -0
  112. {dulus-0.2.39 → dulus-0.2.40}/multi_agent/__init__.py +0 -0
  113. {dulus-0.2.39 → dulus-0.2.40}/multi_agent/subagent.py +0 -0
  114. {dulus-0.2.39 → dulus-0.2.40}/multi_agent/tools.py +0 -0
  115. {dulus-0.2.39 → dulus-0.2.40}/offload_helper.py +0 -0
  116. {dulus-0.2.39 → dulus-0.2.40}/plugin/__init__.py +0 -0
  117. {dulus-0.2.39 → dulus-0.2.40}/plugin/autoadapter.py +0 -0
  118. {dulus-0.2.39 → dulus-0.2.40}/plugin/loader.py +0 -0
  119. {dulus-0.2.39 → dulus-0.2.40}/plugin/recommend.py +0 -0
  120. {dulus-0.2.39 → dulus-0.2.40}/plugin/store.py +0 -0
  121. {dulus-0.2.39 → dulus-0.2.40}/plugin/types.py +0 -0
  122. {dulus-0.2.39 → dulus-0.2.40}/providers.py +0 -0
  123. {dulus-0.2.39 → dulus-0.2.40}/setup.cfg +0 -0
  124. {dulus-0.2.39 → dulus-0.2.40}/skill/__init__.py +0 -0
  125. {dulus-0.2.39 → dulus-0.2.40}/skill/builtin.py +0 -0
  126. {dulus-0.2.39 → dulus-0.2.40}/skill/clawhub.py +0 -0
  127. {dulus-0.2.39 → dulus-0.2.40}/skill/executor.py +0 -0
  128. {dulus-0.2.39 → dulus-0.2.40}/skill/loader.py +0 -0
  129. {dulus-0.2.39 → dulus-0.2.40}/skill/tools.py +0 -0
  130. {dulus-0.2.39 → dulus-0.2.40}/skills.py +0 -0
  131. {dulus-0.2.39 → dulus-0.2.40}/spinner.py +0 -0
  132. {dulus-0.2.39 → dulus-0.2.40}/string_utils.py +0 -0
  133. {dulus-0.2.39 → dulus-0.2.40}/subagent.py +0 -0
  134. {dulus-0.2.39 → dulus-0.2.40}/task/__init__.py +0 -0
  135. {dulus-0.2.39 → dulus-0.2.40}/task/store.py +0 -0
  136. {dulus-0.2.39 → dulus-0.2.40}/task/tools.py +0 -0
  137. {dulus-0.2.39 → dulus-0.2.40}/task/types.py +0 -0
  138. {dulus-0.2.39 → dulus-0.2.40}/tests/test_afk_yolo.py +0 -0
  139. {dulus-0.2.39 → dulus-0.2.40}/tests/test_approval_runtime.py +0 -0
  140. {dulus-0.2.39 → dulus-0.2.40}/tests/test_background_task_tools.py +0 -0
  141. {dulus-0.2.39 → dulus-0.2.40}/tests/test_background_tasks.py +0 -0
  142. {dulus-0.2.39 → dulus-0.2.40}/tests/test_checkpoint.py +0 -0
  143. {dulus-0.2.39 → dulus-0.2.40}/tests/test_clipboard_utils.py +0 -0
  144. {dulus-0.2.39 → dulus-0.2.40}/tests/test_compaction.py +0 -0
  145. {dulus-0.2.39 → dulus-0.2.40}/tests/test_diff_view.py +0 -0
  146. {dulus-0.2.39 → dulus-0.2.40}/tests/test_diff_visualization.py +0 -0
  147. {dulus-0.2.39 → dulus-0.2.40}/tests/test_display_blocks.py +0 -0
  148. {dulus-0.2.39 → dulus-0.2.40}/tests/test_export_import.py +0 -0
  149. {dulus-0.2.39 → dulus-0.2.40}/tests/test_hook_engine.py +0 -0
  150. {dulus-0.2.39 → dulus-0.2.40}/tests/test_injection_fix.py +0 -0
  151. {dulus-0.2.39 → dulus-0.2.40}/tests/test_license.py +0 -0
  152. {dulus-0.2.39 → dulus-0.2.40}/tests/test_mcp.py +0 -0
  153. {dulus-0.2.39 → dulus-0.2.40}/tests/test_memory.py +0 -0
  154. {dulus-0.2.39 → dulus-0.2.40}/tests/test_notification_manager.py +0 -0
  155. {dulus-0.2.39 → dulus-0.2.40}/tests/test_plugin.py +0 -0
  156. {dulus-0.2.39 → dulus-0.2.40}/tests/test_session_fork.py +0 -0
  157. {dulus-0.2.39 → dulus-0.2.40}/tests/test_shell_mode.py +0 -0
  158. {dulus-0.2.39 → dulus-0.2.40}/tests/test_skills.py +0 -0
  159. {dulus-0.2.39 → dulus-0.2.40}/tests/test_steer_input.py +0 -0
  160. {dulus-0.2.39 → dulus-0.2.40}/tests/test_subagent.py +0 -0
  161. {dulus-0.2.39 → dulus-0.2.40}/tests/test_task.py +0 -0
  162. {dulus-0.2.39 → dulus-0.2.40}/tests/test_telegram_buffer.py +0 -0
  163. {dulus-0.2.39 → dulus-0.2.40}/tests/test_think_tool.py +0 -0
  164. {dulus-0.2.39 → dulus-0.2.40}/tests/test_todo_tool.py +0 -0
  165. {dulus-0.2.39 → dulus-0.2.40}/tests/test_todo_visualization.py +0 -0
  166. {dulus-0.2.39 → dulus-0.2.40}/tests/test_tool_registry.py +0 -0
  167. {dulus-0.2.39 → dulus-0.2.40}/tests/test_voice.py +0 -0
  168. {dulus-0.2.39 → dulus-0.2.40}/tests/test_wire_events.py +0 -0
  169. {dulus-0.2.39 → dulus-0.2.40}/tmux_offloader.py +0 -0
  170. {dulus-0.2.39 → dulus-0.2.40}/tmux_tools.py +0 -0
  171. {dulus-0.2.39 → dulus-0.2.40}/tool_registry.py +0 -0
  172. {dulus-0.2.39 → dulus-0.2.40}/tools.py +0 -0
  173. {dulus-0.2.39 → dulus-0.2.40}/ui/__init__.py +0 -0
  174. {dulus-0.2.39 → dulus-0.2.40}/ui/input.py +0 -0
  175. {dulus-0.2.39 → dulus-0.2.40}/ui/render.py +0 -0
  176. {dulus-0.2.39 → dulus-0.2.40}/voice/__init__.py +0 -0
  177. {dulus-0.2.39 → dulus-0.2.40}/voice/keyterms.py +0 -0
  178. {dulus-0.2.39 → dulus-0.2.40}/voice/recorder.py +0 -0
  179. {dulus-0.2.39 → dulus-0.2.40}/voice/stt.py +0 -0
  180. {dulus-0.2.39 → dulus-0.2.40}/voice/tts.py +0 -0
  181. {dulus-0.2.39 → dulus-0.2.40}/webchat.py +0 -0
  182. {dulus-0.2.39 → dulus-0.2.40}/webchat_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.39
3
+ Version: 0.2.40
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -22,8 +22,6 @@ Classifier: Topic :: Terminals
22
22
  Requires-Python: >=3.11
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- License-File: license_manager.py
26
- License-File: license_server.py
27
25
  Requires-Dist: anthropic>=0.40.0
28
26
  Requires-Dist: openai>=1.30.0
29
27
  Requires-Dist: httpx>=0.27.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.39
3
+ Version: 0.2.40
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -22,8 +22,6 @@ Classifier: Topic :: Terminals
22
22
  Requires-Python: >=3.11
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- License-File: license_manager.py
26
- License-File: license_server.py
27
25
  Requires-Dist: anthropic>=0.40.0
28
26
  Requires-Dist: openai>=1.30.0
29
27
  Requires-Dist: httpx>=0.27.0
@@ -13,8 +13,6 @@ context.py
13
13
  dulus.py
14
14
  dulus_gui.py
15
15
  input.py
16
- license_manager.py
17
- license_server.py
18
16
  offload_helper.py
19
17
  providers.py
20
18
  pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dulus"
7
- version = "0.2.39"
7
+ version = "0.2.40"
8
8
  description = "Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,187 +0,0 @@
1
- """Dulus License Manager — Offline-first key validation + feature gating.
2
-
3
- Tiers:
4
- FREE No key required. Limited tool calls, local providers only.
5
- PRO $15/mo. Full features, BYOK, priority support.
6
- ENTERPRISE $50/mo. Team features + admin dashboard + SSO (future).
7
-
8
- Key format (offline):
9
- DULUS-<base64(json_payload + ":" + hmac_signature)>
10
-
11
- The secret lives in ~/.dulus/.license_secret (never commit this file).
12
- If the secret file is missing we fall back to a hardcoded dev-key so
13
- Kev can develop without friction, but distribution builds MUST bundle
14
- a real secret via CI env var or PyInstaller --add-data.
15
- """
16
- from __future__ import annotations
17
-
18
- import base64
19
- import hashlib
20
- import hmac
21
- import json
22
- import os
23
- import sys
24
- import time
25
- from pathlib import Path
26
- from typing import Optional
27
-
28
- # ── Secret resolution ───────────────────────────────────────────────────────
29
- # 1. CI / build-time env var (safest for releases)
30
- # 2. ~/.dulus/.license_secret (Kev's local dev key)
31
- # 3. Fallback dev secret (NEVER use in production builds)
32
- _LICENSE_SECRET = os.environ.get("DULUS_LICENSE_SECRET", "")
33
- if not _LICENSE_SECRET:
34
- _secret_path = Path.home() / ".dulus" / ".license_secret"
35
- if _secret_path.exists():
36
- _LICENSE_SECRET = _secret_path.read_text().strip()
37
- else:
38
- _LICENSE_SECRET = "dulus-dev-secret-do-not-distribute"
39
- import warnings
40
- warnings.warn(
41
- "DULUS_LICENSE_SECRET not set — using hardcoded DEV secret. "
42
- "Generated keys will be trivially forgeable in production!",
43
- RuntimeWarning,
44
- stacklevel=2,
45
- )
46
-
47
-
48
- class LicenseTier:
49
- FREE = "free"
50
- PRO = "pro"
51
- ENTERPRISE = "enterprise"
52
-
53
-
54
- class LicenseManager:
55
- """Parse and validate a Dulus license key."""
56
-
57
- def __init__(self, key: Optional[str] = None):
58
- self.raw_key = key or ""
59
- self.tier = LicenseTier.FREE
60
- self.expiry: float = 0.0
61
- self.features: list[str] = []
62
- self.valid = False
63
- self.error: Optional[str] = None
64
-
65
- if self.raw_key:
66
- self._validate()
67
-
68
- # ── validation core ─────────────────────────────────────────────────────
69
-
70
- def _validate(self) -> None:
71
- if not self.raw_key.startswith("DULUS-"):
72
- self.error = "Invalid key prefix"
73
- return
74
-
75
- try:
76
- b64 = self.raw_key.split("-", 1)[1]
77
- payload_sig = base64.urlsafe_b64decode(b64 + "==")
78
- payload_json, sig_hex = payload_sig.rsplit(b":", 1)
79
- data = json.loads(payload_json)
80
- except Exception as exc:
81
- self.error = f"Malformed key: {exc}"
82
- return
83
-
84
- # Verify HMAC-SHA256 signature
85
- expected_sig = hmac.new(
86
- _LICENSE_SECRET.encode(),
87
- payload_json,
88
- hashlib.sha256,
89
- ).hexdigest()[:24]
90
-
91
- if not hmac.compare_digest(sig_hex.decode(), expected_sig):
92
- self.error = "Invalid signature (tampered or wrong secret)"
93
- return
94
-
95
- self.tier = data.get("tier", LicenseTier.FREE)
96
- self.expiry = data.get("exp", 0)
97
- self.features = data.get("features", [])
98
-
99
- if time.time() > self.expiry:
100
- self.error = "License expired"
101
- return
102
-
103
- self.valid = True
104
-
105
- # ── feature gates ───────────────────────────────────────────────────────
106
-
107
- def can_use(self, feature: str) -> bool:
108
- """Check if a feature is allowed by current tier."""
109
- if self.tier == LicenseTier.ENTERPRISE:
110
- return True
111
- if self.tier == LicenseTier.PRO:
112
- return feature not in {"sso", "audit_logs", "admin_dashboard"}
113
- # FREE
114
- free_features = {"chat", "tools_basic", "local_providers"}
115
- return feature in free_features
116
-
117
- def max_tool_calls(self) -> int:
118
- if self.tier == LicenseTier.ENTERPRISE:
119
- return 999_999
120
- if self.tier == LicenseTier.PRO:
121
- return 10_000
122
- return 25 # FREE daily limit
123
-
124
- def max_providers(self) -> int:
125
- if self.tier in (LicenseTier.PRO, LicenseTier.ENTERPRISE):
126
- return 99
127
- return 2 # FREE: e.g. ollama + 1 cloud
128
-
129
- def max_subagents(self) -> int:
130
- if self.tier == LicenseTier.ENTERPRISE:
131
- return 50
132
- if self.tier == LicenseTier.PRO:
133
- return 10
134
- return 0 # FREE: no subagents
135
-
136
- def max_plugins(self) -> int:
137
- if self.tier == LicenseTier.ENTERPRISE:
138
- return 999
139
- if self.tier == LicenseTier.PRO:
140
- return 50
141
- return 3 # FREE
142
-
143
- def allow_cloudsave(self) -> bool:
144
- return self.tier in (LicenseTier.PRO, LicenseTier.ENTERPRISE)
145
-
146
- def allow_voice(self) -> bool:
147
- return self.tier in (LicenseTier.PRO, LicenseTier.ENTERPRISE)
148
-
149
- def allow_telegram(self) -> bool:
150
- return self.tier in (LicenseTier.PRO, LicenseTier.ENTERPRISE)
151
-
152
- def allow_mcp(self) -> bool:
153
- return self.tier in (LicenseTier.PRO, LicenseTier.ENTERPRISE)
154
-
155
- # ── UI helpers ──────────────────────────────────────────────────────────
156
-
157
- def status_banner(self) -> str:
158
- if self.error:
159
- return f"[LICENSE EXPIRED / INVALID] {self.error} — running in FREE mode"
160
- if self.tier == LicenseTier.FREE:
161
- return "[FREE] Limited features. Upgrade: https://getdulus.dev/pro"
162
- return f"[{self.tier.upper()}] Valid until {time.strftime('%Y-%m-%d', time.localtime(self.expiry))}"
163
-
164
-
165
- # ── CLI helper for Kev ─────────────────────────────────────────────────────
166
-
167
- def _generate_key(tier: str, days: int, secret: str) -> str:
168
- """Generate a signed license key (Kev-only tool)."""
169
- payload = json.dumps({
170
- "tier": tier,
171
- "exp": int(time.time() + days * 86400),
172
- "features": [],
173
- "iat": int(time.time()),
174
- }, separators=(",", ":")).encode()
175
- sig = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()[:24]
176
- token = base64.urlsafe_b64encode(payload + b":" + sig.encode()).decode().rstrip("=")
177
- return f"DULUS-{token}"
178
-
179
-
180
- if __name__ == "__main__":
181
- import argparse
182
- ap = argparse.ArgumentParser(description="Dulus License Key Generator (Kev only)")
183
- ap.add_argument("tier", choices=["free", "pro", "enterprise"])
184
- ap.add_argument("--days", type=int, default=30)
185
- ap.add_argument("--secret", default=_LICENSE_SECRET)
186
- args = ap.parse_args()
187
- print(_generate_key(args.tier, args.days, args.secret))
@@ -1,63 +0,0 @@
1
- """License server validation helpers.
2
-
3
- Mirrors the signing logic in license_manager.py for cross-validation.
4
- """
5
- from __future__ import annotations
6
-
7
- import base64
8
- import hashlib
9
- import hmac
10
- import json
11
-
12
-
13
- def parse_key(key: str) -> dict:
14
- """Parse a DULUS license key into its components.
15
-
16
- Returns a dict with keys:
17
- - payload_b64: base64-encoded payload bytes
18
- - sig: the hex signature string
19
- - payload: the decoded payload dict
20
- On error returns {"error": "..."}.
21
- """
22
- if not key.startswith("DULUS-"):
23
- return {"error": "Invalid key format"}
24
-
25
- token = key[6:] # strip DULUS-
26
- # Add padding if needed
27
- padding = 4 - len(token) % 4
28
- if padding != 4:
29
- token += "=" * padding
30
-
31
- try:
32
- raw = base64.urlsafe_b64decode(token.encode())
33
- except Exception as e:
34
- return {"error": f"Base64 decode failed: {e}"}
35
-
36
- if b":" not in raw:
37
- return {"error": "Missing signature separator"}
38
-
39
- payload_bytes, sig_bytes = raw.rsplit(b":", 1)
40
- try:
41
- payload = json.loads(payload_bytes)
42
- except Exception as e:
43
- return {"error": f"Invalid JSON payload: {e}"}
44
-
45
- payload_b64 = base64.b64encode(payload_bytes).decode()
46
- sig = sig_bytes.decode()
47
-
48
- return {
49
- "payload_b64": payload_b64,
50
- "sig": sig,
51
- "payload": payload,
52
- }
53
-
54
-
55
- def _verify_payload(payload_b64: str, sig: str, secret: str) -> bool:
56
- """Verify that *sig* matches HMAC-SHA256 of the payload using *secret*."""
57
- try:
58
- payload_bytes = base64.b64decode(payload_b64.encode())
59
- except Exception:
60
- return False
61
-
62
- expected = hmac.new(secret.encode(), payload_bytes, hashlib.sha256).hexdigest()[:24]
63
- return hmac.compare_digest(expected, sig)
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
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