agent-devkit 0.3.1 → 0.3.2
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 +5 -2
- package/package.json +1 -1
- package/runtime/cli/aikit/__init__.py +1 -1
- package/runtime/cli/aikit/aliases.py +181 -0
- package/runtime/cli/aikit/cli_dispatch.py +5 -1
- package/runtime/cli/aikit/cli_parser.py +2 -1
- package/runtime/cli/aikit/human_output.py +32 -0
- package/runtime/cli/aikit/interactive_wizard.py +176 -6
- package/runtime/cli/aikit/natural_prompt_runtime.py +17 -1
- package/runtime/cli/aikit/personality.py +3 -0
- package/runtime/scripts/release-catalog-snapshot.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ agent doctor
|
|
|
30
30
|
Expected version for this release:
|
|
31
31
|
|
|
32
32
|
```text
|
|
33
|
-
agent 0.3.
|
|
33
|
+
agent 0.3.2
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## Quick Start
|
|
@@ -45,10 +45,13 @@ agent llm list
|
|
|
45
45
|
agent commands list
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
Agent DevKit `v0.3.
|
|
48
|
+
Agent DevKit `v0.3.2` includes the embedded Qwen2.5-0.5B mini-brain
|
|
49
49
|
contract for local bootstrap conversations without Ollama, Claude, Codex or API
|
|
50
50
|
keys. The npm package stays small; `agent setup mini-brain --yes` downloads the
|
|
51
51
|
GGUF into `.agent-devkit/models` after explicit opt-in.
|
|
52
|
+
When you rename the local agent, Agent DevKit now creates a matching command
|
|
53
|
+
alias under `.agent-devkit/bin` and can add that directory to PATH with
|
|
54
|
+
`agent alias path --yes`.
|
|
52
55
|
The `v0.3.0` deterministic runtime discovery and integration commands remain
|
|
53
56
|
available:
|
|
54
57
|
|
package/package.json
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
6
7
|
import re
|
|
7
8
|
import shutil
|
|
8
9
|
import sys
|
|
10
|
+
import unicodedata
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from typing import Any
|
|
11
13
|
|
|
@@ -14,6 +16,8 @@ from cli.aikit.app_home import app_home, ensure_app_home
|
|
|
14
16
|
|
|
15
17
|
ALIAS_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9_-]{1,63}$")
|
|
16
18
|
RESERVED_ALIASES = {"agent", "aikit", "ai-devkit", "python", "python3"}
|
|
19
|
+
SHELL_BLOCK_BEGIN = "# >>> agent-devkit aliases >>>"
|
|
20
|
+
SHELL_BLOCK_END = "# <<< agent-devkit aliases <<<"
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
def aliases_config_path() -> Path:
|
|
@@ -84,6 +88,38 @@ def add_alias(name: str, *, force: bool = False) -> dict[str, Any]:
|
|
|
84
88
|
return payload
|
|
85
89
|
|
|
86
90
|
|
|
91
|
+
def ensure_alias_for_agent_name(agent_name: str, *, force: bool = False) -> dict[str, Any]:
|
|
92
|
+
suggested = suggest_alias_name(agent_name)
|
|
93
|
+
payload: dict[str, Any] = {
|
|
94
|
+
"kind": "alias",
|
|
95
|
+
"requested_name": agent_name,
|
|
96
|
+
"suggested_name": suggested,
|
|
97
|
+
"created": False,
|
|
98
|
+
}
|
|
99
|
+
if not suggested:
|
|
100
|
+
return {
|
|
101
|
+
**payload,
|
|
102
|
+
"status": "invalid",
|
|
103
|
+
"message": "Agent name cannot be converted to a safe command alias.",
|
|
104
|
+
"path_status": alias_path_status(),
|
|
105
|
+
}
|
|
106
|
+
try:
|
|
107
|
+
added = add_alias(suggested, force=force)
|
|
108
|
+
except ValueError as exc:
|
|
109
|
+
return {
|
|
110
|
+
**payload,
|
|
111
|
+
"name": suggested,
|
|
112
|
+
"status": "blocked",
|
|
113
|
+
"message": str(exc),
|
|
114
|
+
"path_status": alias_path_status(suggested),
|
|
115
|
+
}
|
|
116
|
+
added["created"] = True
|
|
117
|
+
added["requested_name"] = agent_name
|
|
118
|
+
added["suggested_name"] = suggested
|
|
119
|
+
added["path_status"] = alias_path_status(suggested)
|
|
120
|
+
return added
|
|
121
|
+
|
|
122
|
+
|
|
87
123
|
def remove_alias(name: str) -> dict[str, Any]:
|
|
88
124
|
alias = validate_alias_name(name)
|
|
89
125
|
config = load_aliases()
|
|
@@ -138,6 +174,7 @@ def alias_payload(alias: str, item: dict[str, Any]) -> dict[str, Any]:
|
|
|
138
174
|
"cmd_path": str(aliases_bin_dir() / f"{alias}.cmd"),
|
|
139
175
|
"ps1_path": str(aliases_bin_dir() / f"{alias}.ps1"),
|
|
140
176
|
"created_by": item.get("created_by"),
|
|
177
|
+
"path_status": alias_path_status(alias),
|
|
141
178
|
}
|
|
142
179
|
|
|
143
180
|
|
|
@@ -194,3 +231,147 @@ def alias_executable_path(alias: str) -> Path:
|
|
|
194
231
|
|
|
195
232
|
def alias_paths(alias: str) -> list[Path]:
|
|
196
233
|
return [alias_executable_path(alias), aliases_bin_dir() / f"{alias}.cmd", aliases_bin_dir() / f"{alias}.ps1"]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def suggest_alias_name(agent_name: str) -> str | None:
|
|
237
|
+
cleaned = " ".join(str(agent_name or "").split()).strip()
|
|
238
|
+
if not cleaned:
|
|
239
|
+
return None
|
|
240
|
+
try:
|
|
241
|
+
return validate_alias_name(cleaned)
|
|
242
|
+
except ValueError:
|
|
243
|
+
pass
|
|
244
|
+
ascii_name = unicodedata.normalize("NFKD", cleaned).encode("ascii", "ignore").decode("ascii")
|
|
245
|
+
alias = re.sub(r"[^A-Za-z0-9_-]+", "-", ascii_name).strip("-_")
|
|
246
|
+
if alias and not alias[0].isalpha():
|
|
247
|
+
alias = f"agent-{alias}"
|
|
248
|
+
if len(alias) == 1:
|
|
249
|
+
alias = f"{alias}-agent"
|
|
250
|
+
alias = alias[:64].strip("-_")
|
|
251
|
+
if not alias:
|
|
252
|
+
return None
|
|
253
|
+
try:
|
|
254
|
+
return validate_alias_name(alias)
|
|
255
|
+
except ValueError:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def alias_path_status(alias: str | None = None) -> dict[str, Any]:
|
|
260
|
+
bin_dir = aliases_bin_dir()
|
|
261
|
+
path_entries = os.environ.get("PATH", "").split(os.pathsep) if os.environ.get("PATH") else []
|
|
262
|
+
bin_dir_in_path = any(paths_equal(Path(entry), bin_dir) for entry in path_entries if entry)
|
|
263
|
+
found = shutil.which(alias) if alias else None
|
|
264
|
+
available = False
|
|
265
|
+
if alias and found:
|
|
266
|
+
try:
|
|
267
|
+
available = Path(found).resolve() in {path.resolve() for path in alias_paths(alias)}
|
|
268
|
+
except OSError:
|
|
269
|
+
available = False
|
|
270
|
+
plan = alias_path_setup_plan()
|
|
271
|
+
return {
|
|
272
|
+
"bin_dir": str(bin_dir),
|
|
273
|
+
"bin_dir_in_path": bin_dir_in_path,
|
|
274
|
+
"alias_available": available,
|
|
275
|
+
"found": found,
|
|
276
|
+
"setup_required": not bin_dir_in_path,
|
|
277
|
+
"setup": plan,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def paths_equal(left: Path, right: Path) -> bool:
|
|
282
|
+
try:
|
|
283
|
+
return left.expanduser().resolve() == right.expanduser().resolve()
|
|
284
|
+
except OSError:
|
|
285
|
+
return left.expanduser() == right.expanduser()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def alias_path_setup_plan() -> dict[str, Any]:
|
|
289
|
+
bin_dir = aliases_bin_dir()
|
|
290
|
+
if os.name == "nt":
|
|
291
|
+
return {
|
|
292
|
+
"platform": "windows",
|
|
293
|
+
"target": "user PATH",
|
|
294
|
+
"command": f'agent alias path --yes',
|
|
295
|
+
"description": f"Add {bin_dir} to the current user's PATH.",
|
|
296
|
+
}
|
|
297
|
+
profile = detect_shell_profile()
|
|
298
|
+
return {
|
|
299
|
+
"platform": "posix",
|
|
300
|
+
"target": str(profile),
|
|
301
|
+
"command": "agent alias path --yes",
|
|
302
|
+
"description": f"Add {bin_dir} to PATH for future shell sessions.",
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def setup_alias_path(*, yes: bool = False) -> dict[str, Any]:
|
|
307
|
+
status = alias_path_status()
|
|
308
|
+
payload: dict[str, Any] = {
|
|
309
|
+
"kind": "alias-path",
|
|
310
|
+
"bin_dir": status["bin_dir"],
|
|
311
|
+
"path_status": status,
|
|
312
|
+
"executed": False,
|
|
313
|
+
}
|
|
314
|
+
if not status["setup_required"]:
|
|
315
|
+
return {**payload, "status": "ok", "message": "Agent DevKit alias bin directory is already on PATH."}
|
|
316
|
+
if not yes:
|
|
317
|
+
return {
|
|
318
|
+
**payload,
|
|
319
|
+
"status": "needs-confirmation",
|
|
320
|
+
"message": "Run `agent alias path --yes` to make configured aliases available as shell commands.",
|
|
321
|
+
}
|
|
322
|
+
if os.name == "nt":
|
|
323
|
+
return install_windows_user_path(payload)
|
|
324
|
+
return install_posix_shell_path(payload)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def install_posix_shell_path(payload: dict[str, Any]) -> dict[str, Any]:
|
|
328
|
+
profile = detect_shell_profile()
|
|
329
|
+
bin_dir = aliases_bin_dir()
|
|
330
|
+
profile.parent.mkdir(parents=True, exist_ok=True)
|
|
331
|
+
current = profile.read_text(encoding="utf-8") if profile.exists() else ""
|
|
332
|
+
export_line = f'export PATH="{bin_dir}:$PATH"'
|
|
333
|
+
block = f"\n{SHELL_BLOCK_BEGIN}\n{export_line}\n{SHELL_BLOCK_END}\n"
|
|
334
|
+
if str(bin_dir) not in current:
|
|
335
|
+
profile.write_text(current.rstrip() + block, encoding="utf-8")
|
|
336
|
+
return {
|
|
337
|
+
**payload,
|
|
338
|
+
"status": "updated",
|
|
339
|
+
"executed": True,
|
|
340
|
+
"profile": str(profile),
|
|
341
|
+
"message": "Alias PATH configured for future shell sessions. Restart the terminal or source the profile.",
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def install_windows_user_path(payload: dict[str, Any]) -> dict[str, Any]:
|
|
346
|
+
bin_dir = str(aliases_bin_dir())
|
|
347
|
+
try:
|
|
348
|
+
import winreg
|
|
349
|
+
|
|
350
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment", 0, winreg.KEY_READ | winreg.KEY_WRITE) as key:
|
|
351
|
+
try:
|
|
352
|
+
current, value_type = winreg.QueryValueEx(key, "Path")
|
|
353
|
+
except FileNotFoundError:
|
|
354
|
+
current, value_type = "", winreg.REG_EXPAND_SZ
|
|
355
|
+
entries = [entry for entry in str(current).split(os.pathsep) if entry]
|
|
356
|
+
if not any(paths_equal(Path(entry), aliases_bin_dir()) for entry in entries):
|
|
357
|
+
new_value = os.pathsep.join([bin_dir, *entries])
|
|
358
|
+
winreg.SetValueEx(key, "Path", 0, value_type, new_value)
|
|
359
|
+
except OSError as exc:
|
|
360
|
+
return {**payload, "status": "failed", "executed": False, "message": str(exc)}
|
|
361
|
+
return {
|
|
362
|
+
**payload,
|
|
363
|
+
"status": "updated",
|
|
364
|
+
"executed": True,
|
|
365
|
+
"profile": "HKCU\\Environment\\Path",
|
|
366
|
+
"message": "Alias PATH configured for future Windows terminal sessions.",
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def detect_shell_profile() -> Path:
|
|
371
|
+
shell = Path(os.environ.get("SHELL", "")).name
|
|
372
|
+
home = Path.home()
|
|
373
|
+
if shell == "zsh":
|
|
374
|
+
return home / ".zshrc"
|
|
375
|
+
if shell == "bash":
|
|
376
|
+
return home / ".bashrc"
|
|
377
|
+
return home / ".profile"
|
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from cli.aikit import __version__
|
|
10
|
-
from cli.aikit.aliases import add_alias, list_aliases, remove_alias, sync_aliases
|
|
10
|
+
from cli.aikit.aliases import add_alias, list_aliases, remove_alias, setup_alias_path, sync_aliases
|
|
11
11
|
from cli.aikit.agentic_commands import agentic_execute, agentic_plan
|
|
12
12
|
from cli.aikit.app_home import app_home_status, migrate_default_home
|
|
13
13
|
from cli.aikit.architecture import architecture_contract
|
|
@@ -1530,6 +1530,10 @@ def dispatch_alias(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1530
1530
|
if args.name:
|
|
1531
1531
|
raise DevKitError("alias sync does not accept a name")
|
|
1532
1532
|
return sync_aliases()
|
|
1533
|
+
if args.action == "path":
|
|
1534
|
+
if args.name:
|
|
1535
|
+
raise DevKitError("alias path does not accept a name")
|
|
1536
|
+
return setup_alias_path(yes=args.yes)
|
|
1533
1537
|
except ValueError as exc:
|
|
1534
1538
|
raise DevKitError(str(exc)) from exc
|
|
1535
1539
|
raise DevKitError(f"unsupported alias action: {args.action}")
|
|
@@ -251,9 +251,10 @@ def build_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
|
|
251
251
|
|
|
252
252
|
alias_parser = subparsers.add_parser("alias", help="manage local command aliases for agent")
|
|
253
253
|
alias_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
|
|
254
|
-
alias_parser.add_argument("action", nargs="?", default="list", choices=["add", "list", "remove", "sync"])
|
|
254
|
+
alias_parser.add_argument("action", nargs="?", default="list", choices=["add", "list", "remove", "sync", "path"])
|
|
255
255
|
alias_parser.add_argument("name", nargs="?")
|
|
256
256
|
alias_parser.add_argument("--force", action="store_true", help="allow replacing an existing local alias file")
|
|
257
|
+
alias_parser.add_argument("--yes", action="store_true", help="confirm alias PATH setup")
|
|
257
258
|
|
|
258
259
|
session_parser = subparsers.add_parser("session", help="manage local conversation sessions")
|
|
259
260
|
session_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
|
|
@@ -115,6 +115,8 @@ def print_human(result: dict[str, Any]) -> None:
|
|
|
115
115
|
print_aliases(result)
|
|
116
116
|
elif kind == "alias":
|
|
117
117
|
print_alias(result)
|
|
118
|
+
elif kind == "alias-path":
|
|
119
|
+
print_alias_path(result)
|
|
118
120
|
elif kind == "sessions":
|
|
119
121
|
print_sessions(result)
|
|
120
122
|
elif kind == "session":
|
|
@@ -630,6 +632,9 @@ def print_architecture(result: dict[str, Any]) -> None:
|
|
|
630
632
|
def print_agent_response(result: dict[str, Any]) -> None:
|
|
631
633
|
if result.get("status") == "ok":
|
|
632
634
|
print(result.get("response", ""))
|
|
635
|
+
alias = result.get("alias") if isinstance(result.get("alias"), dict) else None
|
|
636
|
+
if alias:
|
|
637
|
+
print_alias_path_hint(alias.get("path_status") if isinstance(alias.get("path_status"), dict) else None)
|
|
633
638
|
return
|
|
634
639
|
print(result.get("message") or result.get("response") or "Agent execution did not complete.")
|
|
635
640
|
question = result.get("next_question") or ((result.get("setup_wizard") or {}).get("next_question") if isinstance(result.get("setup_wizard"), dict) else None)
|
|
@@ -815,6 +820,12 @@ def print_personality(result: dict[str, Any]) -> None:
|
|
|
815
820
|
print("Setup questions:")
|
|
816
821
|
for question in result["questions"]:
|
|
817
822
|
print(f"- {question}")
|
|
823
|
+
alias = result.get("alias") if isinstance(result.get("alias"), dict) else None
|
|
824
|
+
if alias:
|
|
825
|
+
print(f"Alias: {alias.get('name') or alias.get('suggested_name') or '-'} ({alias.get('status')})")
|
|
826
|
+
if alias.get("path"):
|
|
827
|
+
print(f"Alias path: {alias['path']}")
|
|
828
|
+
print_alias_path_hint(alias.get("path_status") if isinstance(alias.get("path_status"), dict) else None)
|
|
818
829
|
|
|
819
830
|
|
|
820
831
|
def print_aliases(result: dict[str, Any]) -> None:
|
|
@@ -824,6 +835,7 @@ def print_aliases(result: dict[str, Any]) -> None:
|
|
|
824
835
|
return
|
|
825
836
|
for item in result["items"]:
|
|
826
837
|
print(f"- {item['name']}: {item['path']}")
|
|
838
|
+
print_alias_path_hint(item.get("path_status") if isinstance(item.get("path_status"), dict) else None, indent=" ")
|
|
827
839
|
|
|
828
840
|
|
|
829
841
|
def print_alias(result: dict[str, Any]) -> None:
|
|
@@ -835,6 +847,26 @@ def print_alias(result: dict[str, Any]) -> None:
|
|
|
835
847
|
for path in result["removed_paths"]:
|
|
836
848
|
print(f"- {path}")
|
|
837
849
|
print(f"Config: {result['config_path']}")
|
|
850
|
+
print_alias_path_hint(result.get("path_status") if isinstance(result.get("path_status"), dict) else None)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def print_alias_path(result: dict[str, Any]) -> None:
|
|
854
|
+
print(f"Alias PATH: {result.get('status')}")
|
|
855
|
+
print(f"Bin: {result.get('bin_dir') or '-'}")
|
|
856
|
+
if result.get("profile"):
|
|
857
|
+
print(f"Profile: {result['profile']}")
|
|
858
|
+
if result.get("message"):
|
|
859
|
+
print(result["message"])
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def print_alias_path_hint(path_status: dict[str, Any] | None, *, indent: str = "") -> None:
|
|
863
|
+
if not path_status or not path_status.get("setup_required"):
|
|
864
|
+
return
|
|
865
|
+
setup = path_status.get("setup") if isinstance(path_status.get("setup"), dict) else {}
|
|
866
|
+
print(f"{indent}PATH: {path_status.get('bin_dir')} is not active in this shell.")
|
|
867
|
+
command = setup.get("command")
|
|
868
|
+
if command:
|
|
869
|
+
print(f"{indent}Run: {command}")
|
|
838
870
|
|
|
839
871
|
|
|
840
872
|
def print_sessions(result: dict[str, Any]) -> None:
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import sys
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
from cli.aikit.core.requests import AgentPromptRequest
|
|
9
10
|
from cli.aikit.core.runtime import run_agent_prompt
|
|
11
|
+
from cli.aikit.aliases import setup_alias_path
|
|
10
12
|
from cli.aikit.llm import BACKENDS, configure_backend
|
|
11
13
|
from cli.aikit.mini_brain import DEFAULT_OLLAMA_MODEL
|
|
12
14
|
from cli.aikit.ollama import ollama_status
|
|
@@ -15,6 +17,30 @@ from cli.aikit.personality import load_personality, update_personality
|
|
|
15
17
|
from cli.aikit.runtime_paths import ROOT
|
|
16
18
|
from cli.aikit.wizard_state import WizardStateError, answer_wizard, cancel_wizard, show_wizard
|
|
17
19
|
|
|
20
|
+
ONBOARDING_MODE_OPTIONS = [
|
|
21
|
+
{
|
|
22
|
+
"id": "minimal",
|
|
23
|
+
"number": "1",
|
|
24
|
+
"label": "minimo",
|
|
25
|
+
"description": "identidade, mini-cerebro local embarcado e memoria",
|
|
26
|
+
"recommended": True,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "complete",
|
|
30
|
+
"number": "2",
|
|
31
|
+
"label": "completo",
|
|
32
|
+
"description": "minimo + toolchain, sources, notificacoes, knowledge e memorias",
|
|
33
|
+
"recommended": False,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "skip",
|
|
37
|
+
"number": "3",
|
|
38
|
+
"label": "pular",
|
|
39
|
+
"description": "",
|
|
40
|
+
"recommended": False,
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
|
|
18
44
|
|
|
19
45
|
def maybe_run_interactive_wizard(result: dict[str, Any]) -> dict[str, Any]:
|
|
20
46
|
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
@@ -126,18 +152,148 @@ def run_interactive_onboarding(result: dict[str, Any]) -> dict[str, Any]:
|
|
|
126
152
|
|
|
127
153
|
|
|
128
154
|
def choose_onboarding_mode() -> str:
|
|
155
|
+
selected = choose_onboarding_mode_with_arrows()
|
|
156
|
+
if selected:
|
|
157
|
+
return selected
|
|
158
|
+
print("\nModos de onboarding:")
|
|
159
|
+
for option in ONBOARDING_MODE_OPTIONS:
|
|
160
|
+
print(format_onboarding_option(option, selected=False, include_selector=False))
|
|
161
|
+
answer = ask_text("Escolha o modo:").strip().lower()
|
|
162
|
+
return parse_onboarding_mode_answer(answer)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def choose_onboarding_mode_with_arrows() -> str | None:
|
|
166
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty() or os.environ.get("TERM") == "dumb":
|
|
167
|
+
return None
|
|
168
|
+
try:
|
|
169
|
+
return read_onboarding_mode_selection()
|
|
170
|
+
except KeyboardInterrupt:
|
|
171
|
+
print()
|
|
172
|
+
return "skip"
|
|
173
|
+
except (OSError, ValueError):
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def read_onboarding_mode_selection() -> str:
|
|
178
|
+
selected_index = 0
|
|
179
|
+
typed_answer = ""
|
|
129
180
|
print("\nModos de onboarding:")
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
181
|
+
render_onboarding_options(selected_index)
|
|
182
|
+
print_onboarding_prompt(typed_answer)
|
|
183
|
+
while True:
|
|
184
|
+
key = read_key()
|
|
185
|
+
if key in {"\x03", "\x04"}:
|
|
186
|
+
raise KeyboardInterrupt
|
|
187
|
+
if key in {"\r", "\n"}:
|
|
188
|
+
if typed_answer:
|
|
189
|
+
parsed = parse_onboarding_mode_answer(typed_answer.strip().lower(), default="")
|
|
190
|
+
if parsed:
|
|
191
|
+
return parsed
|
|
192
|
+
typed_answer = ""
|
|
193
|
+
rerender_onboarding_options(selected_index, typed_answer)
|
|
194
|
+
continue
|
|
195
|
+
return str(ONBOARDING_MODE_OPTIONS[selected_index]["id"])
|
|
196
|
+
if key in {"\x1b[A", "k"}:
|
|
197
|
+
typed_answer = ""
|
|
198
|
+
selected_index = (selected_index - 1) % len(ONBOARDING_MODE_OPTIONS)
|
|
199
|
+
rerender_onboarding_options(selected_index, typed_answer)
|
|
200
|
+
continue
|
|
201
|
+
if key in {"\x1b[B", "j"}:
|
|
202
|
+
typed_answer = ""
|
|
203
|
+
selected_index = (selected_index + 1) % len(ONBOARDING_MODE_OPTIONS)
|
|
204
|
+
rerender_onboarding_options(selected_index, typed_answer)
|
|
205
|
+
continue
|
|
206
|
+
if key in {"\x7f", "\b"}:
|
|
207
|
+
typed_answer = typed_answer[:-1]
|
|
208
|
+
rerender_onboarding_options(selected_index, typed_answer)
|
|
209
|
+
continue
|
|
210
|
+
parsed = parse_onboarding_mode_answer(key.strip().lower(), default="")
|
|
211
|
+
if parsed:
|
|
212
|
+
print()
|
|
213
|
+
return parsed
|
|
214
|
+
if key.isprintable():
|
|
215
|
+
typed_answer += key
|
|
216
|
+
rerender_onboarding_options(selected_index, typed_answer)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def render_onboarding_options(selected_index: int) -> None:
|
|
220
|
+
for index, option in enumerate(ONBOARDING_MODE_OPTIONS):
|
|
221
|
+
print(format_onboarding_option(option, selected=index == selected_index, include_selector=True))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def rerender_onboarding_options(selected_index: int, typed_answer: str) -> None:
|
|
225
|
+
lines_to_move = len(ONBOARDING_MODE_OPTIONS) + 1
|
|
226
|
+
sys.stdout.write(f"\x1b[{lines_to_move}A")
|
|
227
|
+
for index, option in enumerate(ONBOARDING_MODE_OPTIONS):
|
|
228
|
+
sys.stdout.write("\x1b[2K")
|
|
229
|
+
sys.stdout.write(format_onboarding_option(option, selected=index == selected_index, include_selector=True) + "\n")
|
|
230
|
+
sys.stdout.write("\x1b[2K")
|
|
231
|
+
sys.stdout.write(onboarding_prompt_line(typed_answer) + "\n")
|
|
232
|
+
sys.stdout.flush()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def format_onboarding_option(option: dict[str, Any], *, selected: bool, include_selector: bool) -> str:
|
|
236
|
+
selector = "> " if selected else " "
|
|
237
|
+
prefix = selector if include_selector else ""
|
|
238
|
+
description = f": {option['description']}" if option.get("description") else ""
|
|
239
|
+
recommended = " (Recomendado)" if option.get("recommended") else ""
|
|
240
|
+
return f"{prefix}{option['number']}. {option['label']}{description}{recommended}"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def parse_onboarding_mode_answer(answer: str, *, default: str = "minimal") -> str:
|
|
244
|
+
if not answer:
|
|
245
|
+
return default
|
|
134
246
|
if answer in {"1", "minimo", "mínimo", "minimal"}:
|
|
135
247
|
return "minimal"
|
|
136
248
|
if answer in {"2", "completo", "complete", "full"}:
|
|
137
249
|
return "complete"
|
|
138
250
|
if answer in {"3", "pular", "skip", "cancelar", "cancel"}:
|
|
139
251
|
return "skip"
|
|
140
|
-
return
|
|
252
|
+
return default
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def print_onboarding_prompt(typed_answer: str) -> None:
|
|
256
|
+
print(onboarding_prompt_line(typed_answer))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def onboarding_prompt_line(typed_answer: str) -> str:
|
|
260
|
+
suffix = f" {typed_answer}" if typed_answer else ""
|
|
261
|
+
return f"Escolha o modo:{suffix}"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def read_key() -> str:
|
|
265
|
+
if os.name == "nt":
|
|
266
|
+
import msvcrt
|
|
267
|
+
|
|
268
|
+
char = msvcrt.getwch()
|
|
269
|
+
if char in {"\x00", "\xe0"}:
|
|
270
|
+
code = msvcrt.getwch()
|
|
271
|
+
if code == "H":
|
|
272
|
+
return "\x1b[A"
|
|
273
|
+
if code == "P":
|
|
274
|
+
return "\x1b[B"
|
|
275
|
+
return char
|
|
276
|
+
|
|
277
|
+
import termios
|
|
278
|
+
import tty
|
|
279
|
+
import select
|
|
280
|
+
|
|
281
|
+
fd = sys.stdin.fileno()
|
|
282
|
+
old_settings = termios.tcgetattr(fd)
|
|
283
|
+
try:
|
|
284
|
+
tty.setraw(fd)
|
|
285
|
+
char = sys.stdin.read(1)
|
|
286
|
+
if char == "\x1b":
|
|
287
|
+
sequence = ""
|
|
288
|
+
for _ in range(2):
|
|
289
|
+
ready, _, _ = select.select([sys.stdin], [], [], 0.01)
|
|
290
|
+
if not ready:
|
|
291
|
+
break
|
|
292
|
+
sequence += sys.stdin.read(1)
|
|
293
|
+
char += sequence
|
|
294
|
+
return char
|
|
295
|
+
finally:
|
|
296
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
141
297
|
|
|
142
298
|
|
|
143
299
|
def configure_personality_interactively(agent: dict[str, Any]) -> None:
|
|
@@ -149,13 +305,27 @@ def configure_personality_interactively(agent: dict[str, Any]) -> None:
|
|
|
149
305
|
language = ask_text("Idioma padrao das respostas?", default=str(agent.get("language") or "pt-BR"))
|
|
150
306
|
tone = ask_text("Tom das respostas?", default=current_tone)
|
|
151
307
|
detail_level = ask_text("Nivel de detalhe?", default=current_detail)
|
|
152
|
-
update_personality(
|
|
308
|
+
payload = update_personality(
|
|
153
309
|
agent_name=agent_name,
|
|
154
310
|
user_name=user_name,
|
|
155
311
|
language=language,
|
|
156
312
|
tone=tone,
|
|
157
313
|
detail_level=detail_level,
|
|
158
314
|
)
|
|
315
|
+
alias = payload.get("alias") if isinstance(payload.get("alias"), dict) else None
|
|
316
|
+
if not alias:
|
|
317
|
+
return
|
|
318
|
+
if alias.get("status") == "added":
|
|
319
|
+
print(f"Comando local criado: {alias.get('name')}")
|
|
320
|
+
elif alias.get("message"):
|
|
321
|
+
print(f"Alias nao configurado automaticamente: {alias['message']}")
|
|
322
|
+
path_status = alias.get("path_status") if isinstance(alias.get("path_status"), dict) else {}
|
|
323
|
+
if path_status.get("setup_required"):
|
|
324
|
+
bin_dir = path_status.get("bin_dir")
|
|
325
|
+
print(f"O comando foi criado em {bin_dir}, mas essa pasta ainda nao esta no PATH.")
|
|
326
|
+
if ask_yes_no("Deseja habilitar aliases do Agent DevKit no shell para proximas sessoes?", default=True):
|
|
327
|
+
setup = setup_alias_path(yes=True)
|
|
328
|
+
print(setup.get("message") or "PATH de aliases atualizado.")
|
|
159
329
|
|
|
160
330
|
|
|
161
331
|
def configure_llm_interactively() -> None:
|
|
@@ -62,6 +62,21 @@ def clean_requested_name(value: str) -> str | None:
|
|
|
62
62
|
return cleaned[:80]
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
def rename_response(updated: dict[str, Any]) -> str:
|
|
66
|
+
name = updated.get("agent_name")
|
|
67
|
+
alias = updated.get("alias") if isinstance(updated.get("alias"), dict) else {}
|
|
68
|
+
alias_name = alias.get("name") or alias.get("suggested_name")
|
|
69
|
+
path_status = alias.get("path_status") if isinstance(alias.get("path_status"), dict) else {}
|
|
70
|
+
if alias_name and path_status.get("setup_required"):
|
|
71
|
+
return (
|
|
72
|
+
f"Pronto. Meu nome local agora e {name}, e criei o comando {alias_name}. "
|
|
73
|
+
"Para chama-lo diretamente em novas sessoes do shell, execute `agent alias path --yes`."
|
|
74
|
+
)
|
|
75
|
+
if alias_name:
|
|
76
|
+
return f"Pronto. Meu nome local agora e {name}, e voce tambem pode me chamar com o comando {alias_name}."
|
|
77
|
+
return f"Pronto. Meu nome local agora e {name}."
|
|
78
|
+
|
|
79
|
+
|
|
65
80
|
def is_capabilities_help_prompt(prompt: str) -> bool:
|
|
66
81
|
normalized = normalize_text(prompt)
|
|
67
82
|
if normalized in {"ajuda", "help", "o que voce faz", "o que voce consegue fazer"}:
|
|
@@ -220,7 +235,8 @@ def run_agent_prompt_request(request: AgentPromptRequest) -> dict[str, Any]:
|
|
|
220
235
|
"prompt_length": len(prompt),
|
|
221
236
|
"identity": {"name": updated.get("agent_name"), "source": "local"},
|
|
222
237
|
"action": "rename",
|
|
223
|
-
"response":
|
|
238
|
+
"response": rename_response(updated),
|
|
239
|
+
"alias": updated.get("alias"),
|
|
224
240
|
}
|
|
225
241
|
return finalize_agent_session(result, session, prompt, backend=request.llm)
|
|
226
242
|
if is_identity_question(prompt):
|
|
@@ -6,6 +6,7 @@ import re
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from cli.aikit.aliases import ensure_alias_for_agent_name
|
|
9
10
|
from cli.aikit.identity import DEFAULT_PUBLIC_NAME
|
|
10
11
|
from cli.aikit.memory import MEMORY_FILE_TEMPLATES, ensure_memory, memory_home
|
|
11
12
|
|
|
@@ -60,6 +61,8 @@ def update_personality(
|
|
|
60
61
|
path.write_text(render_personality(values), encoding="utf-8")
|
|
61
62
|
payload = load_personality()
|
|
62
63
|
payload["status"] = "updated"
|
|
64
|
+
if agent_name is not None and payload.get("agent_name") != DEFAULT_PUBLIC_NAME:
|
|
65
|
+
payload["alias"] = ensure_alias_for_agent_name(str(payload["agent_name"]))
|
|
63
66
|
return payload
|
|
64
67
|
|
|
65
68
|
|