agent-devkit 0.0.3 → 0.1.0

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 (57) hide show
  1. package/README.md +170 -1
  2. package/package.json +1 -1
  3. package/runtime/README.md +192 -14
  4. package/runtime/agent +29 -13
  5. package/runtime/agents/README.md +3 -0
  6. package/runtime/agents/github-pr-reviewer/AGENTS.md +10 -0
  7. package/runtime/agents/github-pr-reviewer/README.md +7 -0
  8. package/runtime/agents/github-pr-reviewer/agent.yaml +33 -0
  9. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/capability.yaml +20 -0
  10. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/decision-rules.md +8 -0
  11. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/workflow.md +9 -0
  12. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/capability.yaml +20 -0
  13. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/decision-rules.md +7 -0
  14. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/workflow.md +8 -0
  15. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/capability.yaml +20 -0
  16. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/decision-rules.md +7 -0
  17. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/workflow.md +8 -0
  18. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/capability.yaml +20 -0
  19. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/decision-rules.md +9 -0
  20. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/workflow.md +10 -0
  21. package/runtime/agents/github-pr-reviewer/infra/README.md +7 -0
  22. package/runtime/agents/github-pr-reviewer/knowledge/context.md +7 -0
  23. package/runtime/agents/github-pr-reviewer/knowledge/system.md +17 -0
  24. package/runtime/agents/github-pr-reviewer/templates/pr-automation-output.md +10 -0
  25. package/runtime/agents/github-pr-reviewer/templates/pr-inspection-output.md +9 -0
  26. package/runtime/agents/github-pr-reviewer/templates/pr-list-output.md +9 -0
  27. package/runtime/agents/github-pr-reviewer/templates/pr-review-output.md +17 -0
  28. package/runtime/cli/README.md +93 -0
  29. package/runtime/cli/aikit/__init__.py +1 -1
  30. package/runtime/cli/aikit/aliases.py +196 -0
  31. package/runtime/cli/aikit/app_home.py +78 -0
  32. package/runtime/cli/aikit/audit.py +344 -0
  33. package/runtime/cli/aikit/calendar.py +163 -0
  34. package/runtime/cli/aikit/diagnostics.py +9 -0
  35. package/runtime/cli/aikit/github_pr.py +254 -0
  36. package/runtime/cli/aikit/identity.py +75 -0
  37. package/runtime/cli/aikit/llm.py +240 -48
  38. package/runtime/cli/aikit/main.py +924 -24
  39. package/runtime/cli/aikit/memory.py +148 -8
  40. package/runtime/cli/aikit/notifications.py +9 -0
  41. package/runtime/cli/aikit/permissions.py +345 -0
  42. package/runtime/cli/aikit/personality.py +123 -0
  43. package/runtime/cli/aikit/providers.py +4 -5
  44. package/runtime/cli/aikit/scheduler.py +18 -0
  45. package/runtime/cli/aikit/sessions.py +396 -0
  46. package/runtime/cli/aikit/setup_wizard.py +12 -0
  47. package/runtime/cli/aikit/tasks.py +351 -0
  48. package/runtime/cli/aikit/toolchain.py +274 -0
  49. package/runtime/plugins/claude-code-ai-devkit/plugin.json +1 -1
  50. package/runtime/plugins/claude-skill-ai-devkit/plugin.json +1 -1
  51. package/runtime/providers/calendar.yaml +28 -0
  52. package/runtime/providers/github.yaml +42 -0
  53. package/runtime/providers/local-notification.yaml +19 -0
  54. package/runtime/providers/local-scheduler.yaml +24 -0
  55. package/runtime/scripts/README.md +1 -1
  56. package/runtime/scripts/verify-release-alignment.mjs +1 -1
  57. package/runtime/vendor/skills/napkin/napkin.md +13 -3
@@ -8,27 +8,140 @@ from datetime import datetime, timezone
8
8
  from pathlib import Path
9
9
  from typing import Any
10
10
 
11
- from cli.aikit.llm import config_home, config_path, load_config, save_config
11
+ from cli.aikit.app_home import (
12
+ app_path,
13
+ cache_home,
14
+ ensure_app_home,
15
+ memory_home as app_memory_home,
16
+ sessions_home,
17
+ tasks_home,
18
+ )
19
+ from cli.aikit.llm import config_path, load_config, save_config
20
+
21
+
22
+ SECRET_VALUE_PATTERN = re.compile(
23
+ r"(?i)\b("
24
+ r"sk-[a-z0-9_-]{12,}|"
25
+ r"npm_[a-z0-9]{12,}|"
26
+ r"gh[pousr]_[a-z0-9_]{12,}|"
27
+ r"xox[baprs]-[a-z0-9-]{12,}|"
28
+ r"AKIA[0-9A-Z]{12,}|"
29
+ r"ASIA[0-9A-Z]{12,}"
30
+ r")\b"
31
+ )
32
+ SECRET_ASSIGNMENT_PATTERN = re.compile(
33
+ r"(?i)([\"']?)([a-z0-9_]*(?:token|secret|password|passwd|pwd|senha|chave|api[_-]?key|private[_-]?key)[a-z0-9_]*)([\"']?)(\s*[:=]\s*)([\"']?)([^\s,;}\"']+)([\"']?)"
34
+ )
35
+
36
+ MEMORY_FILE_TEMPLATES: dict[str, str] = {
37
+ "profile.md": """# Profile
38
+
39
+ Local user profile for Agent DevKit.
40
+
41
+ ## User
42
+
43
+ - Name:
44
+ - Primary language:
45
+ - Timezone:
46
+
47
+ ## Notes
48
+
49
+ - Add stable facts the agent should remember.
50
+ """,
51
+ "personality.md": """# Personality
52
+
53
+ Configured public identity and response style for Agent DevKit.
54
+
55
+ ## Agent
56
+
57
+ - Name: Agent DevKit
58
+
59
+ ## Style
60
+
61
+ - Tone: direct
62
+ - Detail level: concise
63
+ """,
64
+ "preferences.md": """# Preferences
65
+
66
+ Reusable user preferences.
67
+
68
+ ## Defaults
69
+
70
+ - Prefer local-first execution.
71
+ - Ask before external writes.
72
+ """,
73
+ "projects.md": """# Projects
74
+
75
+ Frequently used projects and repositories.
76
+
77
+ ## Items
78
+
79
+ - Add project names, paths, and non-secret references here.
80
+ """,
81
+ "routines.md": """# Routines
82
+
83
+ Recurring workflows, checks, and habits.
84
+
85
+ ## Items
86
+
87
+ - Add routines that should be easy to reuse.
88
+ """,
89
+ "napkin.md": """# Agent DevKit Napkin
90
+
91
+ Curated local runbook entries promoted from repeated use.
92
+
93
+ ## Execution & Validation
94
+
95
+ - Keep high-value reusable notes here.
96
+ """,
97
+ }
12
98
 
13
99
 
14
100
  def memory_home() -> Path:
15
- return config_home() / "memory"
101
+ return app_memory_home()
102
+
103
+
104
+ def ensure_memory() -> dict[str, Any]:
105
+ ensure_app_home()
106
+ home = memory_home()
107
+ home.mkdir(parents=True, exist_ok=True)
108
+ files: list[dict[str, Any]] = []
109
+ created: list[str] = []
110
+ for name, template in MEMORY_FILE_TEMPLATES.items():
111
+ path = home / name
112
+ if not path.exists():
113
+ path.write_text(template, encoding="utf-8")
114
+ created.append(str(path))
115
+ files.append({"name": name, "path": str(path), "exists": path.exists()})
116
+ return {
117
+ "kind": "memory-path",
118
+ "status": "ok",
119
+ "home": str(home),
120
+ "created": created,
121
+ "files": files,
122
+ }
16
123
 
17
124
 
18
125
  def normalize_prompt(prompt: str) -> str:
19
- text = " ".join(prompt.lower().split())
126
+ text = " ".join(redact_secrets(prompt).lower().split())
20
127
  return text[:160]
21
128
 
22
129
 
130
+ def redact_secrets(value: str) -> str:
131
+ redacted = SECRET_VALUE_PATTERN.sub("[REDACTED_SECRET]", value)
132
+ return SECRET_ASSIGNMENT_PATTERN.sub(r"\1\2\3\4\5[REDACTED_SECRET]\7", redacted)
133
+
134
+
23
135
  def napkin_context(root: Path, *, agent_id: str | None = None, source_id: str | None = None) -> dict[str, Any]:
24
136
  paths = [
25
137
  root / "vendor" / "skills" / "napkin" / "napkin.md",
138
+ memory_home() / "napkin.md",
26
139
  memory_home() / "global" / "napkin.md",
27
140
  ]
28
141
  if agent_id:
29
- paths.append(memory_home() / "agents" / agent_id / "napkin.md")
142
+ paths.append(memory_home() / "agents" / sanitize_segment(agent_id) / "napkin.md")
30
143
  if source_id:
31
- paths.append(memory_home() / "sources" / source_id / "napkin.md")
144
+ paths.append(memory_home() / "sources" / sanitize_segment(source_id) / "napkin.md")
32
145
  return {
33
146
  "loaded": any(path.exists() for path in paths),
34
147
  "paths": [
@@ -61,6 +174,7 @@ def record_usage(
61
174
 
62
175
 
63
176
  def show_memory(root: Path, *, agent_id: str | None = None, source_id: str | None = None) -> dict[str, Any]:
177
+ memory_paths = ensure_memory()
64
178
  config = load_config()
65
179
  memory = config.get("memory") if isinstance(config.get("memory"), dict) else {}
66
180
  usage = memory.get("usage") if isinstance(memory.get("usage"), dict) else {}
@@ -69,6 +183,7 @@ def show_memory(root: Path, *, agent_id: str | None = None, source_id: str | Non
69
183
  "status": "ok",
70
184
  "config_path": str(config_path()),
71
185
  "memory_home": str(memory_home()),
186
+ "files": memory_paths["files"],
72
187
  "usage": {
73
188
  "prompts": sorted_usage(usage.get("prompts") or {}),
74
189
  "routes": sorted_usage(usage.get("routes") or {}),
@@ -78,10 +193,19 @@ def show_memory(root: Path, *, agent_id: str | None = None, source_id: str | Non
78
193
  }
79
194
 
80
195
 
81
- def reset_memory(*, all_memory: bool = False, agent_id: str | None = None, source_id: str | None = None) -> dict[str, Any]:
196
+ def reset_memory(
197
+ *,
198
+ all_memory: bool = False,
199
+ agent_id: str | None = None,
200
+ source_id: str | None = None,
201
+ reset_sessions: bool = False,
202
+ reset_tasks: bool = False,
203
+ reset_cache: bool = False,
204
+ ) -> dict[str, Any]:
82
205
  config = load_config()
83
206
  removed_paths: list[str] = []
84
- if all_memory or not agent_id and not source_id:
207
+ scoped_reset = any([agent_id, source_id, reset_sessions, reset_tasks, reset_cache])
208
+ if all_memory or not scoped_reset:
85
209
  config.pop("memory", None)
86
210
  if memory_home().exists():
87
211
  removed_paths.append(str(memory_home()))
@@ -97,6 +221,14 @@ def reset_memory(*, all_memory: bool = False, agent_id: str | None = None, sourc
97
221
  usage.get("sources", {}).pop(source_id, None)
98
222
  path = memory_home() / "sources" / sanitize_segment(source_id)
99
223
  remove_path(path, removed_paths)
224
+ if all_memory or reset_sessions:
225
+ remove_path(sessions_home(), removed_paths)
226
+ remove_path(app_path("state", "active-session.json"), removed_paths)
227
+ if all_memory or reset_tasks:
228
+ remove_path(tasks_home(), removed_paths)
229
+ if all_memory or reset_cache:
230
+ remove_path(cache_home(), removed_paths)
231
+ ensure_app_home()
100
232
  written_path = save_config(config)
101
233
  return {
102
234
  "kind": "memory-reset",
@@ -104,9 +236,16 @@ def reset_memory(*, all_memory: bool = False, agent_id: str | None = None, sourc
104
236
  "config_path": str(written_path),
105
237
  "removed_paths": removed_paths,
106
238
  "sources_preserved": True,
239
+ "sessions_reset": bool(all_memory or reset_sessions),
240
+ "tasks_reset": bool(all_memory or reset_tasks),
241
+ "cache_reset": bool(all_memory or reset_cache),
107
242
  }
108
243
 
109
244
 
245
+ def memory_path_payload() -> dict[str, Any]:
246
+ return ensure_memory()
247
+
248
+
110
249
  def increment_bucket(bucket: dict[str, Any], key: str, now: str) -> None:
111
250
  item = bucket.setdefault(key, {"count": 0, "first_seen": now, "last_seen": now})
112
251
  item["count"] = int(item.get("count") or 0) + 1
@@ -132,4 +271,5 @@ def remove_path(path: Path, removed_paths: list[str]) -> None:
132
271
 
133
272
 
134
273
  def sanitize_segment(value: str) -> str:
135
- return re.sub(r"[^a-zA-Z0-9._-]+", "-", value)
274
+ sanitized = re.sub(r"[^a-zA-Z0-9._-]+", "-", value).strip(".-")
275
+ return sanitized or "item"
@@ -0,0 +1,9 @@
1
+ """Notification payload helpers for local automations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def terminal_notification(message: str) -> dict[str, Any]:
9
+ return {"type": "terminal", "message": message}
@@ -0,0 +1,345 @@
1
+ """Local permission policy helpers for Agent DevKit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from cli.aikit.app_home import ensure_app_home, policies_home
11
+ from cli.aikit.memory import redact_secrets
12
+
13
+
14
+ PERMISSION_LEVELS = (
15
+ "read-only",
16
+ "draft-only",
17
+ "comment-with-approval",
18
+ "write-with-approval",
19
+ "auto-write",
20
+ "admin",
21
+ )
22
+ DEFAULT_PERMISSION_LEVEL = "read-only"
23
+ PROVIDER_BY_AGENT = {
24
+ "github-pr-reviewer": "github",
25
+ }
26
+ ACTION_REQUIRED_LEVEL = {
27
+ "read": "read-only",
28
+ "draft": "draft-only",
29
+ "comment": "comment-with-approval",
30
+ "write": "write-with-approval",
31
+ "approve": "write-with-approval",
32
+ "request-changes": "write-with-approval",
33
+ "admin": "admin",
34
+ }
35
+
36
+
37
+ def now_iso() -> str:
38
+ return datetime.now(timezone.utc).isoformat()
39
+
40
+
41
+ def ensure_policies_home() -> Path:
42
+ ensure_app_home()
43
+ home = policies_home()
44
+ home.mkdir(parents=True, exist_ok=True)
45
+ return home
46
+
47
+
48
+ def permissions_json_path() -> Path:
49
+ return ensure_policies_home() / "permissions.json"
50
+
51
+
52
+ def permissions_md_path() -> Path:
53
+ return ensure_policies_home() / "permissions.md"
54
+
55
+
56
+ def empty_policy() -> dict[str, Any]:
57
+ return {
58
+ "version": 1,
59
+ "default_level": DEFAULT_PERMISSION_LEVEL,
60
+ "grants": [],
61
+ }
62
+
63
+
64
+ def load_permissions() -> dict[str, Any]:
65
+ path = permissions_json_path()
66
+ if not path.exists():
67
+ policy = empty_policy()
68
+ save_permissions(policy)
69
+ return policy
70
+ try:
71
+ data = json.loads(path.read_text(encoding="utf-8"))
72
+ except json.JSONDecodeError:
73
+ backup = path.with_suffix(f".corrupt-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}.json")
74
+ path.replace(backup)
75
+ data = empty_policy()
76
+ if not isinstance(data, dict):
77
+ data = empty_policy()
78
+ data.setdefault("version", 1)
79
+ data.setdefault("default_level", DEFAULT_PERMISSION_LEVEL)
80
+ if data.get("default_level") not in PERMISSION_LEVELS:
81
+ data["default_level"] = DEFAULT_PERMISSION_LEVEL
82
+ if not isinstance(data.get("grants"), list):
83
+ data["grants"] = []
84
+ return data
85
+
86
+
87
+ def save_permissions(policy: dict[str, Any]) -> Path:
88
+ ensure_policies_home()
89
+ path = permissions_json_path()
90
+ path.write_text(json.dumps(policy, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
91
+ permissions_md_path().write_text(render_permissions_md(policy), encoding="utf-8")
92
+ return path
93
+
94
+
95
+ def show_permissions() -> dict[str, Any]:
96
+ policy = load_permissions()
97
+ return {
98
+ "kind": "permissions",
99
+ "status": "ok",
100
+ "default_level": policy["default_level"],
101
+ "levels": list(PERMISSION_LEVELS),
102
+ "grants": [public_grant(item) for item in policy["grants"] if isinstance(item, dict)],
103
+ "json_path": str(permissions_json_path()),
104
+ "markdown_path": str(permissions_md_path()),
105
+ }
106
+
107
+
108
+ def grant_permission(
109
+ agent: str,
110
+ provider: str,
111
+ level: str,
112
+ *,
113
+ project: str | None = None,
114
+ task_id: str | None = None,
115
+ ) -> dict[str, Any]:
116
+ validate_level(level)
117
+ policy = load_permissions()
118
+ grants = [item for item in policy["grants"] if isinstance(item, dict)]
119
+ grant = find_grant(grants, agent=agent, provider=provider, project=project, task_id=task_id)
120
+ timestamp = now_iso()
121
+ if grant:
122
+ grant["level"] = level
123
+ grant["updated_at"] = timestamp
124
+ else:
125
+ grant = {
126
+ "agent": agent,
127
+ "provider": provider,
128
+ "level": level,
129
+ "project": project,
130
+ "task_id": task_id,
131
+ "created_at": timestamp,
132
+ "updated_at": timestamp,
133
+ }
134
+ grants.append(grant)
135
+ policy["grants"] = grants
136
+ path = save_permissions(policy)
137
+ return {
138
+ "kind": "permissions",
139
+ "status": "granted",
140
+ "grant": public_grant(grant),
141
+ "json_path": str(path),
142
+ "markdown_path": str(permissions_md_path()),
143
+ }
144
+
145
+
146
+ def revoke_permission(
147
+ agent: str,
148
+ provider: str,
149
+ level: str | None = None,
150
+ *,
151
+ project: str | None = None,
152
+ task_id: str | None = None,
153
+ ) -> dict[str, Any]:
154
+ if level:
155
+ validate_level(level)
156
+ policy = load_permissions()
157
+ removed: list[dict[str, Any]] = []
158
+ kept: list[dict[str, Any]] = []
159
+ for item in policy["grants"]:
160
+ if not isinstance(item, dict):
161
+ continue
162
+ matches = (
163
+ item.get("agent") == agent
164
+ and item.get("provider") == provider
165
+ and item.get("project") == project
166
+ and item.get("task_id") == task_id
167
+ and (level is None or item.get("level") == level)
168
+ )
169
+ if matches:
170
+ removed.append(item)
171
+ else:
172
+ kept.append(item)
173
+ policy["grants"] = kept
174
+ path = save_permissions(policy)
175
+ return {
176
+ "kind": "permissions",
177
+ "status": "revoked" if removed else "not-found",
178
+ "removed": [public_grant(item) for item in removed],
179
+ "json_path": str(path),
180
+ "markdown_path": str(permissions_md_path()),
181
+ }
182
+
183
+
184
+ def permission_check(
185
+ *,
186
+ agent: str | None,
187
+ provider: str | None,
188
+ action: str,
189
+ project: str | None = None,
190
+ task_id: str | None = None,
191
+ ) -> dict[str, Any]:
192
+ required = required_level_for_action(action)
193
+ resolved_provider = provider or provider_for_agent(agent)
194
+ policy = load_permissions()
195
+ grant = best_grant(
196
+ policy["grants"],
197
+ agent=agent,
198
+ provider=resolved_provider,
199
+ project=project,
200
+ task_id=task_id,
201
+ )
202
+ granted = normalize_level(grant.get("level") if grant else policy.get("default_level", DEFAULT_PERMISSION_LEVEL))
203
+ allowed = level_allows(granted, required)
204
+ return {
205
+ "kind": "permission-check",
206
+ "status": "allowed" if allowed else "blocked",
207
+ "ok": allowed,
208
+ "agent": agent,
209
+ "provider": resolved_provider,
210
+ "action": action,
211
+ "required_level": required,
212
+ "granted_level": granted,
213
+ "grant": public_grant(grant) if grant else None,
214
+ "default_level": policy.get("default_level", DEFAULT_PERMISSION_LEVEL),
215
+ "requires_permission": not allowed,
216
+ }
217
+
218
+
219
+ def assert_permission(
220
+ *,
221
+ agent: str | None,
222
+ provider: str | None,
223
+ action: str,
224
+ project: str | None = None,
225
+ task_id: str | None = None,
226
+ ) -> dict[str, Any]:
227
+ return permission_check(agent=agent, provider=provider, action=action, project=project, task_id=task_id)
228
+
229
+
230
+ def provider_for_agent(agent: str | None) -> str | None:
231
+ return PROVIDER_BY_AGENT.get(agent or "")
232
+
233
+
234
+ def required_level_for_action(action: str) -> str:
235
+ return ACTION_REQUIRED_LEVEL.get(action, action if action in PERMISSION_LEVELS else "write-with-approval")
236
+
237
+
238
+ def normalize_level(level: Any) -> str:
239
+ return str(level) if str(level) in PERMISSION_LEVELS else DEFAULT_PERMISSION_LEVEL
240
+
241
+
242
+ def level_allows(granted: str, required: str) -> bool:
243
+ validate_level(granted)
244
+ validate_level(required)
245
+ return PERMISSION_LEVELS.index(granted) >= PERMISSION_LEVELS.index(required)
246
+
247
+
248
+ def validate_level(level: str) -> None:
249
+ if level not in PERMISSION_LEVELS:
250
+ available = ", ".join(PERMISSION_LEVELS)
251
+ raise ValueError(f"unknown permission level: {level}. available: {available}")
252
+
253
+
254
+ def best_grant(
255
+ grants: list[Any],
256
+ *,
257
+ agent: str | None,
258
+ provider: str | None,
259
+ project: str | None,
260
+ task_id: str | None,
261
+ ) -> dict[str, Any] | None:
262
+ candidates = [
263
+ item
264
+ for item in grants
265
+ if isinstance(item, dict)
266
+ and item.get("agent") == agent
267
+ and item.get("provider") == provider
268
+ and item.get("project") in {None, project}
269
+ and item.get("task_id") in {None, task_id}
270
+ ]
271
+ if not candidates:
272
+ return None
273
+ candidates.sort(key=grant_specificity, reverse=True)
274
+ return candidates[0]
275
+
276
+
277
+ def find_grant(
278
+ grants: list[dict[str, Any]],
279
+ *,
280
+ agent: str,
281
+ provider: str,
282
+ project: str | None,
283
+ task_id: str | None,
284
+ ) -> dict[str, Any] | None:
285
+ for item in grants:
286
+ if (
287
+ item.get("agent") == agent
288
+ and item.get("provider") == provider
289
+ and item.get("project") == project
290
+ and item.get("task_id") == task_id
291
+ ):
292
+ return item
293
+ return None
294
+
295
+
296
+ def grant_specificity(grant: dict[str, Any]) -> tuple[int, int]:
297
+ scope = int(bool(grant.get("project"))) + int(bool(grant.get("task_id")))
298
+ return scope, PERMISSION_LEVELS.index(normalize_level(grant.get("level")))
299
+
300
+
301
+ def public_grant(grant: dict[str, Any] | None) -> dict[str, Any] | None:
302
+ if not grant:
303
+ return None
304
+ return {
305
+ "agent": grant.get("agent"),
306
+ "provider": grant.get("provider"),
307
+ "level": grant.get("level"),
308
+ "project": grant.get("project"),
309
+ "task_id": grant.get("task_id"),
310
+ "created_at": grant.get("created_at"),
311
+ "updated_at": grant.get("updated_at"),
312
+ }
313
+
314
+
315
+ def render_permissions_md(policy: dict[str, Any]) -> str:
316
+ lines = [
317
+ "# Agent DevKit Permissions",
318
+ "",
319
+ f"- Default level: `{redact_secrets(str(policy.get('default_level') or DEFAULT_PERMISSION_LEVEL))}`",
320
+ "",
321
+ "## Levels",
322
+ "",
323
+ ]
324
+ for level in PERMISSION_LEVELS:
325
+ lines.append(f"- `{level}`")
326
+ lines.extend(["", "## Grants", ""])
327
+ grants = [item for item in policy.get("grants", []) if isinstance(item, dict)]
328
+ if not grants:
329
+ lines.append("- No explicit grants configured.")
330
+ for item in grants:
331
+ scope = []
332
+ if item.get("project"):
333
+ scope.append(f"project={item['project']}")
334
+ if item.get("task_id"):
335
+ scope.append(f"task={item['task_id']}")
336
+ scope_text = f" ({', '.join(scope)})" if scope else ""
337
+ lines.append(
338
+ "- "
339
+ f"`{redact_secrets(str(item.get('agent') or '-'))}` / "
340
+ f"`{redact_secrets(str(item.get('provider') or '-'))}` -> "
341
+ f"`{redact_secrets(str(item.get('level') or DEFAULT_PERMISSION_LEVEL))}`"
342
+ f"{scope_text}"
343
+ )
344
+ lines.append("")
345
+ return "\n".join(lines)
@@ -0,0 +1,123 @@
1
+ """Local Agent DevKit personality persistence."""
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.identity import DEFAULT_PUBLIC_NAME
10
+ from cli.aikit.memory import MEMORY_FILE_TEMPLATES, ensure_memory, memory_home
11
+
12
+
13
+ FIELD_LABELS = {
14
+ "agent_name": "Name",
15
+ "user_name": "User name",
16
+ "language": "Language",
17
+ "tone": "Tone",
18
+ "detail_level": "Detail level",
19
+ }
20
+
21
+
22
+ def personality_path() -> Path:
23
+ ensure_memory()
24
+ return memory_home() / "personality.md"
25
+
26
+
27
+ def load_personality() -> dict[str, Any]:
28
+ path = personality_path()
29
+ text = path.read_text(encoding="utf-8") if path.exists() else ""
30
+ return {
31
+ "kind": "personality",
32
+ "status": "ok",
33
+ "path": str(path),
34
+ "agent_name": parse_field(text, FIELD_LABELS["agent_name"]) or DEFAULT_PUBLIC_NAME,
35
+ "user_name": parse_field(text, FIELD_LABELS["user_name"]),
36
+ "language": parse_field(text, FIELD_LABELS["language"]),
37
+ "tone": parse_field(text, FIELD_LABELS["tone"]) or "direct",
38
+ "detail_level": parse_field(text, FIELD_LABELS["detail_level"]) or "concise",
39
+ "questions": setup_questions(),
40
+ }
41
+
42
+
43
+ def update_personality(
44
+ *,
45
+ agent_name: str | None = None,
46
+ user_name: str | None = None,
47
+ language: str | None = None,
48
+ tone: str | None = None,
49
+ detail_level: str | None = None,
50
+ ) -> dict[str, Any]:
51
+ current = load_personality()
52
+ values = {
53
+ "agent_name": clean_field(agent_name) or current.get("agent_name") or DEFAULT_PUBLIC_NAME,
54
+ "user_name": clean_field(user_name) if user_name is not None else current.get("user_name"),
55
+ "language": clean_field(language) if language is not None else current.get("language"),
56
+ "tone": clean_field(tone) or current.get("tone") or "direct",
57
+ "detail_level": clean_field(detail_level) or current.get("detail_level") or "concise",
58
+ }
59
+ path = personality_path()
60
+ path.write_text(render_personality(values), encoding="utf-8")
61
+ payload = load_personality()
62
+ payload["status"] = "updated"
63
+ return payload
64
+
65
+
66
+ def reset_personality() -> dict[str, Any]:
67
+ path = personality_path()
68
+ path.write_text(MEMORY_FILE_TEMPLATES["personality.md"], encoding="utf-8")
69
+ payload = load_personality()
70
+ payload["status"] = "reset"
71
+ return payload
72
+
73
+
74
+ def setup_personality() -> dict[str, Any]:
75
+ payload = load_personality()
76
+ payload["status"] = "needs-input"
77
+ payload["message"] = "Use `agent personality edit --name <nome>` to configure personality non-interactively."
78
+ return payload
79
+
80
+
81
+ def render_personality(values: dict[str, Any]) -> str:
82
+ return f"""# Personality
83
+
84
+ Configured public identity and response style for Agent DevKit.
85
+
86
+ ## Agent
87
+
88
+ - Name: {values.get("agent_name") or DEFAULT_PUBLIC_NAME}
89
+ - User name: {values.get("user_name") or ""}
90
+ - Language: {values.get("language") or ""}
91
+
92
+ ## Style
93
+
94
+ - Tone: {values.get("tone") or "direct"}
95
+ - Detail level: {values.get("detail_level") or "concise"}
96
+ """
97
+
98
+
99
+ def parse_field(text: str, label: str) -> str | None:
100
+ pattern = re.compile(rf"(?im)^[ \t]*-[ \t]*{re.escape(label)}[ \t]*:[ \t]*([^\r\n]*)[ \t]*$")
101
+ match = pattern.search(text)
102
+ if not match:
103
+ return None
104
+ value = match.group(1).strip()
105
+ return value or None
106
+
107
+
108
+ def clean_field(value: str | None) -> str | None:
109
+ if value is None:
110
+ return None
111
+ cleaned = " ".join(str(value).split())
112
+ return cleaned or None
113
+
114
+
115
+ def setup_questions() -> list[str]:
116
+ return [
117
+ "Como voce deseja que eu me chame?",
118
+ "Deseja criar um comando com esse nome para me chamar diretamente?",
119
+ "Como voce se chama?",
120
+ "Em qual idioma devo responder por padrao?",
121
+ "Voce prefere respostas curtas, detalhadas, criticas ou didaticas?",
122
+ "Qual backend LLM voce prefere como primario?",
123
+ ]