@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 +83 -1
- package/bin/embed-ui-v1-contexts-mcp.mjs +10 -0
- package/bin/embed-ui-v1-contexts.mjs +587 -0
- package/dist/mcp/server.cjs +147 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +129 -0
- package/docs/actions/index.json +10 -0
- package/docs/actions/order-card.yml +107 -0
- package/docs/contexts/customer-card-phone.yml +35 -0
- package/docs/contexts/customer-card.yml +55 -0
- package/docs/contexts/index.json +35 -0
- package/docs/contexts/order-card-settings.yml +221 -0
- package/docs/contexts/order-card.yml +815 -0
- package/docs/contexts/settings.yml +48 -0
- package/docs/contexts/user-current.yml +119 -0
- package/docs/custom-contexts/index.json +10 -0
- package/docs/custom-contexts/order.yml +65 -0
- package/docs/ru/CONCEPT.md +195 -10
- package/package.json +22 -5
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,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()
|