@nitra/cursor 1.13.58 → 1.13.60

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.
@@ -44,9 +44,12 @@ fi
44
44
  # Extract role + text + thinking + tool_use names from JSONL transcript.
45
45
  # We keep reasoning/decisions visible to the analyzer but drop large tool outputs.
46
46
  TRANSCRIPT=$(jq -r '
47
- select(.type == "user" or .type == "assistant")
47
+ select(
48
+ .type == "user" or .type == "assistant"
49
+ or .role == "user" or .role == "assistant"
50
+ )
48
51
  | .message as $m
49
- | ($m.role // .type) as $role
52
+ | ($m.role // .role // .type) as $role
50
53
  | ($m.content
51
54
  | if type == "string" then .
52
55
  else (
@@ -79,6 +82,7 @@ if (( ${#TRANSCRIPT} > MAX_CHARS )); then
79
82
  fi
80
83
 
81
84
  if [[ -z "$TRANSCRIPT" ]]; then
85
+ log " → empty transcript after jq (Claude Code: .type; Cursor Agent: .role)"
82
86
  exit 0
83
87
  fi
84
88
 
package/CHANGELOG.md CHANGED
@@ -4,11 +4,18 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
- ## [1.13.58] - 2026-05-20
7
+ ## [1.13.60] - 2026-05-20
8
+
9
+ ### Fixed
10
+
11
+ - Генерація **`AGENTS.md`** і **`CLAUDE.md`** при `npx @nitra/cursor`: Mustache-секції більше не вставляють порожній рядок між кожним пунктом списку (MD012), а фінальний markdown згортає зайві `\n\n\n` на стиках секцій — не потрібен окремий `lint-text` лише заради зачистки згенерованих файлів. Зачеплено: [generated-markdown.mjs](scripts/utils/generated-markdown.mjs) (`expandMustacheSection` — trim inner + `join('\n')`, `collapseMultipleBlankLines`, `formatGeneratedMarkdownLines`), [n-cursor.js](bin/n-cursor.js) (імпорт утиліт замість inline-логіки), [generated-markdown.test.mjs](scripts/utils/generated-markdown.test.mjs).
12
+
13
+ ## [1.13.59] - 2026-05-20
8
14
 
9
15
  ### Added
10
16
 
11
- - `check security`: новий concern **`security.sample_secret`** — placeholder фейкових credential-значень у прикладних файлах має бути `sample-secret`, а не bare `secret`. Причина: `sample-secret` містить підрядок `sample` із вшитого списку `DefaultFalsePositives` TruffleHog і відсіюється сканером гарантовано та незалежно від версії; bare `secret` наразі ігнорується лише тому, що випадково присутнє у словнику `fp_words.txt` крихка, версієзалежна поведінка. [check.mjs](rules/security/fix/sample_secret/check.mjs) обходить дерево, відбирає прикладні файли (basename із суфіксом `.example`/`.sample`/`.template`/`.dist` чи infix `.example.`/`.sample.`/`.template.`, а також усе всередині каталогів `fixtures`/`fixture`/`__fixtures__`) і порядково шукає `secret` у позиції значення одразу після `=`, `:` або `=>` з опційними лапками; імена ключів (`client_secret`, `JWT_SECRET`) не чіпаються, бо матч прив'язаний до значення. Решта файлів не сканується там `secret` майже завжди частина реального коду. Скан текстовий (regex, не AST/Rego): прикладні файли різнорідні конфіги (`.env`, YAML, JSON, TOML, plain `.dist`) без єдиного AST, а відбір файлів потребує обходу дерева. Зачеплено: [check.mjs](rules/security/fix/sample_secret/check.mjs) і [check.test.mjs](rules/security/fix/sample_secret/check.test.mjs) (новий concern + 9 тестів), [security.mdc](rules/security/security.mdc) (нова секція «Placeholder для секретів`sample-secret`» та секція «Перевірка»). Bump `security.mdc` `2.0` `2.1`.
17
+ - Нове `alwaysApply`-правило **`feedback`** ([feedback.mdc](rules/feedback/feedback.mdc)) ефемерний канал зворотного звʼязку до пакета `@nitra/cursor`. Виконуючи будь-який скіл пакета (`n-lint`, `n-fix`, `n-taze`, `n-adr-normalize`, `n-llm-patch`, `n-publish-telegram`, `mdc-check`), агент проходить крізь `.cursor/rules/`, `SKILL.md` і `npx @nitra/cursor check` і бачить «тертя»: неоднозначні інструкції, відсутні `check-*.mjs`, false positive, порушення без автофіксу, повторювані патерни. Правило вимагає наприкінці скілу, **після** основного резюме, додати у відповідь чату секцію `## 🔧 Покращення @nitra/cursor` з пунктами за схемою `target` (`rule`/`skill`/`check`) · `id` · `kind` (`ambiguous-doc`/`missing-check`/`false-positive`/`no-autofix`/`recurring-pattern`) · `evidence` · `suggestion`. Резюме **навмисно ефемерне** живе лише у відповіді чату: правило забороняє запис файлів/чернеток, GitHub issue/PR і редагування самого пакета; розробник, читаючи відповідь, сам вирішує, чи переносити пункт у пакет. Якщо тертя не булосекція повністю пропускається. Правило чисто документаційне (як `ci4`), `check-*.mjs` не має, бо поведінка агента програмно не верифікується. Зачеплено: новий каталог [rules/feedback/](rules/feedback/) з `feedback.mdc` (`version: '1.0'`), додано `"feedback"` у `rules` кореневого `.n-cursor.json` після синку правило копіюється як `.cursor/rules/n-feedback.mdc` і потрапляє в `AGENTS.md`.
18
+ - `check security`: новий concern **`security.sample_secret`** — placeholder фейкових credential-значень у прикладних файлах має бути `sample-secret`, а не bare `secret`. Причина: `sample-secret` містить підрядок `sample` із вшитого списку `DefaultFalsePositives` TruffleHog і відсіюється сканером гарантовано та незалежно від версії; bare `secret` наразі ігнорується лише тому, що випадково присутнє у словнику `fp_words.txt` — крихка поведінка, що залежить від версії інструмента. [check.mjs](rules/security/fix/sample_secret/check.mjs) обходить дерево, відбирає прикладні файли (basename із суфіксом `.example`/`.sample`/`.template`/`.dist` чи infix `.example.`/`.sample.`/`.template.`, а також усе всередині каталогів `fixtures`/`fixture`/`__fixtures__`) і порядково шукає `secret` у позиції значення — одразу після `=`, `:` або `=>` з опційними лапками; імена ключів (`client_secret`, `JWT_SECRET`) не чіпаються, бо матч прив'язаний до значення. Решта файлів не сканується — там `secret` майже завжди частина реального коду. Скан текстовий (regex, не AST/Rego): прикладні файли — різнорідні конфіги (`.env`, YAML, JSON, TOML, plain `.dist`) без єдиного AST, а відбір файлів потребує обходу дерева. Зачеплено: [check.mjs](rules/security/fix/sample_secret/check.mjs) і [check.test.mjs](rules/security/fix/sample_secret/check.test.mjs) (новий concern + 9 тестів), [security.mdc](rules/security/security.mdc) (нова секція «Placeholder для секретів — `sample-secret`» та секція «Перевірка»). Bump `security.mdc` `2.0` → `2.1`.
12
19
 
13
20
  ## [1.13.57] - 2026-05-19
14
21
 
package/bin/n-cursor.js CHANGED
@@ -67,6 +67,7 @@ import { cwd, env } from 'node:process'
67
67
  import { fileURLToPath } from 'node:url'
68
68
 
69
69
  import { buildAgentsCommandBulletItems } from '../scripts/build-agents-commands.mjs'
70
+ import { formatGeneratedMarkdownLines, renderAgentsTemplate } from '../scripts/utils/generated-markdown.mjs'
70
71
  import { inlineTemplateLinks } from '../scripts/utils/inline-template-links.mjs'
71
72
  import {
72
73
  detectAutoRules,
@@ -481,50 +482,6 @@ function formatClaudeCommandFrontmatter(descriptionRaw) {
481
482
  return `---\ndescription: >-\n ${text}\n---\n\n`
482
483
  }
483
484
 
484
- /**
485
- * Розгортає в шаблоні блок Mustache {{#section}} … {{/section}} для масиву елементів
486
- * @param {string} template вихідний текст шаблону
487
- * @param {string} section ім'я секції (наприклад services)
488
- * @param {Record<string, string>[]} items елементи для повторення тіла секції
489
- * @param {string} prop ключ поля для підстановки замість {{prop}}
490
- * @returns {string} текст після розгортання усіх входжень блоку
491
- */
492
- function expandMustacheSection(template, section, items, prop) {
493
- const open = `{{#${section}}}`
494
- const close = `{{/${section}}}`
495
- const placeholder = `{{${prop}}}`
496
- let result = template
497
- let start = result.indexOf(open)
498
- let end = result.indexOf(close)
499
- while (start !== -1 && end !== -1 && end > start) {
500
- const inner = result.slice(start + open.length, end)
501
- const rendered = items.map(item => inner.split(placeholder).join(String(item[prop]))).join('')
502
- result = result.slice(0, start) + rendered + result.slice(end + close.length)
503
- start = result.indexOf(open)
504
- end = result.indexOf(close)
505
- }
506
- return result
507
- }
508
-
509
- /**
510
- * Підставляє у вміст AGENTS.template.md список шляхів до файлів правил, skills і команд з package.json
511
- * @param {string} templateText вміст AGENTS.template.md
512
- * @param {string[]} mdcBasenames імена файлів (*.mdc) з .cursor/rules
513
- * @param {{ name: string }[]} skillItems рядки для секції Skills
514
- * @param {{ name: string }[]} commandItems рядки для секції commands
515
- * @returns {string} готовий markdown для AGENTS.md
516
- */
517
- function renderAgentsTemplate(templateText, mdcBasenames, skillItems, commandItems) {
518
- let result = templateText
519
- const serviceItems = mdcBasenames.map(mdcName => ({
520
- name: `- ${RULES_DIR}/${mdcName}`
521
- }))
522
- result = expandMustacheSection(result, 'services', serviceItems, 'name')
523
- result = expandMustacheSection(result, 'skills', skillItems, 'name')
524
- result = expandMustacheSection(result, 'commands', commandItems, 'name')
525
- return result
526
- }
527
-
528
485
  /**
529
486
  * Повертає відсортовані імена *.mdc у .cursor/rules поточного проєкту
530
487
  * @returns {Promise<string[]>} базові імена файлів (лише .mdc)
@@ -712,10 +669,10 @@ async function syncClaudeMd(ignore) {
712
669
  lines.push(...buildClaudeLintParallelismSectionLines())
713
670
 
714
671
  const skillsSectionLines = await buildClaudeSkillsSectionLines()
715
- lines.push(...skillsSectionLines, '')
672
+ lines.push(...skillsSectionLines)
716
673
  const claudeMdPath = join(cwd(), 'CLAUDE.md')
717
674
  const hadFile = existsSync(claudeMdPath)
718
- await writeFile(claudeMdPath, lines.join('\n'), 'utf8')
675
+ await writeFile(claudeMdPath, formatGeneratedMarkdownLines(lines), 'utf8')
719
676
  console.log(hadFile ? `📝 Оновлено CLAUDE.md` : `📝 Створено CLAUDE.md`)
720
677
  }
721
678
 
@@ -739,8 +696,7 @@ async function syncAgentsMd(agentsTemplatePath = BUNDLED_AGENTS_TEMPLATE_PATH) {
739
696
  const body = renderAgentsTemplate(templateText, mdcFiles, skillItems, commandItems)
740
697
  const agentsPath = join(cwd(), AGENTS_FILE)
741
698
  const hadFile = existsSync(agentsPath)
742
- const out = body.endsWith('\n') ? body : `${body}\n`
743
- await writeFile(agentsPath, out, 'utf8')
699
+ await writeFile(agentsPath, body.endsWith('\n') ? body : `${body}\n`, 'utf8')
744
700
  console.log(
745
701
  hadFile
746
702
  ? `📝 Оновлено ${AGENTS_FILE} з ${AGENTS_TEMPLATE_FILE}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.58",
3
+ "version": "1.13.60",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,63 @@
1
+ ---
2
+ description: Ефемерне резюме «тертя» з пакетом @nitra/cursor наприкінці кожного скілу — агент називає, що в правилах / SKILL.md / check заважало, без запису у файли
3
+ alwaysApply: true
4
+ version: '1.0'
5
+ ---
6
+
7
+ Правило про **зворотний звʼязок до пакета `@nitra/cursor`**. Виконуючи скіл пакета (`n-lint`, `n-fix`, `n-taze`, `n-adr-normalize`, `n-llm-patch`, `n-publish-telegram`, `mdc-check`), агент проходить крізь правила `.cursor/rules/`, інструкції `SKILL.md` і програмні перевірки `npx @nitra/cursor check`. Саме тут видно, що в пакеті недопрацьовано: неоднозначна інструкція, відсутня перевірка, хибне спрацювання, порушення без автофіксу. Ця інформація цінна для розробників пакета, але без явного кроку зникає разом із сесією.
8
+
9
+ ## Коли застосовувати
10
+
11
+ Лише якщо в цій сесії агент **виконав хоча б один скіл** `@nitra/cursor`. Без виконання skills правило інертне — додавати нічого не треба.
12
+
13
+ ## Що зробити
14
+
15
+ Наприкінці скілу, **після** основного резюме роботи, додай у відповідь чату секцію `## 🔧 Покращення @nitra/cursor` — але лише якщо під час виконання було помічене **тертя** з пакетом.
16
+
17
+ **Тертя** — усе, що ускладнило роботу скілу й стосується самого пакета `@nitra/cursor`, а не коду користувацького проєкту:
18
+
19
+ - неоднозначна чи неповна інструкція в `SKILL.md` або `.mdc`;
20
+ - правило вимагає поведінку, яку можна перевірити програмно, але `check-*.mjs` для неї немає;
21
+ - хибне спрацювання перевірки (false positive);
22
+ - порушення, яке правило вимагає виправляти вручну, хоча реальний автофікс можливий;
23
+ - повторюваний патерн, який варто закодувати в правило чи скіл.
24
+
25
+ Звичайні порушення **коду користувача**, які скіл і має виправляти, — це **не** тертя.
26
+
27
+ ## Формат секції
28
+
29
+ Кожен пункт — за схемою:
30
+
31
+ - **target** — `rule` | `skill` | `check`
32
+ - **id** — який саме (`lint`, `text`, `js-lint`, …)
33
+ - **kind** — `ambiguous-doc` | `missing-check` | `false-positive` | `no-autofix` | `recurring-pattern`
34
+ - **evidence** — конкретний `файл:рядок` або вивід команди з цього запуску
35
+ - **suggestion** — запропонована зміна
36
+
37
+ Приклад:
38
+
39
+ ```text
40
+ ## 🔧 Покращення @nitra/cursor
41
+
42
+ - skill `n-lint`, `ambiguous-doc` — крок 2 не каже, як діяти при частковому autofix
43
+ evidence: `oxlint --fix` лишив 3 no-autofix-помилки
44
+ suggestion: додати в SKILL.md підпункт про частковий autofix
45
+ - rule `js-lint`, `missing-check` — немає програмної перевірки jscpd-порогу
46
+ evidence: jscpd впав, але `check js-lint` цього не ловить
47
+ ```
48
+
49
+ ## Ефемерність — обовʼязково
50
+
51
+ Резюме живе **тільки в цій відповіді чату**. **Заборонено**:
52
+
53
+ - записувати або створювати файли (чернетки, тимчасові нотатки на диску, будь-що в `docs/`);
54
+ - створювати GitHub issue чи PR;
55
+ - редагувати сам пакет `@nitra/cursor`.
56
+
57
+ Це навмисний вибір: канал має бути легким і миттєвим. Розробник, читаючи відповідь, сам вирішує, чи переносити пункт у пакет.
58
+
59
+ ## Чесність
60
+
61
+ - Лише **реальне** тертя, помічене в **цьому** запуску — без припущень і вигаданих прикладів.
62
+ - Якщо тертя не було — **повністю пропусти** секцію; не пиши порожню секцію чи «все добре».
63
+ - Не дублюй у секції переказ роботи скілу — лише конкретні, дієві пропозиції до пакета.
@@ -6,9 +6,9 @@
6
6
  *
7
7
  * `sample-secret` містить підрядок `sample`, який є у вшитому списку
8
8
  * `DefaultFalsePositives` TruffleHog — таке значення сканер відсіює
9
- * гарантовано й незалежно від версії. Bare `secret` наразі не репортиться
10
- * лише тому, що випадково присутнє у словнику `fp_words.txt`; це крихка,
11
- * версієзалежна поведінка, на яку не варто покладатися.
9
+ * гарантовано й незалежно від версії. Bare `secret` наразі не фіксується сканером
10
+ * лише тому, що випадково присутнє у словнику `fp_words.txt`; це крихка поведінка,
11
+ * що залежить від версії інструмента, на яку не варто покладатися.
12
12
  *
13
13
  * Прикладними вважаються файли, чий basename має суфікс `.example` / `.sample`
14
14
  * / `.template` / `.dist` або infix `.example.` / `.sample.` / `.template.`, а
@@ -46,7 +46,7 @@ const FIXTURE_DIR_RE = /(?:^|\/)(?:__fixtures__|fixtures?)(?:\/|$)/u
46
46
  * далі лише пробіли / завершальна пунктуація / коментар до кінця рядка. Прив'язка
47
47
  * до `$` гарантує, що `secret` — увесь токен значення (`secret-key`, `secretValue`
48
48
  * не матчаться); прив'язка до `[:=]` відсікає імена ключів (`client_secret`).
49
- * Регістронезалежно.
49
+ * Без урахування регістру символів.
50
50
  */
51
51
  const VALUE_SECRET_RE = /[:=]>?\s*(['"]?)secret\1[\s,;}\])]*(?:(?:#|\/\/).*)?$/iu
52
52
 
@@ -90,8 +90,8 @@ export async function check() {
90
90
  continue
91
91
  }
92
92
  const lines = content.split('\n')
93
- for (let i = 0; i < lines.length; i++) {
94
- const line = lines[i].endsWith('\r') ? lines[i].slice(0, -1) : lines[i]
93
+ for (const [i, line_] of lines.entries()) {
94
+ const line = line_.endsWith('\r') ? line_.slice(0, -1) : line_
95
95
  if (!VALUE_SECRET_RE.test(line)) continue
96
96
  violations++
97
97
  fail(`${rel}:${i + 1}: \`${line.trim()}\` — заміни placeholder \`secret\` на \`sample-secret\` (security.mdc)`)
@@ -40,7 +40,7 @@ Workflow обовʼязковий — забезпечує незалежний
40
40
 
41
41
  Фейкові credential-значення у **прикладних файлах** (`.env.example`, `.env.dist`, `*.example`, `*.sample`, `*.template`, вміст каталогів `fixtures/`) пиши як `sample-secret`, а не як bare `secret`.
42
42
 
43
- `sample-secret` містить підрядок `sample` із вшитого списку `DefaultFalsePositives` TruffleHog — таке значення сканер відсіює **гарантовано** й незалежно від версії. Bare `secret` наразі не репортиться лише тому, що випадково присутнє у словнику `fp_words.txt`; це крихка, версієзалежна поведінка.
43
+ `sample-secret` містить підрядок `sample` із вшитого списку `DefaultFalsePositives` TruffleHog — таке значення сканер відсіює **гарантовано** й незалежно від версії. Bare `secret` наразі не фіксується сканером лише тому, що випадково присутнє у словнику `fp_words.txt`; це крихка поведінка, що залежить від версії інструмента.
44
44
 
45
45
  - Правильно: `DB_PASSWORD=sample-secret`, `password: "sample-secret"`
46
46
  - Неправильно: `DB_PASSWORD=secret`, `password: "secret"`
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Утиліти генерації AGENTS.md / CLAUDE.md з шаблонів CLI `n-cursor`.
3
+ *
4
+ * Після розгортання Mustache-секцій і збирання рядків CLAUDE.md нормалізує markdown,
5
+ * щоб не лишати подвійні порожні рядки (MD012) між пунктами списку чи на стиках секцій.
6
+ */
7
+
8
+ /**
9
+ * Згортає три й більше послідовних `\n` до рівно двох (один порожній рядок між блоками).
10
+ * @param {string} text вихідний markdown
11
+ * @returns {string} markdown без послідовностей з трьох і більше `\n`
12
+ */
13
+ export function collapseMultipleBlankLines(text) {
14
+ return String(text).replaceAll(/\n{3,}/g, '\n\n')
15
+ }
16
+
17
+ /**
18
+ * Розгортає блок Mustache `{{#section}}…{{/section}}` для масиву елементів.
19
+ * Після `trim` тіла секції елементи зʼєднуються одним `\n` без зайвих порожніх рядків між ними.
20
+ * @param {string} template вихідний текст шаблону
21
+ * @param {string} section ім'я секції (наприклад services)
22
+ * @param {Record<string, string>[]} items елементи для повторення тіла секції
23
+ * @param {string} prop ключ поля для підстановки замість `{{prop}}`
24
+ * @returns {string} шаблон після підстановки всіх входжень блоку секції
25
+ */
26
+ export function expandMustacheSection(template, section, items, prop) {
27
+ const open = `{{#${section}}}`
28
+ const close = `{{/${section}}}`
29
+ const placeholder = `{{${prop}}}`
30
+ let result = template
31
+ let start = result.indexOf(open)
32
+ let end = result.indexOf(close)
33
+ while (start !== -1 && end !== -1 && end > start) {
34
+ const inner = result.slice(start + open.length, end).trim()
35
+ const rendered = items.map(item => inner.split(placeholder).join(String(item[prop]))).join('\n')
36
+ result = result.slice(0, start) + rendered + result.slice(end + close.length)
37
+ start = result.indexOf(open)
38
+ end = result.indexOf(close)
39
+ }
40
+ return result
41
+ }
42
+
43
+ /**
44
+ * Підставляє у AGENTS.template.md списки правил, skills і команд.
45
+ * @param {string} templateText вміст AGENTS.template.md
46
+ * @param {string[]} mdcBasenames імена файлів (*.mdc) з .cursor/rules
47
+ * @param {{ name: string }[]} skillItems рядки для секції Skills
48
+ * @param {{ name: string }[]} commandItems рядки для секції commands
49
+ * @returns {string} готовий вміст AGENTS.md без подвійних порожніх рядків у списках
50
+ */
51
+ export function renderAgentsTemplate(templateText, mdcBasenames, skillItems, commandItems) {
52
+ let result = templateText
53
+ const serviceItems = mdcBasenames.map(mdcName => ({
54
+ name: `- .cursor/rules/${mdcName}`
55
+ }))
56
+ result = expandMustacheSection(result, 'services', serviceItems, 'name')
57
+ result = expandMustacheSection(result, 'skills', skillItems, 'name')
58
+ result = expandMustacheSection(result, 'commands', commandItems, 'name')
59
+ return collapseMultipleBlankLines(result)
60
+ }
61
+
62
+ /**
63
+ * Збирає markdown з рядків, прибираючи подвійні порожні рядки на стиках секцій.
64
+ * @param {string[]} lines рядки документа
65
+ * @returns {string} зібраний markdown із завершальним `\n`
66
+ */
67
+ export function formatGeneratedMarkdownLines(lines) {
68
+ const text = lines.join('\n')
69
+ const collapsed = collapseMultipleBlankLines(text)
70
+ return collapsed.endsWith('\n') ? collapsed : `${collapsed}\n`
71
+ }