delimit-cli 3.15.2 → 3.15.3
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.
- package/gateway/ai/notify.py +31 -1
- package/gateway/ai/project_config.py +173 -0
- package/gateway/ai/server.py +36 -0
- package/package.json +1 -1
package/gateway/ai/notify.py
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/gateway/ai/server.py
CHANGED
|
@@ -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.
|
|
4
|
+
"version": "3.15.3",
|
|
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": [
|