@retailcrm/embed-ui-v1-contexts 0.9.23-alpha.2 → 0.9.23-alpha.3

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/README.md CHANGED
@@ -15,4 +15,86 @@ npm i --save @retailcrm/embed-ui-v1-contexts
15
15
  yarn:
16
16
  ```bash
17
17
  yarn add @retailcrm/embed-ui-v1-contexts
18
- ```
18
+ ```
19
+
20
+ ## AI-friendly profiles
21
+
22
+ Пакет генерирует AI-friendly profiles для predefined contexts, action scopes и custom contexts.
23
+ Они собираются из typed source metadata, а не редактируются вручную.
24
+
25
+ Generated profiles появляются в пакетных docs при сборке:
26
+
27
+ - `docs/contexts/*.yml` — profiles predefined contexts, например `order/card`;
28
+ - `docs/actions/*.yml` — profiles action scopes, например `order/card`;
29
+ - `docs/custom-contexts/*.yml` — profiles custom context entities, например `order`;
30
+ - `docs/*/index.json` — индексы generated resources.
31
+
32
+ Пакет также поставляет MCP stdio server `embed-ui-v1-contexts-mcp`, который отдает generated profiles как
33
+ MCP resources:
34
+
35
+ - `embed-ui-v1-contexts://contexts`;
36
+ - `embed-ui-v1-contexts://contexts/<encoded-context>`;
37
+ - `embed-ui-v1-contexts://actions`;
38
+ - `embed-ui-v1-contexts://actions/<encoded-scope>`;
39
+ - `embed-ui-v1-contexts://custom-contexts`;
40
+ - `embed-ui-v1-contexts://custom-contexts/<encoded-entity>`.
41
+
42
+ Пример Codex project-level MCP config:
43
+
44
+ ```toml
45
+ [mcp_servers.v1-contexts]
46
+ command = "embed-ui-v1-contexts-mcp"
47
+ ```
48
+
49
+ ## AI и инициализация `AGENTS.md`
50
+
51
+ Чтобы агент понимал, когда использовать MCP-сервер и generated profiles пакета,
52
+ можно добавить в целевой проект секцию с инструкциями:
53
+
54
+ ```bash
55
+ npx @retailcrm/embed-ui-v1-contexts init-agents
56
+ ```
57
+
58
+ Если `AGENTS.md` ещё нет, команда создаст файл. Если файл уже есть, команда
59
+ допишет в конец английский блок для `@retailcrm/embed-ui-v1-contexts`, если
60
+ такого блока там ещё нет. С `--force` можно обновить уже существующий блок
61
+ пакета.
62
+
63
+ ## Инициализация MCP-конфига
64
+
65
+ Пакет также может сам добавить project-level MCP-настройки в целевой проект:
66
+
67
+ ```bash
68
+ npx @retailcrm/embed-ui-v1-contexts init-config
69
+ ```
70
+
71
+ Команда создаёт или дополняет корневой `.mcp.json`, добавляет заметку в
72
+ `README.md` и не дублирует уже существующую настройку. Клиентские project-level
73
+ конфиги создаются только явно:
74
+
75
+ ```bash
76
+ npx @retailcrm/embed-ui-v1-contexts init-config --mcp-client-configs codex,cursor,junie,vscode
77
+ ```
78
+
79
+ Корневой `.mcp.json` рассчитан на Claude Code project scope и использует
80
+ `${CLAUDE_PROJECT_DIR:-.}/node_modules/.bin/embed-ui-v1-contexts-mcp`. Для Cursor
81
+ и VS Code генерируются client-specific project configs с `${workspaceFolder}`.
82
+
83
+ С `--force` можно обновить уже существующие управляемые записи. Команда
84
+ обновляет только запись `retailcrm-embed-ui-v1-contexts`, а остальные серверы и
85
+ пользовательские настройки клиентских конфигов оставляет без изменений.
86
+
87
+ Локальная генерация:
88
+
89
+ ```bash
90
+ yarn workspace @retailcrm/embed-ui-v1-contexts run build:docs
91
+ ```
92
+
93
+ Полная сборка workspace тоже запускает генерацию:
94
+
95
+ ```bash
96
+ yarn workspace @retailcrm/embed-ui-v1-contexts run build
97
+ ```
98
+
99
+ Release workflow перед публикацией выполняет `yarn workspaces foreach -A --topological-dev run build`,
100
+ поэтому profiles генерируются в CI и попадают в published package вместе с `docs`.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runContextsMcpServer } from '../dist/mcp/server.js'
4
+
5
+ try {
6
+ await runContextsMcpServer()
7
+ } catch (error) {
8
+ console.error(error)
9
+ process.exit(1)
10
+ }
@@ -0,0 +1,587 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import process from 'node:process'
6
+
7
+ const PACKAGE_NAME = '@retailcrm/embed-ui-v1-contexts'
8
+ const DEFAULT_NEWLINE = '\n'
9
+ const AGENTS_SECTION_HEADER = '## @retailcrm/embed-ui-v1-contexts'
10
+ const AGENTS_SECTION_START = '<!-- embed-ui-agents:@retailcrm/embed-ui-v1-contexts:start -->'
11
+ const AGENTS_SECTION_END = '<!-- embed-ui-agents:@retailcrm/embed-ui-v1-contexts:end -->'
12
+ const README_MCP_SECTION_HEADER = '## MCP For AI Assistants: @retailcrm/embed-ui-v1-contexts'
13
+ const README_MCP_MARKER = 'embed-ui-v1-contexts://contexts'
14
+ const MCP_SERVER_NAME = 'retailcrm-embed-ui-v1-contexts'
15
+ const MCP_BIN_NAME = process.platform === 'win32' ? 'embed-ui-v1-contexts-mcp.cmd' : 'embed-ui-v1-contexts-mcp'
16
+ const RELATIVE_MCP_BIN_PATH = `./node_modules/.bin/${MCP_BIN_NAME}`
17
+ const CLAUDE_PROJECT_MCP_BIN_PATH = `\${CLAUDE_PROJECT_DIR:-.}/node_modules/.bin/${MCP_BIN_NAME}`
18
+ const WORKSPACE_MCP_BIN_PATH = `\${workspaceFolder}/node_modules/.bin/${MCP_BIN_NAME}`
19
+ const MCP_CLIENT_CONFIGS = {
20
+ codex: {
21
+ type: 'codex-file',
22
+ filePath: '.codex/config.toml',
23
+ command: RELATIVE_MCP_BIN_PATH,
24
+ },
25
+ cursor: {
26
+ type: 'file',
27
+ filePath: '.cursor/mcp.json',
28
+ rootField: 'mcpServers',
29
+ command: WORKSPACE_MCP_BIN_PATH,
30
+ },
31
+ junie: {
32
+ type: 'file',
33
+ filePath: '.junie/mcp/mcp.json',
34
+ rootField: 'mcpServers',
35
+ command: RELATIVE_MCP_BIN_PATH,
36
+ },
37
+ vscode: {
38
+ type: 'file',
39
+ filePath: '.vscode/mcp.json',
40
+ rootField: 'servers',
41
+ command: WORKSPACE_MCP_BIN_PATH,
42
+ config: {
43
+ type: 'stdio',
44
+ },
45
+ },
46
+ }
47
+
48
+ const HELP_TEXT = `Usage:
49
+ npx ${PACKAGE_NAME} init-agents [target] [options]
50
+ npx ${PACKAGE_NAME} init-config [target] [options]
51
+
52
+ Options:
53
+ -f, --force Replace existing managed sections and MCP server entries
54
+ --mcp-client-configs Comma-separated project-level MCP client configs to create (codex,cursor,junie,vscode)
55
+ --dry-run Print planned config changes without writing files
56
+ -h, --help Show this help
57
+
58
+ Examples:
59
+ npx ${PACKAGE_NAME} init-agents
60
+ npx ${PACKAGE_NAME} init-agents ./my-project
61
+ npx ${PACKAGE_NAME} init-agents --force
62
+ npx ${PACKAGE_NAME} init-config ./my-project
63
+ npx ${PACKAGE_NAME} init-config ./my-project --mcp-client-configs codex,cursor,junie,vscode
64
+ `
65
+
66
+ const resolveLocalMcpBinPath = (target) => path.join(target, 'node_modules', '.bin', MCP_BIN_NAME)
67
+
68
+ const createMcpServerConfig = (command, config = {}) => ({
69
+ ...config,
70
+ command,
71
+ })
72
+
73
+ const printMcpNotice = (message) => {
74
+ console.log(`MCP: ${message}`)
75
+ }
76
+
77
+ const parseArgs = (argv) => {
78
+ const options = {
79
+ command: null,
80
+ target: process.cwd(),
81
+ force: false,
82
+ dryRun: false,
83
+ mcpClientConfigs: [],
84
+ }
85
+
86
+ const positionals = []
87
+
88
+ for (let index = 0; index < argv.length; index++) {
89
+ const argument = argv[index]
90
+
91
+ if (argument === '-h' || argument === '--help') {
92
+ console.log(HELP_TEXT)
93
+ process.exit(0)
94
+ }
95
+
96
+ if (argument === '-f' || argument === '--force') {
97
+ options.force = true
98
+ continue
99
+ }
100
+
101
+ if (argument === '--dry-run') {
102
+ options.dryRun = true
103
+ continue
104
+ }
105
+
106
+ if (argument === '--mcp-client-configs') {
107
+ const value = argv[index + 1]
108
+ if (!value || value.startsWith('-')) {
109
+ throw new Error('--mcp-client-configs requires a comma-separated value')
110
+ }
111
+
112
+ options.mcpClientConfigs = value
113
+ .split(',')
114
+ .map((entry) => entry.trim())
115
+ .filter(Boolean)
116
+ index++
117
+ continue
118
+ }
119
+
120
+ if (argument.startsWith('-')) {
121
+ throw new Error(`Unknown option: ${argument}`)
122
+ }
123
+
124
+ positionals.push(argument)
125
+ }
126
+
127
+ if (!positionals.length) {
128
+ throw new Error('Command is required')
129
+ }
130
+
131
+ options.command = positionals[0]
132
+
133
+ if (positionals.length >= 2) {
134
+ options.target = path.resolve(process.cwd(), positionals[1])
135
+ }
136
+
137
+ if (positionals.length > 2) {
138
+ throw new Error('Too many positional arguments')
139
+ }
140
+
141
+ return options
142
+ }
143
+
144
+ const createAgentsSection = () => {
145
+ return `${AGENTS_SECTION_START}
146
+ ${AGENTS_SECTION_HEADER}
147
+
148
+ When working with \`${PACKAGE_NAME}\` in this project:
149
+
150
+ 1. Read \`./node_modules/${PACKAGE_NAME}/README.md\`.
151
+ 2. Then read \`./node_modules/${PACKAGE_NAME}/docs/ru/CONCEPT.md\`.
152
+ 3. Then read \`./node_modules/${PACKAGE_NAME}/docs/ru/CUSTOM.md\` if custom fields or custom dictionaries are involved.
153
+ 4. Use documented public entrypoints instead of package internals:
154
+ - \`${PACKAGE_NAME}/remote\`
155
+ - \`${PACKAGE_NAME}/remote/settings\`
156
+ - \`${PACKAGE_NAME}/remote/user/current\`
157
+ - \`${PACKAGE_NAME}/remote/order/card\`
158
+ - \`${PACKAGE_NAME}/remote/order/card-settings\`
159
+ - \`${PACKAGE_NAME}/remote/customer/card\`
160
+ - \`${PACKAGE_NAME}/remote/customer/card-phone\`
161
+ - \`${PACKAGE_NAME}/remote/custom\`
162
+ - \`${PACKAGE_NAME}/host\`
163
+ 5. Do not import from \`${PACKAGE_NAME}/dist/*\`, source files, or repository-only paths.
164
+ 6. When the task involves available contexts, context fields, actions, action scopes, custom contexts, custom fields, or dictionaries, use the package MCP server if it is available.
165
+ 7. First read \`embed-ui-v1-contexts://contexts\`, \`embed-ui-v1-contexts://actions\`, or \`embed-ui-v1-contexts://custom-contexts\` to discover available profiles.
166
+ 8. Then read the relevant resource before answering or changing code:
167
+ - \`embed-ui-v1-contexts://contexts/<encoded-context>\`
168
+ - \`embed-ui-v1-contexts://actions/<encoded-scope>\`
169
+ - \`embed-ui-v1-contexts://custom-contexts/<encoded-entity>\`
170
+ 9. A project \`.mcp.json\` may require restarting or reconnecting the AI client before MCP resources appear in the current session.
171
+ 10. If MCP resources are not available, use generated YAML profiles from \`./node_modules/${PACKAGE_NAME}/docs/contexts/*.yml\`, \`./node_modules/${PACKAGE_NAME}/docs/actions/*.yml\`, and \`./node_modules/${PACKAGE_NAME}/docs/custom-contexts/*.yml\` as fallback sources.
172
+ 11. Prefer generated profiles over guessing context shape, field names, action scopes, or semantic intent from names alone.
173
+
174
+ Suggested MCP stdio server configuration:
175
+
176
+ \`\`\`json
177
+ {
178
+ "command": "${CLAUDE_PROJECT_MCP_BIN_PATH}"
179
+ }
180
+ \`\`\`
181
+ ${AGENTS_SECTION_END}
182
+ `
183
+ }
184
+
185
+ const createMcpReadmeSection = (clientConfigs) => {
186
+ const clientConfigText = clientConfigs.length
187
+ ? `Client MCP configs were also requested: ${clientConfigs.map((clientConfig) => `\`${clientConfig}\``).join(', ')}. Review the generated files and restart the AI client if it is already open.`
188
+ : 'Client MCP configs are not created by default. For supported project-level configs, rerun init with `--mcp-client-configs codex,cursor,junie,vscode`.'
189
+
190
+ return `${README_MCP_SECTION_HEADER}
191
+
192
+ The project has an MCP server configuration for \`${PACKAGE_NAME}\`.
193
+ It exposes AI-friendly context, action, and custom context descriptions as MCP resources.
194
+ If the AI client was already running, restart or reconnect it before expecting these resources
195
+ to appear in that session.
196
+
197
+ Basic check:
198
+
199
+ \`\`\`bash
200
+ ./node_modules/.bin/embed-ui-v1-contexts-mcp
201
+ \`\`\`
202
+
203
+ Primary resources:
204
+
205
+ - \`${README_MCP_MARKER}\` is the context profile index.
206
+ - \`embed-ui-v1-contexts://contexts/<encoded-context>\` is a YAML profile for one context.
207
+ - \`embed-ui-v1-contexts://actions\` is the action scope profile index.
208
+ - \`embed-ui-v1-contexts://actions/<encoded-scope>\` is a YAML profile for one action scope.
209
+ - \`embed-ui-v1-contexts://custom-contexts\` is the custom context profile index.
210
+ - \`embed-ui-v1-contexts://custom-contexts/<encoded-entity>\` is a YAML profile for one custom context entity.
211
+
212
+ ${clientConfigText}
213
+
214
+ The root \`.mcp.json\` is compatible with Claude Code project scope and uses
215
+ \`${CLAUDE_PROJECT_MCP_BIN_PATH}\` so the server is resolved from the project directory.
216
+ Cursor and VS Code project configs use \`${WORKSPACE_MCP_BIN_PATH}\`. Codex and Junie use
217
+ \`${RELATIVE_MCP_BIN_PATH}\`.
218
+
219
+ ### Codex CLI
220
+
221
+ Codex supports project-scoped MCP config in \`.codex/config.toml\` for trusted projects.
222
+ Create it with:
223
+
224
+ \`\`\`bash
225
+ npx ${PACKAGE_NAME} init-config --mcp-client-configs codex
226
+ codex mcp list
227
+ \`\`\`
228
+
229
+ If the server does not appear, trust the project in Codex and restart the session. The
230
+ project-level config keeps this repository pinned to its own local
231
+ \`./node_modules/.bin/embed-ui-v1-contexts-mcp\` binary.
232
+
233
+ ### User-Level MCP Clients
234
+
235
+ Some clients store MCP servers in a user-level config outside this repository. Init does not edit
236
+ those files. Add the same server manually and restart the client. Use this only when this machine
237
+ works with one Embed UI project/version, because multiple user-level servers from different
238
+ projects can expose the same resource URIs and confuse the AI client.
239
+
240
+ Codex CLI user-level setup:
241
+
242
+ \`\`\`bash
243
+ codex mcp add ${MCP_SERVER_NAME} -- "$(realpath ./node_modules/.bin/embed-ui-v1-contexts-mcp)"
244
+ codex mcp list
245
+ codex mcp get ${MCP_SERVER_NAME}
246
+ \`\`\`
247
+
248
+ Equivalent \`~/.codex/config.toml\` block:
249
+
250
+ \`\`\`toml
251
+ [mcp_servers.${MCP_SERVER_NAME}]
252
+ command = "/absolute/path/to/project/node_modules/.bin/embed-ui-v1-contexts-mcp"
253
+ \`\`\`
254
+
255
+ Claude Desktop config paths:
256
+
257
+ - macOS: \`~/Library/Application Support/Claude/claude_desktop_config.json\`
258
+ - Windows: \`%APPDATA%\\Claude\\claude_desktop_config.json\`
259
+
260
+ Config snippet:
261
+
262
+ \`\`\`json
263
+ {
264
+ "mcpServers": {
265
+ "${MCP_SERVER_NAME}": {
266
+ "command": "/absolute/path/to/project/node_modules/.bin/embed-ui-v1-contexts-mcp"
267
+ }
268
+ }
269
+ }
270
+ \`\`\`
271
+ `
272
+ }
273
+
274
+ const createAgentsTemplate = () => {
275
+ return `# AGENTS.md
276
+
277
+ ${createAgentsSection()}` + DEFAULT_NEWLINE
278
+ }
279
+
280
+ const findMarkedSectionRange = (content) => {
281
+ const start = content.indexOf(AGENTS_SECTION_START)
282
+ const end = content.indexOf(AGENTS_SECTION_END, start + AGENTS_SECTION_START.length)
283
+
284
+ if (start === -1 && end === -1) {
285
+ return null
286
+ }
287
+
288
+ if (start === -1 || end === -1) {
289
+ throw new Error(`AGENTS.md contains an incomplete ${PACKAGE_NAME} managed section marker pair`)
290
+ }
291
+
292
+ return {
293
+ start,
294
+ end: end + AGENTS_SECTION_END.length,
295
+ }
296
+ }
297
+
298
+ const hasPackageSection = (content) =>
299
+ content.includes(AGENTS_SECTION_START) || content.includes(AGENTS_SECTION_HEADER)
300
+
301
+ const appendSection = (content, section) => {
302
+ const trimmed = content.replace(/\s+$/u, '')
303
+
304
+ if (!trimmed.length) {
305
+ return `${section}${DEFAULT_NEWLINE}`
306
+ }
307
+
308
+ return `${trimmed}${DEFAULT_NEWLINE}${DEFAULT_NEWLINE}${section}${DEFAULT_NEWLINE}`
309
+ }
310
+
311
+ const replaceSection = (content, section) => {
312
+ const markedRange = findMarkedSectionRange(content)
313
+
314
+ if (markedRange) {
315
+ return `${content.slice(0, markedRange.start)}${section.trimEnd()}${content.slice(markedRange.end)}`
316
+ .replace(/\s+$/u, '') + DEFAULT_NEWLINE
317
+ }
318
+
319
+ const escapedHeader = AGENTS_SECTION_HEADER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
320
+ const sectionPattern = new RegExp(`${escapedHeader}[\\s\\S]*?(?=\\n##\\s|$)`, 'u')
321
+
322
+ if (!sectionPattern.test(content)) {
323
+ return appendSection(content, section)
324
+ }
325
+
326
+ return content
327
+ .replace(sectionPattern, section.trimEnd())
328
+ .replace(/\s+$/u, '') + DEFAULT_NEWLINE
329
+ }
330
+
331
+ const replaceReadmeMcpSection = (content, section) => {
332
+ const escapedHeader = README_MCP_SECTION_HEADER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
333
+ const sectionPattern = new RegExp(`${escapedHeader}[\\s\\S]*?(?=\\n##\\s|$)`, 'u')
334
+
335
+ if (!sectionPattern.test(content)) {
336
+ return appendSection(content, section)
337
+ }
338
+
339
+ return content
340
+ .replace(sectionPattern, section.trimEnd())
341
+ .replace(/\s+$/u, '') + DEFAULT_NEWLINE
342
+ }
343
+
344
+ const readJsonObject = (filePath) => {
345
+ if (!fs.existsSync(filePath)) {
346
+ return {}
347
+ }
348
+
349
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'))
350
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
351
+ throw new Error(`${filePath} must contain a JSON object`)
352
+ }
353
+
354
+ return parsed
355
+ }
356
+
357
+ const ensureObjectField = (object, field, filePath) => {
358
+ const value = object[field]
359
+
360
+ if (!value) {
361
+ object[field] = {}
362
+ return object[field]
363
+ }
364
+
365
+ if (typeof value !== 'object' || Array.isArray(value)) {
366
+ throw new Error(`${filePath} field "${field}" must be a JSON object`)
367
+ }
368
+
369
+ return value
370
+ }
371
+
372
+ const writeJson = (filePath, value, dryRun) => {
373
+ if (dryRun) {
374
+ return
375
+ }
376
+
377
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
378
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}${DEFAULT_NEWLINE}`, 'utf8')
379
+ }
380
+
381
+ const createCodexMcpTomlSection = (serverConfig) => {
382
+ return `[mcp_servers.${MCP_SERVER_NAME}]
383
+ command = "${serverConfig.command.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"
384
+ `
385
+ }
386
+
387
+ const writeCodexMcpServerConfig = (target, options, serverConfig) => {
388
+ const relativePath = MCP_CLIENT_CONFIGS.codex.filePath
389
+ const filePath = path.join(target, relativePath)
390
+ const fileExists = fs.existsSync(filePath)
391
+ const currentContent = fileExists ? fs.readFileSync(filePath, 'utf8') : ''
392
+ const section = createCodexMcpTomlSection(serverConfig)
393
+ const escapedServerName = MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
394
+ const sectionPattern = new RegExp(`(^|\\n)\\[mcp_servers\\.${escapedServerName}\\][\\s\\S]*?(?=\\n\\[|$)`, 'u')
395
+ const hasServerSection = sectionPattern.test(currentContent)
396
+
397
+ if (hasServerSection && !options.force) {
398
+ console.log(`${relativePath} already contains ${MCP_SERVER_NAME}`)
399
+ console.log('Nothing was changed. Re-run with --force to refresh that server entry.')
400
+ return false
401
+ }
402
+
403
+ const nextContent = hasServerSection
404
+ ? currentContent
405
+ .replace(sectionPattern, (match, prefix) => `${prefix}${section.trimEnd()}`)
406
+ .replace(/\s+$/u, '') + DEFAULT_NEWLINE
407
+ : appendSection(currentContent, section)
408
+
409
+ if (!options.dryRun) {
410
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
411
+ fs.writeFileSync(filePath, nextContent, 'utf8')
412
+ }
413
+
414
+ const action = fileExists ? 'updated' : 'created'
415
+ console.log(`${relativePath} ${options.dryRun ? `would be ${action}` : `was ${action}`}`)
416
+ return true
417
+ }
418
+
419
+ const initAgents = (target, force) => {
420
+ if (!fs.existsSync(target)) {
421
+ throw new Error(`Target path does not exist: ${target}`)
422
+ }
423
+
424
+ const stat = fs.statSync(target)
425
+
426
+ if (!stat.isDirectory()) {
427
+ throw new Error(`Target path is not a directory: ${target}`)
428
+ }
429
+
430
+ const agentsPath = path.join(target, 'AGENTS.md')
431
+ const section = createAgentsSection()
432
+
433
+ if (!fs.existsSync(agentsPath)) {
434
+ fs.writeFileSync(agentsPath, createAgentsTemplate(), 'utf8')
435
+
436
+ console.log(`AGENTS.md was created at ${agentsPath}`)
437
+ console.log('Next step: review it and adjust project-specific rules if needed.')
438
+ return
439
+ }
440
+
441
+ const currentContent = fs.readFileSync(agentsPath, 'utf8')
442
+
443
+ if (force) {
444
+ fs.writeFileSync(agentsPath, replaceSection(currentContent, section), 'utf8')
445
+ console.log(`AGENTS.md was updated at ${agentsPath}`)
446
+ console.log(`The ${PACKAGE_NAME} section was refreshed.`)
447
+ return
448
+ }
449
+
450
+ if (hasPackageSection(currentContent)) {
451
+ console.log(`AGENTS.md already contains a ${PACKAGE_NAME} section at ${agentsPath}`)
452
+ console.log('Nothing was changed. Re-run with --force to refresh that section.')
453
+ return
454
+ }
455
+
456
+ fs.writeFileSync(agentsPath, appendSection(currentContent, section), 'utf8')
457
+
458
+ console.log(`AGENTS.md was updated at ${agentsPath}`)
459
+ console.log(`The ${PACKAGE_NAME} instructions were appended to the end of the file.`)
460
+ }
461
+
462
+ const writeMcpServerConfig = (target, relativePath, rootField, options, serverConfig) => {
463
+ const filePath = path.join(target, relativePath)
464
+ const fileExists = fs.existsSync(filePath)
465
+ const config = readJsonObject(filePath)
466
+ const servers = ensureObjectField(config, rootField, filePath)
467
+
468
+ if (servers[MCP_SERVER_NAME] && !options.force) {
469
+ console.log(`${relativePath} already contains ${MCP_SERVER_NAME}`)
470
+ console.log('Nothing was changed. Re-run with --force to refresh that server entry.')
471
+ return false
472
+ }
473
+
474
+ servers[MCP_SERVER_NAME] = serverConfig
475
+ writeJson(filePath, config, options.dryRun)
476
+
477
+ const action = fileExists ? 'updated' : 'created'
478
+ console.log(`${relativePath} ${options.dryRun ? `would be ${action}` : `was ${action}`}`)
479
+ return true
480
+ }
481
+
482
+ const printFileClientMcpNotice = (clientConfig, target, serverConfig) => {
483
+ const config = MCP_CLIENT_CONFIGS[clientConfig]
484
+
485
+ printMcpNotice(`${clientConfig} MCP config points to local binary ${serverConfig.command}. Restart or reconnect the client to use new resources.`)
486
+ printMcpNotice(`${clientConfig} config file: ${path.join(target, config.filePath)}`)
487
+ }
488
+
489
+ const resolveMcpClientConfigs = (tokens) => {
490
+ for (const token of tokens) {
491
+ if (!(token in MCP_CLIENT_CONFIGS)) {
492
+ throw new Error(`Unknown MCP client config: ${token}`)
493
+ }
494
+ }
495
+
496
+ return tokens
497
+ }
498
+
499
+ const updateMcpReadmeNotes = (target, clientConfigs, options) => {
500
+ const readmePath = path.join(target, 'README.md')
501
+ const fileExists = fs.existsSync(readmePath)
502
+ const currentContent = fileExists
503
+ ? fs.readFileSync(readmePath, 'utf8')
504
+ : '# README.md\n'
505
+ const section = createMcpReadmeSection(clientConfigs)
506
+
507
+ if (currentContent.includes(README_MCP_MARKER) && !options.force) {
508
+ console.log(`README.md already contains MCP setup notes at ${readmePath}`)
509
+ console.log('Nothing was changed. Re-run with --force to refresh that section.')
510
+ return
511
+ }
512
+
513
+ const nextContent = replaceReadmeMcpSection(currentContent, section)
514
+
515
+ if (!options.dryRun) {
516
+ fs.writeFileSync(readmePath, nextContent, 'utf8')
517
+ }
518
+
519
+ const action = fileExists ? 'updated' : 'created'
520
+ console.log(`README.md ${options.dryRun ? `would be ${action}` : `was ${action}`} with MCP setup notes`)
521
+ }
522
+
523
+ const initConfig = (target, options) => {
524
+ if (!fs.existsSync(target)) {
525
+ throw new Error(`Target path does not exist: ${target}`)
526
+ }
527
+
528
+ const stat = fs.statSync(target)
529
+
530
+ if (!stat.isDirectory()) {
531
+ throw new Error(`Target path is not a directory: ${target}`)
532
+ }
533
+
534
+ const clientConfigs = resolveMcpClientConfigs(options.mcpClientConfigs)
535
+ const serverConfig = createMcpServerConfig(CLAUDE_PROJECT_MCP_BIN_PATH)
536
+ const absoluteMcpBinPath = resolveLocalMcpBinPath(target)
537
+
538
+ writeMcpServerConfig(target, '.mcp.json', 'mcpServers', options, serverConfig)
539
+ printMcpNotice(`Project MCP config points to local binary ${serverConfig.command}. Restart or reconnect MCP clients to use new resources.`)
540
+ if (!fs.existsSync(absoluteMcpBinPath)) {
541
+ printMcpNotice(`Local MCP binary is not available yet. Install project dependencies before starting MCP clients: ${absoluteMcpBinPath}`)
542
+ }
543
+
544
+ for (const clientConfig of clientConfigs) {
545
+ const config = MCP_CLIENT_CONFIGS[clientConfig]
546
+
547
+ if (config.type === 'codex-file') {
548
+ const codexServerConfig = createMcpServerConfig(config.command)
549
+
550
+ writeCodexMcpServerConfig(target, options, codexServerConfig)
551
+ printMcpNotice(`codex project config points to local binary ${codexServerConfig.command}. Trust the project and restart Codex to use new resources.`)
552
+ continue
553
+ }
554
+
555
+ const clientServerConfig = createMcpServerConfig(config.command, config.config)
556
+
557
+ writeMcpServerConfig(target, config.filePath, config.rootField, options, clientServerConfig)
558
+ printFileClientMcpNotice(clientConfig, target, clientServerConfig)
559
+ }
560
+
561
+ updateMcpReadmeNotes(target, clientConfigs, options)
562
+ }
563
+
564
+ const main = () => {
565
+ try {
566
+ const options = parseArgs(process.argv.slice(2))
567
+
568
+ if (options.command === 'init-agents') {
569
+ initAgents(options.target, options.force)
570
+ return
571
+ }
572
+
573
+ if (options.command === 'init-config') {
574
+ initConfig(options.target, options)
575
+ return
576
+ }
577
+
578
+ throw new Error(`Unknown command: ${options.command}`)
579
+ } catch (error) {
580
+ console.error(error instanceof Error ? error.message : String(error))
581
+ console.error('')
582
+ console.error(HELP_TEXT)
583
+ process.exit(1)
584
+ }
585
+ }
586
+
587
+ main()