agent-devkit 0.0.4 → 0.1.5
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/README.md +42 -2
- package/package.json +1 -1
- package/runtime/README.md +50 -2
- package/runtime/agent +29 -13
- package/runtime/agents/README.md +3 -0
- package/runtime/agents/github-pr-reviewer/AGENTS.md +10 -0
- package/runtime/agents/github-pr-reviewer/README.md +7 -0
- package/runtime/agents/github-pr-reviewer/agent.yaml +33 -0
- package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/decision-rules.md +8 -0
- package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/workflow.md +9 -0
- package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/decision-rules.md +7 -0
- package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/workflow.md +8 -0
- package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/decision-rules.md +7 -0
- package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/workflow.md +8 -0
- package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/decision-rules.md +9 -0
- package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/workflow.md +10 -0
- package/runtime/agents/github-pr-reviewer/infra/README.md +7 -0
- package/runtime/agents/github-pr-reviewer/knowledge/context.md +7 -0
- package/runtime/agents/github-pr-reviewer/knowledge/system.md +17 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-automation-output.md +10 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-inspection-output.md +9 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-list-output.md +9 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-review-output.md +17 -0
- package/runtime/cli/README.md +37 -1
- package/runtime/cli/aikit/__init__.py +1 -1
- package/runtime/cli/aikit/aliases.py +196 -0
- package/runtime/cli/aikit/app_home.py +78 -0
- package/runtime/cli/aikit/audit.py +344 -0
- package/runtime/cli/aikit/calendar.py +163 -0
- package/runtime/cli/aikit/configuration_orchestrator.py +291 -0
- package/runtime/cli/aikit/decision_store.py +158 -0
- package/runtime/cli/aikit/diagnostics.py +9 -0
- package/runtime/cli/aikit/fallback.py +48 -2
- package/runtime/cli/aikit/github_pr.py +254 -0
- package/runtime/cli/aikit/identity.py +75 -0
- package/runtime/cli/aikit/llm.py +249 -48
- package/runtime/cli/aikit/main.py +1240 -32
- package/runtime/cli/aikit/memory.py +148 -8
- package/runtime/cli/aikit/model_router.py +70 -0
- package/runtime/cli/aikit/notifications.py +9 -0
- package/runtime/cli/aikit/ollama.py +237 -0
- package/runtime/cli/aikit/permissions.py +345 -0
- package/runtime/cli/aikit/personality.py +123 -0
- package/runtime/cli/aikit/provider_wizard.py +19 -0
- package/runtime/cli/aikit/providers.py +4 -5
- package/runtime/cli/aikit/review_gate.py +40 -0
- package/runtime/cli/aikit/scheduler.py +18 -0
- package/runtime/cli/aikit/sessions.py +396 -0
- package/runtime/cli/aikit/setup_wizard.py +12 -0
- package/runtime/cli/aikit/tasks.py +351 -0
- package/runtime/cli/aikit/toolchain.py +274 -0
- package/runtime/providers/calendar.yaml +28 -0
- package/runtime/providers/github.yaml +42 -0
- package/runtime/providers/local-notification.yaml +19 -0
- package/runtime/providers/local-scheduler.yaml +24 -0
- package/runtime/vendor/skills/napkin/napkin.md +13 -3
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Calendar MVP backed by local ICS files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import date, datetime, timedelta
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from cli.aikit.app_home import config_path, ensure_app_home
|
|
11
|
+
from cli.aikit.llm import load_config, save_config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DATE_COMPACT = "%Y%m%d"
|
|
15
|
+
DATETIME_COMPACT = "%Y%m%dT%H%M%S"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def configure_calendar(*, ics_path: str | None = None, timezone: str | None = None) -> dict[str, Any]:
|
|
19
|
+
if not ics_path:
|
|
20
|
+
return calendar_needs_input()
|
|
21
|
+
path = Path(ics_path).expanduser().resolve()
|
|
22
|
+
if not path.exists():
|
|
23
|
+
raise ValueError(f"calendar ics file not found: {ics_path}")
|
|
24
|
+
config = load_config()
|
|
25
|
+
calendar = config.setdefault("calendar", {})
|
|
26
|
+
calendar["provider"] = "ics"
|
|
27
|
+
calendar["source_ref"] = str(path)
|
|
28
|
+
if timezone:
|
|
29
|
+
calendar["timezone"] = timezone
|
|
30
|
+
written = save_config(config)
|
|
31
|
+
return {
|
|
32
|
+
"kind": "calendar-configure",
|
|
33
|
+
"status": "configured",
|
|
34
|
+
"provider": "ics",
|
|
35
|
+
"source_ref": str(path),
|
|
36
|
+
"timezone": timezone,
|
|
37
|
+
"config_path": str(written),
|
|
38
|
+
"stored_secret": False,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def calendar_today() -> dict[str, Any]:
|
|
43
|
+
today = date.today()
|
|
44
|
+
return calendar_list(today.isoformat(), today.isoformat(), label="today")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def calendar_tomorrow() -> dict[str, Any]:
|
|
48
|
+
tomorrow = date.today() + timedelta(days=1)
|
|
49
|
+
return calendar_list(tomorrow.isoformat(), tomorrow.isoformat(), label="tomorrow")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def calendar_list(date_from: str | None, date_to: str | None, *, label: str | None = None) -> dict[str, Any]:
|
|
53
|
+
config = load_config()
|
|
54
|
+
calendar = config.get("calendar") if isinstance(config.get("calendar"), dict) else {}
|
|
55
|
+
if calendar.get("provider") != "ics" or not calendar.get("source_ref"):
|
|
56
|
+
return calendar_needs_input()
|
|
57
|
+
start = parse_date(date_from) if date_from else date.today()
|
|
58
|
+
end = parse_date(date_to) if date_to else start
|
|
59
|
+
events = [
|
|
60
|
+
event
|
|
61
|
+
for event in parse_ics(Path(str(calendar["source_ref"])))
|
|
62
|
+
if event_overlaps(event, start, end)
|
|
63
|
+
]
|
|
64
|
+
events.sort(key=lambda item: item.get("start") or "")
|
|
65
|
+
return {
|
|
66
|
+
"kind": "calendar",
|
|
67
|
+
"status": "ok",
|
|
68
|
+
"label": label,
|
|
69
|
+
"provider": "ics",
|
|
70
|
+
"source_ref": str(calendar["source_ref"]),
|
|
71
|
+
"from": start.isoformat(),
|
|
72
|
+
"to": end.isoformat(),
|
|
73
|
+
"events": events,
|
|
74
|
+
"count": len(events),
|
|
75
|
+
"sensitive": True,
|
|
76
|
+
"llm_safe": False,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def calendar_needs_input() -> dict[str, Any]:
|
|
81
|
+
ensure_app_home()
|
|
82
|
+
return {
|
|
83
|
+
"kind": "calendar",
|
|
84
|
+
"status": "needs-input",
|
|
85
|
+
"ok": False,
|
|
86
|
+
"requires_provider": True,
|
|
87
|
+
"config_path": str(config_path()),
|
|
88
|
+
"message": "Calendar provider is not configured.",
|
|
89
|
+
"next_steps": [
|
|
90
|
+
"Configure a local ICS file with `agent calendar configure --ics <path>`.",
|
|
91
|
+
"Then run `agent calendar today` or ask `agent qual minha agenda de hoje?`.",
|
|
92
|
+
],
|
|
93
|
+
"exit_code": 2,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_ics(path: Path) -> list[dict[str, Any]]:
|
|
98
|
+
text = unfold_ics(path.read_text(encoding="utf-8"))
|
|
99
|
+
events: list[dict[str, Any]] = []
|
|
100
|
+
for block in re.findall(r"BEGIN:VEVENT(.*?)END:VEVENT", text, re.DOTALL):
|
|
101
|
+
fields: dict[str, str] = {}
|
|
102
|
+
for raw_line in block.splitlines():
|
|
103
|
+
if ":" not in raw_line:
|
|
104
|
+
continue
|
|
105
|
+
key, value = raw_line.split(":", 1)
|
|
106
|
+
key = key.split(";", 1)[0].upper()
|
|
107
|
+
fields[key] = value.strip()
|
|
108
|
+
start = parse_ics_datetime(fields.get("DTSTART"))
|
|
109
|
+
end = parse_ics_datetime(fields.get("DTEND"))
|
|
110
|
+
events.append(
|
|
111
|
+
{
|
|
112
|
+
"uid": fields.get("UID"),
|
|
113
|
+
"summary": fields.get("SUMMARY") or "(sem titulo)",
|
|
114
|
+
"description": fields.get("DESCRIPTION"),
|
|
115
|
+
"location": fields.get("LOCATION"),
|
|
116
|
+
"start": start.isoformat() if start else None,
|
|
117
|
+
"end": end.isoformat() if end else None,
|
|
118
|
+
"all_day": bool(start and isinstance(start, date) and not isinstance(start, datetime)),
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
return events
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def unfold_ics(text: str) -> str:
|
|
125
|
+
return re.sub(r"\r?\n[ \t]", "", text)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def parse_ics_datetime(value: str | None) -> date | datetime | None:
|
|
129
|
+
if not value:
|
|
130
|
+
return None
|
|
131
|
+
value = value.rstrip("Z")
|
|
132
|
+
for fmt in (DATETIME_COMPACT, DATE_COMPACT):
|
|
133
|
+
try:
|
|
134
|
+
parsed = datetime.strptime(value, fmt)
|
|
135
|
+
except ValueError:
|
|
136
|
+
continue
|
|
137
|
+
return parsed.date() if fmt == DATE_COMPACT else parsed
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def parse_date(value: str) -> date:
|
|
142
|
+
return datetime.strptime(value, "%Y-%m-%d").date()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def event_overlaps(event: dict[str, Any], start: date, end: date) -> bool:
|
|
146
|
+
raw_start = event.get("start")
|
|
147
|
+
if not raw_start:
|
|
148
|
+
return False
|
|
149
|
+
event_date = datetime.fromisoformat(raw_start).date() if "T" in raw_start else date.fromisoformat(raw_start)
|
|
150
|
+
return start <= event_date <= end
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def calendar_summary(payload: dict[str, Any]) -> str:
|
|
154
|
+
if payload.get("status") != "ok":
|
|
155
|
+
return str(payload.get("message") or "")
|
|
156
|
+
if not payload.get("events"):
|
|
157
|
+
return "Nenhum compromisso encontrado no periodo."
|
|
158
|
+
lines = []
|
|
159
|
+
for item in payload["events"]:
|
|
160
|
+
start = item.get("start") or "-"
|
|
161
|
+
summary = item.get("summary") or "-"
|
|
162
|
+
lines.append(f"- {start}: {summary}")
|
|
163
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Global configuration orchestration for missing providers and sources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from cli.aikit.decision_store import is_disabled
|
|
10
|
+
from cli.aikit.providers import ProviderRegistryError, load_providers, provider_or_error
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
PROJECT_PATTERN = re.compile(r"(?i)\bprojeto\s+([A-Za-z0-9._ -]{2,80}?)(?:\s+no\b|\s+na\b|\s+do\b|\s+da\b|$)")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def provider_setup_wizard(
|
|
17
|
+
root: Path,
|
|
18
|
+
provider_id: str,
|
|
19
|
+
*,
|
|
20
|
+
prompt: str | None = None,
|
|
21
|
+
route: dict[str, Any] | None = None,
|
|
22
|
+
agent_id: str | None = None,
|
|
23
|
+
capability_id: str | None = None,
|
|
24
|
+
reason: str | None = None,
|
|
25
|
+
) -> dict[str, Any]:
|
|
26
|
+
provider = load_provider(root, provider_id)
|
|
27
|
+
provider_name = str(provider.get("name") or provider_id)
|
|
28
|
+
intent = str((route or {}).get("intent") or capability_id or "provider-configuration")
|
|
29
|
+
suggested_project = infer_project(prompt or "")
|
|
30
|
+
source_context = bool(route and route.get("intent"))
|
|
31
|
+
suggested_source_id = suggest_source_id(provider_id, suggested_project)
|
|
32
|
+
disabled = is_disabled("tools", provider_id) or is_disabled("integrations", provider_id)
|
|
33
|
+
questions = build_questions(provider, suggested_project=suggested_project, suggested_source_id=suggested_source_id, include_source=source_context)
|
|
34
|
+
next_question = disabled_notice(provider_id, provider_name) if disabled else opt_in_question(provider_id, provider_name)
|
|
35
|
+
return {
|
|
36
|
+
"kind": "provider-setup-wizard",
|
|
37
|
+
"status": "denied-by-user" if disabled else "waiting-for-user",
|
|
38
|
+
"provider": provider_id,
|
|
39
|
+
"provider_name": provider_name,
|
|
40
|
+
"provider_kind": provider.get("kind"),
|
|
41
|
+
"intent": intent,
|
|
42
|
+
"agent_id": agent_id or (route or {}).get("agent_id"),
|
|
43
|
+
"capability_id": capability_id or (route or {}).get("capability_id"),
|
|
44
|
+
"reason": reason,
|
|
45
|
+
"resume_prompt": prompt,
|
|
46
|
+
"suggested_source_id": suggested_source_id if source_context else None,
|
|
47
|
+
"suggested_config": suggested_config(provider_id, suggested_project),
|
|
48
|
+
"next_question": next_question,
|
|
49
|
+
"questions": questions,
|
|
50
|
+
"credential_options": credential_options(provider),
|
|
51
|
+
"config_fields": public_config_fields(provider),
|
|
52
|
+
"auth_methods": public_auth_methods(provider),
|
|
53
|
+
"fallbacks": list(provider.get("fallbacks", []) or []),
|
|
54
|
+
"stores_secret": False,
|
|
55
|
+
"owner_agent": "provider-configurator",
|
|
56
|
+
"message": (
|
|
57
|
+
f"{provider_name} esta desativado. O agente seguira sem essa ferramenta ate voce reativar."
|
|
58
|
+
if disabled
|
|
59
|
+
else f"Para continuar, o agente precisa configurar ou autorizar {provider_name}."
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def provider_wizard_from_requirement(
|
|
65
|
+
root: Path,
|
|
66
|
+
provider_id: str,
|
|
67
|
+
*,
|
|
68
|
+
agent_id: str | None = None,
|
|
69
|
+
capability_id: str | None = None,
|
|
70
|
+
reason: str | None = None,
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
return provider_setup_wizard(
|
|
73
|
+
root,
|
|
74
|
+
provider_id,
|
|
75
|
+
agent_id=agent_id,
|
|
76
|
+
capability_id=capability_id,
|
|
77
|
+
reason=reason or "Capability provider requirement is not configured.",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_provider(root: Path, provider_id: str) -> dict[str, Any]:
|
|
82
|
+
try:
|
|
83
|
+
return provider_or_error(load_providers(root), provider_id)
|
|
84
|
+
except ProviderRegistryError:
|
|
85
|
+
return {
|
|
86
|
+
"id": provider_id,
|
|
87
|
+
"name": provider_id,
|
|
88
|
+
"kind": "unknown",
|
|
89
|
+
"config_fields": [],
|
|
90
|
+
"auth_methods": [],
|
|
91
|
+
"fallbacks": [],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_questions(
|
|
96
|
+
provider: dict[str, Any],
|
|
97
|
+
*,
|
|
98
|
+
suggested_project: str | None,
|
|
99
|
+
suggested_source_id: str,
|
|
100
|
+
include_source: bool,
|
|
101
|
+
) -> list[dict[str, Any]]:
|
|
102
|
+
questions: list[dict[str, Any]] = []
|
|
103
|
+
for field in provider.get("config_fields", []) or []:
|
|
104
|
+
if not isinstance(field, dict):
|
|
105
|
+
continue
|
|
106
|
+
name = str(field.get("name") or "").strip()
|
|
107
|
+
if not name:
|
|
108
|
+
continue
|
|
109
|
+
suggested_value = suggested_value_for_field(provider, name, suggested_project)
|
|
110
|
+
questions.append(
|
|
111
|
+
{
|
|
112
|
+
"id": field_question_id(provider["id"], name),
|
|
113
|
+
"type": "text",
|
|
114
|
+
"text": question_text(provider, name, suggested_value=suggested_value),
|
|
115
|
+
"config_key": name,
|
|
116
|
+
"env": name,
|
|
117
|
+
"required": bool(field.get("required")),
|
|
118
|
+
"secret": bool(field.get("secret")),
|
|
119
|
+
"default": field.get("default"),
|
|
120
|
+
"suggested_value": suggested_value,
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
auth_methods = public_auth_methods(provider)
|
|
124
|
+
if auth_methods:
|
|
125
|
+
questions.append(
|
|
126
|
+
{
|
|
127
|
+
"id": f"{provider_slug(str(provider['id']))}_auth",
|
|
128
|
+
"type": "select",
|
|
129
|
+
"text": "Como deseja autenticar?",
|
|
130
|
+
"options": [method["id"] for method in auth_methods] + ["env", "file", "skip"],
|
|
131
|
+
"auth_methods": auth_methods,
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
if include_source:
|
|
135
|
+
questions.append(
|
|
136
|
+
{
|
|
137
|
+
"id": "source_id",
|
|
138
|
+
"type": "text",
|
|
139
|
+
"text": "Qual nome local deseja dar para esta fonte?",
|
|
140
|
+
"suggested_value": suggested_source_id,
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
questions.append(
|
|
144
|
+
{
|
|
145
|
+
"id": "default_for_intent",
|
|
146
|
+
"type": "confirm",
|
|
147
|
+
"text": "Posso usar esta fonte como padrao para este tipo de pedido?",
|
|
148
|
+
"default": True,
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
if not questions:
|
|
152
|
+
questions.append(
|
|
153
|
+
{
|
|
154
|
+
"id": f"{provider_slug(str(provider['id']))}_confirm",
|
|
155
|
+
"type": "confirm",
|
|
156
|
+
"text": f"Posso tentar usar {provider.get('name') or provider['id']} com a cadeia de credenciais nativa?",
|
|
157
|
+
"default": False,
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
return questions
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def opt_in_question(provider_id: str, provider_name: str) -> dict[str, Any]:
|
|
164
|
+
return {
|
|
165
|
+
"id": f"{provider_slug(provider_id)}_opt_in",
|
|
166
|
+
"type": "confirm",
|
|
167
|
+
"text": f"Posso configurar ou autorizar {provider_name} para atender este pedido?",
|
|
168
|
+
"default": False,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def disabled_notice(provider_id: str, provider_name: str) -> dict[str, Any]:
|
|
173
|
+
return {
|
|
174
|
+
"id": f"{provider_slug(provider_id)}_disabled",
|
|
175
|
+
"type": "notice",
|
|
176
|
+
"text": f"{provider_name} esta desativado por decisao do usuario. Reative para usar esta integracao.",
|
|
177
|
+
"default": False,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def public_config_fields(provider: dict[str, Any]) -> list[dict[str, Any]]:
|
|
182
|
+
fields = []
|
|
183
|
+
for field in provider.get("config_fields", []) or []:
|
|
184
|
+
if not isinstance(field, dict):
|
|
185
|
+
continue
|
|
186
|
+
fields.append(
|
|
187
|
+
{
|
|
188
|
+
"name": field.get("name"),
|
|
189
|
+
"required": bool(field.get("required")),
|
|
190
|
+
"secret": bool(field.get("secret")),
|
|
191
|
+
"default": field.get("default"),
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
return fields
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def public_auth_methods(provider: dict[str, Any]) -> list[dict[str, Any]]:
|
|
198
|
+
methods = []
|
|
199
|
+
for method in provider.get("auth_methods", []) or []:
|
|
200
|
+
if not isinstance(method, dict):
|
|
201
|
+
continue
|
|
202
|
+
methods.append(
|
|
203
|
+
{
|
|
204
|
+
"id": method.get("id"),
|
|
205
|
+
"label": method.get("label") or method.get("id"),
|
|
206
|
+
"native": bool(method.get("native")),
|
|
207
|
+
"config_fields": list(method.get("config_fields", []) or []),
|
|
208
|
+
"secret_fields": list(method.get("secret_fields", []) or []),
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
return methods
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def credential_options(provider: dict[str, Any]) -> list[dict[str, Any]]:
|
|
215
|
+
options = []
|
|
216
|
+
for method in public_auth_methods(provider):
|
|
217
|
+
for secret_field in method.get("secret_fields") or []:
|
|
218
|
+
options.append(
|
|
219
|
+
{
|
|
220
|
+
"id": f"env:{secret_field}",
|
|
221
|
+
"label": f"Variavel de ambiente {secret_field}",
|
|
222
|
+
"env": secret_field,
|
|
223
|
+
"auth_method": method.get("id"),
|
|
224
|
+
"stores_secret": False,
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
options.extend(
|
|
228
|
+
[
|
|
229
|
+
{"id": "file", "label": "Arquivo local de credencial", "stores_secret": False},
|
|
230
|
+
{"id": "native", "label": "Cadeia nativa ou CLI ja autenticada", "stores_secret": False},
|
|
231
|
+
{"id": "skip", "label": "Ignorar esta ferramenta", "stores_secret": False},
|
|
232
|
+
]
|
|
233
|
+
)
|
|
234
|
+
seen: set[str] = set()
|
|
235
|
+
unique = []
|
|
236
|
+
for option in options:
|
|
237
|
+
option_id = str(option.get("id"))
|
|
238
|
+
if option_id not in seen:
|
|
239
|
+
seen.add(option_id)
|
|
240
|
+
unique.append(option)
|
|
241
|
+
return unique
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def suggested_config(provider_id: str, suggested_project: str | None) -> dict[str, Any]:
|
|
245
|
+
config: dict[str, Any] = {}
|
|
246
|
+
if provider_id == "azure-devops" and suggested_project:
|
|
247
|
+
config["project"] = suggested_project
|
|
248
|
+
config["AZURE_DEVOPS_PROJECT"] = suggested_project
|
|
249
|
+
return config
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def suggested_value_for_field(provider: dict[str, Any], name: str, suggested_project: str | None) -> str | None:
|
|
253
|
+
if provider.get("id") == "azure-devops" and name == "AZURE_DEVOPS_PROJECT" and suggested_project:
|
|
254
|
+
return suggested_project
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def question_text(provider: dict[str, Any], name: str, *, suggested_value: str | None) -> str:
|
|
259
|
+
if provider.get("id") == "azure-devops" and name == "AZURE_DEVOPS_ORG":
|
|
260
|
+
return "Qual e a organizacao do Azure DevOps?"
|
|
261
|
+
if provider.get("id") == "azure-devops" and name == "AZURE_DEVOPS_PROJECT":
|
|
262
|
+
if suggested_value:
|
|
263
|
+
return f'O projeto e "{suggested_value}"? Se nao, informe o nome correto.'
|
|
264
|
+
return "Qual e o nome do projeto no Azure DevOps?"
|
|
265
|
+
label = name.replace("_", " ").lower()
|
|
266
|
+
return f"Informe {label} para {provider.get('name') or provider.get('id')}."
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def field_question_id(provider_id: str, name: str) -> str:
|
|
270
|
+
return f"{provider_slug(provider_id)}_{provider_slug(name)}"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def infer_project(prompt: str) -> str | None:
|
|
274
|
+
match = PROJECT_PATTERN.search(prompt)
|
|
275
|
+
if not match:
|
|
276
|
+
return None
|
|
277
|
+
project = " ".join(match.group(1).strip(" .,_-").split())
|
|
278
|
+
return project or None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def suggest_source_id(provider: str, project: str | None) -> str:
|
|
282
|
+
parts = [provider]
|
|
283
|
+
if project:
|
|
284
|
+
parts.append(project)
|
|
285
|
+
raw = "-".join(parts)
|
|
286
|
+
normalized = re.sub(r"[^a-z0-9._-]+", "-", raw.lower()).strip("-")
|
|
287
|
+
return normalized or provider_slug(provider)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def provider_slug(provider: str) -> str:
|
|
291
|
+
return re.sub(r"[^a-z0-9]+", "_", provider.lower()).strip("_") or "provider"
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Persistent local decisions for tools, integrations, skills and LLMs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from cli.aikit.app_home import app_path, ensure_app_home
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DECISION_VERSION = 1
|
|
15
|
+
VALID_CATEGORIES = {"tools", "integrations", "skills", "llms"}
|
|
16
|
+
VALID_STATES = {"enabled", "disabled_by_user", "denied_by_user", "needs_setup", "unavailable", "available", "requires_permission"}
|
|
17
|
+
ITEM_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]*$")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def decisions_path() -> Path:
|
|
21
|
+
ensure_app_home()
|
|
22
|
+
path = app_path("config", "decisions.json")
|
|
23
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
return path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def empty_decisions() -> dict[str, Any]:
|
|
28
|
+
return {"version": DECISION_VERSION, "items": {}}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_decisions() -> dict[str, Any]:
|
|
32
|
+
path = decisions_path()
|
|
33
|
+
if not path.exists():
|
|
34
|
+
return empty_decisions()
|
|
35
|
+
try:
|
|
36
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
37
|
+
except (OSError, json.JSONDecodeError):
|
|
38
|
+
return empty_decisions()
|
|
39
|
+
if not isinstance(data, dict):
|
|
40
|
+
return empty_decisions()
|
|
41
|
+
data.setdefault("version", DECISION_VERSION)
|
|
42
|
+
if not isinstance(data.get("items"), dict):
|
|
43
|
+
data["items"] = {}
|
|
44
|
+
return data
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def save_decisions(data: dict[str, Any]) -> Path:
|
|
48
|
+
path = decisions_path()
|
|
49
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
50
|
+
return path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def list_decisions(category: str | None = None) -> dict[str, Any]:
|
|
54
|
+
data = load_decisions()
|
|
55
|
+
items = [public_decision(item) for item in data.get("items", {}).values() if not category or item.get("category") == category]
|
|
56
|
+
items.sort(key=lambda item: (item["category"], item["id"]))
|
|
57
|
+
return {
|
|
58
|
+
"kind": "decisions",
|
|
59
|
+
"status": "ok",
|
|
60
|
+
"category": category,
|
|
61
|
+
"path": str(decisions_path()),
|
|
62
|
+
"items": items,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def set_decision(
|
|
67
|
+
category: str,
|
|
68
|
+
item_id: str,
|
|
69
|
+
state: str,
|
|
70
|
+
*,
|
|
71
|
+
reason: str | None = None,
|
|
72
|
+
scope: str = "persistent",
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
validate_category(category)
|
|
75
|
+
validate_item_id(item_id)
|
|
76
|
+
if state not in VALID_STATES:
|
|
77
|
+
raise ValueError(f"invalid decision state: {state}")
|
|
78
|
+
data = load_decisions()
|
|
79
|
+
key = decision_key(category, item_id)
|
|
80
|
+
item = {
|
|
81
|
+
"key": key,
|
|
82
|
+
"category": category,
|
|
83
|
+
"id": item_id,
|
|
84
|
+
"state": state,
|
|
85
|
+
"scope": scope,
|
|
86
|
+
"reason": reason,
|
|
87
|
+
"updated_at": now_iso(),
|
|
88
|
+
}
|
|
89
|
+
existing = data["items"].get(key)
|
|
90
|
+
if isinstance(existing, dict) and existing.get("created_at"):
|
|
91
|
+
item["created_at"] = existing["created_at"]
|
|
92
|
+
else:
|
|
93
|
+
item["created_at"] = item["updated_at"]
|
|
94
|
+
data["items"][key] = item
|
|
95
|
+
path = save_decisions(data)
|
|
96
|
+
return {
|
|
97
|
+
"kind": "decision",
|
|
98
|
+
"status": "updated",
|
|
99
|
+
"path": str(path),
|
|
100
|
+
"item": public_decision(item),
|
|
101
|
+
"category": category,
|
|
102
|
+
"id": item_id,
|
|
103
|
+
"state": state,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_decision(category: str, item_id: str) -> dict[str, Any] | None:
|
|
108
|
+
validate_category(category)
|
|
109
|
+
validate_item_id(item_id)
|
|
110
|
+
item = load_decisions().get("items", {}).get(decision_key(category, item_id))
|
|
111
|
+
return public_decision(item) if isinstance(item, dict) else None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def reset_decisions(category: str | None = None) -> dict[str, Any]:
|
|
115
|
+
data = load_decisions()
|
|
116
|
+
if category:
|
|
117
|
+
validate_category(category)
|
|
118
|
+
data["items"] = {key: value for key, value in data.get("items", {}).items() if value.get("category") != category}
|
|
119
|
+
else:
|
|
120
|
+
data["items"] = {}
|
|
121
|
+
path = save_decisions(data)
|
|
122
|
+
return {"kind": "decisions-reset", "status": "reset", "category": category, "path": str(path)}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def is_disabled(category: str, item_id: str) -> bool:
|
|
126
|
+
decision = get_decision(category, item_id)
|
|
127
|
+
return bool(decision and decision.get("state") in {"disabled_by_user", "denied_by_user"})
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def public_decision(item: dict[str, Any]) -> dict[str, Any]:
|
|
131
|
+
return {
|
|
132
|
+
"category": item.get("category"),
|
|
133
|
+
"id": item.get("id"),
|
|
134
|
+
"state": item.get("state"),
|
|
135
|
+
"scope": item.get("scope"),
|
|
136
|
+
"reason": item.get("reason"),
|
|
137
|
+
"created_at": item.get("created_at"),
|
|
138
|
+
"updated_at": item.get("updated_at"),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def decision_key(category: str, item_id: str) -> str:
|
|
143
|
+
return f"{category}:{item_id}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def validate_category(category: str) -> None:
|
|
147
|
+
if category not in VALID_CATEGORIES:
|
|
148
|
+
available = ", ".join(sorted(VALID_CATEGORIES))
|
|
149
|
+
raise ValueError(f"invalid decision category: {category}. available: {available}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def validate_item_id(item_id: str) -> None:
|
|
153
|
+
if not item_id or not ITEM_ID_PATTERN.fullmatch(item_id):
|
|
154
|
+
raise ValueError("item id must use letters, numbers, dots, dashes, underscores or colons")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def now_iso() -> str:
|
|
158
|
+
return datetime.now(timezone.utc).isoformat()
|
|
@@ -7,6 +7,7 @@ import os
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
+
from cli.aikit.app_home import APP_DIRS, app_home
|
|
10
11
|
from cli.aikit.llm import config_path as llm_config_path
|
|
11
12
|
from cli.aikit.llm import doctor_backends
|
|
12
13
|
from cli.aikit.lock import lock_path, lock_status, read_lock
|
|
@@ -42,6 +43,14 @@ def runtime_diagnostics(
|
|
|
42
43
|
"kind": "runtime-summary",
|
|
43
44
|
"status": status,
|
|
44
45
|
"root": str(root),
|
|
46
|
+
"app_home": str(app_home()),
|
|
47
|
+
"app_dirs": {
|
|
48
|
+
name: {
|
|
49
|
+
"path": str(app_home() / name),
|
|
50
|
+
"exists": (app_home() / name).is_dir(),
|
|
51
|
+
}
|
|
52
|
+
for name in APP_DIRS
|
|
53
|
+
},
|
|
45
54
|
"install_home": str(install_home),
|
|
46
55
|
"config_path": str(runtime_config_path()),
|
|
47
56
|
"llm_config_path": str(llm_config_path()),
|