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 CHANGED
@@ -1,14 +1,22 @@
1
1
  # ai-commons — общие правила для всех проектов
2
2
 
3
- Этот файл подгружается в каждую Claude Code сессию проекта-потребителя через
4
- импорт `@node_modules/ai-commons/AGENTS.md` в корневом `CLAUDE.md`. В корневом
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
- Чтобы поменять общие правила, правь этот файл в репозитории `ai-commons`.
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
- `CLAUDE.md` потребителя — он управляется `ai-commons` и перезаписывается
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` вставляет/обновляет блок между метками `ai-commons:begin/end`
68
- в корневом `CLAUDE.md` потребителя. Если файла нет — создаёт его.
69
- - `SessionStart`-хук `ai-commons` обновляет этот блок на каждом старте
70
- сессии это гарантирует, что правила всегда актуальны. Остальное
71
- содержимое `CLAUDE.md` не трогается.
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
- Общие правила для Claude Code + автосохранение диалогов. Поставляется как
4
- npm-зависимость. После `npm install` в проекте появляется:
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
- ```bash
23
- cd my-new-project
24
- npm i -D /Users/os/okneigres-repos/ai-commons
25
- ```
12
+ В файлах с управляемым блоком (`CLAUDE.md`, `AGENTS.md`) содержимое между
13
+ метками `<!-- ai-commons:begin -->` и `<!-- ai-commons:end -->`
14
+ перезаписывается; всё остальное в файле — твоё, не трогается.
26
15
 
27
- Или, если опубликовано в реестре:
16
+ Дополнительно для Claude:
28
17
 
29
- ```bash
30
- npm i -D ai-commons
31
- ```
18
+ - `SessionStart`-хук обновляет все три target-файла на каждом старте сессии.
19
+ - `Stop` / `SessionEnd`-хук сохраняет диалог в
20
+ `.ai-dialogs/YYMMDD-HHII-<topic>.md`.
21
+ - `.ai-dialogs/` добавляется в `.gitignore`.
32
22
 
33
- `postinstall` всё настраивает автоматически. Запустить вручную:
23
+ ## Установка в новый проект
34
24
 
35
25
  ```bash
36
- npx ai-commons install
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 uninstall
43
- # затем удалить зависимость:
44
- npm uninstall ai-commons
34
+ npx ai-commons install # идемпотентно: можно гонять сколько угодно раз
35
+ npx ai-commons uninstall # снять блоки, хуки и Cursor-файл
45
36
  ```
46
37
 
47
- `uninstall` уберёт блок `ai-commons` из `CLAUDE.md` (сам файл остаётся, если в
48
- нём есть другое содержимое — удаляется только если оказался пустым) и
49
- вычистит хуки `ai-commons` из `.claude/settings.json`, не трогая чужие хуки.
38
+ ## Уровни правил в AGENTS.md
50
39
 
51
- ## Уровни правил
52
-
53
- `AGENTS.md` в этом репо разбит на три секции:
54
-
55
- - **MUST**обязательно, игнорировать нельзя ни при каких обстоятельствах
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. В каждом потребителе: `npm update ai-commons` (или `npm install`, если
67
- зависимость через `file:`).
68
-
69
- Содержимое подгружается Claude Code через `@`-импорт каждый сеанс, поэтому
70
- после обновления `node_modules/ai-commons/AGENTS.md` следующая сессия
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 — pointer-блок + merge hooks + .gitignore
98
- └── managed-block.js хелперы для блока в CLAUDE.md
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
- - `ai-commons` **не** распространяет общие сабагенты/скиллы/слэш-команды.
111
- Этот скоуп умышленно убран если понадобится, добавим отдельной командой
112
- `ai-commons sync-agents`.
113
- - `permissions` и `env` из `settings.json` потребителя не трогаются.
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: ensures the consumer's root CLAUDE.md contains an
3
- // up-to-date ai-commons block. The block lives between
4
- // `<!-- ai-commons:begin -->` and `<!-- ai-commons:end -->` markers; any
5
- // content OUTSIDE the markers is preserved.
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 path = require('path');
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 pointer refresh\n');
23
+ process.stderr.write('[ai-commons] no cwd, skipping refresh\n');
27
24
  process.exit(0);
28
25
  }
29
26
 
30
- const pointerPath = path.join(cwd, POINTER_FILE);
31
- const existing = fs.existsSync(pointerPath)
32
- ? fs.readFileSync(pointerPath, 'utf8')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-commons",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Shared Claude Code rules + dialog-saver hook for projects",
5
5
  "bin": {
6
6
  "ai-commons": "bin/ai-commons.js"
@@ -1,5 +1,5 @@
1
1
  // Installs ai-commons into a consumer project:
2
- // - writes/refreshes the root CLAUDE.md pointer
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 { applyBlock, removeBlock } = require('./managed-block');
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 writePointer(consumerRoot) {
43
- const pointerPath = path.join(consumerRoot, POINTER_FILE);
44
- const existing = fs.existsSync(pointerPath)
45
- ? fs.readFileSync(pointerPath, 'utf8')
46
- : null;
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
- writePointer(consumerRoot);
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 block from the pointer file. Leave any user-written
112
- // content intact; remove the file only if it ends up empty.
113
- const pointerPath = path.join(consumerRoot, POINTER_FILE);
114
- if (fs.existsSync(pointerPath)) {
115
- const existing = fs.readFileSync(pointerPath, 'utf8');
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.
@@ -1,44 +1,50 @@
1
- // Shared helpers for inserting/updating the ai-commons block inside a
2
- // host markdown file (typically the consumer's CLAUDE.md).
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 BLOCK_BODY = [
9
- BEGIN,
10
- '<!-- Managed by ai-commons. Anything between these markers is overwritten -->',
11
- '<!-- on every Claude Code session start. To change rules, edit AGENTS.md -->',
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
- const BLOCK_RE = new RegExp(`${escapeRegex(BEGIN)}[\\s\\S]*?${escapeRegex(END)}`);
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
- // Returns the host content with our managed block inserted/refreshed.
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
- if (hostContent == null) return BLOCK_BODY + '\n';
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, BLOCK_BODY);
35
+ return hostContent.replace(BLOCK_RE, block);
31
36
  }
32
- const sep = hostContent.length === 0 || hostContent.startsWith('\n') ? '\n' : '\n\n';
33
- return BLOCK_BODY + sep + hostContent;
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(`${escapeRegex(BEGIN)}[\\s\\S]*?${escapeRegex(END)}\\n*`, 'g');
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, BLOCK_BODY };
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
+ };