@seomi/ssh 0.1.1 → 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/README.md +2 -2
- package/README.ru.md +2 -2
- package/package.json +9 -1
- package/src/commands/init.mjs +23 -1
- package/src/lib/agent-md-target.mjs +71 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @seomi/ssh
|
|
2
2
|
|
|
3
|
-
**English** | [Русский](
|
|
3
|
+
**English** | [Русский](https://github.com/Mikeekb/seomi-ssh/blob/main/README.ru.md)
|
|
4
4
|
|
|
5
5
|
> One command sets up passwordless SSH access for an AI agent to your servers.
|
|
6
6
|
|
|
@@ -102,7 +102,7 @@ $ seomi-ssh init
|
|
|
102
102
|
|
|
103
103
|
| Guide | Description |
|
|
104
104
|
|-------|-------------|
|
|
105
|
-
| [`init` command](docs/init.md) | SSH setup behavior, `.claude/.env` configuration, examples, troubleshooting |
|
|
105
|
+
| [`init` command](https://github.com/Mikeekb/seomi-ssh/blob/main/docs/init.md) | SSH setup behavior, `.claude/.env` configuration, examples, troubleshooting |
|
|
106
106
|
|
|
107
107
|
## License
|
|
108
108
|
|
package/README.ru.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @seomi/ssh
|
|
2
2
|
|
|
3
|
-
[English](
|
|
3
|
+
[English](https://github.com/Mikeekb/seomi-ssh/blob/main/README.md) | **Русский**
|
|
4
4
|
|
|
5
5
|
> Одной командой настраивает беспарольный SSH-доступ для AI-агента к вашим серверам.
|
|
6
6
|
|
|
@@ -102,7 +102,7 @@ $ seomi-ssh init
|
|
|
102
102
|
|
|
103
103
|
| Раздел | Описание |
|
|
104
104
|
|--------|----------|
|
|
105
|
-
| [Команда `init`](docs/init.md) | Поведение `init`, конфигурация `.claude/.env`, примеры, диагностика |
|
|
105
|
+
| [Команда `init`](https://github.com/Mikeekb/seomi-ssh/blob/main/docs/init.md) | Поведение `init`, конфигурация `.claude/.env`, примеры, диагностика |
|
|
106
106
|
|
|
107
107
|
## Лицензия
|
|
108
108
|
|
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": {
|
|
@@ -34,6 +34,14 @@
|
|
|
34
34
|
"deploy"
|
|
35
35
|
],
|
|
36
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
|
+
},
|
|
37
45
|
"publishConfig": {
|
|
38
46
|
"access": "public"
|
|
39
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
|
+
}
|