@seomi/ssh 0.1.0 → 0.1.2
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/LICENSE +19 -0
- package/README.md +93 -36
- package/README.ru.md +118 -0
- package/package.json +10 -1
- package/src/commands/init.mjs +23 -1
- package/src/lib/agent-md-target.mjs +71 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) SEOMI. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software is proprietary and confidential.
|
|
4
|
+
|
|
5
|
+
Permitted use:
|
|
6
|
+
- Internal use within SEOMI and on projects developed or maintained by SEOMI.
|
|
7
|
+
- Use by SEOMI clients on their own projects as part of services provided by SEOMI.
|
|
8
|
+
|
|
9
|
+
Not permitted without prior written consent from SEOMI:
|
|
10
|
+
- Redistribution, sublicensing, or sale of this software or any substantial portion
|
|
11
|
+
of it.
|
|
12
|
+
- Reverse-engineering for the purpose of creating a competing product.
|
|
13
|
+
- Removal of copyright or license notices.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
16
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
17
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
|
|
18
|
+
ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
19
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# @seomi/ssh
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**English** | [Русский](https://github.com/Mikeekb/seomi-ssh/blob/main/README.ru.md)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
по SSH-ключу к одному или нескольким серверам (dev / prod / кастомные) и записывает карту
|
|
7
|
-
доступа в инструкции агента (`AGENTS.md` / `CLAUDE.md`). Облегчённый родственник
|
|
8
|
-
[`@seomi/wp-mcp`](../seomi-wp-mcp) — без WordPress и MCP, только SSH.
|
|
5
|
+
> One command sets up passwordless SSH access for an AI agent to your servers.
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
`@seomi/ssh` is a CLI installer: in a single interactive pass it configures key-based SSH
|
|
8
|
+
access for an agent to one or more servers (dev / prod / custom) and writes an access map
|
|
9
|
+
into the agent instructions (`AGENTS.md` / `CLAUDE.md`). It's a lightweight sibling of
|
|
10
|
+
[`@seomi/wp-mcp`](https://github.com/Mikeekb/seomi-wp-mcp) — the SSH wizard and agent-instruction
|
|
11
|
+
logic, without the WordPress and MCP parts.
|
|
11
12
|
|
|
12
13
|
```bash
|
|
13
14
|
npm install -g @seomi/ssh
|
|
@@ -15,47 +16,103 @@ cd my-project
|
|
|
15
16
|
seomi-ssh init
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
##
|
|
19
|
+
## Features
|
|
19
20
|
|
|
20
|
-
- **`init`** —
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
- **`init`** — interactive setup. Asks about servers **in a loop** (role, host, user, port,
|
|
22
|
+
key path, optional working directory) and repeats "add another?" until you decline.
|
|
23
|
+
Supports any number of servers: prod only; dev + prod; or an arbitrary set.
|
|
24
|
+
- **SSH wizard per server** — generates an ed25519 key (or reuses one), copies the public
|
|
25
|
+
key (`ssh-copy-id` → ssh-pipe fallback), verifies with `ssh -o BatchMode=yes`, and on
|
|
26
|
+
failure prints a manual hint with the `.pub` contents.
|
|
27
|
+
- **Writes agent instructions** — a managed block with a server map and ready-to-use
|
|
28
|
+
`ssh` / `scp` / `rsync` examples, values pulled from `.claude/.env`.
|
|
29
|
+
- **`aif-ssh` skill** — copied into the project's `.claude/skills/` so the agent knows how
|
|
30
|
+
to use the configured access.
|
|
31
|
+
- **Idempotent** — re-running never duplicates keys, env entries, or the managed block.
|
|
30
32
|
|
|
31
|
-
> `update`
|
|
33
|
+
> `update` and `doctor` are declared in `--help` but are not implemented yet (stubs).
|
|
32
34
|
|
|
33
|
-
##
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
| Command | What it does |
|
|
38
|
+
|---------|--------------|
|
|
39
|
+
| `seomi-ssh init` | Interactive setup (see above) |
|
|
40
|
+
| `seomi-ssh init --dry-run` | Run the prompts without touching disk or SSH; preview the block |
|
|
41
|
+
| `seomi-ssh init --verbose` | Add debug-level logging |
|
|
42
|
+
| `seomi-ssh --help` | Show usage |
|
|
43
|
+
| `seomi-ssh --version` | Print version |
|
|
44
|
+
| `seomi-ssh update` / `doctor` | Reserved (not implemented yet) |
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
- **Node 20+** for the CLI itself.
|
|
49
|
+
- **OpenSSH client** (`ssh`, `ssh-keygen`) on the local machine. `ssh-copy-id` is optional —
|
|
50
|
+
without it (typical on Windows OpenSSH) the wizard uses a portable ssh-pipe fallback.
|
|
51
|
+
|
|
52
|
+
## How the SSH wizard works
|
|
53
|
+
|
|
54
|
+
For each server, a strategy chain with graceful degradation:
|
|
55
|
+
|
|
56
|
+
1. **Keygen** — generate `ed25519` if the key is missing (`ssh-keygen -N ''`, empty passphrase
|
|
57
|
+
so the agent can authenticate non-interactively), otherwise reuse the existing key.
|
|
58
|
+
2. **Copy** — `ssh-copy-id` (asks for the password once). If the binary isn't on PATH, fall
|
|
59
|
+
back to piping the public key into `~/.ssh/authorized_keys` over `ssh` (deduplicated).
|
|
60
|
+
3. **Verify** — `ssh -o BatchMode=yes ... 'echo ok'`. BatchMode disables password prompts,
|
|
61
|
+
so a non-zero exit reliably means the key wasn't accepted.
|
|
62
|
+
4. **Fallback** — on failed verification, print a manual hint with the `.pub` contents.
|
|
63
|
+
|
|
64
|
+
A failure on one server doesn't abort the run — the others continue.
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
Connection parameters live in `.claude/.env` (gitignored) as flat keys with a **role prefix**.
|
|
69
|
+
One server = one key group:
|
|
70
|
+
|
|
71
|
+
| Key | Purpose | Written |
|
|
72
|
+
|-----|---------|---------|
|
|
73
|
+
| `SSH_<PREFIX>_HOST` | domain or IP | always |
|
|
74
|
+
| `SSH_<PREFIX>_USER` | SSH user | always |
|
|
75
|
+
| `SSH_<PREFIX>_PORT` | port | if provided |
|
|
76
|
+
| `SSH_<PREFIX>_KEY` | private key path | always |
|
|
77
|
+
| `SSH_<PREFIX>_ROOT` | remote working directory | if provided |
|
|
78
|
+
| `SSH_SERVERS` | csv registry of all prefixes | always |
|
|
79
|
+
|
|
80
|
+
`<PREFIX>` is the role normalized to `UPPER_SNAKE_CASE` (`prod` → `PROD`, `staging-eu` →
|
|
81
|
+
`STAGING_EU`); duplicate roles get a unique suffix (`PROD`, then `PROD_2`). `.claude/.env`
|
|
82
|
+
is the **first place** the agent looks for access details — not `~/.ssh/config`.
|
|
83
|
+
|
|
84
|
+
## Example
|
|
34
85
|
|
|
35
86
|
```bash
|
|
36
87
|
$ seomi-ssh init
|
|
37
|
-
›
|
|
38
|
-
?
|
|
39
|
-
? [prod] Host (
|
|
40
|
-
? [prod] SSH
|
|
41
|
-
?
|
|
42
|
-
›
|
|
43
|
-
[ok]
|
|
44
|
-
›
|
|
45
|
-
› Шаг 4: Инструкции агенту (AGENTS.md / CLAUDE.md)
|
|
88
|
+
› Step 1: Server prompts
|
|
89
|
+
? Server role › prod
|
|
90
|
+
? [prod] Host (domain or IP) › prod.example.com
|
|
91
|
+
? [prod] SSH user › ai-agent
|
|
92
|
+
? Add another server? › No
|
|
93
|
+
› Step 2: SSH keys
|
|
94
|
+
[ok] "prod": key configured and verified
|
|
95
|
+
› Step 4: Agent instructions (AGENTS.md / CLAUDE.md)
|
|
46
96
|
[ok] created: AGENTS.md
|
|
47
97
|
```
|
|
48
98
|
|
|
49
|
-
Используйте `seomi-ssh init --dry-run`, чтобы прогнать опрос без записи на диск и SSH-вызовов.
|
|
50
|
-
|
|
51
99
|
---
|
|
52
100
|
|
|
53
|
-
##
|
|
101
|
+
## Documentation
|
|
102
|
+
|
|
103
|
+
| Guide | Description |
|
|
104
|
+
|-------|-------------|
|
|
105
|
+
| [`init` command](https://github.com/Mikeekb/seomi-ssh/blob/main/docs/init.md) | SSH setup behavior, `.claude/.env` configuration, examples, troubleshooting |
|
|
54
106
|
|
|
55
|
-
|
|
56
|
-
|--------|----------|
|
|
57
|
-
| [Команда `init`](docs/init.md) | Настройка SSH-доступа: поведение, конфигурация `.claude/.env`, примеры, диагностика |
|
|
107
|
+
## License
|
|
58
108
|
|
|
59
|
-
|
|
109
|
+
Proprietary — © SEOMI. See `LICENSE`.
|
|
110
|
+
|
|
111
|
+
## Related projects
|
|
112
|
+
|
|
113
|
+
- [@seomi/wp-mcp](https://github.com/Mikeekb/seomi-wp-mcp) — the WordPress/MCP sibling this package is derived from.
|
|
114
|
+
- [ai-factory](https://github.com/lee-to/ai-factory) — companion project for AI dev context.
|
|
115
|
+
|
|
116
|
+
---
|
|
60
117
|
|
|
61
|
-
|
|
118
|
+
Built and maintained by [SEOmi.ru — Web Development](https://seomi.ru/).
|
package/README.ru.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# @seomi/ssh
|
|
2
|
+
|
|
3
|
+
[English](https://github.com/Mikeekb/seomi-ssh/blob/main/README.md) | **Русский**
|
|
4
|
+
|
|
5
|
+
> Одной командой настраивает беспарольный SSH-доступ для AI-агента к вашим серверам.
|
|
6
|
+
|
|
7
|
+
`@seomi/ssh` — CLI-инсталлятор: за один интерактивный прогон он настраивает доступ агента
|
|
8
|
+
по SSH-ключу к одному или нескольким серверам (dev / prod / кастомные) и записывает карту
|
|
9
|
+
доступа в инструкции агента (`AGENTS.md` / `CLAUDE.md`). Облегчённый родственник
|
|
10
|
+
[`@seomi/wp-mcp`](https://github.com/Mikeekb/seomi-wp-mcp) — взяты только SSH-визард и логика
|
|
11
|
+
инструкций агенту, без WordPress и MCP.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g @seomi/ssh
|
|
15
|
+
cd my-project
|
|
16
|
+
seomi-ssh init
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Возможности
|
|
20
|
+
|
|
21
|
+
- **`init`** — интерактивная настройка. Спрашивает про серверы **в цикле** (роль, host,
|
|
22
|
+
user, port, путь к ключу, опционально рабочая директория) и повторяет «добавить ещё?»,
|
|
23
|
+
пока не откажешься. Любое число серверов: только prod; dev + prod; произвольный набор.
|
|
24
|
+
- **SSH-визард на каждый сервер** — генерация ed25519-ключа (или переиспользование),
|
|
25
|
+
копирование публичного ключа (`ssh-copy-id` → ssh-pipe fallback), проверка по
|
|
26
|
+
`ssh -o BatchMode=yes`, и при неудаче — печать ручной подсказки с содержимым `.pub`.
|
|
27
|
+
- **Запись инструкций агенту** — managed-блок с картой серверов и готовыми примерами
|
|
28
|
+
`ssh` / `scp` / `rsync`, значения берутся из `.claude/.env`.
|
|
29
|
+
- **Skill `aif-ssh`** — копируется в `.claude/skills/` проекта и учит агента пользоваться
|
|
30
|
+
настроенным доступом.
|
|
31
|
+
- **Идемпотентность** — повторный запуск не дублирует ключи, env-записи и managed-блок.
|
|
32
|
+
|
|
33
|
+
> `update` и `doctor` объявлены в `--help`, но пока не реализованы (заглушки).
|
|
34
|
+
|
|
35
|
+
## Команды
|
|
36
|
+
|
|
37
|
+
| Команда | Что делает |
|
|
38
|
+
|---------|------------|
|
|
39
|
+
| `seomi-ssh init` | Интерактивная настройка (см. выше) |
|
|
40
|
+
| `seomi-ssh init --dry-run` | Прогон опроса без записи на диск и SSH-вызовов; превью блока |
|
|
41
|
+
| `seomi-ssh init --verbose` | Включает debug-логирование |
|
|
42
|
+
| `seomi-ssh --help` | Справка |
|
|
43
|
+
| `seomi-ssh --version` | Версия |
|
|
44
|
+
| `seomi-ssh update` / `doctor` | Зарезервированы (пока не реализованы) |
|
|
45
|
+
|
|
46
|
+
## Требования
|
|
47
|
+
|
|
48
|
+
- **Node 20+** для самого CLI.
|
|
49
|
+
- **OpenSSH client** (`ssh`, `ssh-keygen`) на локальной машине. `ssh-copy-id` — опционально:
|
|
50
|
+
без него (типично для Windows OpenSSH) визард использует портируемый ssh-pipe fallback.
|
|
51
|
+
|
|
52
|
+
## Как работает SSH-визард
|
|
53
|
+
|
|
54
|
+
Для каждого сервера — цепочка стратегий с graceful degradation:
|
|
55
|
+
|
|
56
|
+
1. **Keygen** — генерирует `ed25519`, если ключа нет (`ssh-keygen -N ''`, пустая passphrase —
|
|
57
|
+
агенту нужен неинтерактивный доступ), иначе переиспользует существующий.
|
|
58
|
+
2. **Copy** — `ssh-copy-id` (один раз спросит пароль). Если бинарника нет на PATH — fallback:
|
|
59
|
+
публичный ключ передаётся в `~/.ssh/authorized_keys` через `ssh` (с дедупликацией).
|
|
60
|
+
3. **Verify** — `ssh -o BatchMode=yes ... 'echo ok'`. BatchMode отключает запрос пароля,
|
|
61
|
+
поэтому ненулевой код = ключ не принят.
|
|
62
|
+
4. **Fallback** — при неудачной проверке печатается ручная подсказка с содержимым `.pub`.
|
|
63
|
+
|
|
64
|
+
Ошибка одного сервера не прерывает прогон — остальные продолжают.
|
|
65
|
+
|
|
66
|
+
## Конфигурация
|
|
67
|
+
|
|
68
|
+
Параметры доступа лежат в `.claude/.env` (gitignored) плоскими ключами с **префиксом роли**.
|
|
69
|
+
Один сервер = одна группа ключей:
|
|
70
|
+
|
|
71
|
+
| Ключ | Назначение | Пишется |
|
|
72
|
+
|------|------------|---------|
|
|
73
|
+
| `SSH_<PREFIX>_HOST` | домен или IP | всегда |
|
|
74
|
+
| `SSH_<PREFIX>_USER` | SSH-пользователь | всегда |
|
|
75
|
+
| `SSH_<PREFIX>_PORT` | порт | если задан |
|
|
76
|
+
| `SSH_<PREFIX>_KEY` | путь к приватному ключу | всегда |
|
|
77
|
+
| `SSH_<PREFIX>_ROOT` | рабочая директория на сервере | если задана |
|
|
78
|
+
| `SSH_SERVERS` | csv-реестр всех префиксов | всегда |
|
|
79
|
+
|
|
80
|
+
`<PREFIX>` — это роль, нормализованная в `UPPER_SNAKE_CASE` (`prod` → `PROD`, `staging-eu` →
|
|
81
|
+
`STAGING_EU`); при повторе роли префикс получает уникальный суффикс (`PROD`, затем `PROD_2`).
|
|
82
|
+
`.claude/.env` — **первое место**, куда агент смотрит за реквизитами доступа, а не `~/.ssh/config`.
|
|
83
|
+
|
|
84
|
+
## Пример
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
$ seomi-ssh init
|
|
88
|
+
› Шаг 1: Опрос серверов
|
|
89
|
+
? Роль сервера › prod
|
|
90
|
+
? [prod] Host (домен или IP) › prod.example.com
|
|
91
|
+
? [prod] SSH-пользователь › ai-agent
|
|
92
|
+
? Добавить ещё один сервер? › No
|
|
93
|
+
› Шаг 2: Настройка SSH-ключей
|
|
94
|
+
[ok] «prod»: ключ настроен и проверен
|
|
95
|
+
› Шаг 4: Инструкции агенту (AGENTS.md / CLAUDE.md)
|
|
96
|
+
[ok] created: AGENTS.md
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Документация
|
|
102
|
+
|
|
103
|
+
| Раздел | Описание |
|
|
104
|
+
|--------|----------|
|
|
105
|
+
| [Команда `init`](https://github.com/Mikeekb/seomi-ssh/blob/main/docs/init.md) | Поведение `init`, конфигурация `.claude/.env`, примеры, диагностика |
|
|
106
|
+
|
|
107
|
+
## Лицензия
|
|
108
|
+
|
|
109
|
+
Proprietary — © SEOMI. См. `LICENSE`.
|
|
110
|
+
|
|
111
|
+
## Связанные проекты
|
|
112
|
+
|
|
113
|
+
- [@seomi/wp-mcp](https://github.com/Mikeekb/seomi-wp-mcp) — родственный WordPress/MCP-пакет, от которого произошёл этот.
|
|
114
|
+
- [ai-factory](https://github.com/lee-to/ai-factory) — спутник для AI-контекста разработки.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
Разработка и сопровождение — [Разработка сайтов SEOmi.ru](https://seomi.ru/).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seomi/ssh",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "One-command CLI installer that sets up passwordless SSH access for AI agents in any project (dev/prod/custom servers) and writes the access map into AGENTS.md / CLAUDE.md.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"skills",
|
|
13
13
|
"templates",
|
|
14
14
|
"README.md",
|
|
15
|
+
"README.ru.md",
|
|
15
16
|
"LICENSE"
|
|
16
17
|
],
|
|
17
18
|
"engines": {
|
|
@@ -33,6 +34,14 @@
|
|
|
33
34
|
"deploy"
|
|
34
35
|
],
|
|
35
36
|
"license": "SEE LICENSE IN LICENSE",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/Mikeekb/seomi-ssh.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/Mikeekb/seomi-ssh#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/Mikeekb/seomi-ssh/issues"
|
|
44
|
+
},
|
|
36
45
|
"publishConfig": {
|
|
37
46
|
"access": "public"
|
|
38
47
|
},
|
package/src/commands/init.mjs
CHANGED
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { mkdir, copyFile } from 'node:fs/promises';
|
|
18
|
+
import { existsSync } from 'node:fs';
|
|
18
19
|
import { join } from 'node:path';
|
|
19
20
|
import { fileURLToPath } from 'node:url';
|
|
20
21
|
import { logger } from '../lib/logger.mjs';
|
|
21
22
|
import { promptServers, toEnvUpdates } from '../lib/server-prompt.mjs';
|
|
22
23
|
import { mergeEnv } from '../lib/env-writer.mjs';
|
|
23
24
|
import { ensureSshKey } from '../lib/ssh-key-setup.mjs';
|
|
24
|
-
import { detectAgentMdTargets } from '../lib/agent-md-target.mjs';
|
|
25
|
+
import { detectAgentMdTargets, ensureClaudeImportStub, isClaudeImportStub } from '../lib/agent-md-target.mjs';
|
|
25
26
|
import { renderAgentMdBlock } from '../lib/agent-md-renderer.mjs';
|
|
26
27
|
import { insertOrUpdate } from '../lib/markers.mjs';
|
|
27
28
|
|
|
@@ -117,6 +118,10 @@ export async function initCommand( options = {} ) {
|
|
|
117
118
|
if ( dryRun ) {
|
|
118
119
|
const { targets } = await detectAgentMdTargets( { cwd, interactive: false } );
|
|
119
120
|
logger.info( `[dry-run] managed-блок был бы записан в: ${ targets.join( ', ' ) || '(none)' }` );
|
|
121
|
+
const wouldStub = targets.some( ( f ) => f.endsWith( 'AGENTS.md' ) ) && ! existsSync( join( cwd, 'CLAUDE.md' ) );
|
|
122
|
+
if ( wouldStub ) {
|
|
123
|
+
logger.info( '[dry-run] был бы создан CLAUDE.md с импортом @AGENTS.md (Claude Code не читает AGENTS.md)' );
|
|
124
|
+
}
|
|
120
125
|
process.stdout.write( '\n--- managed block preview ---\n' + block + '--- end preview ---\n' );
|
|
121
126
|
} else {
|
|
122
127
|
const { targets } = await detectAgentMdTargets( {
|
|
@@ -128,9 +133,26 @@ export async function initCommand( options = {} ) {
|
|
|
128
133
|
logger.warn( 'Целевой файл инструкций не выбран — managed-блок не записан.' );
|
|
129
134
|
}
|
|
130
135
|
for ( const file of targets ) {
|
|
136
|
+
const name = file.split( /[\\/]/ ).pop();
|
|
137
|
+
// CLAUDE.md, который лишь импортирует AGENTS.md, — это редирект, а не
|
|
138
|
+
// носитель блока: писать в него блок значит снова продублировать
|
|
139
|
+
// контент. Оставляем его чистым импортом `@AGENTS.md`.
|
|
140
|
+
if ( name === 'CLAUDE.md' && isClaudeImportStub( file ) ) {
|
|
141
|
+
logger.info( `${ name }: оставлен как импорт @AGENTS.md (блок живёт в AGENTS.md)` );
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
131
144
|
const res = await insertOrUpdate( file, block, { namespace: MARKER_NS } );
|
|
132
145
|
logger.success( `${ res.action }: ${ file }` );
|
|
133
146
|
}
|
|
147
|
+
// Claude Code не читает AGENTS.md — если блок лёг туда и CLAUDE.md нет,
|
|
148
|
+
// создаём однострочный CLAUDE.md с импортом, чтобы Claude Code видел те
|
|
149
|
+
// же инструкции (включая доступы по SSH).
|
|
150
|
+
if ( targets.length > 0 ) {
|
|
151
|
+
const stub = await ensureClaudeImportStub( { cwd } );
|
|
152
|
+
if ( stub.created ) {
|
|
153
|
+
logger.success( 'Создан CLAUDE.md (импорт @AGENTS.md) — Claude Code теперь читает AGENTS.md' );
|
|
154
|
+
}
|
|
155
|
+
}
|
|
134
156
|
}
|
|
135
157
|
|
|
136
158
|
// --- 6. Copy the access skill ----------------------------------------
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Target-aware detection for AI agent instructions files (AGENTS.md / CLAUDE.md).
|
|
3
3
|
*
|
|
4
|
-
* Claude Code reads `CLAUDE.md`
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
4
|
+
* Claude Code reads `CLAUDE.md` and NEVER `AGENTS.md` — not even when no
|
|
5
|
+
* CLAUDE.md exists next to it (see https://code.claude.com/docs/en/memory →
|
|
6
|
+
* "AGENTS.md"). Other agents (Cursor, ai-factory tooling, etc.) treat
|
|
7
|
+
* `AGENTS.md` as the universal standard. To satisfy both without duplicating
|
|
8
|
+
* content, we keep the managed `seomi-ssh` block in `AGENTS.md` (the single
|
|
9
|
+
* source of truth) and drop a one-line `CLAUDE.md` that imports it via
|
|
10
|
+
* `@AGENTS.md` — so Claude Code reads the same instructions. See
|
|
11
|
+
* `ensureClaudeImportStub` below.
|
|
9
12
|
*
|
|
10
13
|
* Decision tree:
|
|
11
14
|
* 1. Both files exist → targets = [AGENTS.md, CLAUDE.md] (source='both')
|
|
@@ -15,7 +18,8 @@
|
|
|
15
18
|
* 5. Neither, interactive=true → select prompt → user / skipped (source='user'|'skipped')
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
|
-
import { existsSync } from 'node:fs';
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { writeFile } from 'node:fs/promises';
|
|
19
23
|
import { join } from 'node:path';
|
|
20
24
|
import { logger } from './logger.mjs';
|
|
21
25
|
|
|
@@ -23,6 +27,19 @@ export const DEFAULT_TARGET = 'AGENTS.md';
|
|
|
23
27
|
const AGENTS_FILE = 'AGENTS.md';
|
|
24
28
|
const CLAUDE_FILE = 'CLAUDE.md';
|
|
25
29
|
|
|
30
|
+
/**
|
|
31
|
+
* One-line `CLAUDE.md` that imports `AGENTS.md`. The HTML comment is stripped
|
|
32
|
+
* by Claude Code before the file enters context (it only documents the file for
|
|
33
|
+
* humans), so the effective payload is just the `@AGENTS.md` import directive.
|
|
34
|
+
*/
|
|
35
|
+
export const CLAUDE_IMPORT_STUB = `<!--
|
|
36
|
+
Managed by @seomi/ssh. Claude Code loads CLAUDE.md but never AGENTS.md,
|
|
37
|
+
so this stub imports AGENTS.md — the agent-agnostic source of truth — keeping
|
|
38
|
+
Claude Code and other agents on the same instructions. Edit AGENTS.md, not this.
|
|
39
|
+
-->
|
|
40
|
+
@AGENTS.md
|
|
41
|
+
`;
|
|
42
|
+
|
|
26
43
|
/**
|
|
27
44
|
* Detect which agent-instructions file(s) the project uses and return absolute
|
|
28
45
|
* paths to be updated with the managed `seomi-ssh` block.
|
|
@@ -105,3 +122,51 @@ export async function detectAgentMdTargets( { cwd, interactive = false, defaultN
|
|
|
105
122
|
logger.success( `[agent-md] targets: ${ targets.join( ', ' ) }` );
|
|
106
123
|
return { targets, source: 'user' };
|
|
107
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Is `claudePath` a thin import stub (it imports AGENTS.md and carries no
|
|
128
|
+
* managed seomi-ssh block of its own)? Lets callers tell an intended
|
|
129
|
+
* `@AGENTS.md` redirect apart from a real duplicated block.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} claudePath — absolute path to the candidate CLAUDE.md
|
|
132
|
+
* @returns {boolean}
|
|
133
|
+
*/
|
|
134
|
+
export function isClaudeImportStub( claudePath ) {
|
|
135
|
+
if ( ! existsSync( claudePath ) ) return false;
|
|
136
|
+
const text = readFileSync( claudePath, 'utf8' );
|
|
137
|
+
const importsAgents = /(^|\n)[ \t]*@AGENTS\.md[ \t]*(\r?\n|$)/.test( text );
|
|
138
|
+
const hasManagedBlock = /<!--\s*seomi-ssh:start\s*-->/.test( text );
|
|
139
|
+
return importsAgents && ! hasManagedBlock;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Ensure Claude Code can read the project's instructions when the managed block
|
|
144
|
+
* lives in `AGENTS.md`. Claude Code never reads `AGENTS.md`, so when only that
|
|
145
|
+
* file exists we drop a one-line `CLAUDE.md` that imports it (`@AGENTS.md`).
|
|
146
|
+
*
|
|
147
|
+
* Idempotent and non-destructive: does nothing when there is no `AGENTS.md`, or
|
|
148
|
+
* when a `CLAUDE.md` already exists (we never overwrite a user's CLAUDE.md).
|
|
149
|
+
*
|
|
150
|
+
* @param {object} opts
|
|
151
|
+
* @param {string} opts.cwd — project root
|
|
152
|
+
* @returns {Promise<{ created: boolean, reason: 'created'|'no-agents'|'claude-exists', path: string }>}
|
|
153
|
+
*/
|
|
154
|
+
export async function ensureClaudeImportStub( { cwd } = {} ) {
|
|
155
|
+
if ( ! cwd ) throw new Error( 'ensureClaudeImportStub: `cwd` is required' );
|
|
156
|
+
|
|
157
|
+
const agentsPath = join( cwd, AGENTS_FILE );
|
|
158
|
+
const claudePath = join( cwd, CLAUDE_FILE );
|
|
159
|
+
|
|
160
|
+
if ( ! existsSync( agentsPath ) ) {
|
|
161
|
+
logger.debug( '[agent-md] import stub skipped — no AGENTS.md to import' );
|
|
162
|
+
return { created: false, reason: 'no-agents', path: claudePath };
|
|
163
|
+
}
|
|
164
|
+
if ( existsSync( claudePath ) ) {
|
|
165
|
+
logger.debug( '[agent-md] import stub skipped — CLAUDE.md already exists' );
|
|
166
|
+
return { created: false, reason: 'claude-exists', path: claudePath };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await writeFile( claudePath, CLAUDE_IMPORT_STUB, 'utf8' );
|
|
170
|
+
logger.success( '[agent-md] created CLAUDE.md (@AGENTS.md import) so Claude Code reads AGENTS.md' );
|
|
171
|
+
return { created: true, reason: 'created', path: claudePath };
|
|
172
|
+
}
|