ai-docs-gen 0.1.2__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.
ai_docs/summary.py ADDED
@@ -0,0 +1,238 @@
1
+ from pathlib import Path
2
+ from typing import Dict, List
3
+
4
+ from .tokenizer import chunk_text
5
+ from .utils import ensure_dir
6
+
7
+
8
+ SUMMARY_PROMPT = """
9
+ Ты эксперт по технической документации. Сформируй краткое, но информативное описание файла для включения в документацию.
10
+ Укажи назначение, ключевые сущности и важные настройки. Если файл конфигурационный — перечисли ключевые параметры/секции.
11
+ Ответ строго в Markdown, без заголовка. Не используй блоки кода и не оборачивай текст в ```markdown.
12
+ """.strip()
13
+
14
+ MODULE_SUMMARY_PROMPT = """
15
+ Ты технический писатель. Сформируй документацию модуля в стиле Doxygen.
16
+ Сначала дай краткое верхнеуровневое описание модуля (2–4 предложения).
17
+ Затем, если есть важные структуры данных/типы, добавь блок:
18
+ Ключевые структуры данных
19
+ <имя> — <краткое описание>
20
+
21
+ Далее перечисли функции/процедуры и классы строго в Doxygen‑формате.
22
+ Для функций/процедур используй формат:
23
+
24
+ <сигнатура>
25
+ <краткое назначение одной строкой>
26
+ Аргументы
27
+ <имя> — <описание>
28
+ Возвращает
29
+ <описание>
30
+ Исключения
31
+ <описание>
32
+
33
+ Для классов используй формат:
34
+ class <имя>
35
+ <краткое назначение одной строкой>
36
+ Поля
37
+ <имя> — <описание>
38
+ Методы
39
+ <сигнатура> — <краткое назначение>
40
+
41
+ Если аргументов/возвращаемого значения/исключений/полей нет — соответствующий блок пропускай.
42
+ Разделяй сущности строкой из трёх дефисов: `---`.
43
+ Не используй заголовки Markdown, списки, подзаголовки вроде "Основные функции".
44
+ Ответ строго в Markdown, без заголовка документа, сохраняя последовательность блоков.
45
+ """.strip()
46
+
47
+ MODULE_SUMMARY_REFORMAT_PROMPT = """
48
+ Переформатируй текст в строгий Doxygen‑стиль для модуля.
49
+ Требования:
50
+ - Без заголовков Markdown, без списков, без блоков кода.
51
+ - Структура: краткое описание модуля; затем (если есть) "Ключевые структуры данных" с линиями "<имя> — <описание>".
52
+ - Далее только сущности (функции/процедуры/классы) в формате:
53
+ <сигнатура>
54
+ <краткое назначение одной строкой>
55
+ Аргументы
56
+ <имя> — <описание>
57
+ Возвращает
58
+ <описание>
59
+ Исключения
60
+ <описание>
61
+ Для классов:
62
+ class <имя>
63
+ <краткое назначение одной строкой>
64
+ Поля
65
+ <имя> — <описание>
66
+ Методы
67
+ <сигнатура> — <краткое назначение>
68
+
69
+ Если блок пустой — не выводи его. Между сущностями ставь строку `---`.
70
+ Ответ строго в Markdown без заголовка документа.
71
+ """.strip()
72
+
73
+ CONFIG_SUMMARY_PROMPT = """
74
+ Ты технический писатель. Сформируй описание конфигурационного файла в универсальном стиле.
75
+ Сначала дай краткое описание файла (2–4 предложения).
76
+ Затем блок:
77
+ Секции и ключи
78
+ <секция/ключ> — <описание>
79
+
80
+ Далее (если есть важные параметры) добавь блок:
81
+ Важные параметры
82
+ <параметр> — <описание>
83
+
84
+ Не используй заголовки Markdown, списки, нумерацию и блоки кода.
85
+ Ответ строго в Markdown без заголовка документа, соблюдай указанные блоки.
86
+ """.strip()
87
+
88
+ CONFIG_SUMMARY_REFORMAT_PROMPT = """
89
+ Переформатируй текст в универсальный конфиг-стиль.
90
+ Требования:
91
+ - Без заголовков Markdown, списков, нумерации и блоков кода.
92
+ - Структура: краткое описание файла; затем блок "Секции и ключи" с линиями "<секция/ключ> — <описание>".
93
+ - Далее (если есть) блок "Важные параметры" с линиями "<параметр> — <описание>".
94
+ Если блок пустой — не выводи его.
95
+ Ответ строго в Markdown без заголовка документа.
96
+ """.strip()
97
+
98
+
99
+ def _needs_doxygen_fix(text: str) -> bool:
100
+ if "```" in text:
101
+ return True
102
+ for line in text.splitlines():
103
+ stripped = line.strip()
104
+ if stripped.startswith("#"):
105
+ return True
106
+ if stripped.startswith(("-", "*", "•")):
107
+ return True
108
+ if stripped[:2].isdigit() and stripped[1] == ".":
109
+ return True
110
+ lowered = text.lower()
111
+ noisy_markers = [
112
+ "основные функции",
113
+ "основные возможности",
114
+ "обработка ошибок",
115
+ "интеграции",
116
+ "ключевые структуры данных:",
117
+ "##",
118
+ ]
119
+ return any(marker in lowered for marker in noisy_markers)
120
+
121
+
122
+ def _normalize_module_summary(
123
+ summary: str, llm_client, llm_cache: Dict[str, str]
124
+ ) -> str:
125
+ if not _needs_doxygen_fix(summary):
126
+ return summary
127
+ messages = [
128
+ {"role": "system", "content": MODULE_SUMMARY_REFORMAT_PROMPT},
129
+ {"role": "user", "content": summary},
130
+ ]
131
+ return llm_client.chat(messages, cache=llm_cache).strip()
132
+
133
+
134
+ def _normalize_config_summary(summary: str, llm_client, llm_cache: Dict[str, str]) -> str:
135
+ if not _needs_doxygen_fix(summary):
136
+ return _format_config_blocks(summary)
137
+ messages = [
138
+ {"role": "system", "content": CONFIG_SUMMARY_REFORMAT_PROMPT},
139
+ {"role": "user", "content": summary},
140
+ ]
141
+ return _format_config_blocks(llm_client.chat(messages, cache=llm_cache).strip())
142
+
143
+
144
+ def _format_config_blocks(text: str) -> str:
145
+ lines = [line.rstrip() for line in text.strip().splitlines() if line.strip()]
146
+ if not lines:
147
+ return text.strip()
148
+ output: List[str] = []
149
+ i = 0
150
+ headers = {"Секции и ключи", "Важные параметры"}
151
+ while i < len(lines):
152
+ line = lines[i].strip()
153
+ if line in headers:
154
+ entries: List[str] = []
155
+ i += 1
156
+ while i < len(lines) and lines[i].strip() not in headers:
157
+ entries.append(lines[i].strip())
158
+ i += 1
159
+ output.append(line)
160
+ if entries:
161
+ output.append("<br>\n".join(entries))
162
+ continue
163
+ output.append(line)
164
+ i += 1
165
+ return "\n\n".join(output).strip()
166
+
167
+
168
+ def _strip_fenced_markdown(text: str) -> str:
169
+ stripped = text.strip()
170
+ if stripped.startswith("```"):
171
+ lines = stripped.splitlines()
172
+ if len(lines) >= 2 and lines[0].startswith("```") and lines[-1].strip() == "```":
173
+ return "\n".join(lines[1:-1]).strip()
174
+ return text
175
+
176
+
177
+ def summarize_file(
178
+ content: str,
179
+ file_type: str,
180
+ domains: List[str],
181
+ llm_client,
182
+ llm_cache: Dict[str, str],
183
+ model: str,
184
+ detailed: bool = False,
185
+ ) -> str:
186
+ chunks = chunk_text(content, model=model, max_tokens=1800)
187
+ summaries = []
188
+ for chunk in chunks:
189
+ if detailed and file_type == "config":
190
+ prompt = CONFIG_SUMMARY_PROMPT
191
+ else:
192
+ prompt = MODULE_SUMMARY_PROMPT if detailed else SUMMARY_PROMPT
193
+ if not detailed and (file_type == "infra" or domains):
194
+ prompt = SUMMARY_PROMPT + "\nФайл относится к инфраструктуре: " + ", ".join(domains)
195
+ messages = [
196
+ {"role": "system", "content": prompt},
197
+ {"role": "user", "content": chunk},
198
+ ]
199
+ summaries.append(_strip_fenced_markdown(llm_client.chat(messages, cache=llm_cache).strip()))
200
+
201
+ if len(summaries) == 1:
202
+ result = summaries[0]
203
+ if detailed and file_type == "config":
204
+ return _normalize_config_summary(result, llm_client, llm_cache)
205
+ if detailed:
206
+ return _normalize_module_summary(result, llm_client, llm_cache)
207
+ return result
208
+
209
+ combined = "\n\n".join(summaries)
210
+ if detailed and file_type == "config":
211
+ messages = [
212
+ {"role": "system", "content": CONFIG_SUMMARY_REFORMAT_PROMPT},
213
+ {"role": "user", "content": combined},
214
+ ]
215
+ elif detailed:
216
+ messages = [
217
+ {"role": "system", "content": MODULE_SUMMARY_REFORMAT_PROMPT},
218
+ {"role": "user", "content": combined},
219
+ ]
220
+ else:
221
+ messages = [
222
+ {"role": "system", "content": "Собери единое краткое резюме для документации на основе частей ниже. Ответ в Markdown."},
223
+ {"role": "user", "content": combined},
224
+ ]
225
+ result = _strip_fenced_markdown(llm_client.chat(messages, cache=llm_cache).strip())
226
+ if detailed and file_type == "config":
227
+ return _normalize_config_summary(result, llm_client, llm_cache)
228
+ if detailed:
229
+ return _normalize_module_summary(result, llm_client, llm_cache)
230
+ return result
231
+
232
+
233
+ def write_summary(summary_dir: Path, rel_path: str, summary: str) -> Path:
234
+ ensure_dir(summary_dir)
235
+ safe_name = "".join(c if c.isalnum() else "_" for c in rel_path).strip("_").lower()
236
+ out_path = summary_dir / f"{safe_name}.md"
237
+ out_path.write_text(summary, encoding="utf-8")
238
+ return out_path
ai_docs/tokenizer.py ADDED
@@ -0,0 +1,26 @@
1
+ from typing import List
2
+
3
+ import tiktoken
4
+
5
+
6
+ def get_encoding(model: str):
7
+ try:
8
+ return tiktoken.encoding_for_model(model)
9
+ except KeyError:
10
+ return tiktoken.get_encoding("cl100k_base")
11
+
12
+
13
+ def count_tokens(text: str, model: str) -> int:
14
+ enc = get_encoding(model)
15
+ return len(enc.encode(text))
16
+
17
+
18
+ def chunk_text(text: str, model: str, max_tokens: int) -> List[str]:
19
+ enc = get_encoding(model)
20
+ tokens = enc.encode(text)
21
+ chunks = []
22
+ for i in range(0, len(tokens), max_tokens):
23
+ chunk = tokens[i:i + max_tokens]
24
+ chunks.append(enc.decode(chunk))
25
+ return chunks
26
+
ai_docs/utils.py ADDED
@@ -0,0 +1,43 @@
1
+ import hashlib
2
+ import os
3
+ from pathlib import Path
4
+
5
+
6
+ def sha256_bytes(data: bytes) -> str:
7
+ return hashlib.sha256(data).hexdigest()
8
+
9
+
10
+ def sha256_text(text: str) -> str:
11
+ return sha256_bytes(text.encode("utf-8", errors="ignore"))
12
+
13
+
14
+ def read_text_file(path: Path) -> str:
15
+ return path.read_text(encoding="utf-8", errors="ignore")
16
+
17
+
18
+ def safe_slug(path: str) -> str:
19
+ return "".join(c if c.isalnum() else "_" for c in path).strip("_").lower()
20
+
21
+
22
+ def ensure_dir(path: Path) -> None:
23
+ path.mkdir(parents=True, exist_ok=True)
24
+
25
+
26
+ def is_binary_file(path: Path, sample_size: int = 2048) -> bool:
27
+ try:
28
+ with path.open("rb") as f:
29
+ chunk = f.read(sample_size)
30
+ if b"\x00" in chunk:
31
+ return True
32
+ return False
33
+ except OSError:
34
+ return True
35
+
36
+
37
+ def is_url(value: str) -> bool:
38
+ return value.startswith("http://") or value.startswith("https://") or value.startswith("git@")
39
+
40
+
41
+ def to_posix(path: Path) -> str:
42
+ return path.as_posix()
43
+
@@ -0,0 +1,197 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-docs-gen
3
+ Version: 0.1.2
4
+ Summary: CLI-инструмент для генерации технической документации по коду и конфигурациям
5
+ Requires-Python: >=3.8
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: requests
8
+ Requires-Dist: tiktoken
9
+ Requires-Dist: pyyaml
10
+ Requires-Dist: pathspec
11
+ Requires-Dist: tomli
12
+ Requires-Dist: python-dotenv
13
+ Requires-Dist: mkdocs
14
+ Requires-Dist: mkdocs-mermaid2-plugin
15
+ Requires-Dist: pymdown-extensions
16
+
17
+ # ai_docs — генератор технической документации
18
+
19
+ ## Обзор
20
+ `ai_docs` — CLI‑инструмент для генерации технической документации по коду и конфигурациям.
21
+ Поддерживает локальные папки, локальные git‑проекты и удалённые git‑репозитории.
22
+ Генерирует `README.md` и MkDocs‑сайт (с автоматической сборкой).
23
+
24
+ Ключевые возможности:
25
+ - Автоопределение доменов инфраструктуры (Kubernetes, Helm, Terraform, Ansible, Docker, CI/CD, Observability, Service Mesh / Ingress, Data / Storage)
26
+ - Инкрементальная генерация и кэширование
27
+ - Учет `.gitignore` и фильтрация файлов
28
+ - Параллельная LLM‑суммаризация (`--threads` / `AI_DOCS_THREADS`)
29
+ - Отчёт об изменениях в `docs/changes.md`
30
+
31
+ ## Быстрый старт
32
+
33
+ 1) Установка зависимостей:
34
+ ```bash
35
+ python3 -m venv .venv
36
+ . .venv/bin/activate
37
+ pip install -r requirements.txt
38
+ ```
39
+
40
+ Альтернатива (установка как пакет):
41
+ ```bash
42
+ python3 -m venv .venv
43
+ . .venv/bin/activate
44
+ pip install ai-docs-gen
45
+ ```
46
+
47
+ Локальная установка в editable‑режиме:
48
+ ```bash
49
+ python3 -m venv .venv
50
+ . .venv/bin/activate
51
+ pip install -e .
52
+ ```
53
+
54
+ 2) Настройка `.env` (пример — `.env.example`):
55
+ ```env
56
+ OPENAI_API_KEY=your_api_key_here
57
+ OPENAI_BASE_URL=https://api.openai.com/v1
58
+ OPENAI_MODEL=gpt-4o-mini
59
+ OPENAI_MAX_TOKENS=1200
60
+ OPENAI_CONTEXT_TOKENS=8192
61
+ OPENAI_TEMPERATURE=0.2
62
+ AI_DOCS_THREADS=1
63
+ AI_DOCS_LOCAL_SITE=false
64
+ ```
65
+
66
+ 3) Генерация README и MkDocs:
67
+ ```bash
68
+ python -m ai_docs --source .
69
+ ```
70
+
71
+ Альтернативно:
72
+ ```bash
73
+ python ai_docs --source .
74
+ ```
75
+
76
+ Если установлен как пакет:
77
+ ```bash
78
+ ai-docs --source .
79
+ ```
80
+
81
+ ## Примеры использования
82
+
83
+ Локальная папка:
84
+ ```bash
85
+ python -m ai_docs --source /path/to/project
86
+ ```
87
+
88
+ Локальный git‑проект:
89
+ ```bash
90
+ python -m ai_docs --source ~/projects/my-repo
91
+ ```
92
+
93
+ Удалённый репозиторий:
94
+ ```bash
95
+ python -m ai_docs --source https://github.com/org/repo.git
96
+ ```
97
+
98
+ Только README:
99
+ ```bash
100
+ python -m ai_docs --source . --readme
101
+ ```
102
+
103
+ Только MkDocs:
104
+ ```bash
105
+ python -m ai_docs --source . --mkdocs
106
+ ```
107
+
108
+ Локальный режим для MkDocs:
109
+ ```bash
110
+ python -m ai_docs --source . --mkdocs --local-site
111
+ ```
112
+
113
+ ## Что генерируется
114
+ - `README.md` — краткое описание проекта
115
+ - `.ai-docs/` — страницы документации
116
+ - `.ai-docs/changes.md` — изменения с последней генерации
117
+ - `.ai-docs/modules/` — детальная документация модулей (страница на модуль, Doxygen‑подобное описание функций/классов/параметров)
118
+ - `.ai-docs/configs/` — документация конфигов проекта (обзор + страницы конфигов в универсальном стиле)
119
+ - `.ai-docs/_index.json` — навигационный индекс документации (правила маршрутизации, список секций и модулей)
120
+ - `mkdocs.yml` — конфиг MkDocs
121
+ - `ai_docs_site/` — собранный сайт MkDocs
122
+ - `.ai_docs_cache/` — кэш и промежуточные summary‑файлы
123
+
124
+ ## Поддерживаемые языки и расширения
125
+ Поддержка основана на расширениях кода в `ai_docs/domain.py`:
126
+ `.py`, `.pyi`, `.pyx`, `.js`, `.jsx`, `.ts`, `.tsx`, `.go`, `.java`, `.c`, `.cc`, `.cpp`, `.h`, `.hpp`, `.rs`, `.rb`, `.php`, `.cs`, `.kt`, `.kts`, `.swift`, `.m`, `.mm`, `.vb`, `.bas`, `.sql`, `.pas`, `.dpr`, `.pp`, `.r`, `.pl`, `.pm`, `.f`, `.for`, `.f90`, `.f95`, `.f03`, `.f08`, `.sb3`, `.adb`, `.ads`, `.asm`, `.s`, `.ino`, `.htm`, `.html`, `.css`.
127
+
128
+ ## Индекс документации
129
+ Файл `.ai-docs/_index.json` строится автоматически при генерации и содержит:
130
+ - список секций и модулей (пути и краткие описания);
131
+ - правила маршрутизации: приоритет `modules/index.md → modules/* → index.md/architecture.md/conventions.md`;
132
+ - принцип ранжирования: частота ключевых совпадений + приоритет файла.
133
+
134
+ ## .ai-docs.yaml (расширения)
135
+ Если в проекте есть файл `.ai-docs.yaml`, он задаёт приоритетный список расширений для сканирования.
136
+ Если файла нет, он создаётся автоматически на основе текущих `*_EXTENSIONS`.
137
+
138
+ Формат (поддерживаются map и list для расширений):
139
+ ```yaml
140
+ code_extensions:
141
+ .py: Python
142
+ .ts: TypeScript
143
+ doc_extensions:
144
+ .md: Markdown
145
+ .rst: reStructuredText
146
+ config_extensions:
147
+ .yml: YAML
148
+ .json: JSON
149
+ exclude:
150
+ - "temp/*"
151
+ - "*.log"
152
+ ```
153
+
154
+ ## CLI‑параметры
155
+ - `--source <path|url>` — источник
156
+ - `--output <path>` — выходная директория (по умолчанию: source для локальных путей, `./output/<repo>` для URL)
157
+
158
+ ## Тестирование
159
+ Тесты находятся в каталоге `tests/`:
160
+ - `test_cache.py`
161
+ - `test_changes.py`
162
+ - `test_scanner.py`
163
+
164
+ Запуск (из корня проекта):
165
+ ```bash
166
+ python -m pytest
167
+ ```
168
+ - `--readme` — генерировать только README
169
+ - `--mkdocs` — генерировать только MkDocs
170
+ - `--language ru|en` — язык документации
171
+ - `--include/--exclude` — фильтры
172
+ - `--max-size` — максимальный размер файла
173
+ - `--threads` — число потоков LLM
174
+ - `--cache-dir` — директория кэша (по умолчанию `.ai_docs_cache`)
175
+ - `--no-cache` — отключить LLM‑кэш
176
+ - `--local-site` — добавить `site_url` и `use_directory_urls` в `mkdocs.yml`
177
+ - `--force` — перезаписать `README.md`, если он уже существует
178
+
179
+ Поведение по умолчанию: если не указаны `--readme` и `--mkdocs`, генерируются оба артефакта.
180
+
181
+ ## MkDocs
182
+ Сборка выполняется автоматически в конце генерации:
183
+ ```
184
+ mkdocs build -f mkdocs.yml
185
+ ```
186
+
187
+ ## Исключения
188
+ Сканер учитывает `.gitignore`, `.build_ignore` и дефолтные исключения:
189
+ `.venv`, `node_modules`, `ai_docs_site`, `.ai-docs`, `.ai_docs_cache`, `dist`, `build`, т.д.
190
+
191
+ ## Разработка и вклад
192
+ - Установите зависимости (см. «Быстрый старт»)
193
+ - Запускайте через `python -m ai_docs ...` для отладки
194
+ - PR и предложения приветствуются
195
+
196
+ ## Лицензия
197
+ MIT
@@ -0,0 +1,19 @@
1
+ ai_docs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ ai_docs/__main__.py,sha256=yNFl5cGhWoeZLOulgfPCd-pOwjq0dclhAyB-OiG-cvE,512
3
+ ai_docs/cache.py,sha256=Z3Mwg2QEYZNaC6bRyv6Py0FcFDV0d9KnyFiBf-EsyFE,1784
4
+ ai_docs/changes.py,sha256=tA6kf79XPiywqWBBfBaqf-upRyowyInsw-BFYOIUAmA,973
5
+ ai_docs/cli.py,sha256=XhZO6b49lYGqefITwu-R57-pN6t5esyARGlcMz7cXAA,3639
6
+ ai_docs/domain.py,sha256=HrbdDw_Qxe1gk7J4dzYN1MHbYXDGY3-Tyr5--LgSjzc,6253
7
+ ai_docs/generator.py,sha256=n_sdUxHPFF4c4CPplbxEGDYmK32tfXq2w7a1sEVqa8E,41146
8
+ ai_docs/llm.py,sha256=BCVcMM1X_B_LMEuyLBM6jFNz8jYONikM4pEJVhJ16c0,2633
9
+ ai_docs/mkdocs.py,sha256=Zh23S3T3gTou2TryfLSwDWzWTwDtLYf5kM5eTnLpGek,5243
10
+ ai_docs/scanner.py,sha256=KwJnu3GYL1lABeSVTlxlrddOuKkHyomQ39g6AdPbPh0,8793
11
+ ai_docs/summary.py,sha256=01cJer00yJnT7p7nKgvPy-H37A3PqHHVeA8RuzkwX8M,10357
12
+ ai_docs/tokenizer.py,sha256=G8btLH0IRJCx4b2jM8lWSzS0dcOZP-sAqwWYUQ5jF40,614
13
+ ai_docs/utils.py,sha256=kJKgO2R8ZQa58MBUZK2oEr03wFvVkRaWZYruxxJigGo,993
14
+ ai_docs/assets/mermaid.min.js,sha256=LPe7bNxKbqlto9MkpER9gwDR2nA85fMTEWCGQsD4Ymk,2908475
15
+ ai_docs_gen-0.1.2.dist-info/METADATA,sha256=0-ncf5tDSlADitUF5erpLL6Ew2Yl02HYDcoVnV_ejIM,7723
16
+ ai_docs_gen-0.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
17
+ ai_docs_gen-0.1.2.dist-info/entry_points.txt,sha256=C5tKlnOjrwbPgVbOB_zA8WeFjk05DXsMhq2UgTw5BDk,45
18
+ ai_docs_gen-0.1.2.dist-info/top_level.txt,sha256=Uqf4JT1_bI7m3yV5gs5kuL5Nmws5E2XT3W9yajZck2c,8
19
+ ai_docs_gen-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ai-docs = ai_docs.cli:main
@@ -0,0 +1 @@
1
+ ai_docs