@nitra/cursor 12.15.1 → 12.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/bin/n-cursor.js +1 -1
- package/lib/docs/index.md +9 -6
- package/lib/docs/pi-agent-fix.md +28 -0
- package/lib/docs/pi-agent-skill.md +36 -0
- package/lib/docs/pi-model-tiers.md +46 -0
- package/lib/docs/pi-one-shot.md +34 -0
- package/lib/docs/pi-telemetry-store.md +33 -0
- package/lib/docs/pi-trace.md +27 -0
- package/lib/docs/pi-write-guard.md +32 -0
- package/lib/pi-agent-fix.mjs +253 -0
- package/lib/pi-agent-skill.mjs +181 -0
- package/lib/pi-model-tiers.mjs +109 -0
- package/lib/pi-one-shot.mjs +129 -0
- package/lib/pi-telemetry-store.mjs +0 -0
- package/lib/pi-trace.mjs +40 -0
- package/lib/pi-write-guard.mjs +147 -0
- package/package.json +5 -1
- package/rules/doc-files/js/docgen-files-batch.mjs +20 -5
- package/rules/doc-files/js/docgen-gen.mjs +42 -25
- package/rules/doc-files/js/docgen-judge-measure.mjs +16 -13
- package/rules/doc-files/js/docgen-judge.mjs +11 -9
- package/rules/doc-files/js/docs/docgen-files-batch.md +3 -20
- package/rules/doc-files/js/docs/docgen-gen.md +3 -20
- package/rules/doc-files/js/docs/docgen-judge-measure.md +3 -18
- package/rules/doc-files/js/docs/docgen-judge.md +3 -22
- package/rules/npm-module/js/docs/skill_meta.md +22 -15
- package/rules/npm-module/js/skill_meta.mjs +5 -1
- package/rules/text/js/cspell-fix.mjs +15 -16
- package/rules/text/js/docs/cspell-fix.md +16 -9
- package/rules/text/main.mjs +4 -4
- package/schemas/skill-meta.json +8 -0
- package/scripts/docs/skills-cli.md +21 -25
- package/scripts/lib/adr/docs/normalize-cli.md +3 -20
- package/scripts/lib/adr/docs/normalize-pipeline.md +3 -33
- package/scripts/lib/adr/normalize-cli.mjs +2 -2
- package/scripts/lib/adr/normalize-pipeline.mjs +78 -44
- package/scripts/lib/docs/skill-meta.md +27 -10
- package/scripts/lib/fix/docs/escalation-log.md +10 -9
- package/scripts/lib/fix/docs/orchestrator.md +13 -20
- package/scripts/lib/fix/escalation-log.mjs +1 -1
- package/scripts/lib/fix/orchestrator.mjs +65 -31
- package/scripts/lib/skill-meta.mjs +22 -0
- package/scripts/skills-cli.mjs +52 -14
- package/scripts/utils/ast-extract.mjs +105 -0
- package/scripts/utils/docs/ast-extract.md +30 -0
- package/lib/docs/llm.md +0 -33
- package/lib/docs/models.md +0 -48
- package/lib/docs/omlx-trace.md +0 -49
- package/lib/docs/omlx.md +0 -41
- package/lib/llm.mjs +0 -215
- package/lib/models.mjs +0 -75
- package/lib/omlx-trace.mjs +0 -158
- package/lib/omlx.mjs +0 -220
- package/scripts/lib/fix/docs/llm-fix-apply.md +0 -31
- package/scripts/lib/fix/docs/llm-lint-fix.md +0 -31
- package/scripts/lib/fix/docs/llm-worker.md +0 -28
- package/scripts/lib/fix/docs/verbose-block.md +0 -27
- package/scripts/lib/fix/llm-fix-apply.mjs +0 -113
- package/scripts/lib/fix/llm-lint-fix.mjs +0 -82
- package/scripts/lib/fix/llm-worker.mjs +0 -346
- package/scripts/lib/fix/verbose-block.mjs +0 -82
package/lib/omlx.mjs
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
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
|
-
const OMLX_PREFIX = 'omlx/'
|
|
49
|
-
|
|
50
|
-
/** Backoff між transient-ретраями curl (мс): 2 паузи на 3 спроби. */
|
|
51
|
-
const BACKOFF_MS = [2000, 8000]
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Блокуюча пауза без зайнятого циклу (sync — для retry-loop у `callOmlxRaw`).
|
|
55
|
-
* @param {number} ms тривалість паузи
|
|
56
|
-
* @returns {void}
|
|
57
|
-
*/
|
|
58
|
-
function sleepSync(ms) {
|
|
59
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Чи цей model-id адресує локальний omlx-бекенд (префікс `omlx/`).
|
|
64
|
-
* @param {unknown} model перевірюваний model-id
|
|
65
|
-
* @returns {boolean} true, якщо рядок починається з `omlx/`
|
|
66
|
-
*/
|
|
67
|
-
export function isOmlxModel(model) {
|
|
68
|
-
return typeof model === 'string' && model.startsWith(OMLX_PREFIX)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Прибирає `omlx/`-префікс → чистий model-id для omlx API.
|
|
73
|
-
* Не-omlx-рядки повертає без змін.
|
|
74
|
-
* @param {string} model model-id (можливо з префіксом)
|
|
75
|
-
* @returns {string} model-id без `omlx/`
|
|
76
|
-
*/
|
|
77
|
-
export function omlxModelId(model) {
|
|
78
|
-
return isOmlxModel(model) ? model.slice(OMLX_PREFIX.length) : model
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const THINK_TAG_RE = /<think>([\s\S]*?)<\/think>/
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Витягує reasoning (думки моделі) з omlx-`message`. Джерела за пріоритетом:
|
|
85
|
-
* - `field` — окреме поле `message.reasoning_content` (Qwen3-Thinking тощо);
|
|
86
|
-
* - `think_tag` — `<think>…</think>` усередині `content` (інші thinking-моделі);
|
|
87
|
-
* - `truncated` — `finish_reason: "length"` зрізав думку в `content` до закриття
|
|
88
|
-
* тега → сирий reasoning лишився в `content` без `</think>`;
|
|
89
|
-
* - `null` — reasoning немає (не-thinking модель).
|
|
90
|
-
* @param {{content?:string, reasoning_content?:string}} message обʼєкт `choices[0].message`
|
|
91
|
-
* @param {string|null} finishReason `choices[0].finish_reason`
|
|
92
|
-
* @returns {{ reasoning: string|null, reasoningSource: 'field'|'think_tag'|'truncated'|null }} текст думок і його джерело
|
|
93
|
-
*/
|
|
94
|
-
export function extractReasoning(message, finishReason) {
|
|
95
|
-
const field = message?.reasoning_content
|
|
96
|
-
if (field && field.trim()) return { reasoning: field, reasoningSource: 'field' }
|
|
97
|
-
const content = message?.content ?? ''
|
|
98
|
-
const m = content.match(THINK_TAG_RE)
|
|
99
|
-
if (m) return { reasoning: m[1].trim(), reasoningSource: 'think_tag' }
|
|
100
|
-
if (finishReason === 'length' && content.trim()) return { reasoning: content, reasoningSource: 'truncated' }
|
|
101
|
-
return { reasoning: null, reasoningSource: null }
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Парсить успішну (curl-exit 0) omlx-відповідь у багатий обʼєкт.
|
|
106
|
-
* @param {string} stdout сире тіло відповіді curl
|
|
107
|
-
* @param {number} attempt номер успішної спроби (для поля `attempts`)
|
|
108
|
-
* @returns {{ content:string, reasoning:string|null, reasoningSource:string|null, finishReason:string|null, usage:object|null, attempts:number }} багатий результат
|
|
109
|
-
* @throws {Error} на поганому JSON, api-помилці чи порожньому контенті
|
|
110
|
-
*/
|
|
111
|
-
function parseOmlxResponse(stdout, attempt) {
|
|
112
|
-
let j
|
|
113
|
-
try {
|
|
114
|
-
j = JSON.parse(stdout)
|
|
115
|
-
} catch {
|
|
116
|
-
throw new Error(`omlx bad json: ${stdout?.slice(0, 200) ?? ''}`)
|
|
117
|
-
}
|
|
118
|
-
if (j.error) throw new Error(`omlx api: ${JSON.stringify(j.error).slice(0, 300)}`)
|
|
119
|
-
const message = j.choices?.[0]?.message ?? {}
|
|
120
|
-
const finishReason = j.choices?.[0]?.finish_reason ?? null
|
|
121
|
-
const content = message.content?.trim() ?? ''
|
|
122
|
-
if (!content) throw new Error(`omlx empty content (finish=${finishReason})`)
|
|
123
|
-
const { reasoning, reasoningSource } = extractReasoning(message, finishReason)
|
|
124
|
-
return { content, reasoning, reasoningSource, finishReason, usage: j.usage ?? null, attempts: attempt }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Ядро прямого HTTP-виклику до omlx через `curl` (spawnSync). Повертає **багатий**
|
|
129
|
-
* обʼєкт: контент + reasoning + usage + finish_reason + кількість спроб. Ретраїть
|
|
130
|
-
* transient-помилки (curl 18/28/52/56 + spawnSync ETIMEDOUT) із backoff 2s→8s.
|
|
131
|
-
* @param {Array<{role:string, content:string}>} messages OpenAI-messages (system+user збережено)
|
|
132
|
-
* @param {string} model model-id (з/без `omlx/`-префікса); порожній і без `fallbackModel` → throw
|
|
133
|
-
* @param {{ url?: string, timeoutMs?: number, temperature?: number, maxTokens?: number, fallbackModel?: string, apiKey?: string, backoffMs?: number[], thinkingBudget?: number }} [opts] URL, timeout, температура, ліміт виходу, fallback-модель, API-ключ, backoff між ретраями (мс), бюджет thinking-токенів (0 = вимкнено)
|
|
134
|
-
* @returns {{ content:string, reasoning:string|null, reasoningSource:string|null, finishReason:string|null, usage:object|null, attempts:number }} багатий результат виклику
|
|
135
|
-
* @throws {Error} на curl-помилці, не-200 exit, поганому JSON чи порожньому контенті
|
|
136
|
-
*/
|
|
137
|
-
export function callOmlxRaw(messages, model, opts = {}) {
|
|
138
|
-
const {
|
|
139
|
-
url = env.N_CURSOR_OMLX_URL ?? DEFAULT_OMLX_URL,
|
|
140
|
-
timeoutMs = 60_000,
|
|
141
|
-
temperature = 0.2,
|
|
142
|
-
maxTokens = 4096,
|
|
143
|
-
fallbackModel = env.N_CURSOR_OMLX_MODEL ?? '',
|
|
144
|
-
apiKey,
|
|
145
|
-
backoffMs = BACKOFF_MS,
|
|
146
|
-
thinkingBudget = 0
|
|
147
|
-
} = opts
|
|
148
|
-
|
|
149
|
-
const m = omlxModelId(model) || fallbackModel
|
|
150
|
-
if (!m) {
|
|
151
|
-
throw new Error('omlx: модель не задано — постав N_LOCAL_MIN_MODEL (або N_CURSOR_OMLX_MODEL)')
|
|
152
|
-
}
|
|
153
|
-
const body = JSON.stringify({
|
|
154
|
-
model: m,
|
|
155
|
-
messages,
|
|
156
|
-
max_tokens: maxTokens,
|
|
157
|
-
temperature,
|
|
158
|
-
...(thinkingBudget > 0 ? { thinking_budget: thinkingBudget } : {})
|
|
159
|
-
})
|
|
160
|
-
// Ключ локального сервера в argv допустимий: localhost-секрет власної машини,
|
|
161
|
-
// короткоживучий процес; stdin уже зайнятий body (`--data-binary @-`).
|
|
162
|
-
const key = resolveOmlxApiKey(apiKey)
|
|
163
|
-
const authArgs = key ? ['-H', `Authorization: Bearer ${key}`] : []
|
|
164
|
-
|
|
165
|
-
// 18=transfer closed, 28=operation timeout, 52=empty reply, 56=recv failure — усі transient.
|
|
166
|
-
const TRANSIENT_CURL_CODES = new Set([18, 28, 52, 56])
|
|
167
|
-
let lastErr
|
|
168
|
-
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
169
|
-
const r = spawnSync(
|
|
170
|
-
'curl',
|
|
171
|
-
[
|
|
172
|
-
'-sS',
|
|
173
|
-
'-X',
|
|
174
|
-
'POST',
|
|
175
|
-
url,
|
|
176
|
-
'-H',
|
|
177
|
-
'Content-Type: application/json',
|
|
178
|
-
'-H',
|
|
179
|
-
'Connection: close',
|
|
180
|
-
...authArgs,
|
|
181
|
-
'--max-time',
|
|
182
|
-
String(Math.ceil(timeoutMs / 1000)),
|
|
183
|
-
'--data-binary',
|
|
184
|
-
'@-'
|
|
185
|
-
],
|
|
186
|
-
{ input: body, encoding: 'utf8', timeout: timeoutMs + 5000 }
|
|
187
|
-
)
|
|
188
|
-
if (r.error) {
|
|
189
|
-
lastErr = new Error(`omlx curl error: ${r.error.message}`)
|
|
190
|
-
// spawnSync-таймаут (ETIMEDOUT) — transient: сервер перевантажений, ретраїмо з backoff.
|
|
191
|
-
if (r.error.code === 'ETIMEDOUT' && attempt < 3) {
|
|
192
|
-
sleepSync(backoffMs[attempt - 1])
|
|
193
|
-
continue
|
|
194
|
-
}
|
|
195
|
-
break
|
|
196
|
-
}
|
|
197
|
-
if (r.status !== 0) {
|
|
198
|
-
if (TRANSIENT_CURL_CODES.has(r.status) && attempt < 3) {
|
|
199
|
-
lastErr = new Error(`omlx curl exit ${r.status} (transient, retry ${attempt})`)
|
|
200
|
-
sleepSync(backoffMs[attempt - 1])
|
|
201
|
-
continue
|
|
202
|
-
}
|
|
203
|
-
throw new Error(`omlx curl exit ${r.status}: ${r.stderr?.slice(0, 300) ?? ''}`)
|
|
204
|
-
}
|
|
205
|
-
return parseOmlxResponse(r.stdout, attempt)
|
|
206
|
-
}
|
|
207
|
-
throw lastErr ?? new Error('omlx unknown failure')
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Тонка обгортка над `callOmlxRaw` для споживачів, яким потрібен лише текст.
|
|
212
|
-
* Контракт незмінний: повертає непорожній `choices[0].message.content`.
|
|
213
|
-
* @param {Array<{role:string, content:string}>} messages OpenAI-messages
|
|
214
|
-
* @param {string} model model-id (з/без `omlx/`-префікса)
|
|
215
|
-
* @param {object} [opts] ті самі опції, що й у `callOmlxRaw`
|
|
216
|
-
* @returns {string} непорожній контент відповіді
|
|
217
|
-
*/
|
|
218
|
-
export function callOmlx(messages, model, opts = {}) {
|
|
219
|
-
return callOmlxRaw(messages, model, opts).content
|
|
220
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: llm-fix-apply.mjs
|
|
4
|
-
resource: npm/scripts/lib/fix/llm-fix-apply.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: 49f989ca
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 100
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Огляд
|
|
12
|
-
|
|
13
|
-
Це спільне ядро LLM-фіксу, призначене для оркестрації процесу застосування змін, згенерованих моделлю. Модуль використовує `llm-worker.mjs` та `llm-lint-fix.mjs` для забезпечення конформності та виконання лінтер-фіксів. Основний функціонал включає: парсинг відповіді LLM за схемою `{changes:[{path,content}]}` (через `parseChangesResponse`), зчитування необхідних файлів (`readFilesForFix`), та безпечне застосування змін (`applyChanges`). Система реалізує механізм fail-safe, перехоплюючи помилки та повертаючи `null` замість винятків для певних сценаріїв. При цьому шляхи `.git` та `node_modules` свідомо ігноруються.
|
|
14
|
-
|
|
15
|
-
## Поведінка
|
|
16
|
-
|
|
17
|
-
parseChangesResponse парсить сирий текст відповіді моделі, витягуючи структуру змін у форматі патчу, або повертає null, якщо парсинг неможливий.
|
|
18
|
-
readFilesForFix читає вміст файлів, вказаних у списку, відносно кореня проєкту. Якщо прямий шлях не знайдено, шукає файл за його базовим ім'ям у проєкті, ігноруючи каталоги `.git` та `node_modules`.
|
|
19
|
-
applyChanges записує вміст, наданий у змінних, у відповідні файли проєкту, створюючи необхідні каталоги, якщо вони відсутні.
|
|
20
|
-
|
|
21
|
-
## Публічний API
|
|
22
|
-
|
|
23
|
-
parseChangesResponse — розбирає JSON-відповідь моделі, витягуючи вміст першого об'єкта.
|
|
24
|
-
readFilesForFix — зчитує вміст файлів за заданими шляхами, шукаючи їх у системі, якщо прямий шлях не знайдено.
|
|
25
|
-
applyChanges — замінює вміст файлів на новий, наданий у змінній `changes`.
|
|
26
|
-
|
|
27
|
-
## Гарантії поведінки
|
|
28
|
-
|
|
29
|
-
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
30
|
-
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
31
|
-
- Свідомо пропускає шляхи: `.git`, `node_modules`.
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: llm-lint-fix.mjs
|
|
4
|
-
resource: npm/scripts/lib/fix/llm-lint-fix.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: de4439e9
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 90
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
Модуль реалізує механізм `omlx-фікс` для обробки знахідок лінтера, що стосуються `detect-only` тулів, які не мають нативного механізму виправлення (відповідно до `lint-orchestrator-fix-readonly`). Він зчитує уражені файли, ініціює запит до моделі через `callLlm` (маршрутизація через `omlx/<model>` з фолбеком каскаду), щоб отримати пропоновані зміни. Ці зміни застосовуються до файлової системи за допомогою спільного ядра `llm-fix-apply`.
|
|
12
|
-
|
|
13
|
-
## Поведінка
|
|
14
|
-
|
|
15
|
-
1. Зчитує вміст файлів, зазначених у `filePaths`, використовуючи `projectRoot`.
|
|
16
|
-
2. Формує детальний запит для моделі, включаючи назву тула, інструкцію, сирий вивід знахідок та вміст зчитаних файлів.
|
|
17
|
-
3. Надсилає сформований запит до моделі для отримання виправлень.
|
|
18
|
-
4. Парсить відповідь моделі для вилучення пропонованих змін.
|
|
19
|
-
5. Перевіряє, чи містить відповідь помилку або не містить жодних змін.
|
|
20
|
-
6. Застосовує вилучені зміни до файлової системи, використовуючи `projectRoot`.
|
|
21
|
-
7. Повертає результат виконання `llmLintFix`, що включає статус успіху, можливу помилку або список шляхів, які були успішно виправлені.
|
|
22
|
-
|
|
23
|
-
## Публічний API
|
|
24
|
-
|
|
25
|
-
llmLintFix — автоматично виправляє помилки, знайдені лінтером, використовуючи omlx.
|
|
26
|
-
|
|
27
|
-
## Гарантії поведінки
|
|
28
|
-
|
|
29
|
-
- Read-only: файл не виконує операцій запису у файлову систему.
|
|
30
|
-
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
31
|
-
- Не звертається до мережі.
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: llm-worker.mjs
|
|
4
|
-
resource: npm/scripts/lib/fix/llm-worker.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: b5e54981
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 100
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Огляд
|
|
12
|
-
|
|
13
|
-
Модуль витягує унікальні відносні шляхи файлів з даних про порушення, використовуючи конфігурацію з target.json. Потім він викликає LLM для генерації змін, які виправляють кожне порушення.
|
|
14
|
-
|
|
15
|
-
## Поведінка
|
|
16
|
-
|
|
17
|
-
extractFilePaths витягує унікальні відносні шляхи файлів з вихідних даних про порушення, розпізнаючи як явні помилки, так і контекст файлів.
|
|
18
|
-
runLlmWorker виправляє одне порушення правила, викликаючи LLM для генерації змін, парсить відповідь та застосовує зміни до файлової системи.
|
|
19
|
-
|
|
20
|
-
## Публічний API
|
|
21
|
-
|
|
22
|
-
extractFilePaths — Визначає шляхи файлів, які потребують виправлення, використовуючи `target.json` як пріоритет, а у разі його відсутності — всі знайдені шляхи.
|
|
23
|
-
runLlmWorker — Виправляє одне порушення правила, використовуючи модель LLM. Повертає результати виправлення або діагностики, які використовуються для подальшої обробки.
|
|
24
|
-
|
|
25
|
-
## Гарантії поведінки
|
|
26
|
-
|
|
27
|
-
- Read-only: не виконує операцій запису (ФС/БД).
|
|
28
|
-
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: verbose-block.mjs
|
|
4
|
-
resource: npm/scripts/lib/fix/verbose-block.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: 1a921dca
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 100
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
Цей файл виводить детальний блок інформації після кожного LLM-рунга у режимі `--full`. Він друкує стислий опис промпту, включаючи деталі про правила, порушення та зворотний зв'язок. Також виводиться монолог моделі, якщо він присутній. Вивід блоку вимикається при встановленні `N_CURSOR_FIX_VERBOSE=off`.
|
|
12
|
-
|
|
13
|
-
## Поведінка
|
|
14
|
-
|
|
15
|
-
1. Викликається `printVerboseBlock` для виведення детальної інформації після кожного LLM-рунга у режимі `--full`.
|
|
16
|
-
2. `printVerboseBlock` виводить стислий опис промпту, включаючи ID правила, довжину правила, довжину порушення, кількість та розмір файлів, а також інформацію про зворотний зв'язок (наявність, модель, кількість змін, помилка).
|
|
17
|
-
3. Якщо надано монолог моделі (reasoning), `printVerboseBlock` виводить його, показуючи прев'ю (з можливим зазначенням загальної довжини), і вказує джерело цього монологу.
|
|
18
|
-
4. Якщо монологу моделі немає, `printVerboseBlock` виводить повідомлення про відсутність монологу.
|
|
19
|
-
5. Виведення здійснюється лише якщо змінна середовища `N_CURSOR_FIX_VERBOSE` не встановлена як `off`.
|
|
20
|
-
|
|
21
|
-
## Публічний API
|
|
22
|
-
|
|
23
|
-
printVerboseBlock — виводить детальний опис промпту та внутрішній роздум моделі після символу завершення рядка рунга.
|
|
24
|
-
|
|
25
|
-
## Гарантії поведінки
|
|
26
|
-
|
|
27
|
-
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Спільне ядро LLM-фіксу: парс відповіді `{changes:[{path,content}]}`, читання файлів
|
|
3
|
-
* під фікс і застосування змін. Використовують і `llm-worker.mjs` (конформність), і
|
|
4
|
-
* `llm-lint-fix.mjs` (per-tool лінтер-фіксери) — щоб не дублювати парс/apply (knip/jscpd).
|
|
5
|
-
*/
|
|
6
|
-
import { execSync } from 'node:child_process'
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
8
|
-
import { basename, dirname, join } from 'node:path'
|
|
9
|
-
|
|
10
|
-
const JSON_CODE_BLOCK_RE = /```(?:json)?[ \t]{0,8}\n?([\s\S]*?)```/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Парсить JSON-відповідь моделі: прямий JSON → ```json-блок``` → перший `{…}`-блок.
|
|
14
|
-
* @param {string} text сирий текст відповіді
|
|
15
|
-
* @returns {{ changes?: Array<{path:string,content:string}>, error?: string } | null} патч або null
|
|
16
|
-
*/
|
|
17
|
-
export function parseChangesResponse(text) {
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(text)
|
|
20
|
-
} catch {
|
|
21
|
-
/* fallthrough */
|
|
22
|
-
}
|
|
23
|
-
const block = text.match(JSON_CODE_BLOCK_RE)
|
|
24
|
-
if (block) {
|
|
25
|
-
try {
|
|
26
|
-
return JSON.parse(block[1].trim())
|
|
27
|
-
} catch {
|
|
28
|
-
/* fallthrough */
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const start = text.indexOf('{')
|
|
32
|
-
const end = text.lastIndexOf('}')
|
|
33
|
-
if (start !== -1 && end > start) {
|
|
34
|
-
try {
|
|
35
|
-
return JSON.parse(text.slice(start, end + 1))
|
|
36
|
-
} catch {
|
|
37
|
-
/* fallthrough */
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return null
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Шукає файл за basename у дереві проєкту (fallback коли прямий шлях не існує).
|
|
45
|
-
* Повертає відносний шлях якщо знайдено рівно один матч, інакше `null` (ambiguous/not found).
|
|
46
|
-
* @param {string} name basename файлу
|
|
47
|
-
* @param {string} projectRoot абсолютний корінь
|
|
48
|
-
* @returns {string|null} відносний шлях або null
|
|
49
|
-
*/
|
|
50
|
-
function findByBasename(name, projectRoot) {
|
|
51
|
-
try {
|
|
52
|
-
const raw = execSync(
|
|
53
|
-
`find . -maxdepth 7 -name '${name.replace(/'/g, "'\\''")}' -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.worktrees/*'`,
|
|
54
|
-
{ cwd: projectRoot, encoding: 'utf8', timeout: 3000 }
|
|
55
|
-
).trim()
|
|
56
|
-
const hits = raw.split('\n').filter(Boolean)
|
|
57
|
-
return hits.length === 1 ? hits[0].replace(/^\.\//, '') : null
|
|
58
|
-
} catch {
|
|
59
|
-
return null
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Читає існуючі файли за відносними шляхами у форму `{path, content}` (для prompt).
|
|
65
|
-
* Якщо файл не знайдений за прямим шляхом — намагається знайти за basename через `find`.
|
|
66
|
-
* Повертає resolved path (може відрізнятись від вхідного коли `find` знайшов реальне місце).
|
|
67
|
-
* @param {string[]} filePaths відносні шляхи від кореня
|
|
68
|
-
* @param {string} projectRoot абсолютний корінь
|
|
69
|
-
* @returns {Array<{path:string, content:string}>} наявні файли з вмістом
|
|
70
|
-
*/
|
|
71
|
-
export function readFilesForFix(filePaths, projectRoot) {
|
|
72
|
-
return filePaths
|
|
73
|
-
.map(p => {
|
|
74
|
-
let abs = join(projectRoot, p)
|
|
75
|
-
let resolvedPath = p
|
|
76
|
-
if (!existsSync(abs)) {
|
|
77
|
-
const found = findByBasename(basename(p), projectRoot)
|
|
78
|
-
if (found) {
|
|
79
|
-
resolvedPath = found
|
|
80
|
-
abs = join(projectRoot, found)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (!existsSync(abs)) return null
|
|
84
|
-
try {
|
|
85
|
-
return { path: resolvedPath, content: readFileSync(abs, 'utf8') }
|
|
86
|
-
} catch {
|
|
87
|
-
return null
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
.filter(Boolean)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Застосовує `changes` до ФС (повний вміст файлу, не diff).
|
|
95
|
-
* @param {Array<{path:string, content:string}>} changes зміни
|
|
96
|
-
* @param {string} projectRoot абсолютний корінь
|
|
97
|
-
* @returns {{ ok: boolean, error?: string }} статус
|
|
98
|
-
*/
|
|
99
|
-
export function applyChanges(changes, projectRoot) {
|
|
100
|
-
for (const change of changes) {
|
|
101
|
-
if (!change.path || typeof change.content !== 'string') continue
|
|
102
|
-
try {
|
|
103
|
-
const abs = join(projectRoot, change.path)
|
|
104
|
-
// Створюємо батьківську теку перед записом: модель може запропонувати новий файл
|
|
105
|
-
// у ще неіснуючому каталозі (напр. `<ws>/.changes/…`) — інакше writeFileSync ENOENT.
|
|
106
|
-
mkdirSync(dirname(abs), { recursive: true })
|
|
107
|
-
writeFileSync(abs, change.content, 'utf8')
|
|
108
|
-
} catch (error) {
|
|
109
|
-
return { ok: false, error: `write ${change.path}: ${error.message}` }
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return { ok: true }
|
|
113
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-tool omlx-фікс лінтер-знахідок (point 4 спеки lint-orchestrator-fix-readonly).
|
|
3
|
-
*
|
|
4
|
-
* Для detect-only тулів без нативного `--fix` (cspell, knip, actionlint, v8r тощо): читає
|
|
5
|
-
* уражені файли, просить omlx виправити за tool-специфічною інструкцією, застосовує `{changes}`.
|
|
6
|
-
* Re-detect (перевірка, що знахідка закрита) — на стороні caller (convergence-патерн).
|
|
7
|
-
*
|
|
8
|
-
* Маршрут моделі — через `callLlm` за префіксом: `omlx/<model>` → локальний HTTP (дефолт
|
|
9
|
-
* `resolveModel('min')`); cloud — фолбек каскаду. Парс/застосування — спільне ядро `llm-fix-apply`.
|
|
10
|
-
*/
|
|
11
|
-
import { env } from 'node:process'
|
|
12
|
-
|
|
13
|
-
import { resolveModel } from '../../../lib/models.mjs'
|
|
14
|
-
import { callLlm } from '../../../lib/llm.mjs'
|
|
15
|
-
import { applyChanges, parseChangesResponse, readFilesForFix } from './llm-fix-apply.mjs'
|
|
16
|
-
|
|
17
|
-
/** Дефолтний локальний тир (omlx); env `N_CURSOR_FIX_MODEL` перекриває. */
|
|
18
|
-
const DEFAULT_MODEL = env.N_CURSOR_FIX_MODEL ?? resolveModel('min')
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Будує prompt для omlx: tool-інструкція + знахідки + повний вміст файлів.
|
|
22
|
-
* @param {string} tool назва тула (cspell/knip/…)
|
|
23
|
-
* @param {string} instruction що саме виправити (tool-специфічно)
|
|
24
|
-
* @param {string} findings сирий вивід тула (знахідки)
|
|
25
|
-
* @param {Array<{path:string, content:string}>} files файли під фікс
|
|
26
|
-
* @returns {string} prompt
|
|
27
|
-
*/
|
|
28
|
-
function buildLintFixPrompt(tool, instruction, findings, files) {
|
|
29
|
-
const filesBlock = files.map(f => `<file path="${f.path}">\n${f.content}\n</file>`).join('\n\n')
|
|
30
|
-
return [
|
|
31
|
-
`You fix ${tool} lint findings. Return ONLY valid JSON — no explanation, no markdown.`,
|
|
32
|
-
``,
|
|
33
|
-
`Task: ${instruction}`,
|
|
34
|
-
``,
|
|
35
|
-
`${tool} findings:`,
|
|
36
|
-
findings,
|
|
37
|
-
``,
|
|
38
|
-
`Current file contents:`,
|
|
39
|
-
filesBlock,
|
|
40
|
-
``,
|
|
41
|
-
`Return JSON with this exact shape:`,
|
|
42
|
-
`{"changes":[{"path":"relative/path","content":"full corrected file content"}]}`,
|
|
43
|
-
``,
|
|
44
|
-
`Rules:`,
|
|
45
|
-
`- "path" is relative to the project root (use the path from the <file> tag)`,
|
|
46
|
-
`- "content" is the COMPLETE new file content (not a diff)`,
|
|
47
|
-
`- Only include files that actually need to change; preserve everything unrelated verbatim`,
|
|
48
|
-
`- If nothing should be auto-fixed, return {"changes":[],"error":"reason"}`
|
|
49
|
-
].join('\n')
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Виправляє лінтер-знахідки через omlx і застосовує зміни.
|
|
54
|
-
* @param {{ tool:string, instruction:string, findings:string, filePaths:string[], projectRoot:string, model?:string }} opts параметри
|
|
55
|
-
* @returns {{ ok:boolean, error?:string, fixed:string[] }} статус + список змінених шляхів
|
|
56
|
-
*/
|
|
57
|
-
export function llmLintFix({ tool, instruction, findings, filePaths, projectRoot, model }) {
|
|
58
|
-
const m = model ?? DEFAULT_MODEL
|
|
59
|
-
const files = readFilesForFix(filePaths, projectRoot)
|
|
60
|
-
if (files.length === 0) return { ok: false, error: 'no readable files to fix', fixed: [] }
|
|
61
|
-
|
|
62
|
-
let text
|
|
63
|
-
try {
|
|
64
|
-
text = callLlm([{ role: 'user', content: buildLintFixPrompt(tool, instruction, findings, files) }], m, {
|
|
65
|
-
timeoutMs: 120_000,
|
|
66
|
-
caller: `lint:${tool}`
|
|
67
|
-
})
|
|
68
|
-
} catch (error) {
|
|
69
|
-
return { ok: false, error: String(error.message), fixed: [] }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const parsed = parseChangesResponse(text)
|
|
73
|
-
if (!parsed) return { ok: false, error: `cannot parse omlx response: ${String(text).slice(0, 200)}`, fixed: [] }
|
|
74
|
-
if (parsed.error) return { ok: false, error: parsed.error, fixed: [] }
|
|
75
|
-
|
|
76
|
-
const changes = (parsed.changes ?? []).filter(c => c.path && typeof c.content === 'string')
|
|
77
|
-
if (changes.length === 0) return { ok: false, error: 'omlx returned no changes', fixed: [] }
|
|
78
|
-
|
|
79
|
-
const applied = applyChanges(changes, projectRoot)
|
|
80
|
-
if (!applied.ok) return { ok: false, error: applied.error, fixed: [] }
|
|
81
|
-
return { ok: true, fixed: changes.map(c => c.path) }
|
|
82
|
-
}
|