ai-commons 0.2.9 → 0.2.11
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/AGENTS.md +36 -16
- package/README.md +47 -60
- package/hooks/ensure-rules-pointer.js +9 -20
- package/package.json +1 -1
- package/scripts/installer.js +13 -31
- package/scripts/managed-block.js +27 -21
- package/scripts/targets.js +124 -0
package/AGENTS.md
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
# ai-commons — общие правила для всех проектов
|
|
2
2
|
|
|
3
|
-
Этот файл
|
|
4
|
-
|
|
5
|
-
`CLAUDE.md` потребителя есть управляемая секция между метками
|
|
6
|
-
`<!-- ai-commons:begin -->` и `<!-- ai-commons:end -->` — её содержимое
|
|
7
|
-
перезаписывается хуком `ai-commons` на каждом старте сессии. **Не редактируй
|
|
8
|
-
блок между метками** — изменения будут потеряны. Всё, что в `CLAUDE.md`
|
|
9
|
-
снаружи блока, остаётся нетронутым: пиши там свои проектные правила.
|
|
3
|
+
Этот файл — единый источник правды для общих правил. `ai-commons` копирует
|
|
4
|
+
его содержимое в файлы, которые читают популярные AI-агенты:
|
|
10
5
|
|
|
11
|
-
|
|
6
|
+
- `CLAUDE.md` — Claude Code, через `@AGENTS.md` импорт в управляемом блоке.
|
|
7
|
+
- `AGENTS.md` (в корне потребителя) — OpenAI Codex и стандарт AGENTS.md;
|
|
8
|
+
здесь содержимое встраивается в управляемый блок целиком.
|
|
9
|
+
- `.cursor/rules/ai-commons.mdc` — Cursor; файл целиком принадлежит
|
|
10
|
+
`ai-commons` (MDC c `alwaysApply: true`).
|
|
11
|
+
|
|
12
|
+
В файлах `CLAUDE.md` и `AGENTS.md` (корневом) управляемая секция огорожена
|
|
13
|
+
HTML-комментариями `ai-commons:begin` и `ai-commons:end`. **Не редактируй
|
|
14
|
+
блок между ними** — он перезаписывается. Всё, что в файле СНАРУЖИ блока,
|
|
15
|
+
остаётся твоим и не трогается.
|
|
16
|
+
|
|
17
|
+
Чтобы поменять общие правила — правь этот файл в репозитории `ai-commons`.
|
|
18
|
+
После `npm install` (или при старте Claude-сессии — там есть SessionStart
|
|
19
|
+
хук) изменения подтянутся во все target-файлы потребителя.
|
|
12
20
|
|
|
13
21
|
Правила сгруппированы по трём уровням приоритета. Игнорировать MUST нельзя
|
|
14
22
|
ни при каких обстоятельствах. Желательно — стандартное поведение по
|
|
@@ -27,9 +35,19 @@
|
|
|
27
35
|
- **Не запускай деструктивные операции без подтверждения**: `rm -rf`,
|
|
28
36
|
`git reset --hard`, `DROP TABLE`, `git clean -fd`, отключение pre-commit
|
|
29
37
|
хуков (`--no-verify`), force push куда угодно.
|
|
30
|
-
- **Не правь блок между метками `ai-commons:begin / end`** в
|
|
31
|
-
`
|
|
32
|
-
|
|
38
|
+
- **Не правь блок между метками `ai-commons:begin / end`** ни в `CLAUDE.md`,
|
|
39
|
+
ни в корневом `AGENTS.md` потребителя — он управляется `ai-commons` и
|
|
40
|
+
перезаписывается. Не правь и `.cursor/rules/ai-commons.mdc` — это файл,
|
|
41
|
+
целиком принадлежащий `ai-commons`. Свои правила пиши снаружи блоков
|
|
42
|
+
(или в отдельных Cursor-rules рядом).
|
|
43
|
+
- **Для групп связанных проектов используй `git-coherent`**
|
|
44
|
+
(`npm i -g git-coherent`, бинари `git-coherent` / `gitc`) — синхронизация
|
|
45
|
+
git-операций по сиблинг-репозиториям, которые опт-инятся через
|
|
46
|
+
`.git-coherent` маркер. Это «среднее между monorepo и микросервисами»:
|
|
47
|
+
каждый подпроект остаётся отдельным git-репо, `git-coherent` запускает
|
|
48
|
+
операции над всеми отмеченными сразу. Не предлагай и не создавай
|
|
49
|
+
монорепо или независимые multi-repo схемы для таких задач без явного
|
|
50
|
+
запроса.
|
|
33
51
|
- **Не отключай и не модифицируй хуки `ai-commons`** в `.claude/settings.json`
|
|
34
52
|
без явного запроса пользователя.
|
|
35
53
|
|
|
@@ -64,10 +82,12 @@
|
|
|
64
82
|
|
|
65
83
|
- `ai-commons` ставится как обычная npm-зависимость:
|
|
66
84
|
`npm i -D ai-commons` (или `file:~/okneigres-repos/ai-commons` для локальной разработки).
|
|
67
|
-
- `postinstall` вставляет/обновляет блок
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
- `postinstall` вставляет/обновляет управляемый блок в `CLAUDE.md` и
|
|
86
|
+
`AGENTS.md` потребителя, плюс пишет полный `.cursor/rules/ai-commons.mdc`.
|
|
87
|
+
Любого из файлов нет — создаст.
|
|
88
|
+
- `SessionStart`-хук `ai-commons` обновляет всё то же при каждом старте
|
|
89
|
+
Claude-сессии — Claude-юзеры получают авто-рефреш. Codex- и Cursor-юзеры
|
|
90
|
+
обновляются на `npm install` (или ручном `npx ai-commons install`).
|
|
91
|
+
- Содержимое файлов СНАРУЖИ управляемого блока не трогается никогда.
|
|
72
92
|
- `Stop`-хук сохраняет каждый диалог в
|
|
73
93
|
`.ai-dialogs/YYMMDD-HHII-<topic>.md` (см. README модуля).
|
package/README.md
CHANGED
|
@@ -1,78 +1,62 @@
|
|
|
1
1
|
# ai-commons
|
|
2
2
|
|
|
3
|
-
Общие правила для
|
|
4
|
-
npm
|
|
5
|
-
|
|
6
|
-
- В корневом `CLAUDE.md` — управляемый блок между метками:
|
|
7
|
-
```
|
|
8
|
-
<!-- ai-commons:begin -->
|
|
9
|
-
... @node_modules/ai-commons/AGENTS.md ...
|
|
10
|
-
<!-- ai-commons:end -->
|
|
11
|
-
```
|
|
12
|
-
Этот блок **обновляется хуком** при каждом старте сессии. Всё, что в файле
|
|
13
|
-
снаружи блока, — твоё, не трогается. Если `CLAUDE.md` не было — создастся
|
|
14
|
-
с одним блоком.
|
|
15
|
-
- Хуки в `.claude/settings.json`:
|
|
16
|
-
- `SessionStart` — обновляет блок в `CLAUDE.md` на каждом старте сессии.
|
|
17
|
-
- `Stop` и `SessionEnd` — сохраняют диалог в `.ai-dialogs/YYMMDD-HHII-<topic>.md`.
|
|
18
|
-
- Запись `.ai-dialogs/` в `.gitignore`.
|
|
3
|
+
Общие правила для AI-агентов + автосохранение Claude-диалогов. Поставляется
|
|
4
|
+
как npm-зависимость и поддерживает четыре популярных таргета:
|
|
19
5
|
|
|
20
|
-
|
|
6
|
+
| Агент / стандарт | Файл в потребителе | Режим |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| Claude Code | `CLAUDE.md` | управляемый блок (`@AGENTS.md` import) |
|
|
9
|
+
| OpenAI Codex / стандартный AGENTS.md | `AGENTS.md` | управляемый блок (встроенное содержимое) |
|
|
10
|
+
| Cursor | `.cursor/rules/ai-commons.mdc` | весь файл принадлежит ai-commons |
|
|
21
11
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```
|
|
12
|
+
В файлах с управляемым блоком (`CLAUDE.md`, `AGENTS.md`) содержимое между
|
|
13
|
+
метками `<!-- ai-commons:begin -->` и `<!-- ai-commons:end -->`
|
|
14
|
+
перезаписывается; всё остальное в файле — твоё, не трогается.
|
|
26
15
|
|
|
27
|
-
|
|
16
|
+
Дополнительно для Claude:
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
- `SessionStart`-хук обновляет все три target-файла на каждом старте сессии.
|
|
19
|
+
- `Stop` / `SessionEnd`-хук сохраняет диалог в
|
|
20
|
+
`.ai-dialogs/YYMMDD-HHII-<topic>.md`.
|
|
21
|
+
- `.ai-dialogs/` добавляется в `.gitignore`.
|
|
32
22
|
|
|
33
|
-
|
|
23
|
+
## Установка в новый проект
|
|
34
24
|
|
|
35
25
|
```bash
|
|
36
|
-
|
|
26
|
+
cd my-new-project
|
|
27
|
+
npm init -y # если package.json ещё нет
|
|
28
|
+
npm i -D /Users/os/okneigres-repos/ai-commons
|
|
37
29
|
```
|
|
38
30
|
|
|
39
|
-
|
|
31
|
+
`postinstall` всё настраивает автоматически. Запустить вручную:
|
|
40
32
|
|
|
41
33
|
```bash
|
|
42
|
-
npx ai-commons
|
|
43
|
-
#
|
|
44
|
-
npm uninstall ai-commons
|
|
34
|
+
npx ai-commons install # идемпотентно: можно гонять сколько угодно раз
|
|
35
|
+
npx ai-commons uninstall # снять блоки, хуки и Cursor-файл
|
|
45
36
|
```
|
|
46
37
|
|
|
47
|
-
|
|
48
|
-
нём есть другое содержимое — удаляется только если оказался пустым) и
|
|
49
|
-
вычистит хуки `ai-commons` из `.claude/settings.json`, не трогая чужие хуки.
|
|
38
|
+
## Уровни правил в AGENTS.md
|
|
50
39
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
(например: не пушить force в main, не коммитить секреты).
|
|
57
|
-
- **Желательно** — стандарт по умолчанию, отступать можно только с явной
|
|
58
|
-
причиной (например: краткие ответы, без избыточных абстракций).
|
|
59
|
-
- **Остальное** — стиль и привычки (например: `curl` одной строкой,
|
|
60
|
-
ссылки на код в формате `path:line`).
|
|
40
|
+
- **MUST** — обязательно, игнорировать нельзя (force-push в main, секреты,
|
|
41
|
+
деструктивные команды без подтверждения, `git-coherent` для multi-repo).
|
|
42
|
+
- **Желательно** — стандарт по умолчанию (краткость, без лишних абстракций,
|
|
43
|
+
без комментариев "что делает код").
|
|
44
|
+
- **Остальное** — стиль и привычки (`curl` одной строкой, ссылки `path:line`).
|
|
61
45
|
|
|
62
46
|
## Как обновить правила во всех проектах
|
|
63
47
|
|
|
64
48
|
1. Правишь `AGENTS.md` здесь.
|
|
65
49
|
2. `git commit && git push` (или просто коммит, если репо локальный).
|
|
66
|
-
3. В каждом потребителе:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
50
|
+
3. В каждом потребителе:
|
|
51
|
+
- **Claude**: ничего делать не надо — `SessionStart`-хук подтянет новые
|
|
52
|
+
правила в следующей сессии (для `file:`-зависимостей) или после
|
|
53
|
+
`npm update ai-commons` (для опубликованных).
|
|
54
|
+
- **Codex / Cursor**: `npm install` (или `npx ai-commons install`) —
|
|
55
|
+
перепишет блоки и Cursor-файл.
|
|
72
56
|
|
|
73
|
-
## Сохранение диалогов
|
|
57
|
+
## Сохранение диалогов (только Claude)
|
|
74
58
|
|
|
75
|
-
После каждого Claude Stop (и при завершении сессии) хук пишет
|
|
59
|
+
После каждого Claude `Stop` (и при завершении сессии) хук пишет
|
|
76
60
|
`.ai-dialogs/YYMMDD-HHII-<topic>.md`:
|
|
77
61
|
|
|
78
62
|
- Timestamp — момент первой реплики пользователя в сессии.
|
|
@@ -89,15 +73,16 @@ npm uninstall ai-commons
|
|
|
89
73
|
|
|
90
74
|
```
|
|
91
75
|
ai-commons/
|
|
92
|
-
├── AGENTS.md —
|
|
76
|
+
├── AGENTS.md — источник правды, копируется в потребителей
|
|
93
77
|
├── package.json — bin + postinstall
|
|
94
78
|
├── bin/ai-commons.js — CLI: install / uninstall
|
|
95
79
|
├── scripts/
|
|
96
80
|
│ ├── postinstall.js — npm-хук, запускает installer.install
|
|
97
|
-
│ ├── installer.js —
|
|
98
|
-
│
|
|
81
|
+
│ ├── installer.js — установка/удаление в потребителе
|
|
82
|
+
│ ├── targets.js — список таргет-файлов (Claude/Codex/Cursor)
|
|
83
|
+
│ └── managed-block.js — вставка/обновление блока в markdown
|
|
99
84
|
└── hooks/
|
|
100
|
-
├── ensure-rules-pointer.js — SessionStart-хук
|
|
85
|
+
├── ensure-rules-pointer.js — SessionStart-хук (рефреш всех таргетов)
|
|
101
86
|
└── save-dialog.js — Stop / SessionEnd-хук
|
|
102
87
|
```
|
|
103
88
|
|
|
@@ -107,7 +92,9 @@ ai-commons/
|
|
|
107
92
|
|
|
108
93
|
## Что не делается
|
|
109
94
|
|
|
110
|
-
- `
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
95
|
+
- `permissions` и `env` из `settings.json` потребителя не трогаются — только
|
|
96
|
+
свой набор хуков (SessionStart / Stop / SessionEnd) мержится.
|
|
97
|
+
- Старый формат Cursor (`.cursorrules` в корне) не поддерживается — только
|
|
98
|
+
современный `.cursor/rules/*.mdc`.
|
|
99
|
+
- Автосейв диалогов реализован только для Claude (у других агентов нет
|
|
100
|
+
аналогичных hook'ов).
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// SessionStart hook:
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// SessionStart hook: re-applies ai-commons rule files in the consumer
|
|
3
|
+
// project. This refreshes the managed block in CLAUDE.md / AGENTS.md and
|
|
4
|
+
// the full .cursor/rules/ai-commons.mdc file using the latest content from
|
|
5
|
+
// node_modules/ai-commons/AGENTS.md.
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
|
-
const
|
|
9
|
-
const { applyBlock } = require('../scripts/managed-block');
|
|
10
|
-
|
|
11
|
-
const POINTER_FILE = 'CLAUDE.md';
|
|
8
|
+
const { applyAllTargets } = require('../scripts/targets');
|
|
12
9
|
|
|
13
10
|
function readStdinJson() {
|
|
14
11
|
try {
|
|
@@ -23,22 +20,14 @@ function main() {
|
|
|
23
20
|
const input = readStdinJson();
|
|
24
21
|
const cwd = input.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
25
22
|
if (!cwd || !fs.existsSync(cwd)) {
|
|
26
|
-
process.stderr.write('[ai-commons] no cwd, skipping
|
|
23
|
+
process.stderr.write('[ai-commons] no cwd, skipping refresh\n');
|
|
27
24
|
process.exit(0);
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
: null;
|
|
34
|
-
const next = applyBlock(existing);
|
|
35
|
-
|
|
36
|
-
if (existing != null && next === existing) {
|
|
37
|
-
process.exit(0);
|
|
27
|
+
const results = applyAllTargets(cwd);
|
|
28
|
+
for (const { target, result } of results) {
|
|
29
|
+
process.stderr.write(`[ai-commons] ${target.id}: ${target.file} ${result}\n`);
|
|
38
30
|
}
|
|
39
|
-
|
|
40
|
-
fs.writeFileSync(pointerPath, next);
|
|
41
|
-
process.stderr.write(`[ai-commons] refreshed block in ${POINTER_FILE}\n`);
|
|
42
31
|
process.exit(0);
|
|
43
32
|
}
|
|
44
33
|
|
package/package.json
CHANGED
package/scripts/installer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Installs ai-commons into a consumer project:
|
|
2
|
-
// - writes/refreshes
|
|
2
|
+
// - writes/refreshes rule files for Claude / Codex / Cursor (see targets.js)
|
|
3
3
|
// - merges our hooks into .claude/settings.json (preserving any existing hooks)
|
|
4
4
|
// - ensures .ai-dialogs/ is gitignored
|
|
5
5
|
//
|
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const {
|
|
11
|
+
const { applyAllTargets, removeAllTargets } = require('./targets');
|
|
12
12
|
|
|
13
13
|
const PACKAGE_NAME = require('../package.json').name;
|
|
14
|
-
const POINTER_FILE = 'CLAUDE.md';
|
|
15
14
|
const SETTINGS_FILE = path.join('.claude', 'settings.json');
|
|
16
15
|
const GITIGNORE_FILE = '.gitignore';
|
|
17
16
|
|
|
@@ -39,20 +38,11 @@ function readJsonOrEmpty(p) {
|
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
function
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const next = applyBlock(existing);
|
|
48
|
-
if (existing != null && next === existing) return false;
|
|
49
|
-
fs.writeFileSync(pointerPath, next);
|
|
50
|
-
log(
|
|
51
|
-
existing == null
|
|
52
|
-
? `created ${POINTER_FILE} with ai-commons block`
|
|
53
|
-
: `refreshed ai-commons block in ${POINTER_FILE}`
|
|
54
|
-
);
|
|
55
|
-
return true;
|
|
41
|
+
function writeRuleFiles(consumerRoot) {
|
|
42
|
+
const results = applyAllTargets(consumerRoot);
|
|
43
|
+
for (const { target, result } of results) {
|
|
44
|
+
log(`${target.id}: ${target.file} ${result}`);
|
|
45
|
+
}
|
|
56
46
|
}
|
|
57
47
|
|
|
58
48
|
function stripManagedHooksFromEvent(eventEntries) {
|
|
@@ -101,26 +91,18 @@ function install(consumerRoot) {
|
|
|
101
91
|
if (!consumerRoot || !fs.existsSync(consumerRoot)) {
|
|
102
92
|
throw new Error(`Invalid consumer root: ${consumerRoot}`);
|
|
103
93
|
}
|
|
104
|
-
|
|
94
|
+
writeRuleFiles(consumerRoot);
|
|
105
95
|
mergeHooks(consumerRoot);
|
|
106
96
|
ensureGitignore(consumerRoot);
|
|
107
97
|
log('install complete');
|
|
108
98
|
}
|
|
109
99
|
|
|
110
100
|
function uninstall(consumerRoot) {
|
|
111
|
-
// Strip our managed
|
|
112
|
-
//
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const next = removeBlock(existing);
|
|
117
|
-
if (next.trim() === '') {
|
|
118
|
-
fs.rmSync(pointerPath);
|
|
119
|
-
log(`removed empty ${POINTER_FILE}`);
|
|
120
|
-
} else if (next !== existing) {
|
|
121
|
-
fs.writeFileSync(pointerPath, next);
|
|
122
|
-
log(`removed ai-commons block from ${POINTER_FILE}`);
|
|
123
|
-
}
|
|
101
|
+
// Strip our managed content from each target file. Leave user content
|
|
102
|
+
// intact; remove block-mode files only if they end up empty.
|
|
103
|
+
const results = removeAllTargets(consumerRoot);
|
|
104
|
+
for (const { target, result } of results) {
|
|
105
|
+
log(`${target.id}: ${target.file} ${result}`);
|
|
124
106
|
}
|
|
125
107
|
|
|
126
108
|
// Strip our hooks from settings.
|
package/scripts/managed-block.js
CHANGED
|
@@ -1,44 +1,50 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Helpers for inserting/updating an ai-commons block inside a host markdown
|
|
2
|
+
// file (CLAUDE.md, AGENTS.md, etc.). The block content is passed in by the
|
|
3
|
+
// caller, so this module is target-agnostic.
|
|
3
4
|
|
|
4
|
-
const PACKAGE_NAME = 'ai-commons';
|
|
5
5
|
const BEGIN = '<!-- ai-commons:begin -->';
|
|
6
6
|
const END = '<!-- ai-commons:end -->';
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
'<!--
|
|
11
|
-
'<!--
|
|
12
|
-
'<!-- in the ai-commons repo. Everything OUTSIDE these markers is yours. -->',
|
|
13
|
-
`@node_modules/${PACKAGE_NAME}/AGENTS.md`,
|
|
14
|
-
END,
|
|
8
|
+
const HEADER_COMMENTS = [
|
|
9
|
+
'<!-- Managed by ai-commons. Content between these markers is auto-generated. -->',
|
|
10
|
+
'<!-- To change rules, edit AGENTS.md in the ai-commons repo and re-run -->',
|
|
11
|
+
'<!-- `npm install` in this project (Claude users get auto-refresh on session). -->',
|
|
15
12
|
].join('\n');
|
|
16
13
|
|
|
17
14
|
function escapeRegex(s) {
|
|
18
15
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
// Greedy on purpose: if marker strings somehow appear inside the rendered
|
|
19
|
+
// body, we still match the OUTERMOST begin..end pair instead of stopping
|
|
20
|
+
// at the first nested-looking end.
|
|
21
|
+
const BLOCK_RE = new RegExp(`${escapeRegex(BEGIN)}[\\s\\S]*${escapeRegex(END)}`);
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
function buildBlock(innerContent) {
|
|
24
|
+
return `${BEGIN}\n${HEADER_COMMENTS}\n\n${innerContent}\n${END}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Returns the host content with the managed block inserted/refreshed.
|
|
24
28
|
// - If the host already contains markers, replace content between them.
|
|
25
29
|
// - Otherwise, prepend the block at the top, separated by a blank line.
|
|
26
30
|
// - If host is null/undefined, return a file containing just the block.
|
|
27
|
-
function applyBlock(hostContent) {
|
|
28
|
-
|
|
31
|
+
function applyBlock(hostContent, innerContent) {
|
|
32
|
+
const block = buildBlock(innerContent);
|
|
33
|
+
if (hostContent == null) return block + '\n';
|
|
29
34
|
if (BLOCK_RE.test(hostContent)) {
|
|
30
|
-
return hostContent.replace(BLOCK_RE,
|
|
35
|
+
return hostContent.replace(BLOCK_RE, block);
|
|
31
36
|
}
|
|
32
|
-
const sep =
|
|
33
|
-
|
|
37
|
+
const sep =
|
|
38
|
+
hostContent.length === 0 || hostContent.startsWith('\n') ? '\n' : '\n\n';
|
|
39
|
+
return block + sep + hostContent;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
// Returns the host content with the managed block (and surrounding blank
|
|
37
|
-
// lines) removed. Safe to call when no block is present.
|
|
38
42
|
function removeBlock(hostContent) {
|
|
39
43
|
if (!hostContent) return '';
|
|
40
|
-
const re = new RegExp(
|
|
44
|
+
const re = new RegExp(
|
|
45
|
+
`${escapeRegex(BEGIN)}[\\s\\S]*${escapeRegex(END)}\\n*`
|
|
46
|
+
);
|
|
41
47
|
return hostContent.replace(re, '');
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
module.exports = { applyBlock, removeBlock, BEGIN, END
|
|
50
|
+
module.exports = { applyBlock, removeBlock, BEGIN, END };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Defines every file ai-commons writes into a consumer project, and how to
|
|
2
|
+
// render its content. Currently covers Claude, Codex (root AGENTS.md), and
|
|
3
|
+
// Cursor (.cursor/rules/ai-commons.mdc).
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { applyBlock, removeBlock } = require('./managed-block');
|
|
8
|
+
|
|
9
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
function readRules() {
|
|
12
|
+
return fs.readFileSync(path.join(PACKAGE_ROOT, 'AGENTS.md'), 'utf8').trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Each target:
|
|
16
|
+
// id — internal name for logs
|
|
17
|
+
// file — path relative to consumer root
|
|
18
|
+
// mode — 'block' (manage section between markers) | 'fullFile' (own whole file)
|
|
19
|
+
// render(r) — render the block body / file body from the rules text
|
|
20
|
+
//
|
|
21
|
+
// Block mode preserves user content outside the markers.
|
|
22
|
+
// fullFile mode means we own the entire file (created/removed as a unit).
|
|
23
|
+
|
|
24
|
+
const TARGETS = [
|
|
25
|
+
{
|
|
26
|
+
id: 'claude',
|
|
27
|
+
file: 'CLAUDE.md',
|
|
28
|
+
mode: 'block',
|
|
29
|
+
// Claude supports @-imports natively; defer to the local AGENTS.md
|
|
30
|
+
// (which carries the embedded rules), avoiding duplication.
|
|
31
|
+
render: () => '@AGENTS.md',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'codex+standard',
|
|
35
|
+
file: 'AGENTS.md',
|
|
36
|
+
mode: 'block',
|
|
37
|
+
// Codex CLI and the cross-tool AGENTS.md standard read this file
|
|
38
|
+
// directly — embed the full rules text.
|
|
39
|
+
render: (rules) => rules,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'cursor',
|
|
43
|
+
file: path.join('.cursor', 'rules', 'ai-commons.mdc'),
|
|
44
|
+
mode: 'fullFile',
|
|
45
|
+
// Cursor expects MDC: YAML frontmatter + markdown body.
|
|
46
|
+
render: (rules) =>
|
|
47
|
+
`---\ndescription: ai-commons shared rules\nalwaysApply: true\n---\n\n${rules}\n`,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function applyTarget(consumerRoot, target, rules) {
|
|
52
|
+
const fullPath = path.join(consumerRoot, target.file);
|
|
53
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
54
|
+
|
|
55
|
+
if (target.mode === 'fullFile') {
|
|
56
|
+
const next = target.render(rules);
|
|
57
|
+
const existing = fs.existsSync(fullPath)
|
|
58
|
+
? fs.readFileSync(fullPath, 'utf8')
|
|
59
|
+
: null;
|
|
60
|
+
if (existing === next) return null;
|
|
61
|
+
fs.writeFileSync(fullPath, next);
|
|
62
|
+
return existing == null ? 'created' : 'refreshed';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// block mode
|
|
66
|
+
const existing = fs.existsSync(fullPath)
|
|
67
|
+
? fs.readFileSync(fullPath, 'utf8')
|
|
68
|
+
: null;
|
|
69
|
+
const next = applyBlock(existing, target.render(rules));
|
|
70
|
+
if (existing != null && next === existing) return null;
|
|
71
|
+
fs.writeFileSync(fullPath, next);
|
|
72
|
+
return existing == null ? 'created' : 'refreshed';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function removeTarget(consumerRoot, target) {
|
|
76
|
+
const fullPath = path.join(consumerRoot, target.file);
|
|
77
|
+
if (!fs.existsSync(fullPath)) return null;
|
|
78
|
+
|
|
79
|
+
if (target.mode === 'fullFile') {
|
|
80
|
+
fs.rmSync(fullPath);
|
|
81
|
+
return 'removed';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const existing = fs.readFileSync(fullPath, 'utf8');
|
|
85
|
+
const next = removeBlock(existing);
|
|
86
|
+
if (next.trim() === '') {
|
|
87
|
+
fs.rmSync(fullPath);
|
|
88
|
+
return 'removed-empty';
|
|
89
|
+
}
|
|
90
|
+
if (next !== existing) {
|
|
91
|
+
fs.writeFileSync(fullPath, next);
|
|
92
|
+
return 'block-removed';
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function applyAllTargets(consumerRoot) {
|
|
98
|
+
const rules = readRules();
|
|
99
|
+
const results = [];
|
|
100
|
+
for (const target of TARGETS) {
|
|
101
|
+
const result = applyTarget(consumerRoot, target, rules);
|
|
102
|
+
if (result) results.push({ target, result });
|
|
103
|
+
}
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function removeAllTargets(consumerRoot) {
|
|
108
|
+
const results = [];
|
|
109
|
+
for (const target of TARGETS) {
|
|
110
|
+
const result = removeTarget(consumerRoot, target);
|
|
111
|
+
if (result) results.push({ target, result });
|
|
112
|
+
}
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
TARGETS,
|
|
118
|
+
PACKAGE_ROOT,
|
|
119
|
+
readRules,
|
|
120
|
+
applyTarget,
|
|
121
|
+
removeTarget,
|
|
122
|
+
applyAllTargets,
|
|
123
|
+
removeAllTargets,
|
|
124
|
+
};
|