s-agentskit 0.1.0__py3-none-any.whl
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.
- agentskit/__init__.py +65 -0
- agentskit/cli.py +139 -0
- agentskit/data/agents.json +149 -0
- agentskit/detect.py +55 -0
- agentskit/inject.py +84 -0
- agentskit/markers.py +23 -0
- agentskit/onboard.py +104 -0
- agentskit/spec.py +151 -0
- agentskit/targets.py +94 -0
- s_agentskit-0.1.0.dist-info/METADATA +92 -0
- s_agentskit-0.1.0.dist-info/RECORD +14 -0
- s_agentskit-0.1.0.dist-info/WHEEL +4 -0
- s_agentskit-0.1.0.dist-info/entry_points.txt +2 -0
- s_agentskit-0.1.0.dist-info/licenses/LICENSE +21 -0
agentskit/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""agentskit — AI-агенты как сущности: автономный онбординг инструкций.
|
|
2
|
+
|
|
3
|
+
Переиспользуемый слой поверх ``librarykit``. Делает любой навык/инструмент
|
|
4
|
+
**автономным онбордером**: сам прописывает свои инструкции (managed-блок) в
|
|
5
|
+
конфиг-файлы любого AI-агента (``.claude``/``.cursor``/``.codex``/``.gemini``/
|
|
6
|
+
``.github``/…) идемпотентно и аддитивно — чужой текст не затирается.
|
|
7
|
+
|
|
8
|
+
Механизм generic: реестр агент→файл как ДАННЫЕ (``data/agents.json``), детект по
|
|
9
|
+
ФС, инъекция managed-блока между параметризуемыми маркерами, резолв global/project.
|
|
10
|
+
Контент (тело инструкции + namespace) приносит потребитель — это НЕ код кита.
|
|
11
|
+
|
|
12
|
+
Роль «агент-как-адаптер» (драйв агента из комбайна) — зона ``adapterkit``, не
|
|
13
|
+
этого кита (agentskit отвечает только за онбординг-данные и инъекцию).
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
from .detect import DetectedAgent, detect_agents
|
|
20
|
+
from .inject import (
|
|
21
|
+
has_managed_block,
|
|
22
|
+
inject_managed_block,
|
|
23
|
+
managed_block,
|
|
24
|
+
strip_managed_block,
|
|
25
|
+
)
|
|
26
|
+
from .markers import begin_marker, end_marker
|
|
27
|
+
from .onboard import ApplyResult, onboard, uninstall
|
|
28
|
+
from .spec import (
|
|
29
|
+
AgentSpec,
|
|
30
|
+
SkillLayout,
|
|
31
|
+
agent_registry,
|
|
32
|
+
get_agent_spec,
|
|
33
|
+
list_agents,
|
|
34
|
+
register_agent_spec,
|
|
35
|
+
resolve_agent_keys,
|
|
36
|
+
)
|
|
37
|
+
from .targets import Target, global_path_for, resolve_targets
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"__version__",
|
|
41
|
+
# инъекция / маркеры
|
|
42
|
+
"begin_marker",
|
|
43
|
+
"end_marker",
|
|
44
|
+
"managed_block",
|
|
45
|
+
"has_managed_block",
|
|
46
|
+
"inject_managed_block",
|
|
47
|
+
"strip_managed_block",
|
|
48
|
+
# реестр агентов (данные)
|
|
49
|
+
"AgentSpec",
|
|
50
|
+
"SkillLayout",
|
|
51
|
+
"agent_registry",
|
|
52
|
+
"get_agent_spec",
|
|
53
|
+
"list_agents",
|
|
54
|
+
"register_agent_spec",
|
|
55
|
+
"resolve_agent_keys",
|
|
56
|
+
# детект / резолв целей / онбординг
|
|
57
|
+
"DetectedAgent",
|
|
58
|
+
"detect_agents",
|
|
59
|
+
"Target",
|
|
60
|
+
"global_path_for",
|
|
61
|
+
"resolve_targets",
|
|
62
|
+
"ApplyResult",
|
|
63
|
+
"onboard",
|
|
64
|
+
"uninstall",
|
|
65
|
+
]
|
agentskit/cli.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""CLI кита: ``agentskit detect / agents / onboard / uninstall`` (на clikit).
|
|
2
|
+
|
|
3
|
+
Кит — прежде всего библиотека (потребитель зовёт ``agentskit.onboard(...)``), но
|
|
4
|
+
даёт тонкий автономный CLI для самостоятельного использования и для раздачи как
|
|
5
|
+
tooling-навык. json-by-default (clikit); ``--text`` — человекочитаемо.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from clikit import CliError, build_root_app, command, emit_data
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .detect import detect_agents
|
|
17
|
+
from .onboard import onboard as _onboard
|
|
18
|
+
from .onboard import uninstall as _uninstall
|
|
19
|
+
from .spec import list_agents, resolve_agent_keys
|
|
20
|
+
|
|
21
|
+
app = build_root_app(
|
|
22
|
+
"agentskit",
|
|
23
|
+
version=__version__,
|
|
24
|
+
help="AI-агенты как сущности: онбординг инструкций в агентские файлы (--json по умолчанию).",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _keys(agents: str) -> list[str] | None:
|
|
29
|
+
if not agents.strip():
|
|
30
|
+
return None
|
|
31
|
+
try:
|
|
32
|
+
return resolve_agent_keys(agents)
|
|
33
|
+
except ValueError as exc:
|
|
34
|
+
raise CliError("bad_agents", str(exc)) from exc
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command("agents")
|
|
38
|
+
@command
|
|
39
|
+
def agents_cmd() -> None:
|
|
40
|
+
"""Весь канонический реестр агентов (ключ/label/config_dir/файлы)."""
|
|
41
|
+
data = [
|
|
42
|
+
{
|
|
43
|
+
"key": s.key,
|
|
44
|
+
"label": s.label,
|
|
45
|
+
"config_dir": s.config_dir,
|
|
46
|
+
"repo_file": s.repo_file,
|
|
47
|
+
"global": list(s.global_subpath) if s.global_subpath else None,
|
|
48
|
+
"aliases": list(s.aliases),
|
|
49
|
+
}
|
|
50
|
+
for s in list_agents()
|
|
51
|
+
]
|
|
52
|
+
emit_data(data, text_renderer=lambda rows: [
|
|
53
|
+
print(f"{r['key']:<12} {r['config_dir']:<10} "
|
|
54
|
+
f"{r['repo_file'] or '—':<28} {r['label']}") for r in rows
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command("detect")
|
|
59
|
+
@command
|
|
60
|
+
def detect_cmd(
|
|
61
|
+
glob: bool = typer.Option(False, "--global", help="Считать и $HOME-агентов."),
|
|
62
|
+
include_absent: bool = typer.Option(False, "--all", help="Показать и ненайденных."),
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Какие AI-агенты присутствуют (config-папка в cwd и/или $HOME)."""
|
|
65
|
+
found = detect_agents(include_absent=include_absent)
|
|
66
|
+
data = [{"key": d.key, "label": d.label, "present": d.present, "scope": d.scope}
|
|
67
|
+
for d in found]
|
|
68
|
+
emit_data(data, text_renderer=lambda rows: (
|
|
69
|
+
[print(f"{r['key']:<12} {r['scope']:<8} {r['label']}") for r in rows]
|
|
70
|
+
or [print("агентов не найдено — трактуй как 'all'")] if not rows else None
|
|
71
|
+
))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command("onboard")
|
|
75
|
+
@command
|
|
76
|
+
def onboard_cmd(
|
|
77
|
+
namespace: str = typer.Option(..., "--namespace", "-n", help="namespace плагина"),
|
|
78
|
+
body_file: str | None = typer.Option(None, "--body-file", help="Файл с телом (markdown)."),
|
|
79
|
+
body: str | None = typer.Option(None, "--body", help="Тело инструкции строкой ('-' — stdin)."),
|
|
80
|
+
scope: str = typer.Option("all", "--scope", help="global | repo | all."),
|
|
81
|
+
agents: str = typer.Option("", "--agents", help="CSV ключей/алиасов или 'all'."),
|
|
82
|
+
create: bool = typer.Option(False, "--create", help="Создать файлы выбранных агентов."),
|
|
83
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Показать, что изменится, без записи."),
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Прописать managed-блок плагина в выбранных AI-агентов (reference-режим)."""
|
|
86
|
+
if scope not in ("global", "repo", "all"):
|
|
87
|
+
raise CliError("bad_scope", f"--scope: global|repo|all, не '{scope}'.")
|
|
88
|
+
text = _read_body(body, body_file)
|
|
89
|
+
results = _onboard(
|
|
90
|
+
namespace=namespace, body=text, scope=scope, agents=_keys(agents),
|
|
91
|
+
create=create, dry_run=dry_run,
|
|
92
|
+
)
|
|
93
|
+
emit_data(
|
|
94
|
+
[{"path": r.path, "action": r.action, "agent": r.agent_key, "reason": r.reason}
|
|
95
|
+
for r in results],
|
|
96
|
+
text_renderer=lambda rows: [
|
|
97
|
+
print(f" {r['action']:<12} {r['agent']:<10} {r['path']}"
|
|
98
|
+
+ (f" ({r['reason']})" if r['reason'] else "")) for r in rows
|
|
99
|
+
] or [print("целей нет")],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command("uninstall")
|
|
104
|
+
@command
|
|
105
|
+
def uninstall_cmd(
|
|
106
|
+
namespace: str = typer.Option(..., "--namespace", "-n", help="namespace плагина."),
|
|
107
|
+
scope: str = typer.Option("all", "--scope", help="global | repo | all."),
|
|
108
|
+
agents: str = typer.Option("", "--agents", help="CSV ключей/алиасов или 'all'."),
|
|
109
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Показать без записи."),
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Снять managed-блок плагина (чужие блоки не трогаются)."""
|
|
112
|
+
results = _uninstall(
|
|
113
|
+
namespace=namespace, scope=scope, agents=_keys(agents), dry_run=dry_run,
|
|
114
|
+
)
|
|
115
|
+
emit_data(
|
|
116
|
+
[{"path": r.path, "action": r.action, "agent": r.agent_key, "reason": r.reason}
|
|
117
|
+
for r in results],
|
|
118
|
+
text_renderer=lambda rows: [
|
|
119
|
+
print(f" {r['action']:<12} {r['agent']:<10} {r['path']}") for r in rows
|
|
120
|
+
] or [print("целей нет")],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _read_body(body: str | None, body_file: str | None) -> str:
|
|
125
|
+
if body_file:
|
|
126
|
+
return Path(body_file).read_text(encoding="utf-8")
|
|
127
|
+
if body == "-":
|
|
128
|
+
return sys.stdin.read()
|
|
129
|
+
if body is not None:
|
|
130
|
+
return body
|
|
131
|
+
raise CliError("no_body", "Нужно --body '<текст>' / --body - (stdin) / --body-file <md>.")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main() -> None:
|
|
135
|
+
app()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
main()
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_schema": "agentskit.agents/1 — реестр AI-агентов как ДАННЫЕ (не код). config_dir → детект; repo_file/global_subpath → reference-инъекция; skill_layout → full-материализация (фаза 2). Источник канона папок: ui-ux-pro-max AI_FOLDERS + global-память Atlas.",
|
|
3
|
+
"agents": {
|
|
4
|
+
"claude": {
|
|
5
|
+
"label": "Claude Code",
|
|
6
|
+
"config_dir": ".claude",
|
|
7
|
+
"repo_file": "CLAUDE.md",
|
|
8
|
+
"global_subpath": [".claude", "CLAUDE.md"],
|
|
9
|
+
"aliases": ["claude-code", "claudecode"],
|
|
10
|
+
"skill_layout": {"subdir": ["skills", "{skill}"], "filename": "SKILL.md", "install_type": "full", "frontmatter": true}
|
|
11
|
+
},
|
|
12
|
+
"codex": {
|
|
13
|
+
"label": "Codex / AGENTS.md",
|
|
14
|
+
"config_dir": ".codex",
|
|
15
|
+
"repo_file": "AGENTS.md",
|
|
16
|
+
"global_subpath": [".codex", "AGENTS.md"],
|
|
17
|
+
"aliases": ["agents", "agents.md"],
|
|
18
|
+
"skill_layout": {"subdir": [], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
19
|
+
},
|
|
20
|
+
"gemini": {
|
|
21
|
+
"label": "Gemini CLI",
|
|
22
|
+
"config_dir": ".gemini",
|
|
23
|
+
"repo_file": "GEMINI.md",
|
|
24
|
+
"global_subpath": [".gemini", "GEMINI.md"],
|
|
25
|
+
"aliases": ["gemini-cli"],
|
|
26
|
+
"skill_layout": {"subdir": [], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
27
|
+
},
|
|
28
|
+
"cursor": {
|
|
29
|
+
"label": "Cursor",
|
|
30
|
+
"config_dir": ".cursor",
|
|
31
|
+
"repo_file": ".cursorrules",
|
|
32
|
+
"global_subpath": null,
|
|
33
|
+
"aliases": [],
|
|
34
|
+
"skill_layout": {"subdir": [".shared"], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
35
|
+
},
|
|
36
|
+
"copilot": {
|
|
37
|
+
"label": "GitHub Copilot",
|
|
38
|
+
"config_dir": ".github",
|
|
39
|
+
"repo_file": ".github/copilot-instructions.md",
|
|
40
|
+
"global_subpath": null,
|
|
41
|
+
"aliases": ["copilot-instructions"],
|
|
42
|
+
"skill_layout": {"subdir": ["prompts"], "filename": "{skill}.prompt.md", "install_type": "full", "frontmatter": true}
|
|
43
|
+
},
|
|
44
|
+
"windsurf": {
|
|
45
|
+
"label": "Windsurf",
|
|
46
|
+
"config_dir": ".windsurf",
|
|
47
|
+
"repo_file": ".windsurfrules",
|
|
48
|
+
"global_subpath": null,
|
|
49
|
+
"aliases": [],
|
|
50
|
+
"skill_layout": {"subdir": ["rules"], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
51
|
+
},
|
|
52
|
+
"antigravity": {
|
|
53
|
+
"label": "Antigravity",
|
|
54
|
+
"config_dir": ".agents",
|
|
55
|
+
"repo_file": "AGENTS.md",
|
|
56
|
+
"global_subpath": null,
|
|
57
|
+
"aliases": ["agent"],
|
|
58
|
+
"skill_layout": {"subdir": [], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
59
|
+
},
|
|
60
|
+
"kiro": {
|
|
61
|
+
"label": "Kiro",
|
|
62
|
+
"config_dir": ".kiro",
|
|
63
|
+
"repo_file": null,
|
|
64
|
+
"global_subpath": null,
|
|
65
|
+
"aliases": [],
|
|
66
|
+
"skill_layout": {"subdir": ["steering"], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
67
|
+
},
|
|
68
|
+
"roocode": {
|
|
69
|
+
"label": "Roo Code",
|
|
70
|
+
"config_dir": ".roo",
|
|
71
|
+
"repo_file": null,
|
|
72
|
+
"global_subpath": null,
|
|
73
|
+
"aliases": ["roo"],
|
|
74
|
+
"skill_layout": {"subdir": ["rules"], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
75
|
+
},
|
|
76
|
+
"qoder": {
|
|
77
|
+
"label": "Qoder",
|
|
78
|
+
"config_dir": ".qoder",
|
|
79
|
+
"repo_file": null,
|
|
80
|
+
"global_subpath": null,
|
|
81
|
+
"aliases": [],
|
|
82
|
+
"skill_layout": {"subdir": [], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
83
|
+
},
|
|
84
|
+
"trae": {
|
|
85
|
+
"label": "Trae",
|
|
86
|
+
"config_dir": ".trae",
|
|
87
|
+
"repo_file": null,
|
|
88
|
+
"global_subpath": null,
|
|
89
|
+
"aliases": [],
|
|
90
|
+
"skill_layout": {"subdir": ["rules"], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
91
|
+
},
|
|
92
|
+
"opencode": {
|
|
93
|
+
"label": "OpenCode",
|
|
94
|
+
"config_dir": ".opencode",
|
|
95
|
+
"repo_file": null,
|
|
96
|
+
"global_subpath": null,
|
|
97
|
+
"aliases": [],
|
|
98
|
+
"skill_layout": {"subdir": [], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
99
|
+
},
|
|
100
|
+
"continue": {
|
|
101
|
+
"label": "Continue",
|
|
102
|
+
"config_dir": ".continue",
|
|
103
|
+
"repo_file": null,
|
|
104
|
+
"global_subpath": null,
|
|
105
|
+
"aliases": [],
|
|
106
|
+
"skill_layout": {"subdir": ["rules"], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
107
|
+
},
|
|
108
|
+
"codebuddy": {
|
|
109
|
+
"label": "CodeBuddy",
|
|
110
|
+
"config_dir": ".codebuddy",
|
|
111
|
+
"repo_file": null,
|
|
112
|
+
"global_subpath": null,
|
|
113
|
+
"aliases": [],
|
|
114
|
+
"skill_layout": {"subdir": [], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
115
|
+
},
|
|
116
|
+
"droid": {
|
|
117
|
+
"label": "Factory Droid",
|
|
118
|
+
"config_dir": ".factory",
|
|
119
|
+
"repo_file": null,
|
|
120
|
+
"global_subpath": null,
|
|
121
|
+
"aliases": ["factory"],
|
|
122
|
+
"skill_layout": {"subdir": [], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
123
|
+
},
|
|
124
|
+
"kilocode": {
|
|
125
|
+
"label": "Kilo Code",
|
|
126
|
+
"config_dir": ".kilocode",
|
|
127
|
+
"repo_file": null,
|
|
128
|
+
"global_subpath": null,
|
|
129
|
+
"aliases": ["kilo"],
|
|
130
|
+
"skill_layout": {"subdir": ["rules"], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
131
|
+
},
|
|
132
|
+
"warp": {
|
|
133
|
+
"label": "Warp",
|
|
134
|
+
"config_dir": ".warp",
|
|
135
|
+
"repo_file": null,
|
|
136
|
+
"global_subpath": null,
|
|
137
|
+
"aliases": [],
|
|
138
|
+
"skill_layout": {"subdir": [], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
139
|
+
},
|
|
140
|
+
"augment": {
|
|
141
|
+
"label": "Augment",
|
|
142
|
+
"config_dir": ".augment",
|
|
143
|
+
"repo_file": null,
|
|
144
|
+
"global_subpath": null,
|
|
145
|
+
"aliases": [],
|
|
146
|
+
"skill_layout": {"subdir": [], "filename": "{skill}.md", "install_type": "full", "frontmatter": false}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
agentskit/detect.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Детект AI-агентов по файловой системе (перенос ui-ux-pro-max ``detectAIType``).
|
|
2
|
+
|
|
3
|
+
Структурный детект: для каждого ``AgentSpec`` проверяем наличие его ``config_dir``
|
|
4
|
+
в проекте (cwd) и/или в $HOME. Пусто → caller трактует как 'all'.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .spec import AgentSpec, list_agents
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class DetectedAgent:
|
|
16
|
+
"""Найденный агент + где (project/global/both)."""
|
|
17
|
+
|
|
18
|
+
key: str
|
|
19
|
+
label: str
|
|
20
|
+
present: bool # config_dir существует (cwd и/или $HOME)
|
|
21
|
+
scope: str # "project" | "global" | "both" | "none"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _scope_of(spec: AgentSpec, cwd: Path, home: Path) -> str:
|
|
25
|
+
in_proj = (cwd / spec.config_dir).exists()
|
|
26
|
+
in_home = (home / spec.config_dir).exists()
|
|
27
|
+
if in_proj and in_home:
|
|
28
|
+
return "both"
|
|
29
|
+
if in_proj:
|
|
30
|
+
return "project"
|
|
31
|
+
if in_home:
|
|
32
|
+
return "global"
|
|
33
|
+
return "none"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def detect_agents(
|
|
37
|
+
cwd: Path | None = None,
|
|
38
|
+
*,
|
|
39
|
+
home: Path | None = None,
|
|
40
|
+
include_absent: bool = False,
|
|
41
|
+
) -> list[DetectedAgent]:
|
|
42
|
+
"""Просканировать ФС по реестру. ``include_absent=False`` — только найденные.
|
|
43
|
+
|
|
44
|
+
Возвращает список в порядке реестра. Пустой список найденных → caller
|
|
45
|
+
трактует как 'all' (как ``suggested='all'`` в ui-ux-pro-max).
|
|
46
|
+
"""
|
|
47
|
+
base = Path(cwd) if cwd is not None else Path.cwd()
|
|
48
|
+
h = Path(home) if home is not None else Path.home()
|
|
49
|
+
out: list[DetectedAgent] = []
|
|
50
|
+
for spec in list_agents():
|
|
51
|
+
scope = _scope_of(spec, base, h)
|
|
52
|
+
present = scope != "none"
|
|
53
|
+
if present or include_absent:
|
|
54
|
+
out.append(DetectedAgent(spec.key, spec.label, present, scope))
|
|
55
|
+
return out
|
agentskit/inject.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Идемпотентная инъекция managed-блока в текст агентского файла.
|
|
2
|
+
|
|
3
|
+
Чистый текст-ин → текст-аут (без I/O) — тестируемо; запись/детект файлов — в
|
|
4
|
+
``onboard``/``targets``. Алгоритм перенесён из Atlas ``agent_discipline`` 1:1
|
|
5
|
+
(выстраданные тонкости сохранены — см. ниже) и параметризован ``namespace``.
|
|
6
|
+
|
|
7
|
+
Выстраданные тонкости (НЕ переписывать):
|
|
8
|
+
- ``pattern.sub(lambda _m: managed, ...)`` — lambda во избежание интерпретации
|
|
9
|
+
``\\``-групп в теле блока как backreference (RUF-safe).
|
|
10
|
+
- логика разделителя при дописывании (`""` / `"\\n"` / `"\\n\\n"`).
|
|
11
|
+
- ``re.DOTALL`` — блок может быть многострочным.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from .markers import begin_marker, end_marker
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def managed_block(body: str, *, namespace: str) -> str:
|
|
21
|
+
"""Полный managed-блок с маркерами ``namespace`` (то, что вставляется)."""
|
|
22
|
+
return f"{begin_marker(namespace)}\n{body}\n{end_marker(namespace)}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def has_managed_block(text: str, *, namespace: str) -> bool:
|
|
26
|
+
"""Есть ли уже наш managed-блок (оба маркера ``namespace``) в тексте."""
|
|
27
|
+
return begin_marker(namespace) in text and end_marker(namespace) in text
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def inject_managed_block(
|
|
31
|
+
text: str,
|
|
32
|
+
body: str | None = None,
|
|
33
|
+
*,
|
|
34
|
+
namespace: str,
|
|
35
|
+
block: str | None = None,
|
|
36
|
+
begin: str | None = None,
|
|
37
|
+
end: str | None = None,
|
|
38
|
+
) -> str:
|
|
39
|
+
"""Вставить/обновить managed-блок ``namespace`` в ``text``. Чужое не затирает.
|
|
40
|
+
|
|
41
|
+
- маркеры уже есть → заменяем СОДЕРЖИМОЕ между ними (обновление);
|
|
42
|
+
- маркеров нет → дописываем блок в КОНЕЦ (с корректным разделителем).
|
|
43
|
+
|
|
44
|
+
``body`` — тело блока (без маркеров); ``block`` — целиком готовый блок (тогда
|
|
45
|
+
``body`` игнорируется). Идемпотентно: повторный вызов с тем же содержимым →
|
|
46
|
+
no-op по смыслу (текст совпадает).
|
|
47
|
+
"""
|
|
48
|
+
b = begin if begin is not None else begin_marker(namespace)
|
|
49
|
+
e = end if end is not None else end_marker(namespace)
|
|
50
|
+
if block is not None:
|
|
51
|
+
managed = block
|
|
52
|
+
else:
|
|
53
|
+
if body is None:
|
|
54
|
+
raise ValueError("inject_managed_block: нужен body или block.")
|
|
55
|
+
managed = managed_block(body, namespace=namespace)
|
|
56
|
+
|
|
57
|
+
pattern = re.compile(re.escape(b) + r".*?" + re.escape(e), re.DOTALL)
|
|
58
|
+
if pattern.search(text):
|
|
59
|
+
# lambda во избежание интерпретации \-групп в managed как backreference.
|
|
60
|
+
return pattern.sub(lambda _m: managed, text, count=1)
|
|
61
|
+
if not text.strip():
|
|
62
|
+
return managed + "\n"
|
|
63
|
+
sep = "\n" if text.endswith("\n") else "\n\n"
|
|
64
|
+
if text.endswith("\n\n"):
|
|
65
|
+
sep = ""
|
|
66
|
+
return text + sep + managed + "\n"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def strip_managed_block(text: str, *, namespace: str) -> str:
|
|
70
|
+
"""Удалить managed-блок ``namespace`` из текста (для uninstall).
|
|
71
|
+
|
|
72
|
+
Чужие блоки других namespace не трогаются. Если блока нет — текст возвращается
|
|
73
|
+
как есть. Подчищает лишний разделитель, оставшийся после вырезания.
|
|
74
|
+
"""
|
|
75
|
+
b = begin_marker(namespace)
|
|
76
|
+
e = end_marker(namespace)
|
|
77
|
+
pattern = re.compile(
|
|
78
|
+
r"\n*" + re.escape(b) + r".*?" + re.escape(e) + r"\n*", re.DOTALL
|
|
79
|
+
)
|
|
80
|
+
if not pattern.search(text):
|
|
81
|
+
return text
|
|
82
|
+
out = pattern.sub("\n", text, count=1)
|
|
83
|
+
# не оставляем ведущий перевод строки, если блок был в начале файла.
|
|
84
|
+
return out.lstrip("\n") if not text[: text.find(b)].strip() else out
|
agentskit/markers.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Managed-маркеры онбординг-блока, параметризованные namespace плагина.
|
|
2
|
+
|
|
3
|
+
Один движок инъекции обслуживает N плагинов в ОДНОМ файле без коллизий: каждый
|
|
4
|
+
плагин владеет своей парой маркеров, выведенной из его namespace. Например
|
|
5
|
+
``atlas init`` пишет ``<!-- ATLAS:BEGIN managed -->``, гипотетический ``uipro
|
|
6
|
+
init`` — ``<!-- UIPRO:BEGIN managed -->``; оба блока мирно сосуществуют в одном
|
|
7
|
+
``CLAUDE.md``.
|
|
8
|
+
|
|
9
|
+
Обратная совместимость: ``begin_marker("atlas")`` == исторический Atlas-маркер
|
|
10
|
+
``<!-- ATLAS:BEGIN managed -->`` (был хардкодом ``BEGIN_MARKER``), поэтому уже
|
|
11
|
+
прописанные ATLAS-блоки находятся и обновляются как раньше.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def begin_marker(namespace: str) -> str:
|
|
17
|
+
"""Открывающий маркер managed-блока для ``namespace`` (UPPER-регистр)."""
|
|
18
|
+
return f"<!-- {namespace.upper()}:BEGIN managed -->"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def end_marker(namespace: str) -> str:
|
|
22
|
+
"""Закрывающий маркер managed-блока для ``namespace`` (UPPER-регистр)."""
|
|
23
|
+
return f"<!-- {namespace.upper()}:END -->"
|
agentskit/onboard.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Оркестратор автономного онбординга — главная точка роли A.
|
|
2
|
+
|
|
3
|
+
Для каждой цели (``resolve_targets``) применяет managed-блок плагина:
|
|
4
|
+
- **reference** (MVP): идемпотентная инъекция блока в memory-файл агента
|
|
5
|
+
(перенос Atlas ``_apply_one``: детект exists, granular-verbs, dry-run);
|
|
6
|
+
- **full** (фаза 2): материализация контента в per-agent layout — пока заглушка.
|
|
7
|
+
|
|
8
|
+
``uninstall`` снимает managed-блок namespace (чужие блоки не трогает).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .inject import has_managed_block, inject_managed_block, strip_managed_block
|
|
16
|
+
from .targets import Target, resolve_targets
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ApplyResult:
|
|
21
|
+
"""Результат применения к одной цели."""
|
|
22
|
+
|
|
23
|
+
path: str
|
|
24
|
+
action: str # created|appended|updated|unchanged|skipped|removed|would-<verb>
|
|
25
|
+
agent_key: str
|
|
26
|
+
reason: str = ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _apply_one(
|
|
30
|
+
t: Target, body: str, *, namespace: str, dry_run: bool
|
|
31
|
+
) -> ApplyResult:
|
|
32
|
+
if t.mode != "reference":
|
|
33
|
+
return ApplyResult(
|
|
34
|
+
str(t.path), "skipped", t.agent_key,
|
|
35
|
+
reason="full-режим пока не реализован (фаза 2)",
|
|
36
|
+
)
|
|
37
|
+
exists = t.path.exists()
|
|
38
|
+
if not exists and not t.create_if_missing:
|
|
39
|
+
return ApplyResult(str(t.path), "skipped", t.agent_key, reason="файла нет")
|
|
40
|
+
existing = t.path.read_text(encoding="utf-8") if exists else ""
|
|
41
|
+
updated = inject_managed_block(existing, body, namespace=namespace)
|
|
42
|
+
if updated == existing:
|
|
43
|
+
return ApplyResult(str(t.path), "unchanged", t.agent_key)
|
|
44
|
+
had = has_managed_block(existing, namespace=namespace)
|
|
45
|
+
verb = ("updated" if had else "appended") if exists else "created"
|
|
46
|
+
if dry_run:
|
|
47
|
+
return ApplyResult(str(t.path), f"would-{verb}", t.agent_key)
|
|
48
|
+
t.path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
t.path.write_text(updated, encoding="utf-8")
|
|
50
|
+
return ApplyResult(str(t.path), verb, t.agent_key)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def onboard(
|
|
54
|
+
*,
|
|
55
|
+
namespace: str,
|
|
56
|
+
body: str,
|
|
57
|
+
scope: str = "all",
|
|
58
|
+
agents: list[str] | None = None,
|
|
59
|
+
mode: str = "reference",
|
|
60
|
+
create: bool = False,
|
|
61
|
+
dry_run: bool = False,
|
|
62
|
+
force: bool = False, # full-режим (фаза 2); reference идемпотентен и так
|
|
63
|
+
cwd: Path | None = None,
|
|
64
|
+
) -> list[ApplyResult]:
|
|
65
|
+
"""Прописать managed-блок ``namespace`` (тело ``body``) выбранным агентам.
|
|
66
|
+
|
|
67
|
+
``agents=None`` → легаси (существующие агентские файлы); список ключей →
|
|
68
|
+
точечный выбор (``resolve_agent_keys`` на стороне caller). Идемпотентно:
|
|
69
|
+
повторный вызов с тем же ``body`` → ``unchanged``.
|
|
70
|
+
"""
|
|
71
|
+
targets = resolve_targets(
|
|
72
|
+
scope=scope, agents=agents, mode=mode, create=create, cwd=cwd,
|
|
73
|
+
)
|
|
74
|
+
return [_apply_one(t, body, namespace=namespace, dry_run=dry_run) for t in targets]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def uninstall(
|
|
78
|
+
*,
|
|
79
|
+
namespace: str,
|
|
80
|
+
scope: str = "all",
|
|
81
|
+
agents: list[str] | None = None,
|
|
82
|
+
dry_run: bool = False,
|
|
83
|
+
cwd: Path | None = None,
|
|
84
|
+
) -> list[ApplyResult]:
|
|
85
|
+
"""Снять managed-блок ``namespace`` (reference). Чужие блоки не трогаются."""
|
|
86
|
+
targets = resolve_targets(
|
|
87
|
+
scope=scope, agents=agents, mode="reference", create=False, cwd=cwd,
|
|
88
|
+
)
|
|
89
|
+
results: list[ApplyResult] = []
|
|
90
|
+
for t in targets:
|
|
91
|
+
if not t.path.exists():
|
|
92
|
+
results.append(ApplyResult(str(t.path), "skipped", t.agent_key, reason="файла нет"))
|
|
93
|
+
continue
|
|
94
|
+
existing = t.path.read_text(encoding="utf-8")
|
|
95
|
+
if not has_managed_block(existing, namespace=namespace):
|
|
96
|
+
results.append(ApplyResult(str(t.path), "skipped", t.agent_key, reason="нет блока"))
|
|
97
|
+
continue
|
|
98
|
+
stripped = strip_managed_block(existing, namespace=namespace)
|
|
99
|
+
if dry_run:
|
|
100
|
+
results.append(ApplyResult(str(t.path), "would-removed", t.agent_key))
|
|
101
|
+
continue
|
|
102
|
+
t.path.write_text(stripped, encoding="utf-8")
|
|
103
|
+
results.append(ApplyResult(str(t.path), "removed", t.agent_key))
|
|
104
|
+
return results
|
agentskit/spec.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Реестр AI-агентов: ``AgentSpec`` как ДАННЫЕ (загружаются из ``data/agents.json``).
|
|
2
|
+
|
|
3
|
+
Принцип (из эталона ui-ux-pro-max): layout у каждого агента СВОЙ и описан
|
|
4
|
+
декларативно — добавить агента = добавить запись в ``agents.json`` (или
|
|
5
|
+
``register_agent_spec`` / entry-points ``agentskit.agent_specs``), без правки кода.
|
|
6
|
+
``agents.json`` — единый language-neutral источник правды (его же прочитает будущий
|
|
7
|
+
npm/npx-CLI).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from functools import lru_cache
|
|
14
|
+
from importlib import resources
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class SkillLayout:
|
|
19
|
+
"""Per-agent layout для full-материализации (фаза 2) — ДАННЫЕ, не код."""
|
|
20
|
+
|
|
21
|
+
subdir: tuple[str, ...] # claude: ("skills","{skill}"); copilot: ("prompts",)
|
|
22
|
+
filename: str # "SKILL.md" | "{skill}.prompt.md"
|
|
23
|
+
install_type: str = "full" # "full" | "reference"
|
|
24
|
+
frontmatter: bool = True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class AgentSpec:
|
|
29
|
+
"""Один AI-агент как ДАННЫЕ: куда и как писать инструкции."""
|
|
30
|
+
|
|
31
|
+
key: str # "claude" | "cursor" | ...
|
|
32
|
+
label: str # "Claude Code"
|
|
33
|
+
config_dir: str # корневая config-папка: ".claude"
|
|
34
|
+
repo_file: str | None = None # reference: файл инструкций в корне репо
|
|
35
|
+
global_subpath: tuple[str, ...] | None = None # reference: путь под $HOME
|
|
36
|
+
skill_layout: SkillLayout | None = None # full: layout (фаза 2)
|
|
37
|
+
aliases: tuple[str, ...] = () # дружественные алиасы для --agents
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _spec_from_dict(key: str, d: dict) -> AgentSpec:
|
|
41
|
+
sl = d.get("skill_layout")
|
|
42
|
+
layout = (
|
|
43
|
+
SkillLayout(
|
|
44
|
+
subdir=tuple(sl.get("subdir") or ()),
|
|
45
|
+
filename=sl["filename"],
|
|
46
|
+
install_type=sl.get("install_type", "full"),
|
|
47
|
+
frontmatter=bool(sl.get("frontmatter", True)),
|
|
48
|
+
)
|
|
49
|
+
if sl
|
|
50
|
+
else None
|
|
51
|
+
)
|
|
52
|
+
gs = d.get("global_subpath")
|
|
53
|
+
return AgentSpec(
|
|
54
|
+
key=key,
|
|
55
|
+
label=d.get("label", key),
|
|
56
|
+
config_dir=d["config_dir"],
|
|
57
|
+
repo_file=d.get("repo_file"),
|
|
58
|
+
global_subpath=tuple(gs) if gs else None,
|
|
59
|
+
skill_layout=layout,
|
|
60
|
+
aliases=tuple(d.get("aliases") or ()),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@lru_cache(maxsize=1)
|
|
65
|
+
def _builtin_registry() -> dict[str, AgentSpec]:
|
|
66
|
+
"""Канон из ``data/agents.json`` + расширения из entry-points ``agentskit.agent_specs``."""
|
|
67
|
+
raw = resources.files("agentskit").joinpath("data", "agents.json").read_text("utf-8")
|
|
68
|
+
data = json.loads(raw)
|
|
69
|
+
reg: dict[str, AgentSpec] = {
|
|
70
|
+
key: _spec_from_dict(key, d) for key, d in data["agents"].items()
|
|
71
|
+
}
|
|
72
|
+
# внешние пакеты добавляют агентов через entry-points (не правя кит).
|
|
73
|
+
try:
|
|
74
|
+
from importlib.metadata import entry_points
|
|
75
|
+
|
|
76
|
+
for ep in entry_points(group="agentskit.agent_specs"):
|
|
77
|
+
try:
|
|
78
|
+
obj = ep.load()
|
|
79
|
+
spec = obj() if callable(obj) else obj
|
|
80
|
+
if isinstance(spec, AgentSpec):
|
|
81
|
+
reg[spec.key] = spec
|
|
82
|
+
except Exception: # pragma: no cover — битый плагин не валит реестр
|
|
83
|
+
continue
|
|
84
|
+
except Exception: # pragma: no cover
|
|
85
|
+
pass
|
|
86
|
+
return reg
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
#: In-tree ручные расширения (перебивают встроенный канон). Заполняется
|
|
90
|
+
#: ``register_agent_spec``; собирается в финальный реестр в ``agent_registry()``.
|
|
91
|
+
_OVERRIDES: dict[str, AgentSpec] = {}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def register_agent_spec(spec: AgentSpec) -> None:
|
|
95
|
+
"""Зарегистрировать/переопределить агента в процессе (перебивает встроенный)."""
|
|
96
|
+
_OVERRIDES[spec.key] = spec
|
|
97
|
+
agent_registry.cache_clear()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@lru_cache(maxsize=1)
|
|
101
|
+
def agent_registry() -> dict[str, AgentSpec]:
|
|
102
|
+
"""Итоговый реестр: встроенный канон + entry-points + ручные override."""
|
|
103
|
+
reg = dict(_builtin_registry())
|
|
104
|
+
reg.update(_OVERRIDES)
|
|
105
|
+
return reg
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _alias_index() -> dict[str, str]:
|
|
109
|
+
idx: dict[str, str] = {}
|
|
110
|
+
for key, spec in agent_registry().items():
|
|
111
|
+
idx[key] = key
|
|
112
|
+
for a in spec.aliases:
|
|
113
|
+
idx[a.lower()] = key
|
|
114
|
+
return idx
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def list_agents() -> list[AgentSpec]:
|
|
118
|
+
"""Весь канонический реестр (для меню/CLI ``agents``)."""
|
|
119
|
+
return list(agent_registry().values())
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_agent_spec(key: str) -> AgentSpec | None:
|
|
123
|
+
"""AgentSpec по ключу или алиасу (или ``None``)."""
|
|
124
|
+
return agent_registry().get(_alias_index().get(key.strip().lower(), key))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def resolve_agent_keys(raw: str) -> list[str]:
|
|
128
|
+
"""Разобрать строку ``--agents`` в список валидных ключей реестра.
|
|
129
|
+
|
|
130
|
+
Принимает ``"all"`` или CSV ключей/алиасов (``"claude,gemini"`` /
|
|
131
|
+
``"agents, cursor"``). Дубликаты схлопываются с сохранением порядка.
|
|
132
|
+
Неизвестный ключ → ``ValueError`` со списком валидных.
|
|
133
|
+
"""
|
|
134
|
+
raw = (raw or "").strip()
|
|
135
|
+
idx = _alias_index()
|
|
136
|
+
if raw.lower() == "all":
|
|
137
|
+
return list(agent_registry().keys())
|
|
138
|
+
out: list[str] = []
|
|
139
|
+
for tok in raw.split(","):
|
|
140
|
+
name = tok.strip().lower()
|
|
141
|
+
if not name:
|
|
142
|
+
continue
|
|
143
|
+
key = idx.get(name)
|
|
144
|
+
if key is None:
|
|
145
|
+
valid = ", ".join([*agent_registry().keys(), "all"])
|
|
146
|
+
raise ValueError(f"Неизвестный агент '{tok.strip()}'. Доступно: {valid}.")
|
|
147
|
+
if key not in out:
|
|
148
|
+
out.append(key)
|
|
149
|
+
if not out:
|
|
150
|
+
raise ValueError("Пустой список --agents.")
|
|
151
|
+
return out
|
agentskit/targets.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Резолв целевых файлов онбординга под scope (global/project) и выбор агентов.
|
|
2
|
+
|
|
3
|
+
Перенос Atlas ``commands/init._resolve_targets`` + ``_global_path_for``,
|
|
4
|
+
обобщённый на реестр ``agentskit.spec``. ``_global_claude_md`` — единая
|
|
5
|
+
override-точка (мокается в тестах), как в Atlas.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .spec import AgentSpec, agent_registry, list_agents
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _global_claude_md() -> Path:
|
|
16
|
+
"""Глобальный CLAUDE.md (override-точка для тестов)."""
|
|
17
|
+
return Path.home() / ".claude" / "CLAUDE.md"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def global_path_for(spec: AgentSpec) -> Path | None:
|
|
21
|
+
"""Глобальный memory-файл агента под $HOME (claude — через override-точку)."""
|
|
22
|
+
if spec.key == "claude":
|
|
23
|
+
return _global_claude_md()
|
|
24
|
+
if spec.global_subpath is None:
|
|
25
|
+
return None
|
|
26
|
+
return Path.home().joinpath(*spec.global_subpath)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Target:
|
|
31
|
+
"""Цель онбординга: путь + создавать-ли-если-нет + агент + режим."""
|
|
32
|
+
|
|
33
|
+
path: Path
|
|
34
|
+
create_if_missing: bool
|
|
35
|
+
agent_key: str
|
|
36
|
+
mode: str = "reference"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _reference_repo_files() -> list[tuple[str, str]]:
|
|
40
|
+
"""[(agent_key, repo_file)] для агентов с reference-файлом (порядок реестра)."""
|
|
41
|
+
return [(s.key, s.repo_file) for s in list_agents() if s.repo_file]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_targets(
|
|
45
|
+
*,
|
|
46
|
+
scope: str = "all",
|
|
47
|
+
agents: list[str] | None = None,
|
|
48
|
+
mode: str = "reference",
|
|
49
|
+
create: bool = False,
|
|
50
|
+
cwd: Path | None = None,
|
|
51
|
+
) -> list[Target]:
|
|
52
|
+
"""Список целей под scope.
|
|
53
|
+
|
|
54
|
+
- ``agents is None`` — ЛЕГАСИ (как Atlas): global → ~/.claude/CLAUDE.md; repo →
|
|
55
|
+
существующие reference-файлы агентов в cwd (или AGENTS.md при --create).
|
|
56
|
+
- ``agents`` задан — точечный выбор: для каждого выбранного агента его
|
|
57
|
+
global-файл (создаётся всегда — выбор явный) и/или repo-файл (--create).
|
|
58
|
+
Reference-режим: агенты без ``repo_file`` пропускаются в repo-scope.
|
|
59
|
+
"""
|
|
60
|
+
base = Path(cwd) if cwd is not None else Path.cwd()
|
|
61
|
+
targets: list[Target] = []
|
|
62
|
+
|
|
63
|
+
if agents is not None:
|
|
64
|
+
for key in agents:
|
|
65
|
+
spec = agent_registry().get(key)
|
|
66
|
+
if spec is None:
|
|
67
|
+
continue
|
|
68
|
+
if scope in ("global", "all"):
|
|
69
|
+
gp = global_path_for(spec)
|
|
70
|
+
if gp is not None:
|
|
71
|
+
targets.append(Target(gp, True, key, mode))
|
|
72
|
+
if scope in ("repo", "all") and spec.repo_file:
|
|
73
|
+
targets.append(Target(base / spec.repo_file, create, key, mode))
|
|
74
|
+
return targets
|
|
75
|
+
|
|
76
|
+
# Легаси (без --agents) — прежнее поведение Atlas.
|
|
77
|
+
if scope in ("global", "all"):
|
|
78
|
+
targets.append(Target(_global_claude_md(), True, "claude", mode))
|
|
79
|
+
if scope in ("repo", "all"):
|
|
80
|
+
seen: set[Path] = set()
|
|
81
|
+
found: list[Target] = []
|
|
82
|
+
for key, rf in _reference_repo_files():
|
|
83
|
+
p = base / rf
|
|
84
|
+
if p in seen:
|
|
85
|
+
continue
|
|
86
|
+
if p.exists():
|
|
87
|
+
seen.add(p)
|
|
88
|
+
found.append(Target(p, False, key, mode))
|
|
89
|
+
if found:
|
|
90
|
+
targets.extend(found)
|
|
91
|
+
elif create:
|
|
92
|
+
# дефолт Atlas: создать AGENTS.md (codex-конвенция).
|
|
93
|
+
targets.append(Target(base / "AGENTS.md", True, "codex", mode))
|
|
94
|
+
return targets
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: s-agentskit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-агенты как сущности: автономный онбординг инструкций (идемпотентные managed-блоки в .claude/.cursor/.codex/.gemini/.github/…) поверх единого реестра агентов-данных. Тонкий слой на clikit.
|
|
5
|
+
Author: Dmitry
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: s-clikit
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
12
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# agentskit
|
|
16
|
+
|
|
17
|
+
**AI-агенты как сущности: автономный онбординг инструкций.** Переиспользуемый
|
|
18
|
+
слой (dist `s-agentskit` / import `agentskit`) поверх `clikit`. Делает любой
|
|
19
|
+
навык/инструмент **автономным онбордером**: он сам прописывает свои инструкции
|
|
20
|
+
(managed-блок) в конфиг-файлы любого AI-агента — `.claude` / `.cursor` / `.codex`
|
|
21
|
+
/ `.gemini` / `.github` / … — идемпотентно и аддитивно (чужой текст не затирается).
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
agentskit (реестр агентов = данные) → onboard(namespace, body) → managed-блок в N агентов
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Зачем
|
|
28
|
+
|
|
29
|
+
Знание «где живёт каждый AI-агент» (его config-папка и memory-файл) не должно
|
|
30
|
+
дублироваться в каждом инструменте. agentskit держит **единый реестр агентов как
|
|
31
|
+
ДАННЫЕ** (`data/agents.json`, 18 агентов) — потребитель приносит только контент
|
|
32
|
+
(тело инструкции + свой namespace), а механизм (детект, инъекция, резолв
|
|
33
|
+
global/project) общий.
|
|
34
|
+
|
|
35
|
+
> Роль «агент-как-адаптер» (драйв агента из комбайна gateway/bublictr) — зона
|
|
36
|
+
> **adapterkit**, не этого кита. agentskit отвечает только за онбординг.
|
|
37
|
+
|
|
38
|
+
## Установка
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install s-agentskit # import agentskit (dist-имя ≠ import-имя)
|
|
42
|
+
# dev: uv sync --extra dev
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Библиотека (основной способ)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from agentskit import onboard, resolve_agent_keys
|
|
49
|
+
|
|
50
|
+
onboard(
|
|
51
|
+
namespace="atlas", # маркеры <!-- ATLAS:BEGIN/END -->
|
|
52
|
+
body="## Работай в Atlas\n- atlas task …",
|
|
53
|
+
scope="all", # global | repo | all
|
|
54
|
+
agents=resolve_agent_keys("claude,gemini"), # или None — все существующие файлы
|
|
55
|
+
create=True,
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Идемпотентно: повторный вызов с тем же `body` → `unchanged`. Несколько плагинов
|
|
60
|
+
(разные `namespace`) сосуществуют в одном `CLAUDE.md` без коллизий.
|
|
61
|
+
|
|
62
|
+
## CLI
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
agentskit agents # весь реестр агентов
|
|
66
|
+
agentskit detect # какие агенты есть в проекте/$HOME
|
|
67
|
+
agentskit onboard -n mytool --body-file INSTRUCTIONS.md --agents claude,cursor --create
|
|
68
|
+
agentskit uninstall -n mytool --agents all
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Публичный API
|
|
72
|
+
|
|
73
|
+
`onboard` / `uninstall` · `detect_agents` / `list_agents` · `resolve_agent_keys`
|
|
74
|
+
· `resolve_targets` · `inject_managed_block` / `has_managed_block` /
|
|
75
|
+
`strip_managed_block` / `managed_block` · `begin_marker` / `end_marker` ·
|
|
76
|
+
`AgentSpec` / `SkillLayout` / `register_agent_spec` / `agent_registry`.
|
|
77
|
+
|
|
78
|
+
## Расширение реестра
|
|
79
|
+
|
|
80
|
+
Добавить агента без правки кита: запись в `data/agents.json`, либо
|
|
81
|
+
`register_agent_spec(AgentSpec(...))`, либо entry-points группа
|
|
82
|
+
`agentskit.agent_specs` во внешнем пакете.
|
|
83
|
+
|
|
84
|
+
## Режимы
|
|
85
|
+
|
|
86
|
+
- **reference** (MVP): managed-блок-указатель в memory-файл агента (`CLAUDE.md` и т.п.).
|
|
87
|
+
- **full** (фаза 2): материализация контента целиком в per-agent layout (как
|
|
88
|
+
`uipro`-инсталлеры).
|
|
89
|
+
|
|
90
|
+
## Лицензия
|
|
91
|
+
|
|
92
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
agentskit/__init__.py,sha256=Bdads3VJIReyYFOsu4UxaMvt2zreJVUpMmgrHsbTEdk,2419
|
|
2
|
+
agentskit/cli.py,sha256=6zRkodoCsRdS8tz-A6QLKUoR0BX49U-Ky1uw6PbqAc4,5721
|
|
3
|
+
agentskit/detect.py,sha256=sJefxFEYF7cFhY2r9OZ4N_JIHngMOSYDVS5ByJoXiK0,1965
|
|
4
|
+
agentskit/inject.py,sha256=Tq7uV3kFK2Nt6Op4jv0tTJg45b6O4NDMOhjwvxmjHvc,4126
|
|
5
|
+
agentskit/markers.py,sha256=WVqGCbepeqHASLRmX_OYKpK5w6ipxP7tt-4jVbtzhNE,1390
|
|
6
|
+
agentskit/onboard.py,sha256=1pkMaFiA4NrrHotrhrM3jHmr5qm_XIrZNwP-vorI2nc,4400
|
|
7
|
+
agentskit/spec.py,sha256=zlCuBvlYvaHexvCg5iiezgHT8ppphQbaHgBmxI2UDAQ,6177
|
|
8
|
+
agentskit/targets.py,sha256=MN_I7x1F5nq-oN6OMAIiiozdV1xiVMkCkorlI7dWyg0,3717
|
|
9
|
+
agentskit/data/agents.json,sha256=mJrSnYGujmzLWY8syOe8gfDkaOT6fIsIiTgqS5xO0tI,5517
|
|
10
|
+
s_agentskit-0.1.0.dist-info/METADATA,sha256=_Kouwd-9wNuodwWqoJXOA_54fBDSfRCrK5uIWsNdL50,4274
|
|
11
|
+
s_agentskit-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
12
|
+
s_agentskit-0.1.0.dist-info/entry_points.txt,sha256=w7lzzIEhSjceWC-Nxjk07GoslPm1qowVrmwmpGIy6Ck,49
|
|
13
|
+
s_agentskit-0.1.0.dist-info/licenses/LICENSE,sha256=j9GKJmUNdQuKRUbKhbpv0uyMaL99xsxE6L2TDtXuaZ4,1063
|
|
14
|
+
s_agentskit-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|