dulus 0.2.40__tar.gz → 0.2.41__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.
- {dulus-0.2.40/dulus.egg-info → dulus-0.2.41}/PKG-INFO +2 -1
- {dulus-0.2.40 → dulus-0.2.41/dulus.egg-info}/PKG-INFO +2 -1
- {dulus-0.2.40 → dulus-0.2.41}/dulus.egg-info/SOURCES.txt +1 -0
- dulus-0.2.41/license_manager.py +187 -0
- {dulus-0.2.40 → dulus-0.2.41}/pyproject.toml +1 -1
- {dulus-0.2.40 → dulus-0.2.41}/LICENSE +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/MANIFEST.in +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/README.md +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/agent.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/backend/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/backend/compressor.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/backend/context.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/backend/githook.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/backend/marketplace.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/backend/mempalace_bridge.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/backend/personas.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/backend/plugins.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/backend/server.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/backend/tasks.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/batch_api.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/checkpoint/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/checkpoint/hooks.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/checkpoint/store.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/checkpoint/types.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/claude_code_watcher.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/clipboard_utils.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/cloudsave.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/common.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/compaction.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/config.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/context.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/data/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/data/active_persona.json +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/data/context.json +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/data/marketplace.json +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/data/personas.json +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/data/plugins/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/data/plugins/composio/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/data/plugins/composio/composio_plugin/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/data/plugins/composio/plugin.json +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/data/plugins/composio/plugin_tool.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/data/tasks.json +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/README.md +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/api.html +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/architecture.md +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/azure-speech-template.json +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/dashboard/index.html +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/divider.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/generate.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/hero.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/index.html +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/news.md +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/nvidia-models.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/particle-playground.html +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/personas/index.html +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/poetry-banner.png +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/preview.html +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/sec-agents.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/sec-brainstorm.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/sec-bridges.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/sec-features.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/sec-freetier.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/sec-memory.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/sec-models.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/sec-perms.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/sec-plugins.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/sec-quickstart.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/sec-ssj.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/spinners.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/split-pane.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/terminal-boot.svg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/docs/uploads/particle-playground.html +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/dulus.egg-info/dependency_links.txt +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/dulus.egg-info/entry_points.txt +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/dulus.egg-info/requires.txt +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/dulus.egg-info/top_level.txt +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/dulus.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/dulus_gui.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/dulus_mcp/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/dulus_mcp/client.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/dulus_mcp/config.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/dulus_mcp/tools.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/dulus_mcp/types.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/gui/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/gui/agent_bridge.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/gui/chat_widget.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/gui/main_window.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/gui/personas.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/gui/session_utils.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/gui/settings_dialog.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/gui/sidebar.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/gui/tasks_view.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/gui/themes.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/gui/tool_panel.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/input.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/memory/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/memory/audit.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/memory/consolidator.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/memory/context.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/memory/offload.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/memory/palace.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/memory/scan.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/memory/sessions.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/memory/store.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/memory/tools.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/memory/types.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/memory/vector_search.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/multi_agent/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/multi_agent/subagent.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/multi_agent/tools.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/offload_helper.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/plugin/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/plugin/autoadapter.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/plugin/loader.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/plugin/recommend.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/plugin/store.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/plugin/types.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/providers.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/setup.cfg +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/skill/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/skill/builtin.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/skill/clawhub.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/skill/executor.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/skill/loader.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/skill/tools.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/skills.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/spinner.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/string_utils.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/subagent.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/task/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/task/store.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/task/tools.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/task/types.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_afk_yolo.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_approval_runtime.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_background_task_tools.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_background_tasks.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_checkpoint.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_clipboard_utils.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_compaction.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_diff_view.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_diff_visualization.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_display_blocks.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_export_import.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_hook_engine.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_injection_fix.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_license.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_mcp.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_memory.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_notification_manager.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_plugin.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_session_fork.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_shell_mode.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_skills.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_steer_input.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_subagent.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_task.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_telegram_buffer.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_think_tool.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_todo_tool.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_todo_visualization.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_tool_registry.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_voice.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tests/test_wire_events.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tmux_offloader.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tmux_tools.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tool_registry.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/tools.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/ui/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/ui/input.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/ui/render.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/voice/__init__.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/voice/keyterms.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/voice/recorder.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/voice/stt.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/voice/tts.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/webchat.py +0 -0
- {dulus-0.2.40 → dulus-0.2.41}/webchat_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dulus
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.41
|
|
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,6 +22,7 @@ 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
|
|
25
26
|
Requires-Dist: anthropic>=0.40.0
|
|
26
27
|
Requires-Dist: openai>=1.30.0
|
|
27
28
|
Requires-Dist: httpx>=0.27.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dulus
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.41
|
|
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,6 +22,7 @@ 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
|
|
25
26
|
Requires-Dist: anthropic>=0.40.0
|
|
26
27
|
Requires-Dist: openai>=1.30.0
|
|
27
28
|
Requires-Dist: httpx>=0.27.0
|
|
@@ -0,0 +1,187 @@
|
|
|
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))
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dulus"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.41"
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|