claudenv 1.2.5 → 1.3.1
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 +93 -3
- package/bin/cli.js +294 -0
- package/package.json +1 -1
- package/scaffold/.claude/memories/README.md +44 -0
- package/scaffold/global/.claude/commands/add-source.md +31 -0
- package/scaffold/global/.claude/commands/canon.md +33 -0
- package/scaffold/global/.claude/commands/decisions.md +29 -0
- package/scaffold/global/.claude/commands/deeper.md +55 -0
- package/scaffold/global/.claude/commands/just-code.md +26 -0
- package/scaffold/global/.claude/commands/why.md +33 -0
- package/scaffold/global/.claude/skills/source-connector/SKILL.md +128 -0
- package/scaffold/global/.claude/skills/vibe-decisions/SKILL.md +127 -0
- package/scaffold/global/.claude/skills/vibe-decisions/deep-dive-template.md +41 -0
- package/scaffold/global-claudenv/config.yaml +16 -0
- package/scaffold/global-claudenv/memories/INDEX.md +25 -0
- package/scaffold/global-claudenv/memories/canon/index.yaml +21 -0
- package/scaffold/global-claudenv/memories/user/preferences.md.example +16 -0
- package/src/autonomy.js +6 -1
- package/src/canon.js +214 -0
- package/src/decisions.js +161 -0
- package/src/doctor.js +201 -0
- package/src/hooks/decisions-logger.js +183 -0
- package/src/hooks/dispatcher.js +91 -0
- package/src/hooks/regen-index.js +198 -0
- package/src/hooks-gen.js +43 -1
- package/src/installer.js +57 -14
- package/src/loop.js +26 -1
- package/src/memory-context.js +63 -0
- package/src/memory-paths.js +128 -0
- package/src/memory.js +111 -0
- package/src/sources.js +78 -0
- package/src/workspaces.js +129 -0
- package/templates/connector-mssql.ejs +62 -0
- package/templates/connector-rest.ejs +59 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Объяснить технологию или подход в контексте текущей задачи — без принятия решения и без записи в decisions/
|
|
3
|
+
allowed-tools: Read, Glob, Grep, WebSearch, WebFetch, Bash(claudenv:*)
|
|
4
|
+
argument-hint: <technology-or-question>
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /why — Краткое объяснение
|
|
8
|
+
|
|
9
|
+
Объяснить `$ARGUMENTS` в контексте текущей задачи. Не вызывает vibe-decisions, не пишет в `~/.claudenv/memories/decisions/`.
|
|
10
|
+
|
|
11
|
+
## Step 1 — Check canon first
|
|
12
|
+
|
|
13
|
+
Run `claudenv canon search "$ARGUMENTS"` (Bash). If matches found — упомяни их как primary references.
|
|
14
|
+
|
|
15
|
+
## Step 2 — Brief explanation (3-5 lines)
|
|
16
|
+
|
|
17
|
+
Структура:
|
|
18
|
+
|
|
19
|
+
- **Что это:** 1 строка
|
|
20
|
+
- **Когда используется:** 1-2 строки
|
|
21
|
+
- **Когда не подходит:** 1 строка
|
|
22
|
+
- **Ссылки (опционально):** канон или WebSearch если канон пуст
|
|
23
|
+
|
|
24
|
+
## Не делать
|
|
25
|
+
|
|
26
|
+
- Не выводи vibe-decisions overview format (это explainer, не decision)
|
|
27
|
+
- Не пиши файл в decisions/
|
|
28
|
+
- Если пользователь после `/why` начинает реальный выбор технологии — тогда уже триггерится vibe-decisions skill
|
|
29
|
+
|
|
30
|
+
## Style
|
|
31
|
+
|
|
32
|
+
- User's language
|
|
33
|
+
- Краткость > полнота. Для полноты есть `/deeper` после реального decision.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: source-connector
|
|
3
|
+
description: |
|
|
4
|
+
Trigger when the user wants to connect to a data source that has no ready
|
|
5
|
+
MCP server - internal SQL/DWH behind VPN, corporate wiki (Confluence),
|
|
6
|
+
Redash, YouTrack, a custom REST API - or says "добавь источник",
|
|
7
|
+
"подключись к <система>", "/add-source". The skill interviews the user,
|
|
8
|
+
generates a working connector script, stores credentials in .env.local
|
|
9
|
+
(gitignored, never in memory), and caches connector knowledge (params +
|
|
10
|
+
provenance) in the ACTIVE workspace memory, isolated from other workspaces.
|
|
11
|
+
Do NOT trigger for sources that already have an MCP server (prefer MCP),
|
|
12
|
+
or for trivial local file reads.
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Source connector
|
|
16
|
+
|
|
17
|
+
Создаёт коннектор к источнику данных без готового MCP: внутренний SQL/DWH,
|
|
18
|
+
корпоративная вики, Redash, YouTrack, самописный REST. Опрашивает юзера,
|
|
19
|
+
генерирует рабочий скрипт, кладёт секреты в `.env.local`, кэширует знание о
|
|
20
|
+
коннекторе (параметры + provenance) в память АКТИВНОГО workspace.
|
|
21
|
+
|
|
22
|
+
## Step 0 - Workspace resolution (FIRST, every time)
|
|
23
|
+
|
|
24
|
+
Память изолирована по пространствам (workspaces). Прежде чем что-либо писать:
|
|
25
|
+
|
|
26
|
+
1. Определи активный workspace:
|
|
27
|
+
- env `CLAUDENV_WORKSPACE`, иначе файл `~/.claudenv/active-workspace`.
|
|
28
|
+
- если есть `claudenv` CLI: `claudenv workspace show`.
|
|
29
|
+
2. Если активного нет - предложи выбрать из `~/.claudenv/workspaces/` или создать
|
|
30
|
+
новый. НИКОГДА не подтягивай и не пиши в «глобальную» память доступы/коннекторы.
|
|
31
|
+
3. Все записи коннектора пиши ТОЛЬКО в активный workspace:
|
|
32
|
+
`~/.claudenv/workspaces/<id>/memories/connectors/`.
|
|
33
|
+
4. НИКОГДА не читай чужие workspaces - это барьер от ликов между компаниями.
|
|
34
|
+
|
|
35
|
+
## Security invariants (нарушать нельзя)
|
|
36
|
+
|
|
37
|
+
- **Секреты (пароли, токены, ключи) - только в `<project>/.env.local`** и только
|
|
38
|
+
значениями там. Убедись, что `.env.local` есть в `.gitignore` (добавь, если нет).
|
|
39
|
+
- **В память коннектора - НИКОГДА значения секретов.** Только `secret_refs` -
|
|
40
|
+
имена переменных из `.env.local`.
|
|
41
|
+
- **Не эхай секреты** в вывод/логи/коммиты.
|
|
42
|
+
- Память активного workspace изолирована: не смешивай контекст разных компаний.
|
|
43
|
+
|
|
44
|
+
## Trigger criteria
|
|
45
|
+
|
|
46
|
+
Триггерь, если юзеру нужно регулярно/программно ходить в источник:
|
|
47
|
+
- БД (MSSQL/Postgres/ClickHouse), DWH за VPN
|
|
48
|
+
- корпоративная вики (Confluence), Redash, YouTrack, Grafana
|
|
49
|
+
- самописный REST/GraphQL API
|
|
50
|
+
|
|
51
|
+
НЕ триггерь:
|
|
52
|
+
- если для источника есть MCP-сервер - предложи MCP (см. ниже)
|
|
53
|
+
- разовое чтение локального файла
|
|
54
|
+
- источник уже подключён (тогда это обновление, см. Step 6)
|
|
55
|
+
|
|
56
|
+
## Step 1 - MCP vs connector
|
|
57
|
+
|
|
58
|
+
Сначала проверь, нет ли готового MCP для источника (claudenv умеет настраивать MCP
|
|
59
|
+
в `.mcp.json`). Если есть - предложи MCP-путь. Коннектор - для источников БЕЗ MCP.
|
|
60
|
+
|
|
61
|
+
## Step 2 - Interview (спроси у юзера, кратко)
|
|
62
|
+
|
|
63
|
+
Собери минимум для подключения:
|
|
64
|
+
- тип источника (mssql / postgres / clickhouse / rest / confluence / redash / youtrack)
|
|
65
|
+
- хост и порт, база/endpoint
|
|
66
|
+
- метод аутентификации (sql login / domain / token / trusted)
|
|
67
|
+
- что нужно достать (таблицы/эндпоинты) - для первого запроса
|
|
68
|
+
- как зовутся переменные кред в `.env.local` (или предложи имена)
|
|
69
|
+
|
|
70
|
+
Не выдумывай параметры - спрашивай. Если юзер уже дал часть (вики/git/переписка) -
|
|
71
|
+
зафиксируй это в provenance.
|
|
72
|
+
|
|
73
|
+
## Step 3 - Generate connector script
|
|
74
|
+
|
|
75
|
+
Сгенерируй рабочий скрипт из шаблона (`templates/connector-*.ejs`):
|
|
76
|
+
- MSSQL -> pymssql/pyodbc, строка `mssql+pymssql://{user}:{pwd}@{host}:{port}/{db}`
|
|
77
|
+
(СУЗ - логин без домена; экранируй пароль через urllib `quote_plus`).
|
|
78
|
+
- REST -> requests + Bearer-токен из env.
|
|
79
|
+
Скрипт читает креды из `.env.local` (через env), не хардкодит.
|
|
80
|
+
|
|
81
|
+
## Step 4 - Secrets to .env.local
|
|
82
|
+
|
|
83
|
+
- Запиши значения в `<project>/.env.local` (создай при отсутствии).
|
|
84
|
+
- Проверь/добавь `.env.local` в `.gitignore`.
|
|
85
|
+
- В память пойдут только имена этих переменных (`secret_refs`).
|
|
86
|
+
|
|
87
|
+
## Step 5 - Write connector record (в активный workspace)
|
|
88
|
+
|
|
89
|
+
Создай `~/.claudenv/workspaces/<id>/memories/connectors/<name>.md`:
|
|
90
|
+
|
|
91
|
+
```markdown
|
|
92
|
+
---
|
|
93
|
+
name: <name>
|
|
94
|
+
type: <mssql|rest|...>
|
|
95
|
+
status: <draft|working|blocked>
|
|
96
|
+
host: <host>
|
|
97
|
+
database: <db|->
|
|
98
|
+
port: <port|->
|
|
99
|
+
auth: <sql|domain|token|trusted>
|
|
100
|
+
secret_refs: {user: <ENV_VAR>, password: <ENV_VAR>} # ИМЕНА, не значения
|
|
101
|
+
connector_script: <path в проекте>
|
|
102
|
+
provenance: # откуда собрана инфа
|
|
103
|
+
- {source: <wiki|git|chat|docs>, ref: <url|путь>, note: <кратко>}
|
|
104
|
+
verified_at: <YYYY-MM-DD|->
|
|
105
|
+
updated_at: <YYYY-MM-DD>
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
Детали: как устроено подключение, что блокирует, как обновлять.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Provenance обязателен** - фиксируй, откуда взял каждый факт (вики-страница, файл
|
|
112
|
+
в git, сообщение в чате). Это позволяет потом перепроверить и обновить.
|
|
113
|
+
|
|
114
|
+
## Step 6 - Test and iterate
|
|
115
|
+
|
|
116
|
+
Протестируй подключение, если возможно. Зафиксируй `status` и `verified_at`.
|
|
117
|
+
При ошибке - запиши симптом в запись (раздел деталей) и предложи следующий шаг.
|
|
118
|
+
|
|
119
|
+
## Step 7 - Updating an existing connector
|
|
120
|
+
|
|
121
|
+
Если коннектор уже есть в активном workspace:
|
|
122
|
+
- найди запись, обнови `status`/`host`/`secret_refs`, допиши `provenance`,
|
|
123
|
+
обнови `updated_at`. Запись - единый источник правды по коннектору.
|
|
124
|
+
|
|
125
|
+
## После создания
|
|
126
|
+
|
|
127
|
+
Предложи дальнейшую автоматизацию через коннектор (регулярная выгрузка, отчёт,
|
|
128
|
+
интеграция в пайплайн). Язык общения - русский.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vibe-decisions
|
|
3
|
+
description: |
|
|
4
|
+
Trigger before writing code that involves a non-trivial technical
|
|
5
|
+
choice (library, algorithm, data structure, architecture pattern,
|
|
6
|
+
sync vs async, correctness vs performance). Output a brief overview
|
|
7
|
+
with concrete trade-offs and 1-2 alternatives, then log the decision.
|
|
8
|
+
Two modes: auto-log (no pauses) inside `claudenv loop`, pause-and-ask
|
|
9
|
+
in interactive Claude Code. Also triggers on: "почему X?", "X vs Y?",
|
|
10
|
+
"/why", "/deeper", "/decisions", "alternatives?", "trade-offs?".
|
|
11
|
+
Do NOT trigger for trivial syntax, naming, or formatting choices.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Vibe decisions
|
|
15
|
+
|
|
16
|
+
## Mode detection (FIRST step every time)
|
|
17
|
+
|
|
18
|
+
Check the system prompt for the marker `Vibe-decisions mode (loop)`.
|
|
19
|
+
|
|
20
|
+
- **Marker present** → AUTO-LOG mode. Pick the approach, write the decision file, continue. NEVER pause. NEVER ask "делать так?". This is `claudenv loop` and pause violates autonomy=law.
|
|
21
|
+
- **Marker absent** → INTERACTIVE mode. Output brief overview, wait for user confirmation or `/deeper`.
|
|
22
|
+
|
|
23
|
+
Both modes share the same trigger criteria, file format, and slash-command behavior. Only output shape and pause behavior differ.
|
|
24
|
+
|
|
25
|
+
## Trigger criteria
|
|
26
|
+
|
|
27
|
+
Trigger if the decision involves:
|
|
28
|
+
|
|
29
|
+
- choosing a library or framework from 2+ viable options
|
|
30
|
+
- choosing an algorithm or data structure with meaningful trade-offs
|
|
31
|
+
- architecture pattern (sync/async, push/pull, where state lives, monolith/services)
|
|
32
|
+
- correctness vs performance vs simplicity trade-off
|
|
33
|
+
- anything where reasonable engineers would disagree
|
|
34
|
+
|
|
35
|
+
DO NOT trigger for:
|
|
36
|
+
|
|
37
|
+
- syntax-level choices in a single language (for vs map, ternary vs if)
|
|
38
|
+
- obvious choices (JSON for a config file, regex for simple pattern)
|
|
39
|
+
- pure refactoring within established patterns
|
|
40
|
+
- when the user typed `/just-code` in the previous turn
|
|
41
|
+
|
|
42
|
+
## Step 1 — Check memory before deciding
|
|
43
|
+
|
|
44
|
+
Before writing brief or auto-log, read related precedents:
|
|
45
|
+
|
|
46
|
+
- `view` `/memories/decisions/` — list files; if any topic looks related, read it
|
|
47
|
+
- Mention the precedent briefly: "Раньше для X выбирали Y (см. <file>)"
|
|
48
|
+
|
|
49
|
+
## Step 2A — AUTO-LOG mode output (inside `claudenv loop`)
|
|
50
|
+
|
|
51
|
+
Output exactly this shape in the user's language, then continue with code in the same turn:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
**Выбрано:** <approach>
|
|
55
|
+
**Почему:** <one sentence>
|
|
56
|
+
**Альтернативы:** <A> — <terse trade-off>; <B> — <terse trade-off>
|
|
57
|
+
**Лог:** /memories/decisions/<YYYY-MM-DD>-<topic-slug>.md (scope: <global|project>)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Immediately after the block, use the Write tool to create the decision file (see Step 4 for format). Then proceed with the actual code. Do NOT pause.
|
|
61
|
+
|
|
62
|
+
## Step 2B — INTERACTIVE mode output (no loop)
|
|
63
|
+
|
|
64
|
+
Output this shape and STOP. Wait for the user.
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
**Подход:** <approach>
|
|
68
|
+
**Почему для этой задачи:** <1-2 lines, concrete to the task>
|
|
69
|
+
**Минусы:** <1-2 lines, concrete to the task>
|
|
70
|
+
**Альтернативы:** <A> — <one sentence>; <B> — <one sentence>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then ask: "Делать так, или разобрать варианты подробнее?"
|
|
74
|
+
|
|
75
|
+
DO NOT proceed with code until the user confirms OR asks for deep dive.
|
|
76
|
+
|
|
77
|
+
## Step 3 — Deep dive (on `/deeper` or "расскажи подробнее")
|
|
78
|
+
|
|
79
|
+
Expand the most recent decision into:
|
|
80
|
+
|
|
81
|
+
1. **Концептуально** — what it is and the problem it solves (3-5 lines)
|
|
82
|
+
2. **Как работает** — simplified model (5-10 lines, optionally a code sketch)
|
|
83
|
+
3. **2-3 варианта реализации** — for each: code + 1-2 lines of trade-offs
|
|
84
|
+
4. **Канон** — 2-4 references. First check `~/.claudenv/memories/canon/index.yaml` for a topic match. If a match exists — cite those. If not — WebSearch for authoritative sources and after the response suggest `claudenv canon add <topic> <url> --why "..."`.
|
|
85
|
+
|
|
86
|
+
After deep dive, in auto-log mode update the existing decision file's `deep_dive_done: yes` field. In interactive mode wait for confirmation.
|
|
87
|
+
|
|
88
|
+
## Step 4 — Write the decision file
|
|
89
|
+
|
|
90
|
+
Path:
|
|
91
|
+
|
|
92
|
+
- `scope: global` (default) → `~/.claudenv/memories/decisions/<YYYY-MM-DD>-<topic-slug>.md`
|
|
93
|
+
- `scope: project` → `<cwd>/.claude/memories/decisions/<YYYY-MM-DD>-<topic-slug>.md`
|
|
94
|
+
|
|
95
|
+
**Choose scope BEFORE writing — don't rely on hook to rewrite.** Project scope if the choice mentions project-only artifacts (module name, env var, deployment target, internal service). Otherwise global.
|
|
96
|
+
|
|
97
|
+
File content:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
---
|
|
101
|
+
date: <ISO 8601 datetime with timezone>
|
|
102
|
+
project: <basename of cwd, or "(none)" if no project context>
|
|
103
|
+
topic: <plain-language summary of what was decided>
|
|
104
|
+
chose: <approach>
|
|
105
|
+
alternatives_considered: [<A>, <B>]
|
|
106
|
+
reason: <one sentence why>
|
|
107
|
+
deep_dive_done: <yes|no>
|
|
108
|
+
sources_consulted: [<canon-id-or-url>, ...]
|
|
109
|
+
scope: <global|project>
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
<optional body — caveats, follow-ups, links>
|
|
113
|
+
|
|
114
|
+
__VIBE_DECISION__
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The `__VIBE_DECISION__` marker on the last line lets the `decisions-logger` hook detect the file even if the path check fails. Always include it.
|
|
118
|
+
|
|
119
|
+
## Slash-commands the skill responds to
|
|
120
|
+
|
|
121
|
+
- `/why <X>` — explain X briefly without committing a decision; do NOT write a decision file
|
|
122
|
+
- `/deeper` — deep dive on the most recent decision (Step 3)
|
|
123
|
+
- `/decisions [N]` — execute Bash `claudenv decisions list --limit ${N:-10}` and show output
|
|
124
|
+
- `/canon [<topic>]` — execute Bash `claudenv canon list <topic>` and show output
|
|
125
|
+
- `/just-code` — suppress vibe-decisions on the NEXT response only; resume on the one after
|
|
126
|
+
|
|
127
|
+
Trigger phrases that also count (in any language): "расскажи подробнее", "почему именно X?", "X vs Y?", "alternatives?", "trade-offs?".
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Deep dive template
|
|
2
|
+
|
|
3
|
+
Used by the `vibe-decisions` skill when the user requests `/deeper` or asks "расскажи подробнее", "X vs Y?", "trade-offs?".
|
|
4
|
+
|
|
5
|
+
Fill each section concretely against the current task — generic prose is the failure mode here.
|
|
6
|
+
|
|
7
|
+
## 1. Концептуально (3-5 строк)
|
|
8
|
+
|
|
9
|
+
What this approach is and what problem it solves. Mention the canonical name and 1-2 alternatives by name (so the reader can search later). Avoid marketing language.
|
|
10
|
+
|
|
11
|
+
## 2. Как работает (5-10 строк, опц. code sketch)
|
|
12
|
+
|
|
13
|
+
A simplified mental model. If a 10-15 line code sketch clarifies the mechanism, include it. Otherwise prose. Mention 1-2 invariants that matter — what the approach guarantees and what it explicitly does not.
|
|
14
|
+
|
|
15
|
+
## 3. Варианты реализации (2-3)
|
|
16
|
+
|
|
17
|
+
For each variant:
|
|
18
|
+
|
|
19
|
+
### Variant N — <name>
|
|
20
|
+
|
|
21
|
+
```<lang>
|
|
22
|
+
# minimal sketch, 5-15 lines
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Когда подходит:** <one line>
|
|
26
|
+
**Когда не подходит:** <one line>
|
|
27
|
+
|
|
28
|
+
## 4. Канон (2-4 ссылки)
|
|
29
|
+
|
|
30
|
+
Priority: matches from `~/.claudenv/memories/canon/index.yaml` by topic. If no match, WebSearch and propose `claudenv canon add` after the response.
|
|
31
|
+
|
|
32
|
+
Format each entry:
|
|
33
|
+
|
|
34
|
+
- **<title>** — <author or venue> — <1 line why>
|
|
35
|
+
<url>
|
|
36
|
+
|
|
37
|
+
## After deep dive
|
|
38
|
+
|
|
39
|
+
In AUTO-LOG mode: update the existing decision file to set `deep_dive_done: yes` and append source URLs to `sources_consulted`.
|
|
40
|
+
|
|
41
|
+
In INTERACTIVE mode: ask the user "Принимаем этот подход?" before writing/updating the decision file.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# claudenv global config.
|
|
2
|
+
# Created by `claudenv install` if missing; preserved on subsequent installs.
|
|
3
|
+
|
|
4
|
+
# Default model for `claudenv loop` if --model is not passed.
|
|
5
|
+
# Options: opus, sonnet, haiku
|
|
6
|
+
default_model: sonnet
|
|
7
|
+
|
|
8
|
+
# Auto-commit ~/.claudenv/ after each session (planned for 1.4 — currently unused).
|
|
9
|
+
auto_commit: false
|
|
10
|
+
|
|
11
|
+
# Memory scope default when vibe-decisions can't infer.
|
|
12
|
+
# Options: global, project
|
|
13
|
+
default_scope: global
|
|
14
|
+
|
|
15
|
+
# INDEX.md regeneration: maximum number of recent decisions to surface.
|
|
16
|
+
index_recent_limit: 5
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# claudenv memory — INDEX
|
|
2
|
+
|
|
3
|
+
> Auto-generated by `claudenv hook regen-index` after each session and after each `claudenv loop` iteration. Hand-edits will be overwritten.
|
|
4
|
+
> Last regenerated: <never>
|
|
5
|
+
|
|
6
|
+
## Recent decisions
|
|
7
|
+
|
|
8
|
+
_No decisions logged yet. Make a non-trivial technical choice in a session — the `vibe-decisions` skill will log it here._
|
|
9
|
+
|
|
10
|
+
## Active preferences
|
|
11
|
+
|
|
12
|
+
_No `user/preferences.md` yet. Create one at `~/.claudenv/memories/user/preferences.md` (template at `preferences.md.example`)._
|
|
13
|
+
|
|
14
|
+
## Active projects
|
|
15
|
+
|
|
16
|
+
_Per-project facts live in each project's `.claude/memories/project.md`. This index lists projects seen recently._
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
Files in this directory:
|
|
21
|
+
|
|
22
|
+
- `INDEX.md` — this file
|
|
23
|
+
- `decisions/` — global cross-project tech decisions
|
|
24
|
+
- `canon/` — personal canon of references
|
|
25
|
+
- `user/` — cross-project preferences
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Личный канон. Только то что вы прочитали и сочли важным.
|
|
2
|
+
# Добавляйте через: claudenv canon add <topic> <url> --why "<reason>"
|
|
3
|
+
# Используется vibe-decisions skill в /deeper для подбора ссылок.
|
|
4
|
+
|
|
5
|
+
# Стартовое наполнение пустое — канон растёт от реальных decision-моментов.
|
|
6
|
+
# Пример (раскомментируйте и адаптируйте):
|
|
7
|
+
|
|
8
|
+
# distributed_locks:
|
|
9
|
+
# - title: "How to do distributed locking"
|
|
10
|
+
# author: Martin Kleppmann
|
|
11
|
+
# url: https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
|
|
12
|
+
# why: "обязательное чтение перед выбором Redis для locks"
|
|
13
|
+
# added: 2026-05-27
|
|
14
|
+
|
|
15
|
+
# raft_consensus:
|
|
16
|
+
# - title: "In Search of an Understandable Consensus Algorithm"
|
|
17
|
+
# authors: [Ongaro, Ousterhout]
|
|
18
|
+
# venue: "USENIX ATC 2014"
|
|
19
|
+
# url: https://raft.github.io/raft.pdf
|
|
20
|
+
# why: "оригинальный paper"
|
|
21
|
+
# added: 2026-05-27
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# User preferences
|
|
2
|
+
|
|
3
|
+
Copy this file to `preferences.md` (in the same directory) and edit. The `vibe-decisions` skill reads it and uses it as context when suggesting approaches.
|
|
4
|
+
|
|
5
|
+
Format: free-form markdown, but short lines (one preference per line) work best for cache stability.
|
|
6
|
+
|
|
7
|
+
## Examples
|
|
8
|
+
|
|
9
|
+
- Default branch naming: `<initials>/<short-desc>`
|
|
10
|
+
- Commit style: conventional commits, lowercase subject
|
|
11
|
+
- Editor: helix
|
|
12
|
+
- Package manager: pnpm (Node), uv (Python), cargo (Rust)
|
|
13
|
+
- TypeScript: strict + noUncheckedIndexedAccess
|
|
14
|
+
- Python: type hints required, no `Any` без TODO
|
|
15
|
+
- Prefer integration tests с реальной БД over heavy mocking
|
|
16
|
+
- Не использовать `as` import unless prefix collision
|
package/src/autonomy.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
generateAuditLogHook,
|
|
10
10
|
generateAliases,
|
|
11
11
|
generateCIWorkflow,
|
|
12
|
+
isGlobalInstall,
|
|
12
13
|
} from './hooks-gen.js';
|
|
13
14
|
import { detectTechStack } from './detector.js';
|
|
14
15
|
|
|
@@ -25,10 +26,14 @@ export async function generateAutonomyConfig(profileName, projectDir, options =
|
|
|
25
26
|
const profile = getProfile(profileName);
|
|
26
27
|
const detected = options.detected || (await detectTechStack(projectDir));
|
|
27
28
|
|
|
29
|
+
// Hook commands need `claudenv` on PATH. If user installed via npm install -g
|
|
30
|
+
// we can call `claudenv` directly; otherwise fall back to `npx claudenv`.
|
|
31
|
+
const claudenvCmd = (await isGlobalInstall()) ? 'claudenv' : 'npx claudenv';
|
|
32
|
+
|
|
28
33
|
const files = [
|
|
29
34
|
{
|
|
30
35
|
path: '.claude/settings.json',
|
|
31
|
-
content: generateSettingsJson(profile, detected),
|
|
36
|
+
content: generateSettingsJson(profile, detected, { claudenvCmd }),
|
|
32
37
|
},
|
|
33
38
|
{
|
|
34
39
|
path: '.claude/hooks/pre-tool-use.sh',
|
package/src/canon.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: `claudenv canon add/list/search/prune`
|
|
3
|
+
*
|
|
4
|
+
* Owns ~/.claudenv/memories/canon/index.yaml — a topic-keyed list of links.
|
|
5
|
+
* Format intentionally simple (flat YAML) so it's human-editable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
9
|
+
import { dirname } from 'node:path';
|
|
10
|
+
import { canonIndexPath } from './memory-paths.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read the canon yaml as a JS object. Tolerant to missing or empty files.
|
|
14
|
+
*
|
|
15
|
+
* Parser is intentionally minimal — full YAML would be overkill for a flat
|
|
16
|
+
* topic → [entries] mapping and would add a dependency.
|
|
17
|
+
*/
|
|
18
|
+
async function readCanon() {
|
|
19
|
+
let text;
|
|
20
|
+
try {
|
|
21
|
+
text = await readFile(canonIndexPath(), 'utf-8');
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = {};
|
|
27
|
+
let currentTopic = null;
|
|
28
|
+
let currentEntry = null;
|
|
29
|
+
|
|
30
|
+
for (const rawLine of text.split('\n')) {
|
|
31
|
+
const line = rawLine.replace(/\s+$/, '');
|
|
32
|
+
if (!line || line.startsWith('#')) continue;
|
|
33
|
+
|
|
34
|
+
// Topic key: `topic_name:` at column 0
|
|
35
|
+
const topicMatch = /^([a-zA-Z][a-zA-Z0-9_-]*):\s*$/.exec(line);
|
|
36
|
+
if (topicMatch) {
|
|
37
|
+
currentTopic = topicMatch[1];
|
|
38
|
+
result[currentTopic] = [];
|
|
39
|
+
currentEntry = null;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// List entry start: ` - key: value` or ` - key:` (multiline value)
|
|
44
|
+
const entryStartMatch = /^\s+-\s+([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)$/.exec(line);
|
|
45
|
+
if (entryStartMatch && currentTopic) {
|
|
46
|
+
currentEntry = {};
|
|
47
|
+
currentEntry[entryStartMatch[1]] = unquote(entryStartMatch[2]);
|
|
48
|
+
result[currentTopic].push(currentEntry);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Subsequent key in same entry: ` key: value`
|
|
53
|
+
const subKeyMatch = /^\s+([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)$/.exec(line);
|
|
54
|
+
if (subKeyMatch && currentEntry) {
|
|
55
|
+
currentEntry[subKeyMatch[1]] = unquote(subKeyMatch[2]);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function unquote(s) {
|
|
64
|
+
const trimmed = s.trim();
|
|
65
|
+
if (!trimmed) return '';
|
|
66
|
+
if (
|
|
67
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
68
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
69
|
+
) {
|
|
70
|
+
return trimmed.slice(1, -1);
|
|
71
|
+
}
|
|
72
|
+
// Inline arrays — pass through as raw string for now (rare in canon).
|
|
73
|
+
return trimmed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Append an entry to the canon. Creates the topic group if absent.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} opts
|
|
80
|
+
* @param {string} opts.topic - Topic slug
|
|
81
|
+
* @param {string} opts.url - Required
|
|
82
|
+
* @param {string} opts.why - Required
|
|
83
|
+
* @param {string} [opts.title]
|
|
84
|
+
* @param {string} [opts.author]
|
|
85
|
+
*/
|
|
86
|
+
export async function canonAdd({ topic, url, why, title, author }) {
|
|
87
|
+
if (!topic || !url || !why) {
|
|
88
|
+
throw new Error('claudenv canon add <topic> <url> --why "<reason>" required');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const canon = await readCanon();
|
|
92
|
+
if (!canon[topic]) canon[topic] = [];
|
|
93
|
+
|
|
94
|
+
const entry = {
|
|
95
|
+
...(title ? { title } : {}),
|
|
96
|
+
...(author ? { author } : {}),
|
|
97
|
+
url,
|
|
98
|
+
why,
|
|
99
|
+
added: new Date().toISOString().slice(0, 10),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Skip duplicates by URL within the same topic.
|
|
103
|
+
const dup = canon[topic].find((e) => e.url === url);
|
|
104
|
+
if (dup) return { added: false, reason: 'duplicate url in same topic', entry: dup };
|
|
105
|
+
|
|
106
|
+
canon[topic].push(entry);
|
|
107
|
+
await writeCanon(canon);
|
|
108
|
+
return { added: true, topic, entry };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* List all topics with their entries, or filter by one topic.
|
|
113
|
+
*/
|
|
114
|
+
export async function canonList(topic) {
|
|
115
|
+
const canon = await readCanon();
|
|
116
|
+
if (topic) {
|
|
117
|
+
return { [topic]: canon[topic] || [] };
|
|
118
|
+
}
|
|
119
|
+
return canon;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Substring search across title/why/url, returning matching entries grouped by topic.
|
|
124
|
+
*/
|
|
125
|
+
export async function canonSearch(query) {
|
|
126
|
+
if (!query) throw new Error('claudenv canon search <query> required');
|
|
127
|
+
const q = query.toLowerCase();
|
|
128
|
+
const canon = await readCanon();
|
|
129
|
+
const result = {};
|
|
130
|
+
for (const [topic, entries] of Object.entries(canon)) {
|
|
131
|
+
const hits = entries.filter((e) =>
|
|
132
|
+
[e.title, e.why, e.url, e.author].some((v) => v && v.toLowerCase().includes(q))
|
|
133
|
+
);
|
|
134
|
+
if (hits.length) result[topic] = hits;
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Find entries whose `added` date is older than `months` months — candidates
|
|
141
|
+
* for review. Returns an array of `{topic, entry}` items.
|
|
142
|
+
*/
|
|
143
|
+
export async function canonPrune(months = 6) {
|
|
144
|
+
const cutoff = new Date();
|
|
145
|
+
cutoff.setMonth(cutoff.getMonth() - months);
|
|
146
|
+
const canon = await readCanon();
|
|
147
|
+
const stale = [];
|
|
148
|
+
for (const [topic, entries] of Object.entries(canon)) {
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
if (!entry.added) {
|
|
151
|
+
stale.push({ topic, entry, reason: 'no `added` date' });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const added = new Date(entry.added);
|
|
155
|
+
if (!Number.isNaN(added.getTime()) && added < cutoff) {
|
|
156
|
+
stale.push({ topic, entry });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return stale;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Serialize canon back to YAML form. Comment-light, human-editable.
|
|
165
|
+
*/
|
|
166
|
+
async function writeCanon(canon) {
|
|
167
|
+
const lines = [];
|
|
168
|
+
lines.push('# Личный канон. Добавляйте через: claudenv canon add <topic> <url> --why "<reason>"');
|
|
169
|
+
lines.push('');
|
|
170
|
+
for (const [topic, entries] of Object.entries(canon)) {
|
|
171
|
+
lines.push(`${topic}:`);
|
|
172
|
+
for (const e of entries) {
|
|
173
|
+
const keys = Object.keys(e);
|
|
174
|
+
if (keys.length === 0) continue;
|
|
175
|
+
const [firstKey, ...rest] = keys;
|
|
176
|
+
lines.push(` - ${firstKey}: ${quote(e[firstKey])}`);
|
|
177
|
+
for (const k of rest) {
|
|
178
|
+
lines.push(` ${k}: ${quote(e[k])}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
lines.push('');
|
|
182
|
+
}
|
|
183
|
+
await mkdir(dirname(canonIndexPath()), { recursive: true });
|
|
184
|
+
await writeFile(canonIndexPath(), lines.join('\n'), 'utf-8');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function quote(v) {
|
|
188
|
+
if (typeof v !== 'string') return String(v);
|
|
189
|
+
if (/[:#"\n]/.test(v)) return JSON.stringify(v);
|
|
190
|
+
return v;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Format canon for terminal output.
|
|
195
|
+
*/
|
|
196
|
+
export function formatCanon(canon) {
|
|
197
|
+
const topics = Object.keys(canon);
|
|
198
|
+
if (topics.length === 0) {
|
|
199
|
+
return 'Канон пуст. Добавь первые записи через `claudenv canon add <topic> <url> --why "..."`.';
|
|
200
|
+
}
|
|
201
|
+
const lines = [];
|
|
202
|
+
for (const t of topics) {
|
|
203
|
+
lines.push(`## ${t}`);
|
|
204
|
+
for (const e of canon[t] || []) {
|
|
205
|
+
const title = e.title || '(no title)';
|
|
206
|
+
const author = e.author ? ` — ${e.author}` : '';
|
|
207
|
+
lines.push(` - ${title}${author}`);
|
|
208
|
+
lines.push(` ${e.url}`);
|
|
209
|
+
if (e.why) lines.push(` why: ${e.why}`);
|
|
210
|
+
}
|
|
211
|
+
lines.push('');
|
|
212
|
+
}
|
|
213
|
+
return lines.join('\n').trimEnd();
|
|
214
|
+
}
|