delimit-cli 3.15.2 → 3.15.4

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.
@@ -37,7 +37,37 @@ INBOX_ROUTING_FILE = Path.home() / ".delimit" / "inbox_routing.jsonl"
37
37
  IMAP_HOST = "mail.spacemail.com"
38
38
  IMAP_PORT = 993
39
39
  IMAP_USER = "pro@delimit.ai"
40
- FORWARD_TO = os.environ.get("DELIMIT_FORWARD_TO", "")
40
+ def _resolve_forward_to():
41
+ """Resolve forward email from env or secrets broker."""
42
+ # 1. Environment variable (highest priority)
43
+ val = os.environ.get("DELIMIT_FORWARD_TO", "")
44
+ if val:
45
+ return val
46
+ # 2. DELIMIT_SMTP_TO env var
47
+ val = os.environ.get("DELIMIT_SMTP_TO", "")
48
+ if val:
49
+ return val
50
+ # 3. Read from secrets broker config
51
+ try:
52
+ import json as _json
53
+ from pathlib import Path as _Path
54
+ # Check smtp-all.json for configured accounts
55
+ smtp_all = _Path.home() / ".delimit" / "secrets" / "smtp-all.json"
56
+ if smtp_all.exists():
57
+ data = _json.loads(smtp_all.read_text())
58
+ # The forward target is typically stored separately
59
+ # Check for a dedicated forward-to secret
60
+ fwd_file = _Path.home() / ".delimit" / "secrets" / "forward-to.json"
61
+ if fwd_file.exists():
62
+ fwd_data = _json.loads(fwd_file.read_text())
63
+ val = fwd_data.get("value", fwd_data.get("email", ""))
64
+ if val:
65
+ return val
66
+ except Exception:
67
+ pass
68
+ return ""
69
+
70
+ FORWARD_TO = _resolve_forward_to()
41
71
 
42
72
  # Domains/senders whose emails require owner action
43
73
  OWNER_ACTION_DOMAINS = {
@@ -0,0 +1,173 @@
1
+ """delimit.yml — committable project configuration (STR-049).
2
+
3
+ A single YAML file that teams commit to their repo. Defines:
4
+ - Context directories (what the AI should know about)
5
+ - Preferred models per task type
6
+ - Policy preset
7
+ - Playbook references
8
+ - Governance mode (advisory/guarded/enforce)
9
+
10
+ Focus group: "My AI setup on my laptop should match my teammate's."
11
+ """
12
+
13
+ import json
14
+ import time
15
+ from pathlib import Path
16
+ from typing import Any, Dict, Optional
17
+
18
+ try:
19
+ import yaml as _yaml
20
+ except ImportError:
21
+ _yaml = None
22
+
23
+
24
+ DEFAULT_CONFIG = {
25
+ "version": 1,
26
+ "governance": {
27
+ "mode": "advisory",
28
+ "preset": "default",
29
+ },
30
+ "context": {
31
+ "include": ["src/", "lib/", "app/"],
32
+ "exclude": ["node_modules/", ".git/", "dist/", "__pycache__/"],
33
+ },
34
+ "models": {
35
+ "default": "auto",
36
+ "tasks": {
37
+ "refactoring": "claude-opus",
38
+ "testing": "claude-sonnet",
39
+ "documentation": "gemini-flash",
40
+ "debugging": "auto",
41
+ },
42
+ },
43
+ "playbooks": [],
44
+ "team": {
45
+ "shared_memory": True,
46
+ "shared_ledger": True,
47
+ "require_approval_for": ["deploy", "publish"],
48
+ },
49
+ }
50
+
51
+ CONFIG_FILENAMES = ["delimit.yml", "delimit.yaml", ".delimit.yml", ".delimit.yaml"]
52
+
53
+
54
+ def find_project_config(project_path: str = ".") -> Optional[Path]:
55
+ """Find delimit.yml in the project directory or parents."""
56
+ p = Path(project_path).resolve()
57
+ for _ in range(10): # Max 10 parent dirs
58
+ for name in CONFIG_FILENAMES:
59
+ candidate = p / name
60
+ if candidate.exists():
61
+ return candidate
62
+ parent = p.parent
63
+ if parent == p:
64
+ break
65
+ p = parent
66
+ return None
67
+
68
+
69
+ def load_project_config(project_path: str = ".") -> Dict[str, Any]:
70
+ """Load project config from delimit.yml or return defaults."""
71
+ config_file = find_project_config(project_path)
72
+
73
+ if not config_file:
74
+ return {
75
+ "status": "no_config",
76
+ "config": DEFAULT_CONFIG,
77
+ "source": "defaults",
78
+ "message": "No delimit.yml found. Using defaults. Run delimit_project_init to create one.",
79
+ }
80
+
81
+ try:
82
+ content = config_file.read_text()
83
+ if _yaml:
84
+ config = _yaml.safe_load(content)
85
+ else:
86
+ config = json.loads(content)
87
+
88
+ # Merge with defaults for missing keys
89
+ merged = {**DEFAULT_CONFIG}
90
+ if isinstance(config, dict):
91
+ for key in config:
92
+ if key in merged and isinstance(merged[key], dict) and isinstance(config[key], dict):
93
+ merged[key] = {**merged[key], **config[key]}
94
+ else:
95
+ merged[key] = config[key]
96
+
97
+ return {
98
+ "status": "loaded",
99
+ "config": merged,
100
+ "source": str(config_file),
101
+ "message": f"Loaded from {config_file.name}",
102
+ }
103
+ except Exception as e:
104
+ return {
105
+ "status": "error",
106
+ "config": DEFAULT_CONFIG,
107
+ "source": str(config_file),
108
+ "error": str(e),
109
+ "message": f"Error loading {config_file.name}: {e}. Using defaults.",
110
+ }
111
+
112
+
113
+ def init_project_config(
114
+ project_path: str = ".",
115
+ mode: str = "advisory",
116
+ preset: str = "default",
117
+ ) -> Dict[str, Any]:
118
+ """Create a delimit.yml in the project root."""
119
+ p = Path(project_path).resolve()
120
+
121
+ # Check if already exists
122
+ existing = find_project_config(project_path)
123
+ if existing:
124
+ return {
125
+ "status": "exists",
126
+ "path": str(existing),
127
+ "message": f"Config already exists at {existing}",
128
+ }
129
+
130
+ config = dict(DEFAULT_CONFIG)
131
+ config["governance"]["mode"] = mode
132
+ config["governance"]["preset"] = preset
133
+
134
+ # Auto-detect context dirs
135
+ detected_dirs = []
136
+ for d in ["src", "lib", "app", "api", "server", "client", "packages"]:
137
+ if (p / d).exists():
138
+ detected_dirs.append(f"{d}/")
139
+ if detected_dirs:
140
+ config["context"]["include"] = detected_dirs
141
+
142
+ config_path = p / "delimit.yml"
143
+
144
+ if _yaml:
145
+ content = _yaml.dump(config, default_flow_style=False, sort_keys=False)
146
+ else:
147
+ content = json.dumps(config, indent=2)
148
+
149
+ config_path.write_text(content)
150
+
151
+ return {
152
+ "status": "created",
153
+ "path": str(config_path),
154
+ "config": config,
155
+ "message": f"Created delimit.yml with {mode} mode, {preset} preset",
156
+ }
157
+
158
+
159
+ def get_model_for_task(task_type: str, project_path: str = ".") -> Dict[str, Any]:
160
+ """Get the recommended model for a specific task type."""
161
+ result = load_project_config(project_path)
162
+ config = result.get("config", DEFAULT_CONFIG)
163
+
164
+ models = config.get("models", {})
165
+ tasks = models.get("tasks", {})
166
+
167
+ model = tasks.get(task_type, models.get("default", "auto"))
168
+
169
+ return {
170
+ "task": task_type,
171
+ "model": model,
172
+ "source": result.get("source", "defaults"),
173
+ }
@@ -3631,6 +3631,42 @@ STANDARD_WORKFLOWS = [
3631
3631
  ]
3632
3632
 
3633
3633
 
3634
+ @mcp.tool()
3635
+ def delimit_project_config(action: str = "load", project_path: str = ".",
3636
+ mode: str = "advisory", preset: str = "default",
3637
+ task_type: str = "") -> Dict[str, Any]:
3638
+ """Manage delimit.yml project configuration.
3639
+
3640
+ A committable YAML file that defines AI governance for your repo.
3641
+ Your teammates get the same AI setup when they clone.
3642
+
3643
+ Actions:
3644
+ load: Read current project config (or defaults if no delimit.yml)
3645
+ init: Create a delimit.yml in your project root
3646
+ model: Get recommended model for a task type
3647
+
3648
+ Args:
3649
+ action: "load", "init", or "model".
3650
+ project_path: Project root directory.
3651
+ mode: Governance mode for init (advisory/guarded/enforce).
3652
+ preset: Policy preset for init (strict/default/relaxed).
3653
+ task_type: Task type for model lookup (refactoring/testing/docs/debugging).
3654
+ """
3655
+ from ai.project_config import load_project_config, init_project_config, get_model_for_task
3656
+
3657
+ if action == "init":
3658
+ return _with_next_steps("project_config", _safe_call(
3659
+ init_project_config, project_path=project_path, mode=mode, preset=preset,
3660
+ ))
3661
+ if action == "model":
3662
+ return _with_next_steps("project_config", _safe_call(
3663
+ get_model_for_task, task_type=task_type, project_path=project_path,
3664
+ ))
3665
+ return _with_next_steps("project_config", _safe_call(
3666
+ load_project_config, project_path=project_path,
3667
+ ))
3668
+
3669
+
3634
3670
  @mcp.tool()
3635
3671
  def delimit_playbook(action: str = "list", name: str = "", prompt: str = "",
3636
3672
  description: str = "", variables: str = "",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
3
  "mcpName": "io.github.delimit-ai/delimit-mcp-server",
4
- "version": "3.15.2",
4
+ "version": "3.15.4",
5
5
  "description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
6
6
  "main": "index.js",
7
7
  "files": [