@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @seomi/ssh
2
2
 
3
- **English** | [Русский](./README.ru.md)
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](./README.md) | **Русский**
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.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
  },
@@ -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` if present and IGNORES `AGENTS.md` next to it.
5
- * Other agents (e.g. ai-factory tooling) treat `AGENTS.md` as the universal
6
- * standard. This detector lets `init` / `update` / `doctor` keep the managed
7
- * `seomi-ssh` block in the file the project actually uses and, when both
8
- * coexist, synchronize the block into both so no agent loses context.
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
+ }