@7n/test 0.9.0 → 0.10.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 (41) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +4 -2
  3. package/src/assess-need.mjs +4 -1
  4. package/src/classify-exports.mjs +108 -0
  5. package/src/coverage-classify/docs/apply.md +28 -0
  6. package/src/coverage-classify/docs/cache.md +34 -0
  7. package/src/coverage-classify/docs/index.md +40 -0
  8. package/src/coverage-classify/docs/prompt.md +30 -0
  9. package/src/coverage-classify/docs/verdict-schema.md +29 -0
  10. package/src/coverage-fix-extract.mjs +10 -10
  11. package/src/coverage-per-file.mjs +68 -43
  12. package/src/docs/assess-need.md +31 -0
  13. package/src/docs/classify-exports.md +30 -0
  14. package/src/docs/coverage-fix-extract.md +35 -0
  15. package/src/docs/coverage-fix.md +31 -0
  16. package/src/docs/coverage-per-file.md +34 -0
  17. package/src/docs/fix-tests.md +32 -0
  18. package/src/docs/gen-tests.md +38 -0
  19. package/src/docs/index.md +34 -0
  20. package/src/docs/run.md +33 -0
  21. package/src/fix-tests.mjs +233 -87
  22. package/src/gen-tests.mjs +1102 -49
  23. package/src/lib/ast-analyze.mjs +287 -0
  24. package/src/lib/docs/ast-analyze.md +45 -0
  25. package/src/lib/docs/index.md +14 -0
  26. package/src/lib/docs/pi-client.md +29 -0
  27. package/src/lib/docs/runtime-probe.md +36 -0
  28. package/src/lib/docs/vitest-shim.md +31 -0
  29. package/src/lib/pi-client.mjs +18 -3
  30. package/src/lib/runtime-probe.mjs +390 -0
  31. package/src/lib/vitest-shim.mjs +73 -0
  32. package/src/run.mjs +3 -1
  33. package/src/scripts/lib/changed-files.mjs +5 -5
  34. package/src/scripts/lib/docs/changed-files.md +32 -0
  35. package/src/scripts/lib/docs/read-n-cursor-config-lite.md +39 -0
  36. package/src/scripts/utils/docs/index.md +13 -0
  37. package/src/scripts/utils/docs/lock-cache-dir.md +32 -0
  38. package/src/scripts/utils/docs/with-lock.md +38 -0
  39. package/src/scripts/utils/docs/worktree-fingerprint.md +34 -0
  40. package/src/scripts/utils/lock-cache-dir.mjs +1 -1
  41. package/src/scripts/utils/with-lock.mjs +2 -3
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Static analysis of JS/ESM source files via oxc (rollup/parseAst).
3
+ *
4
+ * Extracts:
5
+ * - externalMocks — exact vi.mock() lines with shapes derived from import usage
6
+ * - exportedNames — names of exported symbols
7
+ * - internalNames — non-exported top-level declarations
8
+ * - hasSideEffects — top-level call expressions (e.g. checkEnv([...]))
9
+ * - envReads — process.env / env.KEY accesses (for vi.stubEnv hints)
10
+ * - usesFetch — whether source calls fetch()
11
+ */
12
+ import { parseAstAsync } from 'rollup/parseAst'
13
+
14
+ const FETCH_CALL_RE = /\bfetch\s*\(/
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // AST walker
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /**
21
+ * Обходить AST та викликає callback для кожного вузла.
22
+ * @param {unknown} node AST-вузол або значення вузла
23
+ * @param {(node: Record<string, unknown>) => void} fn callback для кожного вузла
24
+ * @returns {void}
25
+ */
26
+ function walk(node, fn) {
27
+ if (!node || typeof node !== 'object') return
28
+ fn(/** @type {Record<string, unknown>} */ (node))
29
+ for (const val of Object.values(node)) {
30
+ if (Array.isArray(val)) {
31
+ for (const child of val) walk(child, fn)
32
+ } else if (val && typeof val === 'object' && val.type) {
33
+ walk(val, fn)
34
+ }
35
+ }
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Mock shape derivation
40
+ // ---------------------------------------------------------------------------
41
+
42
+ /**
43
+ * Collects all member-access chains for a binding name.
44
+ * log.error(x) → ['error']
45
+ * log → [] (direct call/use)
46
+ * @param {unknown} ast parsed AST
47
+ * @param {string} binding import binding name
48
+ * @returns {string[][]} member-access chains for the binding
49
+ */
50
+ function collectUsagePaths(ast, binding) {
51
+ const paths = []
52
+ walk(ast, node => {
53
+ if (node.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === binding) {
54
+ paths.push([])
55
+ return
56
+ }
57
+ if (node.type === 'MemberExpression' && !node.computed) {
58
+ const chain = []
59
+ let cur = node
60
+ while (cur?.type === 'MemberExpression' && !cur.computed) {
61
+ chain.unshift(cur.property?.name ?? '')
62
+ cur = cur.object
63
+ }
64
+ if (cur?.type === 'Identifier' && cur.name === binding) {
65
+ paths.push(chain)
66
+ }
67
+ }
68
+ })
69
+ return paths
70
+ }
71
+
72
+ /**
73
+ * Builds a nested shape object from usage paths.
74
+ * [['error'], ['warn']] → { error: 'vi.fn()', warn: 'vi.fn()' }
75
+ * [[]] → 'vi.fn()' (direct call)
76
+ * @param {string[][]} paths usage chains for one binding
77
+ * @returns {string | Record<string, unknown>} mock shape
78
+ */
79
+ function buildShape(paths) {
80
+ if (!paths.length || paths.some(p => p.length === 0)) return 'vi.fn()'
81
+ const shape = {}
82
+ for (const path of paths) {
83
+ let obj = shape
84
+ for (let i = 0; i < path.length - 1; i++) {
85
+ if (!obj[path[i]] || typeof obj[path[i]] === 'string') obj[path[i]] = {}
86
+ obj = obj[path[i]]
87
+ }
88
+ const last = path.at(-1)
89
+ if (!obj[last]) obj[last] = 'vi.fn()'
90
+ }
91
+ return shape
92
+ }
93
+
94
+ /**
95
+ * Serializes a mock shape back to formatted JS object text.
96
+ * @param {string | Record<string, unknown>} shape mock shape tree
97
+ * @param {number} depth indentation depth
98
+ * @returns {string} formatted JS object text
99
+ */
100
+ function serializeShape(shape, depth = 0) {
101
+ if (shape === 'vi.fn()') return 'vi.fn()'
102
+ const pad = ' '.repeat(depth + 1)
103
+ const close = ' '.repeat(depth)
104
+ const entries = Object.entries(shape).map(([k, v]) => `${pad}${k}: ${serializeShape(v, depth + 1)}`)
105
+ return `{
106
+ ${entries.join(',\n')}
107
+ ${close}}`
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Env reads
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /**
115
+ * Collects env keys read as `env.KEY` or `process.env.KEY`.
116
+ * @param {unknown} ast parsed AST
117
+ * @returns {string[]} unique env keys
118
+ */
119
+ function collectEnvReads(ast) {
120
+ const keys = new Set()
121
+ walk(ast, node => {
122
+ // env.KEY or process.env.KEY
123
+ if (
124
+ node.type === 'MemberExpression' &&
125
+ !node.computed &&
126
+ node.object?.type === 'Identifier' &&
127
+ (node.object.name === 'env' || node.object.name === 'process')
128
+ ) {
129
+ const prop = node.property?.name
130
+ if (prop === 'env') return // process.env itself, not a key
131
+ if (node.object.name === 'env' && prop) {
132
+ keys.add(prop)
133
+ }
134
+ }
135
+ if (
136
+ node.type === 'MemberExpression' &&
137
+ !node.computed &&
138
+ node.object?.type === 'MemberExpression' &&
139
+ node.object.object?.name === 'process' &&
140
+ node.object.property?.name === 'env' &&
141
+ node.property?.name
142
+ ) {
143
+ keys.add(node.property.name)
144
+ }
145
+ })
146
+ return [...keys]
147
+ }
148
+
149
+ /**
150
+ * Maps import bindings to package names.
151
+ * @param {unknown} ast parsed AST
152
+ * @returns {Map<string, string>} binding → package map
153
+ */
154
+ function collectBindingToPkg(ast) {
155
+ const bindingToPkg = new Map()
156
+ for (const node of ast.body) {
157
+ if (node.type !== 'ImportDeclaration') continue
158
+ const pkg = node.source.value
159
+ if (pkg.startsWith('node:') || pkg.startsWith('.')) continue
160
+ for (const spec of node.specifiers ?? []) {
161
+ bindingToPkg.set(spec.local.name, pkg)
162
+ }
163
+ }
164
+ return bindingToPkg
165
+ }
166
+
167
+ /**
168
+ * Collects mock shapes for every package based on binding usage.
169
+ * @param {unknown} ast parsed AST
170
+ * @param {Map<string, string>} bindingToPkg binding → package map
171
+ * @returns {Map<string, Record<string, string | Record<string, unknown>>>} package shapes
172
+ */
173
+ function collectPackageShapes(ast, bindingToPkg) {
174
+ const pkgShapes = new Map()
175
+ for (const [binding, pkg] of bindingToPkg) {
176
+ const paths = collectUsagePaths(ast, binding)
177
+ const shape = buildShape(paths)
178
+ if (!pkgShapes.has(pkg)) pkgShapes.set(pkg, {})
179
+ pkgShapes.get(pkg)[binding] = shape
180
+ }
181
+ return pkgShapes
182
+ }
183
+
184
+ /**
185
+ * Converts package shapes to `vi.mock(...)` lines.
186
+ * @param {Map<string, Record<string, string | Record<string, unknown>>>} pkgShapes package shapes
187
+ * @returns {Array<{pkg: string, mockLine: string}>} mock line definitions
188
+ */
189
+ function collectExternalMocks(pkgShapes) {
190
+ const externalMocks = []
191
+ for (const [pkg, bindings] of pkgShapes) {
192
+ const shape =
193
+ Object.keys(bindings).length === 1 && Object.values(bindings)[0] === 'vi.fn()'
194
+ ? `{ ${Object.keys(bindings)[0]}: vi.fn() }`
195
+ : serializeShape(bindings)
196
+ externalMocks.push({ pkg, mockLine: `vi.mock("${pkg}", () => (${shape}))` })
197
+ }
198
+ return externalMocks
199
+ }
200
+
201
+ /**
202
+ * Collects explicitly exported top-level names.
203
+ * @param {unknown} ast parsed AST
204
+ * @returns {string[]} exported names
205
+ */
206
+ function collectExportedNames(ast) {
207
+ const exportedNames = []
208
+ for (const node of ast.body) {
209
+ if (node.type !== 'ExportNamedDeclaration' || !node.declaration) continue
210
+ if (node.declaration.type === 'VariableDeclaration') {
211
+ exportedNames.push(...node.declaration.declarations.map(d => d.id?.name).filter(Boolean))
212
+ } else if (node.declaration.id?.name) {
213
+ exportedNames.push(node.declaration.id.name)
214
+ }
215
+ }
216
+ return exportedNames
217
+ }
218
+
219
+ /**
220
+ * Collects non-exported top-level declarations.
221
+ * @param {unknown} ast parsed AST
222
+ * @returns {string[]} internal names
223
+ */
224
+ function collectInternalNames(ast) {
225
+ const internalNames = []
226
+ for (const node of ast.body) {
227
+ if (node.type === 'VariableDeclaration') {
228
+ internalNames.push(...node.declarations.map(d => d.id?.name).filter(Boolean))
229
+ } else if (node.type === 'FunctionDeclaration' && node.id?.name) {
230
+ internalNames.push(node.id.name)
231
+ }
232
+ }
233
+ return internalNames
234
+ }
235
+
236
+ /**
237
+ * Detects top-level call expressions.
238
+ * @param {unknown} ast parsed AST
239
+ * @returns {boolean} true when module has top-level side effects
240
+ */
241
+ function hasTopLevelSideEffects(ast) {
242
+ return ast.body.some(n => n.type === 'ExpressionStatement' && n.expression?.type === 'CallExpression')
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Main export
247
+ // ---------------------------------------------------------------------------
248
+
249
+ /**
250
+ * Analyses ESM source and returns structured info for test generation.
251
+ * @param {string} source JS/ESM source text
252
+ * @returns {Promise<{
253
+ * externalMocks: Array<{pkg: string, mockLine: string}>,
254
+ * exportedNames: string[],
255
+ * internalNames: string[],
256
+ * hasSideEffects: boolean,
257
+ * envReads: string[],
258
+ * usesFetch: boolean,
259
+ * }>} структуровані дані аналізу модуля
260
+ */
261
+ export async function analyzeModule(source) {
262
+ let ast
263
+ try {
264
+ ast = await parseAstAsync(source)
265
+ } catch {
266
+ return {
267
+ externalMocks: [],
268
+ exportedNames: [],
269
+ internalNames: [],
270
+ hasSideEffects: false,
271
+ envReads: [],
272
+ usesFetch: false
273
+ }
274
+ }
275
+
276
+ const bindingToPkg = collectBindingToPkg(ast)
277
+ const pkgShapes = collectPackageShapes(ast, bindingToPkg)
278
+
279
+ return {
280
+ externalMocks: collectExternalMocks(pkgShapes),
281
+ exportedNames: collectExportedNames(ast),
282
+ internalNames: collectInternalNames(ast),
283
+ hasSideEffects: hasTopLevelSideEffects(ast),
284
+ envReads: collectEnvReads(ast),
285
+ usesFetch: FETCH_CALL_RE.test(source)
286
+ }
287
+ }
@@ -0,0 +1,45 @@
1
+ ---
2
+ type: JS Module
3
+ title: ast-analyze.mjs
4
+ resource: npm/src/lib/ast-analyze.mjs
5
+ docgen:
6
+ crc: 7e4d00fc
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ issues: judge:inaccurate:0.98
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ Цей модуль виконує статичний аналіз файлів JS/ESM за допомогою oxc. Він витягує точні рядки `vi.mock` та відповідні форми (shapes) на основі використання імпорту, зіставляє експортовані та внутрішні символи, визначає побічні ефекти, читання змінних середовища (`process.env`/`env.KEY`) та використання `fetch` у коді.
16
+
17
+ ## Поведінка
18
+
19
+ 1. Викликає функцію analyzeModule для аналізу коду.
20
+ 2. Парсить вхідний код за допомогою rollup/parseAst. У разі помилки парсингу повертає структуру з порожніми значеннями для всіх полів.
21
+ 3. Визначає прив'язки імпортів, зіставляючи їх з зовнішніми пакетами.
22
+ 4. Для кожної прив'язки аналізує всі шляхи використання, щоб побудувати структуру заглушки (mock shape).
23
+ 5. Формує список `externalMocks` у вигляді рядків `vi.mock` на основі виявлених заглушок.
24
+ 6. Сканує тіло коду, щоб зібрати назви всіх символів, які явно експортуються.
25
+ 7. Сканує тіло коду, щоб зібрати назви усіх оголошень верхнього рівня (змінних чи функцій), які не є експортованими.
26
+ 8. Перевіряє наявність виразів виклику на верхньому рівні, щоб визначити, чи є у модулі побічні ефекти.
27
+ 9. Збирає всі ключі, які зчитуються з середовища (`env` або `process.env`).
28
+ 10. Перевіряє вихідний код на наявність викликів `fetch`.
29
+ 11. Повертає об'єкт, що містить результати: `externalMocks`, `exportedNames`, `internalNames`, `hasSideEffects`, `envReads`, та `usesFetch`.
30
+
31
+ ## Публічний API
32
+
33
+ analyzeModule — аналізує вихідний код ESM та повертає структуровану інформацію для створення тестів.
34
+
35
+ externalMocks — масив об'єктів, що містить необхідні моки для зовнішніх пакетів, включаючи сам рядок моку.
36
+ exportedNames — список назв, які експортуються із модуля.
37
+ internalNames — список внутрішніх (неекспортованих) назв елементів у модулі.
38
+ hasSideEffects — булеве значення, що вказує на наявність побічних ефектів у модулі.
39
+ envReads — список змінних середовища, які зчитує модуль.
40
+ usesFetch — булеве значення, що вказує на використання функції `fetch` у модулі.
41
+
42
+ ## Гарантії поведінки
43
+
44
+ - Read-only: не виконує операцій запису (ФС/БД).
45
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
@@ -0,0 +1,14 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/src/lib
4
+ resource: npm/src/lib/
5
+ ---
6
+
7
+ # npm/src/lib
8
+
9
+ | Файл | Тип |
10
+ | ------------------------------------- | --------- |
11
+ | [ast-analyze.mjs](ast-analyze.md) | JS Module |
12
+ | [pi-client.mjs](pi-client.md) | JS Module |
13
+ | [runtime-probe.mjs](runtime-probe.md) | JS Module |
14
+ | [vitest-shim.mjs](vitest-shim.md) | JS Module |
@@ -0,0 +1,29 @@
1
+ ---
2
+ type: JS Module
3
+ title: pi-client.mjs
4
+ resource: npm/src/lib/pi-client.mjs
5
+ docgen:
6
+ crc: 4649ce83
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ issues: judge:inaccurate:0.98
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ Цей модуль надає прямий, інпроцесний програмний інтерфейс для взаємодії з агентами pi SDK, замінюючи синхронні виклики зовнішнього процесу. Він реалізує два режими роботи: текстовий режим, де функція `callText` повертає рядок на основі текстового запиту без активації інструментів, та режим агента, де функція `callAgent` активує кодові інструменти для прямого внесення змін у файли у заданій робочій директорії. Зверніть увагу, що у цьому інтерфейсі відсутній механізм кешування.
16
+
17
+ ## Поведінка
18
+
19
+ callText відправляє текстовий запит до моделі pi та повертає текстову відповідь.
20
+ callAgent відправляє завдання до агента pi з активними інструментами кодування та виконує зміни у файлах у вказаній директорії.
21
+
22
+ ## Публічний API
23
+
24
+ callText — надсилає одноразовий запит у текстовому режимі, не використовуючи інструменти, і повертає відповідь, зчитуючи конфігурацію з ~/.pi/.
25
+ callAgent — надсилає запит у режимі агента, надаючи доступ до повного набору інструментів кодування (читання, запис, bash, редагування); агент сам створює тестові файли.
26
+
27
+ ## Гарантії поведінки
28
+
29
+ - (специфічних машинно-виведених гарантій немає)
@@ -0,0 +1,36 @@
1
+ ---
2
+ type: JS Module
3
+ title: runtime-probe.mjs
4
+ resource: npm/src/lib/runtime-probe.mjs
5
+ docgen:
6
+ crc: 31dd4fd6
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ issues: judge:inaccurate:0.98
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ Файл проводить runtime probing експортованих функцій через чотири стратегії: `probeModule` викликає кожен експорт з граничними примітивами для отримання фактичних вихідних даних; `probeFetchCalls` перехоплює глобальні HTTP-запити, щоб фіксувати реальні URL та ініціалізацію для кожного експорту; `probeTimeVariants` запускає кожен експорт у фіксовані часові вікна ([0,9,14,22]) для виявлення чутливих до часу змін; а `probeHelpers` витягує неекспортовані допоміжні функції з вихідного коду для визначення їхніх фактичних форм виходу. Усі стратегії мають принцип best-effort: у разі будь-якої помилки вони повертають порожнє значення, не викидаючи винятків. Функціонал не залежить від кешування.
16
+
17
+ ## Поведінка
18
+
19
+ Поведінка:
20
+ probeModule викликає кожен експортований функціонал з граничними примітивами, щоб визначити фактичні вихідні дані.
21
+ probeFetchCalls перехоплює глобальний запит `fetch` та збирає реальні URL та ініціалізацію для кожного експортованого функціоналу.
22
+ probeTimeVariants виконує кожен експортований функціонал у різні години доби та доповідає про ті, чий вихід залежить від часу.
23
+ probeHelpers витягує внутрішні допоміжні функції з коду та викликає їх з загальними комбінаціями параметрів для виявлення фактичних форм виходу.
24
+
25
+ ## Публічний API
26
+
27
+ probeModule — Виявляє функції, експортовані модулем, за допомогою їх запуску в дочірньому процесі.
28
+ probeFetchCalls — Перехоплює глобальний виклик `fetch` і виконує кожну експортовану асинхронну функцію з різними аргументами, щоб фіксувати фактичний URL та ініціалізацію, які вона конструює.
29
+ probeTimeVariants — Запускає кожну експортовану функцію у різний час доби. Повідомляє про функції, чий результат змінюється залежно від години.
30
+ probeHelpers — Витягує неекспортовані допоміжні функції з вихідного коду та викликає їх із загальними комбінаціями параметрів, щоб модель штучного інтелекту бачила фактичну форму виводу замість припущення.
31
+
32
+ ## Гарантії поведінки
33
+
34
+ - Read-only: не виконує операцій запису (ФС/БД).
35
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
36
+ - За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
@@ -0,0 +1,31 @@
1
+ ---
2
+ type: JS Module
3
+ title: vitest-shim.mjs
4
+ resource: npm/src/lib/vitest-shim.mjs
5
+ docgen:
6
+ crc: 5d7aaaf0
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ issues: judge:inaccurate:0.97
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ Цей файл надає спільні утиліти для тестування на основі Vitest. Він дозволяє виконувати тести у цільових проектах, де Vitest не встановлений локально, оскільки використовується Vitest, вбудований у `@7n/test`. Механізм конфігурації (залежить від `package.json`) забезпечує обхід будь-яких помилок завантаження конфігурації у цільовому проекті. Публічні функції включають `VITEST_BIN`, `VITEST_SHIM_CONFIG` та `ensureVitestShim`.
16
+
17
+ ## Поведінка
18
+
19
+ VITEST_BIN — Абсолютний шлях до вбудованого виконуваного файлу Vitest.
20
+ VITEST_SHIM_CONFIG — Абсолютний шлях до тимчасового файлу конфігурації Vitest, що забезпечує роботу збірки без встановлення Vitest у цільовому проекті.
21
+ ensureVitestShim — Гарантує існування файлу конфігурації Vitest, записуючи мінімальний конфіг у тимчасову директорію, ігноруючи шляхи до `node_modules`.
22
+
23
+ ## Публічний API
24
+
25
+ VITEST_BIN — Шлях до виконуваного файлу Vitest, скомпільованого для використання.
26
+ VITEST_SHIM_CONFIG — Мінімальна конфігурація Vitest, яку створюють у тимчасовій директорії ОС, щоб імпортований скомпільований Vitest міг отримати доступ до нього.
27
+ ensureVitestShim — Гарантує існування файлу конфігурації шиму (повторне виконання не змінює стан).
28
+
29
+ ## Гарантії поведінки
30
+
31
+ - Свідомо пропускає шляхи: `node_modules`.
@@ -6,7 +6,14 @@
6
6
  * callText(prompt, model?) — text-only, no tools, returns string
7
7
  * callAgent(prompt, cwd) — coding tools enabled, writes files directly
8
8
  */
9
- import { createAgentSession, SessionManager } from '@earendil-works/pi-coding-agent'
9
+ import { createAgentSession, SessionManager, ModelRegistry, AuthStorage } from '@earendil-works/pi-coding-agent'
10
+
11
+ let _registry = null
12
+ async function getRegistry() {
13
+ if (_registry) return _registry
14
+ _registry = ModelRegistry.create(AuthStorage.create())
15
+ return _registry
16
+ }
10
17
 
11
18
  /**
12
19
  * Sends a single prompt to pi in text mode (no tools) and returns the response.
@@ -23,8 +30,16 @@ export async function callText(prompt, opts = {}) {
23
30
  const sessionOpts = {
24
31
  tools: [],
25
32
  sessionManager: SessionManager.inMemory(cwd),
26
- cwd,
27
- ...(opts.model ? { model: opts.model } : {})
33
+ cwd
34
+ }
35
+ if (opts.model) {
36
+ const registry = await getRegistry()
37
+ const slashIdx = opts.model.indexOf('/')
38
+ const provider = slashIdx >= 0 ? opts.model.slice(0, slashIdx) : null
39
+ const modelId = slashIdx >= 0 ? opts.model.slice(slashIdx + 1) : opts.model
40
+ const resolved = provider ? registry.find(provider, modelId) : null
41
+ sessionOpts.modelRegistry = registry
42
+ sessionOpts.model = resolved ?? opts.model
28
43
  }
29
44
  const { session } = await createAgentSession(sessionOpts)
30
45