@nitra/cursor 5.2.0 → 5.3.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.
@@ -7,11 +7,12 @@ import { DEFAULT_OMLX_MODEL } from '../../../lib/omlx.mjs'
7
7
  import { callLlm } from '../../../lib/llm.mjs'
8
8
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
9
9
  import { extractFacts } from './docgen-extract.mjs'
10
- import { extractAnchors } from './docgen-extract-anchors.mjs'
10
+ import { extractAnchors, anchorTokens } from './docgen-extract-anchors.mjs'
11
11
  import { QUALITY_THRESHOLD } from './docgen-crc.mjs'
12
12
  import {
13
13
  oneShotMessages,
14
14
  sectionMessages,
15
+ overviewMessages,
15
16
  criticMessages,
16
17
  refineMessages,
17
18
  guaranteesFromMarkers
@@ -25,6 +26,15 @@ const SECTION_KEY_CLEAN_RE = /[^а-яіїєґa-z0-9]/gi
25
26
  const CACHE_MENTION_RE = /кеш/i
26
27
  const CACHE_NEGATION_RE = /(?:не|без)\s+(?:\S+\s+)?кеш|немає\s+кеш/i
27
28
  const CRITIC_NONE_RE = /^\s*NONE\s*$/i
29
+ // R4: абстрактні «нічого-не-кажучі» формули, які обходять exact-blocklist і дають score=100
30
+ const GENERIC_RE =
31
+ /відповідност\S*\s+(?:даних\s+)?(?:визначеному\s+)?контракту|валідаці\S*\s+даних|перевірк\S*\s+(?:відповідності\s+)?даних|обробк\S*\s+даних|застосову\S*\s+логіку|інспекту\S*\s+та\s+збира\S*\s+дан/i
32
+ // R7: часті русизми/суржик (курований безпечний список — без false-positive на нормальній мові).
33
+ // Без \b: кирилиця не є ASCII-`\w`, тож межі слова в JS-regex не спрацьовують — терміни специфічні.
34
+ const SURZHIK_RE =
35
+ /пропуская|являється|в залежності|по замовчуванню|на протязі|відповідаюч|слідуюч|наступним разом|приймати участь|у відповідності/i
36
+ const ANCHOR_MISS_PENALTY = 5
37
+ const ANCHOR_MISS_CAP = 20
28
38
 
29
39
  /**
30
40
  * Прибирає код-фенс-обгортку (потрійні бектіки) й випадковий провідний
@@ -86,18 +96,26 @@ function hasName(text, sym) {
86
96
  * Stage 2.5 — детермінований скоринг (0 токенів): перевіряє вихід проти фактів.
87
97
  * @param {string} md зібраний документ
88
98
  * @param {object} facts факт-лист про файл
99
+ * @param {{ anchors?: object|null, src?: string }} [ctx] анкори й джерело для R5
89
100
  * @returns {{ score: number, issues: string[] }} оцінка 0–100 і коди проблем
90
101
  */
91
- function scoreDoc(md, facts) {
102
+ export function scoreDoc(md, facts, { anchors = null, src = '' } = {}) {
92
103
  const s = parseSections(md)
93
104
  let score = 100
94
105
  const issues = []
106
+ const overview = s['огляд'] ?? ''
95
107
 
96
108
  if (!s['огляд']) {
97
109
  score -= 25
98
110
  issues.push('no-overview')
99
111
  }
100
112
 
113
+ // R4: generic-Огляд (парафрази, які обходять exact-blocklist) — як майже-відсутній.
114
+ if (GENERIC_RE.test(overview)) {
115
+ score -= 35
116
+ issues.push('generic-overview')
117
+ }
118
+
101
119
  const behavior = s['поведінка'] ?? ''
102
120
  if (behavior.length < 60) {
103
121
  score -= 20
@@ -113,14 +131,35 @@ function scoreDoc(md, facts) {
113
131
  issues.push('cache-hallucination')
114
132
  }
115
133
 
116
- for (const sym of facts.internalSymbols ?? []) {
117
- const inDoc = hasName(guarantees, sym) || hasName(s['огляд'] ?? '', sym) || hasName(s['поведінка'] ?? '', sym)
134
+ // R6: службові (неекспортовані) функції не мають фігурувати як публічні
135
+ const api = s['публічнийapi'] ?? ''
136
+ for (const sym of [...(facts.internalSymbols ?? []), ...(facts.localSymbols ?? [])]) {
137
+ const inDoc = hasName(guarantees, sym) || hasName(overview, sym) || hasName(behavior, sym) || hasName(api, sym)
118
138
  if (inDoc) {
119
139
  score -= 10
120
140
  issues.push(`internal-name:${sym}`)
121
141
  }
122
142
  }
123
143
 
144
+ // R5: кожен валідний анкор (дослівний підрядок src) має зʼявитися в документі
145
+ if (anchors && src) {
146
+ let missPenalty = 0
147
+ for (const tok of anchorTokens(anchors)) {
148
+ if (!src.includes(tok)) continue // валідність: фейковий анкор не вимагаємо
149
+ if (!md.includes(tok) && missPenalty < ANCHOR_MISS_CAP) {
150
+ missPenalty += ANCHOR_MISS_PENALTY
151
+ issues.push(`anchor-miss:${tok}`)
152
+ }
153
+ }
154
+ score -= missPenalty
155
+ }
156
+
157
+ // R7: суржик/русизми
158
+ if (SURZHIK_RE.test(md)) {
159
+ score -= 10
160
+ issues.push('surzhik')
161
+ }
162
+
124
163
  return { score: Math.max(0, score), issues }
125
164
  }
126
165
 
@@ -205,15 +244,21 @@ function orchestratedDoc(facts, src, model, timeoutMs, { anchors = null, tempera
205
244
  const anc = anchors ?? extractAnchors(src)
206
245
  // E3: «Гарантії» — детермінований шаблон з markers (0 LLM-запитів, 0 generic-фраз)
207
246
  sections.guarantees = guaranteesFromMarkers(facts)
247
+ // Спершу Поведінка (+API) — секції з фактажем
208
248
  for (const s of sectionMessages(facts, src, anc)) {
209
- if (s.key === 'guarantees') continue // вже згенеровано детерміновано
210
249
  let draft = stripSignatures(stripSection(callLlm(s.messages, model, { timeoutMs, temperature })))
211
- // E2 + E3: critique→refine лише для секцій, де мала модель зриває на generic
212
- if (s.key === 'overview' || (s.key === 'api' && apiNeedsRefine(facts))) {
250
+ // E2: critique→refine для API, коли всі описи порожні (модель зриває на generic)
251
+ if (s.key === 'api' && apiNeedsRefine(facts)) {
213
252
  draft = critiqueRefineSection(s.key, draft, facts, anc, model, timeoutMs)
214
253
  }
215
254
  sections[s.key] = draft
216
255
  }
256
+ // R3: «Огляд» — ОСТАННІМ, узагальненням уже написаної Поведінки (не голого факт-листа)
257
+ let overview = stripSignatures(
258
+ stripSection(callLlm(overviewMessages(facts, sections.behavior ?? '', anc), model, { timeoutMs, temperature }))
259
+ )
260
+ overview = critiqueRefineSection('overview', overview, facts, anc, model, timeoutMs)
261
+ sections.overview = overview
217
262
  return { md: assemble(basename(facts.relPath), sections) }
218
263
  }
219
264
 
@@ -253,13 +298,13 @@ export function generateDoc(file, { model = DEFAULT_LOCAL_MODEL, threshold = QUA
253
298
  }
254
299
 
255
300
  // Stage 2.5: детермінований скоринг (0 токенів)
256
- let { score, issues } = scoreDoc(r.md, facts)
301
+ let { score, issues } = scoreDoc(r.md, facts, { anchors, src })
257
302
 
258
303
  // E4: best-of-2 — один retry з вищою температурою, det-вибір кращого
259
304
  if (score < threshold && env.N_CURSOR_DOCGEN_BEST_OF !== '0') {
260
305
  try {
261
306
  const r2 = orchestratedDoc(facts, src, model, LOCAL_TIMEOUT_MS, { anchors, temperature: 0.5 })
262
- const s2 = scoreDoc(r2.md, facts)
307
+ const s2 = scoreDoc(r2.md, facts, { anchors, src })
263
308
  if (s2.score > score) {
264
309
  r = r2
265
310
  score = s2.score
@@ -52,31 +52,26 @@ const msgs = (system, user) => [
52
52
 
53
53
  /**
54
54
  * Секційні набори messages з МІНІМАЛЬНИМ контекстом під кожну секцію.
55
- * Код потрапляє лише в `behavior`; решта секцій на факт-листі.
55
+ * Код потрапляє лише в `behavior`; «Огляд» генерується окремо ОСТАННІМ
56
+ * (`overviewMessages`) з уже написаної Поведінки — тут його немає.
56
57
  * @param {object} facts факт-лист про файл
57
58
  * @param {string} src вміст файлу
58
59
  * @param {object|null} [anchors] анкори файлу для обовʼязкового включення
59
- * @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів
60
+ * @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів (behavior[, api])
60
61
  */
61
62
  export function sectionMessages(facts, src, anchors = null) {
62
63
  const factsTxt = factsSummary(facts)
63
64
  const anch = anchorsBlock(anchors)
64
65
  const multi = (facts.exports?.length || 0) > 1
65
66
 
66
- // Огляд лише факти (без коду)
67
- const overview = {
68
- key: 'overview',
69
- numPredict: 220,
70
- messages: msgs(
71
- `${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
72
- 'Напиши вміст секції «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Без заголовка, без переліку функцій. Заборонені generic-фрази типу «забезпечує перевірку», «виконує валідацію» — пиши КОНКРЕТНО що саме і за яким контрактом.'
73
- )
74
- }
75
-
76
- // Поведінка — ЄДИНА секція, якій потрібен код
67
+ // R6: Поведінка описує РІВНО експортовані імена, не службові помічники
68
+ const exportNames = (facts.exports ?? []).map(e => e.name)
77
69
  const behaviorTask = multi
78
70
  ? 'для кожної публічної функції — один короткий пункт «що вона робить»'
79
71
  : 'нумерований алгоритм у бізнес-термінах'
72
+ const onlyExports = exportNames.length
73
+ ? ` Описуй РІВНО ці публічні імена і жодних інших: ${exportNames.join(', ')}.`
74
+ : ''
80
75
  const noInternal = facts.internalSymbols?.length
81
76
  ? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.`
82
77
  : ''
@@ -85,12 +80,12 @@ export function sectionMessages(facts, src, anchors = null) {
85
80
  numPredict: 500,
86
81
  messages: msgs(
87
82
  `${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
88
- `Напиши вміст секції «Поведінка»: ${behaviorTask}. Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${noInternal} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
83
+ `Напиши вміст секції «Поведінка»: ${behaviorTask}.${onlyExports} Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${noInternal} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
89
84
  )
90
85
  }
91
86
 
92
87
  // API — лише список експортів (без коду)
93
- if (!multi && !facts.exports?.some(e => e.desc)) return [overview, behavior]
88
+ if (!multi && !facts.exports?.some(e => e.desc)) return [behavior]
94
89
  const list = facts.exports.map(e => `- ${e.name}: ${e.desc || '(сформулюй стисло з наміру файлу)'}`).join('\n')
95
90
  const api = {
96
91
  key: 'api',
@@ -100,7 +95,24 @@ export function sectionMessages(facts, src, anchors = null) {
100
95
  `Перепиши цей список як стислі маркери «назва — що робить», СВОЇМИ словами (не копіюй дослівно), без типів і сигнатур. Використовуй РІВНО ці назви, не додавай і не прибирай:\n${list}\nБез заголовка. Без generic-фраз «застосовує логіку», «перевіряє коректність» — пиши конкретно ЩО саме застосовує/перевіряє.`
101
96
  )
102
97
  }
103
- return [overview, behavior, api]
98
+ return [behavior, api]
99
+ }
100
+
101
+ /**
102
+ * R3 — «Огляд» ОСТАННІМ: узагальнення вже написаної Поведінки, а не здогад із
103
+ * голого факт-листа. Лікує generic/хибний Огляд на складних файлах.
104
+ * @param {object} facts факт-лист про файл
105
+ * @param {string} behaviorText готовий текст секції «Поведінка»
106
+ * @param {object|null} [anchors] анкори файлу
107
+ * @returns {Array<{role:string,content:string}>} messages-масив для Огляду
108
+ */
109
+ export function overviewMessages(facts, behaviorText, anchors = null) {
110
+ const factsTxt = factsSummary(facts)
111
+ const anch = anchorsBlock(anchors)
112
+ return msgs(
113
+ `${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
114
+ `На основі вже написаної секції «Поведінка» (нижче) напиши «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Узагальнюй САМЕ описану поведінку, не додавай нових фактів. Без заголовка, без переліку функцій. Заборонені абстрактні формули без конкретики («перевірка/валідація/обробка даних», «відповідність контракту», «застосовує логіку») — пиши, ЩО саме і за яким контрактом.\n\nПОВЕДІНКА:\n${behaviorText}`
115
+ )
104
116
  }
105
117
 
106
118
  /**
@@ -0,0 +1,139 @@
1
+ /** @see ./docs/units-js.md */
2
+
3
+ import { parseProgramOrNull, walkAstWithAncestors } from '../../../scripts/utils/ast-scan-utils.mjs'
4
+
5
+ // JSDoc-блок, що стоїть впритул перед позицією (лише пробіли між ними).
6
+ const JSDOC_BEFORE_RE = /\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/
7
+ const JSDOC_OPEN_RE = /^\s*\/\*\*?/
8
+ const JSDOC_CLOSE_RE = /\*\/\s*$/
9
+ const STAR_PREFIX_RE = /^\s*\*?\s?/
10
+
11
+ /**
12
+ * Очищає JSDoc від обрамлення `/** *​/` і `*`-префіксів.
13
+ * @param {string} raw сирий блок або порожній рядок
14
+ * @returns {string} текст опису без тегів-обрамлення
15
+ */
16
+ function cleanDoc(raw) {
17
+ if (!raw) return ''
18
+ return raw
19
+ .replace(JSDOC_OPEN_RE, '')
20
+ .replace(JSDOC_CLOSE_RE, '')
21
+ .split('\n')
22
+ .map(l => l.replace(STAR_PREFIX_RE, '').trimEnd())
23
+ .join('\n')
24
+ .trim()
25
+ }
26
+
27
+ /**
28
+ * JSDoc, що передує позиції `start` у джерелі (або порожній рядок).
29
+ * @param {string} src вміст файлу
30
+ * @param {number} start зміщення початку декларації
31
+ * @returns {string} очищений опис
32
+ */
33
+ function precedingDoc(src, start) {
34
+ const m = src.slice(0, start).match(JSDOC_BEFORE_RE)
35
+ return cleanDoc(m ? m[0] : '')
36
+ }
37
+
38
+ /**
39
+ * Імʼя функції, що викликається (проста Identifier або `obj.method`).
40
+ * @param {Record<string, unknown>} node CallExpression
41
+ * @returns {string|null} імʼя callee або null
42
+ */
43
+ function calleeName(node) {
44
+ const c = node.callee
45
+ if (!c || typeof c !== 'object') return null
46
+ if (c.type === 'Identifier') return c.name
47
+ if (c.type === 'MemberExpression' && !c.computed && c.property?.type === 'Identifier') return c.property.name
48
+ return null
49
+ }
50
+
51
+ /**
52
+ * Множина імен, що викликаються у тілі вузла (сирі callee — фільтрація на ребра
53
+ * call-graph робиться у `extractUnitsJs` після збору всіх імен юнітів).
54
+ * @param {unknown} node AST-вузол юніта
55
+ * @returns {Set<string>} імена викликів
56
+ */
57
+ function collectCalls(node) {
58
+ const names = new Set()
59
+ walkAstWithAncestors(node, [], n => {
60
+ if (n.type === 'CallExpression') {
61
+ const name = calleeName(n)
62
+ if (name) names.add(name)
63
+ }
64
+ })
65
+ return names
66
+ }
67
+
68
+ /**
69
+ * Будує юніт із декларації, додає у `units`. Розпізнає function/class та
70
+ * const-функції (`const x = () => {}` / `function expression`).
71
+ * @param {Record<string, unknown>} decl декларація (function/class/variable)
72
+ * @param {boolean} exported чи експортується
73
+ * @param {number} docStart зміщення для пошуку JSDoc (зовнішній export-вузол)
74
+ * @param {string} src вміст файлу
75
+ * @param {Array<object>} units акумулятор
76
+ * @returns {void}
77
+ */
78
+ function pushUnits(decl, exported, docStart, src, units) {
79
+ if (!decl || typeof decl !== 'object') return
80
+ const doc = precedingDoc(src, docStart)
81
+ if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
82
+ const name = decl.id?.name
83
+ if (!name) return
84
+ units.push({
85
+ name,
86
+ kind: decl.type === 'ClassDeclaration' ? 'class' : 'function',
87
+ exported,
88
+ span: { start: decl.start, end: decl.end },
89
+ body: src.slice(decl.start, decl.end),
90
+ calls: collectCalls(decl),
91
+ doc
92
+ })
93
+ return
94
+ }
95
+ if (decl.type === 'VariableDeclaration') {
96
+ for (const d of decl.declarations ?? []) {
97
+ const init = d.init
98
+ const isFn = init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')
99
+ if (!isFn || d.id?.type !== 'Identifier') continue
100
+ units.push({
101
+ name: d.id.name,
102
+ kind: 'const',
103
+ exported,
104
+ span: { start: init.start, end: init.end },
105
+ body: src.slice(init.start, init.end),
106
+ calls: collectCalls(init),
107
+ doc
108
+ })
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Юніт-шар для js/mjs/ts: top-level функції/класи/const-функції з тілом, JSDoc,
115
+ * прапором експорту і ребрами call-graph (виклики ІНШИХ юнітів у тілі).
116
+ * @param {string} src вміст файлу
117
+ * @param {string} [relPath] шлях (для вибору мови oxc)
118
+ * @returns {Array<{name:string, kind:string, exported:boolean, span:{start:number,end:number}, body:string, calls:string[], doc:string}>|null} юніти або null, якщо файл не парситься
119
+ */
120
+ export function extractUnitsJs(src, relPath = 'scan.ts') {
121
+ const program = parseProgramOrNull(src, relPath)
122
+ if (!program || !Array.isArray(program.body)) return null
123
+
124
+ const units = []
125
+ for (const node of program.body) {
126
+ if (node.type === 'ExportNamedDeclaration' && node.declaration) {
127
+ pushUnits(node.declaration, true, node.start, src, units)
128
+ } else if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
129
+ pushUnits(node.declaration, true, node.start, src, units)
130
+ } else {
131
+ pushUnits(node, false, node.start, src, units)
132
+ }
133
+ }
134
+
135
+ // Ребра call-graph: лишаємо тільки виклики інших внутрішніх юнітів
136
+ const names = new Set(units.map(u => u.name))
137
+ for (const u of units) u.calls = [...u.calls].filter(n => names.has(n) && n !== u.name)
138
+ return units
139
+ }
@@ -0,0 +1,19 @@
1
+ /** @see ./docs/units.md */
2
+
3
+ import { extractUnitsJs } from './units-js.mjs'
4
+
5
+ const JS_EXT = new Set(['js', 'mjs', 'ts', 'jsx', 'tsx', 'cts', 'mts'])
6
+
7
+ /**
8
+ * Мовно-агностичний фасад юніт-шару (Інкремент 1). Диспатчить за розширенням:
9
+ * js/mjs/ts → oxc; vue/py — додаються наступними кроками (поки `null` → виклик
10
+ * відкочується на whole-file шлях, як і раніше).
11
+ * @param {string} src вміст файлу
12
+ * @param {string} relPath шлях файлу
13
+ * @returns {Array<object>|null} юніти або null, якщо мова ще не підтримана / файл не парситься
14
+ */
15
+ export function extractUnits(src, relPath) {
16
+ const ext = (relPath.split('.').pop() || '').toLowerCase()
17
+ if (JS_EXT.has(ext)) return extractUnitsJs(src, relPath)
18
+ return null
19
+ }
@@ -2,10 +2,9 @@
2
2
 
3
3
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
4
4
  import { join } from 'node:path'
5
- import { spawnSync } from 'node:child_process'
6
5
  import { env } from 'node:process'
7
6
  import { resolveModel } from '../../../lib/models.mjs'
8
- import { callOmlx, isOmlxModel } from '../../../lib/omlx.mjs'
7
+ import { callLlm } from '../../../lib/llm.mjs'
9
8
 
10
9
  // Тир за замовчуванням: min → avg при ескалації (каскад local→cloud).
11
10
  // Перевизначення через N_CURSOR_FIX_MODEL / N_CURSOR_FIX_MODEL_HEAVY.
@@ -13,6 +12,7 @@ export const MODEL = env.N_CURSOR_FIX_MODEL ?? resolveModel('min')
13
12
  export const MODEL_HEAVY = env.N_CURSOR_FIX_MODEL_HEAVY ?? resolveModel('avg')
14
13
 
15
14
  const JSON_CODE_BLOCK_RE = /```(?:json)?[ \t]{0,8}\n?([\s\S]*?)```/
15
+ const API_KEY_RE = /api key/i
16
16
 
17
17
  /**
18
18
  * Витягує відносні шляхи файлів із violation output.
@@ -87,29 +87,18 @@ function buildPrompt(ruleId, ruleMdc, output, files) {
87
87
  }
88
88
 
89
89
  /**
90
- * Викликає LLM за model-id і повертає текст відповіді.
91
- * `omlx/...` прямий HTTP до omlx (text-only, локально); решта → pi CLI.
90
+ * Викликає LLM через спільний `callLlm` (маршрут за префіксом model-id; wire-trace).
91
+ * Зберігає дружнє повідомлення про відсутній API-ключ для хмарних провайдерів.
92
92
  * @param {string} prompt текст промпта
93
93
  * @param {string} model назва моделі (provider/id, `omlx/...` або '')
94
94
  * @returns {{ text: string, error?: string }} текст відповіді або повідомлення про помилку
95
95
  */
96
96
  function callModel(prompt, model) {
97
- if (isOmlxModel(model)) {
98
- try {
99
- return { text: callOmlx([{ role: 'user', content: prompt }], model, { timeoutMs: 120_000 }) }
100
- } catch (error) {
101
- return { text: '', error: error.message }
102
- }
103
- }
104
- const modelArgs = model ? ['--model', model] : []
105
- const r = spawnSync('pi', ['-p', prompt, ...modelArgs, '--no-session', '--mode', 'text', '--no-tools'], {
106
- encoding: 'utf8',
107
- timeout: 120_000
108
- })
109
- if (r.error) return { text: '', error: r.error.message }
110
- if (r.status !== 0) {
111
- const stderr = r.stderr?.slice(0, 300) ?? ''
112
- if (stderr.toLowerCase().includes('no api key') || stderr.toLowerCase().includes('api key')) {
97
+ try {
98
+ return { text: callLlm([{ role: 'user', content: prompt }], model, { timeoutMs: 120_000, caller: 'fix' }) }
99
+ } catch (error) {
100
+ const msg = String(error.message)
101
+ if (API_KEY_RE.test(msg)) {
113
102
  const provider = model ? model.split('/')[0] : 'дефолтного провайдера'
114
103
  return {
115
104
  text: '',
@@ -120,9 +109,8 @@ function callModel(prompt, model) {
120
109
  ].join(' ')
121
110
  }
122
111
  }
123
- return { text: '', error: `pi exit ${r.status}: ${stderr}` }
112
+ return { text: '', error: msg }
124
113
  }
125
- return { text: r.stdout?.trim() ?? '' }
126
114
  }
127
115
 
128
116
  /**