Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@nitra/cursor

Package Overview
Dependencies
Maintainers
1
Versions
401
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nitra/cursor - npm Package Compare versions

Comparing version
5.0.3
to
5.1.0
+102
lib/omlx.mjs
/**
* Спільний транспорт до локального omlx-сервера (OpenAI-сумісний MLX,
* `http://localhost:8000/v1/chat/completions`). Text-only: жодних `tools`/
* `tool_calls` — сервер їх не підтримує (див. ADR
* `260610-1349-агентна-пастка-js-owned-loop-через-omlx-замість-pi-tool-loop`).
*
* Маршрутизація між omlx і pi — за конвенцією префікса в model-id:
* `omlx/<model>` → прямий HTTP до omlx (локальний inference, без pi)
* будь-що інше → pi CLI (хмарні провайдери або pi-дефолт)
*
* Так `resolveModel(tier)` лишається незмінним: достатньо виставити локальний
* тир у форматі `N_LOCAL_MIN_MODEL=omlx/mlx-community--gemma-4-e2b-it-4bit`, і
* виклик сам піде напряму в omlx замість pi.
*/
import { spawnSync } from 'node:child_process'
import { env } from 'node:process'
/** Дефолтний endpoint omlx (override — `N_CURSOR_OMLX_URL`). */
export const DEFAULT_OMLX_URL = 'http://127.0.0.1:8000/v1/chat/completions'
/** Дефолтна модель, якщо в id лишився голий `omlx/` (override — `N_CURSOR_OMLX_MODEL`). */
export const DEFAULT_OMLX_MODEL = 'mlx-community--gemma-4-e2b-it-4bit'
const OMLX_PREFIX = 'omlx/'
/**
* Чи цей model-id адресує локальний omlx-бекенд (префікс `omlx/`).
* @param {unknown} model перевірюваний model-id
* @returns {boolean} true, якщо рядок починається з `omlx/`
*/
export function isOmlxModel(model) {
return typeof model === 'string' && model.startsWith(OMLX_PREFIX)
}
/**
* Прибирає `omlx/`-префікс → чистий model-id для omlx API.
* Не-omlx-рядки повертає без змін.
* @param {string} model model-id (можливо з префіксом)
* @returns {string} model-id без `omlx/`
*/
export function omlxModelId(model) {
return isOmlxModel(model) ? model.slice(OMLX_PREFIX.length) : model
}
/**
* Прямий HTTP-виклик до omlx через `curl` (spawnSync). Повертає текст
* `choices[0].message.content`. Ретраїть лише transient curl-помилки
* (18 = transfer closed, 52 = empty reply, 56 = recv failure).
*
* @param {Array<{role:string, content:string}>} messages OpenAI-messages (system+user збережено)
* @param {string} model model-id (з/без `omlx/`-префікса); порожній → дефолт
* @param {{ url?: string, timeoutMs?: number, temperature?: number, maxTokens?: number, fallbackModel?: string }} [opts]
* @returns {string} непорожній контент відповіді
* @throws на curl-помилці, не-200 exit, поганому JSON чи порожньому контенті
*/
export function callOmlx(messages, model, opts = {}) {
const {
url = env.N_CURSOR_OMLX_URL ?? DEFAULT_OMLX_URL,
timeoutMs = 60_000,
temperature = 0.2,
maxTokens = 4096,
fallbackModel = env.N_CURSOR_OMLX_MODEL ?? DEFAULT_OMLX_MODEL
} = opts
const m = omlxModelId(model) || fallbackModel
const body = JSON.stringify({ model: m, messages, max_tokens: maxTokens, temperature })
const TRANSIENT_CURL_CODES = new Set([18, 52, 56])
let lastErr
for (let attempt = 1; attempt <= 3; attempt++) {
const r = spawnSync(
'curl',
['-sS', '-X', 'POST', url, '-H', 'Content-Type: application/json', '-H', 'Connection: close', '--max-time', String(Math.ceil(timeoutMs / 1000)), '--data-binary', '@-'],
{ input: body, encoding: 'utf8', timeout: timeoutMs + 5000 }
)
if (r.error) {
lastErr = new Error(`omlx curl error: ${r.error.message}`)
break
}
if (r.status !== 0) {
if (TRANSIENT_CURL_CODES.has(r.status) && attempt < 3) {
lastErr = new Error(`omlx curl exit ${r.status} (transient, retry ${attempt})`)
continue
}
throw new Error(`omlx curl exit ${r.status}: ${r.stderr?.slice(0, 300) ?? ''}`)
}
let j
try {
j = JSON.parse(r.stdout)
} catch {
throw new Error(`omlx bad json: ${r.stdout?.slice(0, 200) ?? ''}`)
}
if (j.error) throw new Error(`omlx api: ${JSON.stringify(j.error).slice(0, 300)}`)
const content = j.choices?.[0]?.message?.content?.trim() ?? ''
if (!content) {
const finish = j.choices?.[0]?.finish_reason
throw new Error(`omlx empty content (finish=${finish})`)
}
return content
}
throw lastErr ?? new Error('omlx unknown failure')
}
+9
-1

@@ -8,3 +8,3 @@ /**

* Приклад ~/.bashrc або .env:
* N_LOCAL_MIN_MODEL=ollama/gemma3:4b
* N_LOCAL_MIN_MODEL=omlx/mlx-community--gemma-4-e2b-it-4bit
* N_CLOUD_MIN_MODEL=openai/gpt-5.4-mini

@@ -16,2 +16,10 @@ * N_CLOUD_AVG_MODEL=openai/gpt-5.4

*
* ## Бекенд за префіксом model-id
*
* model-id з префіксом `omlx/...` маршрутизується прямим HTTP до локального
* omlx-сервера (`npm/lib/omlx.mjs`), минаючи pi; решта (`openai/...`,
* `ollama/...`, '') — через pi CLI. Тому локальні тири варто задавати у форматі
* `omlx/<model>`, аби local-inference йшов напряму, а pi лишався шаром для хмари
* (див. ADR 260610-1349).
*
* ## Каскад local → cloud (контракт)

@@ -18,0 +26,0 @@ *

+1
-1
{
"name": "@nitra/cursor",
"version": "5.0.3",
"version": "5.1.0",
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",

@@ -5,0 +5,0 @@ "keywords": [

@@ -6,5 +6,9 @@ /**

* 1. Cache lookup → hit → використати збережений verdict.
* 2. Cache miss → Tier 1 (LOCAL_MIN через pi) → parseVerdict.
* 3. Tier 1 fail (pi error / bad JSON / Zod) → Tier 2 (CLOUD_MIN через pi).
* 2. Cache miss → Tier 1 (resolveModel('min')) → parseVerdict.
* 3. Tier 1 fail (model error / bad JSON / Zod) → Tier 2 (CLOUD_MIN через pi).
* 4. Tier 2 fail → conservative fallback worth-testing/confidence=0.
*
* Бекенд обирається за model-id: `omlx/...` → прямий HTTP до omlx (локально),
* решта → pi CLI. Якщо omlx-Tier 1 недоступний, помилка падає в той самий catch
* і класифікація відкочується на хмарний Tier 2 через pi.
*/

@@ -15,2 +19,3 @@ import { spawnSync } from 'node:child_process'

import { CLOUD_MIN, resolveModel } from '../../lib/models.mjs'
import { callOmlx, isOmlxModel } from '../../lib/omlx.mjs'
import { deriveCacheKey, readCache, writeCache } from './cache.mjs'

@@ -27,9 +32,13 @@ import { buildUserPrompt, SYSTEM_PROMPT } from './prompt.mjs'

/**
* Викликає pi і повертає raw stdout.
* Викликає LLM за model-id і повертає raw текст відповіді.
* `omlx/...` → прямий HTTP до omlx (text-only); решта → pi CLI.
* @param {string} prompt текст промпта
* @param {string} model provider/model-id або '' для pi-дефолту
* @returns {string} stdout pi-процесу
* @throws якщо pi не знайдено або повертає ненульовий exit code
* @param {string} model provider/model-id, `omlx/...` або '' для pi-дефолту
* @returns {string} текст відповіді моделі
* @throws якщо backend недоступний або повертає помилку
*/
function callPi(prompt, model) {
function callModel(prompt, model) {
if (isOmlxModel(model)) {
return callOmlx([{ role: 'user', content: prompt }], model, { timeoutMs: 60_000 })
}
const modelArgs = model ? ['--model', model] : []

@@ -50,6 +59,6 @@ const r = spawnSync('pi', ['-p', prompt, ...modelArgs, '--no-session', '--mode', 'text', '--no-tools'], {

* @param {string} cwd корінь проєкту
* @param {(prompt: string, model: string) => string} callPiFn ін'єкція для тестів
* @param {(prompt: string, model: string) => string} callModelFn ін'єкція для тестів
* @returns {object} verdict класифікації
*/
function classifyOne(group, mutant, cwd, callPiFn) {
function classifyOne(group, mutant, cwd, callModelFn) {
const prompt = `${SYSTEM_PROMPT}\n\n${buildUserPrompt({ ...mutant, file: group.file }, cwd)}`

@@ -60,3 +69,3 @@ const loc = `${group.file}:${mutant.line}:${mutant.col}`

try {
const text = callPiFn(prompt, resolveModel('min'))
const text = callModelFn(prompt, resolveModel('min'))
return parseVerdict(text)

@@ -66,3 +75,3 @@ } catch {

try {
const text = callPiFn(prompt, CLOUD_MIN)
const text = callModelFn(prompt, CLOUD_MIN)
return parseVerdict(text)

@@ -77,6 +86,6 @@ } catch (error) {

/**
* Класифікує survived мутантів через pi (LOCAL_MIN → CLOUD_MIN → fallback).
* Класифікує survived мутантів (resolveModel('min') → CLOUD_MIN → fallback).
* @param {Array<{file: string, mutants: object[], exampleTest?: object|null, recommendationText?: string|null}>} survived список вцілілих мутантів
* @param {string} cwd корінь проєкту
* @param {{cachePath?: string, callPi?: Function}} [opts] ін'єкції для тестів
* @param {{cachePath?: string, callModel?: Function}} [opts] ін'єкції для тестів
* @returns {Promise<Array<{key: string, verdict: object}>>} verdicts

@@ -86,3 +95,3 @@ */

const cachePath = opts.cachePath ?? join(cwd, 'npm/reports/coverage-classify.cache.json')
const callPiFn = opts.callPi ?? callPi
const callModelFn = opts.callModel ?? callModel
const cacheModel = `${resolveModel('min') || 'default'}+${CLOUD_MIN || 'cloud'}`

@@ -113,3 +122,3 @@

if (!verdict) {
verdict = classifyOne(group, mutant, cwd, callPiFn)
verdict = classifyOne(group, mutant, cwd, callModelFn)
if (cacheKey) {

@@ -116,0 +125,0 @@ cache.entries[cacheKey] = { ...verdict, classifiedAt: new Date().toISOString() }

@@ -80,3 +80,3 @@ /**

if (!a.length) return 0
const s = [...a].sort((x, y) => x - y)
const s = a.toSorted((x, y) => x - y)
return s[Math.floor(s.length / 2)]

@@ -83,0 +83,0 @@ }

@@ -49,3 +49,3 @@ /**

export function extractAnchors(src) {
const urls = uniq([...src.matchAll(URL_RE)].map(m => m[0]))
const urls = uniq(Array.from(src.matchAll(URL_RE), m => m[0]))

@@ -63,8 +63,8 @@ const magicStrings = []

const errorMarkers = uniq([...src.matchAll(ERROR_MARKER_RE)].map(m => m[1]))
const configRefs = uniq([...src.matchAll(CONFIG_REF_RE)].map(m => m[1]))
const errorMarkers = uniq(Array.from(src.matchAll(ERROR_MARKER_RE), m => m[1]))
const configRefs = uniq(Array.from(src.matchAll(CONFIG_REF_RE), m => m[1]))
// Витягуємо code-block приклади тільки з file-header — там автор зазвичай показує контракт.
const headerMatch = src.match(FILE_HEADER_RE)
const examples = headerMatch ? uniq([...headerMatch[1].matchAll(CODE_BLOCK_RE)].map(m => m[1].trim())) : []
const examples = headerMatch ? uniq(Array.from(headerMatch[1].matchAll(CODE_BLOCK_RE), m => m[1].trim())) : []

@@ -71,0 +71,0 @@ return { urls, magicStrings, errorMarkers, configRefs, examples }

@@ -7,2 +7,3 @@ /** @see ./docs/docgen-gen.md */

import { resolveModel } from '../../../lib/models.mjs'
import { callOmlx } from '../../../lib/omlx.mjs'
import { extractFacts } from './docgen-extract.mjs'

@@ -96,46 +97,12 @@ import { extractAnchors } from './docgen-extract-anchors.mjs'

* omlx-бекенд: справжні OpenAI-сумісні messages (system+user збереженi).
* Вмикається `N_CURSOR_DOCGEN_BACKEND=omlx`.
* URL: `N_CURSOR_DOCGEN_OMLX_URL` або http://127.0.0.1:8000/v1/chat/completions.
* Модель: переданий `model`, потім `N_CURSOR_DOCGEN_OMLX_MODEL`, потім дефолт.
* Вмикається `N_CURSOR_DOCGEN_BACKEND=omlx`. Делегує у спільний `callOmlx`
* (npm/lib/omlx.mjs) з docgen-специфічними env-дефолтами URL/моделі.
*/
function callOmlxMessages(messages, model, timeoutMs, temperature = 0.2) {
const url = env.N_CURSOR_DOCGEN_OMLX_URL ?? 'http://127.0.0.1:8000/v1/chat/completions'
const m = model || env.N_CURSOR_DOCGEN_OMLX_MODEL || 'mlx-community--gemma-4-e2b-it-4bit'
const body = JSON.stringify({
model: m,
messages,
max_tokens: 4096,
temperature
return callOmlx(messages, model, {
url: env.N_CURSOR_DOCGEN_OMLX_URL,
timeoutMs,
temperature,
fallbackModel: env.N_CURSOR_DOCGEN_OMLX_MODEL
})
// Ретраїмо лише transient curl-помилки (18 = transfer closed, 56 = recv failure, 52 = empty reply).
const TRANSIENT_CURL_CODES = new Set([18, 52, 56])
let lastErr
for (let attempt = 1; attempt <= 3; attempt++) {
const r = spawnSync(
'curl',
['-sS', '-X', 'POST', url, '-H', 'Content-Type: application/json', '-H', 'Connection: close', '--max-time', String(Math.ceil(timeoutMs / 1000)), '--data-binary', '@-'],
{ input: body, encoding: 'utf8', timeout: timeoutMs + 5000 }
)
if (r.error) {
lastErr = new Error(`omlx curl error: ${r.error.message}`)
break
}
if (r.status !== 0) {
if (TRANSIENT_CURL_CODES.has(r.status) && attempt < 3) {
lastErr = new Error(`omlx curl exit ${r.status} (transient, retry ${attempt})`)
continue
}
throw new Error(`omlx curl exit ${r.status}: ${r.stderr?.slice(0, 300) ?? ''}`)
}
let j
try { j = JSON.parse(r.stdout) } catch { throw new Error(`omlx bad json: ${r.stdout?.slice(0, 200) ?? ''}`) }
if (j.error) throw new Error(`omlx api: ${JSON.stringify(j.error).slice(0, 300)}`)
const content = j.choices?.[0]?.message?.content?.trim() ?? ''
if (!content) {
const finish = j.choices?.[0]?.finish_reason
throw new Error(`omlx empty content (finish=${finish})`)
}
return content
}
throw lastErr ?? new Error('omlx unknown failure')
}

@@ -142,0 +109,0 @@

@@ -8,2 +8,3 @@ /** @see ./docs/llm-worker.md */

import { resolveModel } from '../../../lib/models.mjs'
import { callOmlx, isOmlxModel } from '../../../lib/omlx.mjs'

@@ -90,8 +91,16 @@ // Тир за замовчуванням: min → avg при ескалації (каскад local→cloud).

/**
* Запускає pi і повертає stdout як рядок.
* Викликає LLM за model-id і повертає текст відповіді.
* `omlx/...` → прямий HTTP до omlx (text-only, локально); решта → pi CLI.
* @param {string} prompt текст промпта
* @param {string} model назва моделі (provider/id)
* @returns {{ text: string, error?: string }} stdout pi або повідомлення про помилку
* @param {string} model назва моделі (provider/id, `omlx/...` або '')
* @returns {{ text: string, error?: string }} текст відповіді або повідомлення про помилку
*/
function callPi(prompt, model) {
function callModel(prompt, model) {
if (isOmlxModel(model)) {
try {
return { text: callOmlx([{ role: 'user', content: prompt }], model, { timeoutMs: 120_000 }) }
} catch (error) {
return { text: '', error: error.message }
}
}
const modelArgs = model ? ['--model', model] : []

@@ -122,5 +131,5 @@ const r = spawnSync('pi', ['-p', prompt, ...modelArgs, '--no-session', '--mode', 'text', '--no-tools'], {

/**
* Парсить JSON-відповідь від pi.
* pi може обгорнути JSON у ```json ... ```, тому пробуємо витягти.
* @param {string} text сирий stdout pi
* Парсить JSON-відповідь від моделі.
* Модель може обгорнути JSON у ```json ... ```, тому пробуємо витягти.
* @param {string} text сирий текст відповіді
* @returns {{ changes: Array<{path:string,content:string}>, error?: string } | null} розпарсений патч або null

@@ -189,8 +198,8 @@ */

// 3. Будуємо prompt і викликаємо pi
// 3. Будуємо prompt і викликаємо модель
const prompt = buildPrompt(ruleId, ruleMdc, violationOutput, files)
const { text, error: piError } = callPi(prompt, model)
const { text, error: modelError } = callModel(prompt, model)
if (piError) return { ok: false, error: piError }
if (!text) return { ok: false, error: 'pi returned empty response' }
if (modelError) return { ok: false, error: modelError }
if (!text) return { ok: false, error: 'model returned empty response' }

@@ -197,0 +206,0 @@ // 4. Парсимо відповідь

Sorry, the diff of this file is too big to display