@nitra/cursor 5.0.3 → 5.2.0

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.
Files changed (206) hide show
  1. package/.claude-template/settings.template.json +22 -0
  2. package/.pi-template/extensions/n-cursor-adr/docs/index.md +15 -9
  3. package/CHANGELOG.md +18 -1
  4. package/bin/n-cursor.js +73 -16
  5. package/docs/stryker.config.md +6 -0
  6. package/docs/vitest.config.md +6 -0
  7. package/lib/docs/llm.md +29 -0
  8. package/lib/docs/omlx.md +32 -0
  9. package/lib/llm.mjs +137 -0
  10. package/lib/models.mjs +9 -1
  11. package/lib/omlx.mjs +147 -0
  12. package/package.json +1 -1
  13. package/rules/abie/docs/fix.md +6 -0
  14. package/rules/abie/js/docs/applies.md +6 -0
  15. package/rules/abie/js/docs/env_dns.md +25 -22
  16. package/rules/abie/js/docs/firebase_hosting.md +6 -0
  17. package/rules/abie/js/docs/hc_pairing.md +21 -25
  18. package/rules/abie/js/docs/ua_http_route.md +27 -19
  19. package/rules/abie/js/docs/ua_node_selector.md +24 -19
  20. package/rules/abie/lib/docs/enabled.md +13 -7
  21. package/rules/abie/lib/docs/env-dns.md +9 -3
  22. package/rules/abie/lib/docs/hc-yaml.md +6 -0
  23. package/rules/abie/lib/docs/http-route.md +6 -0
  24. package/rules/abie/lib/docs/k8s-tree.md +6 -0
  25. package/rules/abie/lib/docs/kustomization-patches.md +6 -0
  26. package/rules/abie/lib/docs/overlay-paths.md +6 -0
  27. package/rules/abie/lib/docs/yaml.md +6 -0
  28. package/rules/adr/docs/fix.md +6 -0
  29. package/rules/adr/js/docs/hooks.md +29 -244
  30. package/rules/bun/docs/fix.md +6 -0
  31. package/rules/bun/js/docs/layout.md +37 -375
  32. package/rules/capacitor/docs/fix.md +22 -108
  33. package/rules/capacitor/js/docs/platforms.md +62 -268
  34. package/rules/changelog/docs/fix.md +6 -0
  35. package/rules/changelog/lib/docs/package-manifest.md +6 -0
  36. package/rules/ci4/docs/fix.md +23 -165
  37. package/rules/ci4/js/docs/marksman_config.md +9 -1
  38. package/rules/docker/docs/fix.md +6 -0
  39. package/rules/docker/js/docs/lint.md +55 -239
  40. package/rules/docker/lib/docs/docker-hadolint.md +6 -0
  41. package/rules/docker/lib/docs/docker-mirror.md +6 -0
  42. package/rules/docker/lib/docs/docker-native-addon.md +6 -0
  43. package/rules/docker/lib/docs/docker-nginx-user.md +6 -0
  44. package/rules/docker/lint/docs/lint.md +9 -1
  45. package/rules/efes/docs/fix.md +6 -0
  46. package/rules/ga/lint/docs/lint.md +6 -0
  47. package/rules/graphql/docs/fix.md +6 -0
  48. package/rules/graphql/lib/docs/graphql-gql-scan.md +6 -0
  49. package/rules/image-avif/docs/fix.md +6 -0
  50. package/rules/image-avif/js/docs/avif_generation.md +6 -0
  51. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +9 -3
  52. package/rules/js-bun-redis/lib/docs/redis-imports.md +6 -0
  53. package/rules/js-lint/js/docs/utils_imports.md +6 -0
  54. package/rules/js-lint-ci/docs/fix.md +7 -1
  55. package/rules/js-mssql/docs/fix.md +6 -0
  56. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +6 -0
  57. package/rules/js-run/docs/fix.md +6 -0
  58. package/rules/js-run/lib/docs/bunyan-imports.md +6 -0
  59. package/rules/js-run/lib/docs/check-env-scan.md +6 -0
  60. package/rules/js-run/lib/docs/conn-file-rules.md +6 -0
  61. package/rules/js-run/lib/docs/conn-imports-scan.md +6 -0
  62. package/rules/js-run/lib/docs/promise-settimeout-scan.md +6 -0
  63. package/rules/js-run/lib/docs/temporal-scan.md +6 -0
  64. package/rules/k8s/docs/fix.md +6 -0
  65. package/rules/k8s/lint/docs/lint.md +6 -0
  66. package/rules/nginx-default-tpl/docs/fix.md +6 -0
  67. package/rules/npm-module/js/docs/header_doc_pointer.md +7 -0
  68. package/rules/npm-module/js/header_doc_pointer.mjs +2 -8
  69. package/rules/php/docs/fix.md +6 -0
  70. package/rules/php/lint/docs/lint.md +6 -0
  71. package/rules/python/docs/fix.md +6 -0
  72. package/rules/python/lint/docs/lint.md +6 -0
  73. package/rules/rego/lint/docs/lint.md +6 -0
  74. package/rules/release/docs/change.md +6 -0
  75. package/rules/release/docs/fix.md +6 -0
  76. package/rules/release/docs/release.md +6 -0
  77. package/rules/release/lib/docs/aggregate.md +6 -0
  78. package/rules/release/lib/docs/change-file.md +6 -0
  79. package/rules/release/lib/docs/fallback.md +6 -0
  80. package/rules/rust/lib/docs/has-cargo-toml.md +6 -0
  81. package/rules/security/docs/fix.md +7 -1
  82. package/rules/security/js/docs/lint.md +6 -0
  83. package/rules/style-lint/docs/fix.md +6 -0
  84. package/rules/tauri/docs/fix.md +6 -0
  85. package/rules/test/docs/fix.md +6 -0
  86. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +6 -0
  87. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +6 -0
  88. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +6 -0
  89. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +6 -0
  90. package/rules/text/docs/fix.md +6 -0
  91. package/rules/text/lint/docs/lint.md +6 -0
  92. package/rules/text/lint/docs/run-dotenv-linter.md +6 -0
  93. package/rules/text/lint/docs/run-shellcheck.md +6 -0
  94. package/rules/text/lint/docs/run-v8r.md +6 -0
  95. package/rules/vue/lib/docs/vue-forbidden-imports.md +6 -0
  96. package/scripts/coverage-classify/cache.mjs +1 -1
  97. package/scripts/coverage-classify/docs/apply.md +6 -0
  98. package/scripts/coverage-classify/docs/cache.md +6 -0
  99. package/scripts/coverage-classify/docs/prompt.md +6 -0
  100. package/scripts/coverage-classify/docs/verdict-schema.md +6 -0
  101. package/scripts/coverage-classify/index.mjs +24 -15
  102. package/scripts/coverage-classify/prompt.mjs +1 -1
  103. package/scripts/coverage-fix-extract.mjs +1 -1
  104. package/scripts/coverage-fix.mjs +2 -1
  105. package/scripts/docs/auto-skills.md +6 -0
  106. package/scripts/docs/build-agents-commands.md +7 -1
  107. package/scripts/docs/cli-entry.md +6 -0
  108. package/scripts/docs/coverage-fix-extract.md +6 -0
  109. package/scripts/docs/coverage-fix.md +6 -0
  110. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +6 -0
  111. package/scripts/docs/lint-cli.md +6 -0
  112. package/scripts/docs/post-tool-use-fix.md +6 -0
  113. package/scripts/docs/rename-yaml-extensions.md +6 -0
  114. package/scripts/docs/skills-cli.md +6 -0
  115. package/scripts/docs/sync-setup-bun-deps-action.md +6 -0
  116. package/scripts/docs/upgrade-nitra-cursor-and-install.md +6 -0
  117. package/scripts/docs/worktree-cli.md +6 -0
  118. package/scripts/lib/docs/assert-project-root.md +6 -0
  119. package/scripts/lib/docs/check-mdc-template-refs.md +6 -0
  120. package/scripts/lib/docs/check-reporter.md +6 -0
  121. package/scripts/lib/docs/diff-added-lines.md +6 -0
  122. package/scripts/lib/docs/discover-check-rules-from-cursor.md +6 -0
  123. package/scripts/lib/docs/discover-checkable-rules.md +6 -0
  124. package/scripts/lib/docs/ensure-tool.md +6 -0
  125. package/scripts/lib/docs/generated-markdown.md +6 -0
  126. package/scripts/lib/docs/gha-workflow.md +6 -0
  127. package/scripts/lib/docs/inline-template-links.md +6 -0
  128. package/scripts/lib/docs/list-rule-ids.md +6 -0
  129. package/scripts/lib/docs/load-cursor-config.md +6 -0
  130. package/scripts/lib/docs/mirror-parity.md +6 -0
  131. package/scripts/lib/docs/read-n-cursor-config-lite.md +6 -0
  132. package/scripts/lib/docs/resolve-target-files.md +6 -0
  133. package/scripts/lib/docs/root-notice.md +6 -0
  134. package/scripts/lib/docs/rule-meta-helpers.md +6 -0
  135. package/scripts/lib/docs/rule-meta.md +6 -0
  136. package/scripts/lib/docs/run-conftest-batch.md +6 -0
  137. package/scripts/lib/docs/run-lint-step.md +6 -0
  138. package/scripts/lib/docs/run-rule-cli.md +6 -0
  139. package/scripts/lib/docs/run-rule.md +6 -0
  140. package/scripts/lib/docs/run-standard-lint.md +6 -0
  141. package/scripts/lib/docs/run-standard-rule.md +6 -0
  142. package/scripts/lib/docs/skill-meta.md +6 -0
  143. package/scripts/lib/docs/template.md +6 -0
  144. package/scripts/lib/docs/timing-summary.md +6 -0
  145. package/scripts/lib/docs/workspaces.md +6 -0
  146. package/scripts/lib/docs/worktree-notice.md +6 -0
  147. package/scripts/lib/docs/worktree.md +6 -0
  148. package/scripts/lib/mirror-parity.mjs +1 -1
  149. package/scripts/lib/root-notice.mjs +1 -1
  150. package/scripts/lib/worktree-notice.mjs +5 -5
  151. package/scripts/lib/worktree.mjs +1 -1
  152. package/scripts/sync-claude-config.mjs +3 -0
  153. package/scripts/utils/docs/ast-scan-utils.md +6 -0
  154. package/scripts/utils/docs/ensure-gitignore-entries.md +6 -0
  155. package/scripts/utils/docs/find-package-json-paths.md +6 -0
  156. package/scripts/utils/docs/lock-cache-dir.md +6 -0
  157. package/scripts/utils/docs/pass.md +6 -0
  158. package/scripts/utils/docs/resolve-cargo-manifest.md +6 -0
  159. package/scripts/utils/docs/resolve-cmd.md +6 -0
  160. package/scripts/utils/docs/resolve-js-root.md +6 -0
  161. package/scripts/utils/docs/test-helpers.md +6 -0
  162. package/scripts/utils/docs/walk-cache.md +6 -0
  163. package/scripts/utils/docs/walkDir.md +6 -0
  164. package/scripts/utils/docs/worktree-fingerprint.md +6 -0
  165. package/scripts/utils/resolve-js-root.mjs +1 -1
  166. package/skills/doc-aggregate/SKILL.md +129 -0
  167. package/skills/doc-aggregate/js/docgen-ignore.mjs +9 -0
  168. package/skills/{docgen → doc-aggregate}/js/docgen-scan.mjs +22 -67
  169. package/skills/doc-aggregate/js/docs/docgen-ignore.md +21 -0
  170. package/skills/doc-files/SKILL.md +100 -0
  171. package/skills/doc-files/js/docgen-crc.mjs +164 -0
  172. package/skills/{docgen → doc-files}/js/docgen-extract-anchors.mjs +24 -15
  173. package/skills/{docgen → doc-files}/js/docgen-extract.mjs +15 -9
  174. package/skills/doc-files/js/docgen-files-batch.mjs +181 -0
  175. package/skills/doc-files/js/docgen-gen.mjs +291 -0
  176. package/skills/{docgen → doc-files}/js/docgen-prompts.mjs +43 -40
  177. package/skills/doc-files/js/docgen-scan.mjs +298 -0
  178. package/skills/doc-files/js/docs/docgen-crc.md +32 -0
  179. package/skills/doc-files/js/docs/docgen-extract-anchors.md +27 -0
  180. package/skills/doc-files/js/docs/docgen-extract.md +29 -0
  181. package/skills/doc-files/js/docs/docgen-files-batch.md +25 -0
  182. package/skills/doc-files/js/docs/docgen-gen.md +30 -0
  183. package/skills/doc-files/js/docs/docgen-prompts.md +32 -0
  184. package/skills/doc-files/js/docs/docgen-scan.md +25 -0
  185. package/skills/doc-files/meta.json +1 -0
  186. package/skills/fix/js/docs/llm-worker.md +6 -0
  187. package/skills/fix/js/docs/orchestrator.md +6 -0
  188. package/skills/fix/js/llm-worker.mjs +23 -14
  189. package/skills/fix/js/orchestrator.mjs +1 -1
  190. package/skills/start-check/js/check.mjs +5 -3
  191. package/skills/start-check/js/docs/check.md +6 -0
  192. package/skills/docgen/SKILL.md +0 -224
  193. package/skills/docgen/bench/etalon/firebase_hosting.md +0 -19
  194. package/skills/docgen/bench/etalon/k8s-tree.md +0 -24
  195. package/skills/docgen/bench/etalon/overlay-paths.md +0 -24
  196. package/skills/docgen/js/docgen-batch-omlx.mjs +0 -82
  197. package/skills/docgen/js/docgen-batch.mjs +0 -95
  198. package/skills/docgen/js/docgen-compare-pi-vs-direct.mjs +0 -95
  199. package/skills/docgen/js/docgen-gen.mjs +0 -339
  200. package/skills/docgen/js/docs/docgen-extract.md +0 -28
  201. package/skills/docgen/js/docs/docgen-gen.md +0 -41
  202. package/skills/docgen/js/docs/docgen-ignore.md +0 -24
  203. package/skills/docgen/js/docs/docgen-prompts.md +0 -24
  204. package/skills/docgen/js/docs/docgen-scan.md +0 -48
  205. /package/skills/{docgen → doc-aggregate}/meta.json +0 -0
  206. /package/skills/{docgen → doc-files}/js/docgen-ignore.mjs +0 -0
@@ -20,6 +20,28 @@
20
20
  "timeout": 300
21
21
  }
22
22
  ]
23
+ },
24
+ {
25
+ "matcher": "Edit|Write|MultiEdit",
26
+ "hooks": [
27
+ {
28
+ "type": "command",
29
+ "command": "npx --no @nitra/cursor doc-files check --hook",
30
+ "timeout": 120
31
+ }
32
+ ]
33
+ }
34
+ ],
35
+ "Stop": [
36
+ {
37
+ "matcher": "",
38
+ "hooks": [
39
+ {
40
+ "type": "command",
41
+ "command": "npx --no @nitra/cursor doc-files check --git",
42
+ "timeout": 120
43
+ }
44
+ ]
23
45
  }
24
46
  ]
25
47
  }
@@ -1,3 +1,9 @@
1
+ ---
2
+ docgen:
3
+ source: npm/.pi-template/extensions/n-cursor-adr/index.ts
4
+ crc: 3233716f
5
+ ---
6
+
1
7
  # index.ts
2
8
 
3
9
  ## Огляд
@@ -23,12 +29,12 @@
23
29
 
24
30
  ## Гарантії поведінки
25
31
 
26
- * Доступ до файлу дозволений
27
- * Операція запису та модифікації даних дозволена
28
- * При виникненні помилок система перехоплює їх
29
- * Система не генерує винятків назовні
30
- * Система не використовує кешування
31
- * Система не виконує операцій з мережею
32
- * Логіка пропуску та обмеження швидкості залишається у бах (bash)
33
- * Логіка вибору LLM-CLI залишається у бах (bash)
34
- * Перевірка рекурсії здійснюється через змінні середовища встановлені бах перед запуском LLM CLI
32
+ - Доступ до файлу дозволений
33
+ - Операція запису та модифікації даних дозволена
34
+ - При виникненні помилок система перехоплює їх
35
+ - Система не генерує винятків назовні
36
+ - Система не використовує кешування
37
+ - Система не виконує операцій з мережею
38
+ - Логіка пропуску та обмеження швидкості залишається у бах (bash)
39
+ - Логіка вибору LLM-CLI залишається у бах (bash)
40
+ - Перевірка рекурсії здійснюється через змінні середовища встановлені бах перед запуском LLM CLI
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.2.0] - 2026-06-11
4
+
5
+ ### Changed
6
+
7
+ - doc-files: local-only конвеєр (Крок 0 за ADR 260610-2228) — спільний npm/lib/llm.mjs з роутингом за префіксом omlx/ і wire-trace; видалено cloud-ескалації та pre-route sym; degraded-маркер score/issues у frontmatter, gen --retry-degraded і check --degraded; preflight omlx (down/memory-guard/auth) перед масовим прогоном; callOmlx шле Authorization Bearer (N_CURSOR_OMLX_KEY або ~/.omlx/settings.json)
8
+ - doc-files: міграція корпусу док на CRC-контракт — 126 git-актуальним докам застамповано frontmatter (дока не старіша за джерело за git-історією); 76 файлів (73 git-відсталі доки + 3 без док) лишено stale як чесний беклог перегенерації локальним конвеєром
9
+
10
+ ### Fixed
11
+
12
+ - lint: санація шарів, що ховалися за першим падінням oxlint — повний bun run lint доведено до lint-js/style зелених: JSDoc/regex/sonarjs фікси у ~25 файлах, slow-regex обмежено bounded-квантифікаторами, eslint ignore для згенерованих docs/*.md, jscpd ignore (.git, COVERAGE.md, docs, відомі легасі-клони), knip ignoreBinaries (pi/claude/fix-t0), stylelint/cspell ignore згенерованих артефактів; docgen-ignore у doc-aggregate став re-export з doc-files
13
+
14
+ ## [5.1.0] - 2026-06-10
15
+
16
+ ### Changed
17
+
18
+ - local-inference: маршрут моделей за префіксом `omlx/` напряму в omlx HTTP (npm/lib/omlx.mjs) минаючи pi; coverage-classify і fix/llm-worker переведено на спільний callOmlx, docgen де-дубльовано; pi лишається шаром для хмари й агентних задач (ADR 260610-1349)
19
+
3
20
  ## [5.0.3] - 2026-06-10
4
21
 
5
22
  ### Changed
@@ -69,7 +86,7 @@
69
86
 
70
87
  ### Added
71
88
 
72
- - lib/models.mjs: global model tier classification (LOCAL_MIN/AVG/MAX, CLOUD_MIN/AVG/MAX) via N_*_MODEL env vars; fix llm-worker uses CLOUD_MIN/AVG by default
89
+ - lib/models.mjs: global model tier classification (LOCAL*MIN/AVG/MAX, CLOUD_MIN/AVG/MAX) via N*\*\_MODEL env vars; fix llm-worker uses CLOUD_MIN/AVG by default
73
90
 
74
91
  ### Changed
75
92
 
package/bin/n-cursor.js CHANGED
@@ -25,8 +25,11 @@
25
25
  * `npx \@nitra/cursor lint-docker` — канонічний lint-docker (docker.mdc): `hadolint` по `Dockerfile`/`*.Dockerfile`
26
26
  * `npx \@nitra/cursor lint-text` — канонічний lint-text (text.mdc): `cspell` → `shellcheck` (з auto-fix) →
27
27
  * `markdownlint-cli2 --fix` → `v8r` (json/json5/yaml/yml/toml)
28
- * `npx \@nitra/cursor docgen scan` детермінований JSON-лістинг кодових файлів для скілу docgen (відносні `sourcePath`; ignore-glob snippet у `npm/skills/docgen/js/docgen-ignore.mjs`; тека `docs/` поряд із джерелом)
29
- * `npx \@nitra/cursor docgen modules` — детермінований JSON-лістинг логічних модулів (межі за `package.json`) для Tier 2 скілу docgen
28
+ * `npx \@nitra/cursor doc-files scan` — JSON-лістинг кодових файлів зі станом застарілості (`stale`: `missing`|`crc-mismatch`) для обовʼязкового скілу doc-files (ignore-glob у `npm/skills/doc-files/js/docgen-ignore.mjs`; тека `docs/` поряд із джерелом)
29
+ * `npx \@nitra/cursor doc-files check` — детектор застарілості для hook'ів (`--hook` PostToolUse, `--git` Stop-гейт із порогом `--max`); exit 2 стале знайдено
30
+ * `npx \@nitra/cursor doc-files gen` — JS-оркестрована генерація файлових док (роутинг local/cloud) зі штампом CRC у frontmatter (`--limit`/`--from`/`--overwrite`)
31
+ * `npx \@nitra/cursor doc-files stamp` — детерміновано (пере)штампувати CRC у наявних доках без LLM
32
+ * `npx \@nitra/cursor doc-aggregate modules` — JSON-лістинг логічних модулів (межі за `package.json`) для Tier 2 скілу doc-aggregate
30
33
  * `npx \@nitra/cursor skill list` — скіли пакета без синку в проєкт
31
34
  * `npx \@nitra/cursor skill taze` — промпт на stdout
32
35
  * `npx \@nitra/cursor skill cursor taze ["task"]` — Cursor CLI (`cursor-agent -p`)
@@ -663,6 +666,22 @@ function buildClaudeWorktreeEnforcementSectionLines() {
663
666
  ]
664
667
  }
665
668
 
669
+ /**
670
+ * Рендерить секцію для CLAUDE.md: doc-files — обовʼязковий крок задачі (як lint).
671
+ * Після зміни кодового файлу його дока має бути перегенерована; застарілість
672
+ * детермінується за CRC і контролюється Stop-hook'ом.
673
+ * @returns {string[]} рядки для вставки (з порожнім рядком на початку)
674
+ */
675
+ function buildClaudeDocFilesSectionLines() {
676
+ return [
677
+ '',
678
+ '## Файлова документація (`doc-files` — обовʼязковий крок, як lint)',
679
+ '',
680
+ 'Після зміни чи додавання кодового файлу його файлова дока (`<dir>/docs/<stem>.md`) має бути **актуальною** — це **обовʼязковий крок кожної задачі**, нарівні з lint. Застарілість детермінується за **CRC** джерела у frontmatter доки. PostToolUse hook (`doc-files check --hook`) **сигналить** про дрейф після правки; Stop-hook (`doc-files check --git`) **блокує завершення** задачі за наявності застарілих док (виняток — масовий прогін понад поріг `N_CURSOR_DOC_FILES_GATE_MAX`, дефолт 50). Регенерація — `/doc-files` (JS-оркестрована, не диспатч субагентів). Агрегуюча дока (module-summary, доменні) — окремий скіл `/doc-aggregate`, за запитом.',
681
+ ''
682
+ ]
683
+ }
684
+
666
685
  /**
667
686
  * Рендерить секцію Skills для CLAUDE.md з урахуванням наявних slash-команд.
668
687
  * @returns {Promise<string[]>} готові рядки секції (або порожній масив)
@@ -722,7 +741,11 @@ async function syncClaudeMd(ignore) {
722
741
  lines.push(`@${RULES_DIR}/${mdcFile}`)
723
742
  }
724
743
 
725
- lines.push(...buildClaudeLintParallelismSectionLines(), ...buildClaudeWorktreeEnforcementSectionLines())
744
+ lines.push(
745
+ ...buildClaudeLintParallelismSectionLines(),
746
+ ...buildClaudeWorktreeEnforcementSectionLines(),
747
+ ...buildClaudeDocFilesSectionLines()
748
+ )
726
749
 
727
750
  const skillsSectionLines = await buildClaudeSkillsSectionLines()
728
751
  lines.push(...skillsSectionLines)
@@ -1550,7 +1573,7 @@ async function runSync() {
1550
1573
  /**
1551
1574
  * Команди, що мутують проєкт у CWD і вимагають кореня репо. `undefined`/`''` —
1552
1575
  * дефолтний sync; `check` — deprecated-alias `fix`. Решта (read-only `trace`,
1553
- * `--root`-команди `docgen`/`rename-yaml-extensions`, `worktree`,
1576
+ * `--root`-команди `doc-files`/`doc-aggregate`/`rename-yaml-extensions`, `worktree`,
1554
1577
  * sub-лінтери) гард не зачіпає.
1555
1578
  */
1556
1579
  const ROOT_GUARDED_COMMANDS = new Set([undefined, '', 'fix', 'check', 'lint', 'coverage', 'change', 'release'])
@@ -1596,7 +1619,7 @@ try {
1596
1619
  // .n-cursor.json + bun install, а fix/lint/coverage/change/release переписують файли в CWD —
1597
1620
  // усе це ключиться на cwd(). Запуск із піддиректорії git-репо (типово прямий
1598
1621
  // `bun npm/bin/n-cursor.js` не з кореня) зачепив би не той каталог → STOP. Read-only та
1599
- // `--root`-команди (trace, graph, docgen, rename-yaml-extensions) не зачіпаємо.
1622
+ // `--root`-команди (trace, graph, doc-files, doc-aggregate, rename-yaml-extensions) не зачіпаємо.
1600
1623
  if (ROOT_GUARDED_COMMANDS.has(command)) {
1601
1624
  assertCwdIsProjectRoot(cwd(), describeRootGuardedAction(command))
1602
1625
  }
@@ -1765,17 +1788,51 @@ try {
1765
1788
 
1766
1789
  break
1767
1790
  }
1768
- case 'docgen': {
1769
- // n-cursor docgen scan|modulesдетермінований лістинг для скілу docgen.
1770
- // scan кодові файли; modules логічні модулі (межі за package.json).
1771
- // Друкує JSON; генерацію доки робить скіл, диспатчачи субагентів.
1772
- const { runDocgenScanCli, runDocgenModulesCli } = await import('../skills/docgen/js/docgen-scan.mjs')
1773
- if (args[0] === 'scan') {
1774
- process.exitCode = await runDocgenScanCli(args.slice(1))
1775
- } else if (args[0] === 'modules') {
1776
- process.exitCode = await runDocgenModulesCli(args.slice(1))
1791
+ case 'doc-files': {
1792
+ // n-cursor doc-files scan|check|gen|stampобовʼязковий крок задачі (як lint):
1793
+ // файлова дока поряд із джерелом, застарілість за CRC у frontmatter.
1794
+ // scan — JSON-лістинг кодових файлів зі станом stale (missing|crc-mismatch);
1795
+ // check детектор застарілості для hook'ів (--hook PostToolUse, --git Stop-гейт);
1796
+ // gen — JS-оркестрована генерація доки (роутинг local/cloud) + CRC-штамп;
1797
+ // stamp детерміновано (пере)штампувати CRC у наявних доках без LLM.
1798
+ switch (args[0]) {
1799
+ case 'scan': {
1800
+ const { runDocFilesScanCli } = await import('../skills/doc-files/js/docgen-scan.mjs')
1801
+ process.exitCode = runDocFilesScanCli(args.slice(1))
1802
+ break
1803
+ }
1804
+ case 'check': {
1805
+ const { runDocFilesCheckCli } = await import('../skills/doc-files/js/docgen-scan.mjs')
1806
+ process.exitCode = await runDocFilesCheckCli(args.slice(1))
1807
+ break
1808
+ }
1809
+ case 'gen': {
1810
+ const { runDocFilesGenCli } = await import('../skills/doc-files/js/docgen-files-batch.mjs')
1811
+ process.exitCode = await runDocFilesGenCli(args.slice(1))
1812
+ break
1813
+ }
1814
+ case 'stamp': {
1815
+ const { runDocFilesStampCli } = await import('../skills/doc-files/js/docgen-files-batch.mjs')
1816
+ process.exitCode = runDocFilesStampCli(args.slice(1))
1817
+ break
1818
+ }
1819
+ default: {
1820
+ console.error('Usage: npx @nitra/cursor doc-files <scan|check|gen|stamp> [--root <dir>]')
1821
+ process.exitCode = 1
1822
+ }
1823
+ }
1824
+
1825
+ break
1826
+ }
1827
+ case 'doc-aggregate': {
1828
+ // n-cursor doc-aggregate modules — детермінований лістинг логічних модулів
1829
+ // (межі за package.json) для Tier 2 module-summary скілу doc-aggregate.
1830
+ // Друкує JSON; module-summary і доменні доки пише скіл, диспатчачи субагентів.
1831
+ const { runDocAggregateModulesCli } = await import('../skills/doc-aggregate/js/docgen-scan.mjs')
1832
+ if (args[0] === 'modules') {
1833
+ process.exitCode = await runDocAggregateModulesCli(args.slice(1))
1777
1834
  } else {
1778
- console.error('Usage: npx @nitra/cursor docgen <scan|modules> [--root <dir>]')
1835
+ console.error('Usage: npx @nitra/cursor doc-aggregate <modules> [--root <dir>]')
1779
1836
  process.exitCode = 1
1780
1837
  }
1781
1838
 
@@ -1790,7 +1847,7 @@ try {
1790
1847
  default: {
1791
1848
  console.error(`❌ Невідома команда: ${command}`)
1792
1849
  console.error(
1793
- ` Очікується: (без аргументів) синхронізація правил, fix, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, coverage-fix, taze, start-check, fix-t0, change, release, skill, worktree, lint-ci, trace, docgen`
1850
+ ` Очікується: (без аргументів) синхронізація правил, fix, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, coverage-fix, taze, start-check, fix-t0, change, release, skill, worktree, lint-ci, trace, doc-files, doc-aggregate`
1794
1851
  )
1795
1852
  process.exitCode = 1
1796
1853
  }
@@ -1,3 +1,9 @@
1
+ ---
2
+ docgen:
3
+ source: npm/stryker.config.mjs
4
+ crc: b8c7377b
5
+ ---
6
+
1
7
  # stryker.config.mjs
2
8
 
3
9
  ## Огляд
@@ -1,3 +1,9 @@
1
+ ---
2
+ docgen:
3
+ source: npm/vitest.config.js
4
+ crc: d4917e1f
5
+ ---
6
+
1
7
  # vitest.config.js
2
8
 
3
9
  ## Огляд
@@ -0,0 +1,29 @@
1
+ ---
2
+ docgen:
3
+ source: npm/lib/llm.mjs
4
+ crc: 9ef9f20f
5
+ ---
6
+
7
+ # llm
8
+
9
+ ## Огляд
10
+
11
+ Єдина точка LLM-викликів для JS-оркестраторів монорепо: ховає вибір транспорту за конвенцією префікса model-id і дає preflight-перевірку локального сервера перед масовими прогонами.
12
+
13
+ ## Поведінка
14
+
15
+ 1. Виклик із model-id з префіксом `omlx/` іде прямим HTTP до локального omlx-сервера зі збереженням ролей повідомлень; будь-який інший model-id (зокрема порожній) — через `pi` CLI, де повідомлення конкатенуються в один текстовий промпт, а інструменти вимкнено.
16
+ 2. Якщо виставлено `N_CURSOR_LLM_TRACE` зі шляхом до файлу, кожен виклик дописує один JSONL-рядок: бекенд, модель, тривалість, розміри промпта/відповіді, успіх або текст помилки. Невдалий запис трейсу не впливає на сам виклик.
17
+ 3. Preflight локального сервера робить мінімальний запит (одна позиція виходу) і класифікує стан: сервер лежить (`down`), модель не влазить у динамічну стелю пам'яті зайнятої машини (`memory-guard`), сервер вимагає API-ключ (`auth`), інша помилка (`error`). Порожня відповідь живого сервера вважається здоровим станом.
18
+
19
+ ## Публічний API
20
+
21
+ - `callLlm` — універсальний виклик за messages-масивом із маршрутизацією за префіксом model-id.
22
+ - `pickBackend` — назва бекенда (`omlx` чи `pi`) для заданого model-id.
23
+ - `omlxHealthCheck` — preflight-стан локального сервера: `{ ok, reason, detail }`.
24
+
25
+ ## Гарантії поведінки
26
+
27
+ - Маршрутизація залежить лише від рядка моделі — жодних env-перемикачів бекенда.
28
+ - Помилка транспорту прокидається винятком; трейс при цьому фіксує невдачу.
29
+ - `memory-guard` означає «машина зайнята, спробуй пізніше», а не дефект моделі чи конвеєра.
@@ -0,0 +1,32 @@
1
+ ---
2
+ docgen:
3
+ source: npm/lib/omlx.mjs
4
+ crc: e29efc97
5
+ ---
6
+
7
+ # omlx
8
+
9
+ ## Огляд
10
+
11
+ Спільний транспорт до локального omlx-сервера (OpenAI-сумісний MLX-inference на `http://127.0.0.1:8000/v1/chat/completions`). Text-only: сервер не підтримує інструменти, тож модуль шле лише повідомлення й читає текст відповіді.
12
+
13
+ ## Поведінка
14
+
15
+ 1. Виклик будує запит з повідомленнями, моделлю, температурою і лімітом виходу; model-id з префіксом `omlx/` нормалізується до чистого імені, порожній — замінюється дефолтною моделлю (`mlx-community--gemma-4-e2b-it-4bit`, override через `N_CURSOR_OMLX_MODEL`).
16
+ 2. Endpoint береться з `N_CURSOR_OMLX_URL` або дефолтний.
17
+ 3. Якщо на сервері ввімкнено API-ключ, він резолвиться в порядку: явний параметр виклику → `N_CURSOR_OMLX_KEY` → поле `auth.api_key` з локального `~/.omlx/settings.json` (читання fail-safe) — і шлеться як `Authorization: Bearer`. Без знайденого ключа заголовок не додається.
18
+ 4. Тимчасові обриви з'єднання (передача перервана, порожня відповідь, збій прийому) ретраяться до трьох спроб; решта помилок — виняток одразу.
19
+ 5. Помилка API у тілі відповіді, невалідний JSON чи порожній контент — виняток із діагностичним префіксом `omlx …`.
20
+
21
+ ## Публічний API
22
+
23
+ - `callOmlx` — синхронний chat-виклик, повертає непорожній текст відповіді.
24
+ - `isOmlxModel` / `omlxModelId` — перевірка і зняття префікса `omlx/`.
25
+ - `resolveOmlxApiKey` — резолвінг API-ключа за описаним порядком джерел.
26
+ - `DEFAULT_OMLX_URL` / `DEFAULT_OMLX_MODEL` — дефолти endpoint і моделі.
27
+
28
+ ## Гарантії поведінки
29
+
30
+ - Жодних `tools`/`tool_calls` у запиті — сумісність із text-only сервером.
31
+ - Ролі повідомлень (`system`/`user`) передаються без злиття.
32
+ - Виняток ніколи не ковтається: викликач завжди дізнається про збій.
package/lib/llm.mjs ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Єдина точка LLM-викликів для JS-оркестраторів (див. ADR 260610-2228).
3
+ *
4
+ * Маршрутизація — виключно за префіксом model-id (конвенція `npm/lib/models.mjs`):
5
+ * `omlx/<model>` → прямий HTTP до локального omlx-сервера (`callOmlx`)
6
+ * будь-що інше → `pi` CLI (хмарні провайдери або pi-дефолт)
7
+ *
8
+ * Жодних env-перемикачів бекенда: рядок моделі сам визначає транспорт.
9
+ *
10
+ * Wire-trace (ADR 260610-1516/1524): якщо виставлено `N_CURSOR_LLM_TRACE=<file>`,
11
+ * кожен виклик append-ить один JSONL-рядок з бекендом, моделлю, тривалістю і
12
+ * розмірами prompt/output. Трейс fail-safe: помилка запису не ламає виклик.
13
+ */
14
+ import { spawnSync } from 'node:child_process'
15
+ import { appendFileSync } from 'node:fs'
16
+ import { env } from 'node:process'
17
+
18
+ import { callOmlx, isOmlxModel } from './omlx.mjs'
19
+
20
+ /** Дефолтний timeout одного виклику (узгоджено з LOCAL_TIMEOUT доки-конвеєра). */
21
+ const DEFAULT_TIMEOUT_MS = 120_000
22
+
23
+ /**
24
+ * Бекенд для model-id: `omlx` — прямий HTTP, `pi` — CLI.
25
+ * @param {string} model model-id (можливо порожній — pi-дефолт)
26
+ * @returns {'omlx'|'pi'} назва бекенда
27
+ */
28
+ export function pickBackend(model) {
29
+ return isOmlxModel(model) ? 'omlx' : 'pi'
30
+ }
31
+
32
+ /**
33
+ * Fail-safe append JSONL-рядка трейсу у файл з `N_CURSOR_LLM_TRACE`.
34
+ * @param {object} entry один запис трейсу
35
+ */
36
+ function trace(entry) {
37
+ const file = env.N_CURSOR_LLM_TRACE
38
+ if (!file) return
39
+ try {
40
+ appendFileSync(file, JSON.stringify(entry) + '\n')
41
+ } catch {
42
+ // трейс не має ламати основний виклик
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Виклик через `pi` CLI: messages конкатенуються у plain prompt
48
+ * (pi не приймає messages-масив), tools вимкнено.
49
+ * @param {Array<{role:string, content:string}>} messages OpenAI-style messages
50
+ * @param {string} model model-id для `--model` (порожній — pi-дефолт)
51
+ * @param {number} timeoutMs ліміт очікування процесу
52
+ * @returns {string} stdout відповіді
53
+ */
54
+ function callPi(messages, model, timeoutMs) {
55
+ const prompt = messages.map(m => m.content).join('\n\n')
56
+ const modelArgs = model ? ['--model', model] : []
57
+ const r = spawnSync('pi', ['-p', prompt, ...modelArgs, '--no-session', '--mode', 'text', '--no-tools'], {
58
+ encoding: 'utf8',
59
+ timeout: timeoutMs
60
+ })
61
+ if (r.error) throw new Error(`pi error: ${r.error.message}`)
62
+ if (r.status !== 0) throw new Error(`pi exit ${r.status}: ${r.stderr?.slice(0, 300) ?? ''}`)
63
+ return r.stdout?.trim() ?? ''
64
+ }
65
+
66
+ /**
67
+ * Універсальний LLM-виклик з маршрутизацією за префіксом model-id.
68
+ * @param {Array<{role:string, content:string}>} messages OpenAI-style messages (system зберігається на omlx)
69
+ * @param {string} model model-id; `omlx/<m>` → прямий HTTP, інакше → pi CLI
70
+ * @param {{ timeoutMs?: number, temperature?: number, maxTokens?: number, url?: string }} [opts] timeout, температура, ліміт виходу, override URL
71
+ * @returns {string} текст відповіді (непорожній на omlx; pi може повернути '')
72
+ */
73
+ export function callLlm(messages, model, opts = {}) {
74
+ const { timeoutMs = DEFAULT_TIMEOUT_MS, temperature = 0.2, maxTokens, url } = opts
75
+ const backend = pickBackend(model)
76
+ const t0 = Date.now()
77
+ const promptChars = messages.reduce((n, m) => n + (m.content?.length ?? 0), 0)
78
+ try {
79
+ const out =
80
+ backend === 'omlx'
81
+ ? callOmlx(messages, model, { url, timeoutMs, temperature, ...(maxTokens ? { maxTokens } : {}) })
82
+ : callPi(messages, model, timeoutMs)
83
+ trace({
84
+ ts: new Date().toISOString(),
85
+ backend,
86
+ model,
87
+ ms: Date.now() - t0,
88
+ promptChars,
89
+ outChars: out.length,
90
+ ok: true
91
+ })
92
+ return out
93
+ } catch (error) {
94
+ trace({
95
+ ts: new Date().toISOString(),
96
+ backend,
97
+ model,
98
+ ms: Date.now() - t0,
99
+ promptChars,
100
+ ok: false,
101
+ error: String(error.message).slice(0, 200)
102
+ })
103
+ throw error
104
+ }
105
+ }
106
+
107
+ /** Фрагмент повідомлення omlx про memory-guard (динамічна стеля пам'яті). */
108
+ const MEMORY_GUARD_MARKER = 'memory ceiling'
109
+ /** Тип помилки omlx про відсутній/хибний API-ключ. */
110
+ const AUTH_ERROR_MARKER = 'authentication_error'
111
+
112
+ /**
113
+ * Preflight-перевірка omlx перед масовим прогоном: мінімальний chat-виклик
114
+ * (`max_tokens: 1`). Розрізняє стани, які вимагають різних дій:
115
+ * - `down` — сервер не відповідає (не запущений / не той порт);
116
+ * - `memory-guard` — модель не влазить у динамічну стелю пам'яті зайнятої
117
+ * машини → «відклади прогін», а не «модель погана»;
118
+ * - `auth` — сервер вимагає API-ключ → вистав `N_CURSOR_OMLX_KEY`;
119
+ * - `error` — інша помилка API.
120
+ * Порожній контент відповіді — це ok: сервер живий і модель завантажена.
121
+ * @param {{ url?: string, model?: string, timeoutMs?: number }} [opts] override URL/моделі/timeout перевірки
122
+ * @returns {{ ok: boolean, reason: 'down'|'memory-guard'|'auth'|'error'|null, detail: string }} стан сервера і класифікована причина збою
123
+ */
124
+ export function omlxHealthCheck(opts = {}) {
125
+ const { url, model = '', timeoutMs = DEFAULT_TIMEOUT_MS } = opts
126
+ try {
127
+ callOmlx([{ role: 'user', content: 'ok' }], model, { url, timeoutMs, maxTokens: 1, temperature: 0 })
128
+ return { ok: true, reason: null, detail: '' }
129
+ } catch (error) {
130
+ const detail = String(error.message)
131
+ if (detail.includes(MEMORY_GUARD_MARKER)) return { ok: false, reason: 'memory-guard', detail }
132
+ if (detail.includes(AUTH_ERROR_MARKER)) return { ok: false, reason: 'auth', detail }
133
+ if (detail.startsWith('omlx empty content')) return { ok: true, reason: null, detail }
134
+ if (detail.startsWith('omlx curl')) return { ok: false, reason: 'down', detail }
135
+ return { ok: false, reason: 'error', detail }
136
+ }
137
+ }
package/lib/models.mjs CHANGED
@@ -5,13 +5,21 @@
5
5
  * Налаштовується один раз у середовищі; кожен скіл посилається на потрібний тир.
6
6
  *
7
7
  * Приклад ~/.bashrc або .env:
8
- * N_LOCAL_MIN_MODEL=ollama/gemma3:4b
8
+ * N_LOCAL_MIN_MODEL=omlx/mlx-community--gemma-4-e2b-it-4bit
9
9
  * N_CLOUD_MIN_MODEL=openai/gpt-5.4-mini
10
10
  * N_CLOUD_AVG_MODEL=openai/gpt-5.4
11
11
  * N_CLOUD_MAX_MODEL=openai/gpt-5.5
12
12
  *
13
13
  * Значення '' означає "pi дефолтний провайдер" (залежить від ~/.pi конфігу).
14
14
  *
15
+ * ## Бекенд за префіксом model-id
16
+ *
17
+ * model-id з префіксом `omlx/...` маршрутизується прямим HTTP до локального
18
+ * omlx-сервера (`npm/lib/omlx.mjs`), минаючи pi; решта (`openai/...`,
19
+ * `ollama/...`, '') — через pi CLI. Тому локальні тири варто задавати у форматі
20
+ * `omlx/<model>`, аби local-inference йшов напряму, а pi лишався шаром для хмари
21
+ * (див. ADR 260610-1349).
22
+ *
15
23
  * ## Каскад local → cloud (контракт)
16
24
  *
17
25
  * Використовуйте resolveModel(tier) замість прямих констант — система прозоро
package/lib/omlx.mjs ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Спільний транспорт до локального omlx-сервера (OpenAI-сумісний MLX,
3
+ * `http://localhost:8000/v1/chat/completions`). Text-only: жодних `tools`/
4
+ * `tool_calls` — сервер їх не підтримує (див. ADR
5
+ * `260610-1349-агентна-пастка-js-owned-loop-через-omlx-замість-pi-tool-loop`).
6
+ *
7
+ * Маршрутизація між omlx і pi — за конвенцією префікса в model-id:
8
+ * `omlx/<model>` → прямий HTTP до omlx (локальний inference, без pi)
9
+ * будь-що інше → pi CLI (хмарні провайдери або pi-дефолт)
10
+ *
11
+ * Так `resolveModel(tier)` лишається незмінним: достатньо виставити локальний
12
+ * тир у форматі `N_LOCAL_MIN_MODEL=omlx/mlx-community--gemma-4-e2b-it-4bit`, і
13
+ * виклик сам піде напряму в omlx замість pi.
14
+ *
15
+ * Auth: якщо в omlx увімкнено API-ключ, він резолвиться через
16
+ * `resolveOmlxApiKey` (opts → `N_CURSOR_OMLX_KEY` → `~/.omlx/settings.json`)
17
+ * і шлеться як `Authorization: Bearer …`.
18
+ */
19
+ import { spawnSync } from 'node:child_process'
20
+ import { readFileSync } from 'node:fs'
21
+ import { homedir } from 'node:os'
22
+ import { join } from 'node:path'
23
+ import { env } from 'node:process'
24
+
25
+ /** Дефолтний endpoint omlx (override — `N_CURSOR_OMLX_URL`). */
26
+ export const DEFAULT_OMLX_URL = 'http://127.0.0.1:8000/v1/chat/completions'
27
+
28
+ /**
29
+ * API-ключ для omlx-сервера, коли в ньому ввімкнено auth
30
+ * (`~/.omlx/settings.json` → `auth.skip_api_key_verification: false`).
31
+ * Порядок: явний `apiKey` → env `N_CURSOR_OMLX_KEY` → `auth.api_key` із
32
+ * локального `~/.omlx/settings.json` (zero-config для власної машини; читання
33
+ * fail-safe) → `null` (заголовок не шлеться).
34
+ * @param {string} [apiKey] явний ключ із opts виклику
35
+ * @returns {string|null} ключ для `Authorization: Bearer …` або null
36
+ */
37
+ export function resolveOmlxApiKey(apiKey) {
38
+ if (apiKey) return apiKey
39
+ if (env.N_CURSOR_OMLX_KEY) return env.N_CURSOR_OMLX_KEY
40
+ try {
41
+ const settings = JSON.parse(readFileSync(join(homedir(), '.omlx', 'settings.json'), 'utf8'))
42
+ return settings?.auth?.api_key || null
43
+ } catch {
44
+ return null
45
+ }
46
+ }
47
+
48
+ /** Дефолтна модель, якщо в id лишився голий `omlx/` (override — `N_CURSOR_OMLX_MODEL`). */
49
+ export const DEFAULT_OMLX_MODEL = 'mlx-community--gemma-4-e2b-it-4bit'
50
+
51
+ const OMLX_PREFIX = 'omlx/'
52
+
53
+ /**
54
+ * Чи цей model-id адресує локальний omlx-бекенд (префікс `omlx/`).
55
+ * @param {unknown} model перевірюваний model-id
56
+ * @returns {boolean} true, якщо рядок починається з `omlx/`
57
+ */
58
+ export function isOmlxModel(model) {
59
+ return typeof model === 'string' && model.startsWith(OMLX_PREFIX)
60
+ }
61
+
62
+ /**
63
+ * Прибирає `omlx/`-префікс → чистий model-id для omlx API.
64
+ * Не-omlx-рядки повертає без змін.
65
+ * @param {string} model model-id (можливо з префіксом)
66
+ * @returns {string} model-id без `omlx/`
67
+ */
68
+ export function omlxModelId(model) {
69
+ return isOmlxModel(model) ? model.slice(OMLX_PREFIX.length) : model
70
+ }
71
+
72
+ /**
73
+ * Прямий HTTP-виклик до omlx через `curl` (spawnSync). Повертає текст
74
+ * `choices[0].message.content`. Ретраїть лише transient curl-помилки
75
+ * (18 = transfer closed, 52 = empty reply, 56 = recv failure).
76
+ * @param {Array<{role:string, content:string}>} messages OpenAI-messages (system+user збережено)
77
+ * @param {string} model model-id (з/без `omlx/`-префікса); порожній → дефолт
78
+ * @param {{ url?: string, timeoutMs?: number, temperature?: number, maxTokens?: number, fallbackModel?: string, apiKey?: string }} [opts] URL, timeout, температура, ліміт виходу, fallback-модель, API-ключ
79
+ * @returns {string} непорожній контент відповіді
80
+ * @throws на curl-помилці, не-200 exit, поганому JSON чи порожньому контенті
81
+ */
82
+ export function callOmlx(messages, model, opts = {}) {
83
+ const {
84
+ url = env.N_CURSOR_OMLX_URL ?? DEFAULT_OMLX_URL,
85
+ timeoutMs = 60_000,
86
+ temperature = 0.2,
87
+ maxTokens = 4096,
88
+ fallbackModel = env.N_CURSOR_OMLX_MODEL ?? DEFAULT_OMLX_MODEL,
89
+ apiKey
90
+ } = opts
91
+
92
+ const m = omlxModelId(model) || fallbackModel
93
+ const body = JSON.stringify({ model: m, messages, max_tokens: maxTokens, temperature })
94
+ // Ключ локального сервера в argv допустимий: localhost-секрет власної машини,
95
+ // короткоживучий процес; stdin уже зайнятий body (`--data-binary @-`).
96
+ const key = resolveOmlxApiKey(apiKey)
97
+ const authArgs = key ? ['-H', `Authorization: Bearer ${key}`] : []
98
+
99
+ const TRANSIENT_CURL_CODES = new Set([18, 52, 56])
100
+ let lastErr
101
+ for (let attempt = 1; attempt <= 3; attempt++) {
102
+ const r = spawnSync(
103
+ 'curl',
104
+ [
105
+ '-sS',
106
+ '-X',
107
+ 'POST',
108
+ url,
109
+ '-H',
110
+ 'Content-Type: application/json',
111
+ '-H',
112
+ 'Connection: close',
113
+ ...authArgs,
114
+ '--max-time',
115
+ String(Math.ceil(timeoutMs / 1000)),
116
+ '--data-binary',
117
+ '@-'
118
+ ],
119
+ { input: body, encoding: 'utf8', timeout: timeoutMs + 5000 }
120
+ )
121
+ if (r.error) {
122
+ lastErr = new Error(`omlx curl error: ${r.error.message}`)
123
+ break
124
+ }
125
+ if (r.status !== 0) {
126
+ if (TRANSIENT_CURL_CODES.has(r.status) && attempt < 3) {
127
+ lastErr = new Error(`omlx curl exit ${r.status} (transient, retry ${attempt})`)
128
+ continue
129
+ }
130
+ throw new Error(`omlx curl exit ${r.status}: ${r.stderr?.slice(0, 300) ?? ''}`)
131
+ }
132
+ let j
133
+ try {
134
+ j = JSON.parse(r.stdout)
135
+ } catch {
136
+ throw new Error(`omlx bad json: ${r.stdout?.slice(0, 200) ?? ''}`)
137
+ }
138
+ if (j.error) throw new Error(`omlx api: ${JSON.stringify(j.error).slice(0, 300)}`)
139
+ const content = j.choices?.[0]?.message?.content?.trim() ?? ''
140
+ if (!content) {
141
+ const finish = j.choices?.[0]?.finish_reason
142
+ throw new Error(`omlx empty content (finish=${finish})`)
143
+ }
144
+ return content
145
+ }
146
+ throw lastErr ?? new Error('omlx unknown failure')
147
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "5.0.3",
3
+ "version": "5.2.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,3 +1,9 @@
1
+ ---
2
+ docgen:
3
+ source: npm/rules/abie/fix.mjs
4
+ crc: 12fc1644
5
+ ---
6
+
1
7
  # fix.mjs
2
8
 
3
9
  ## Огляд