therookie 0.4.38 → 0.4.40

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "therookie",
3
- "version": "0.4.38",
3
+ "version": "0.4.40",
4
4
  "description": "Persistent memory for Claude Code and Codex CLI — teach your AI once and it remembers across sessions, tools, and machines. The AI new hire that remembers what you teach it.",
5
5
  "license": "MIT",
6
6
  "keywords": [
package/src/cli.js CHANGED
@@ -18,9 +18,9 @@ import { runOrphanExtract } from './commands/orphan-extract.js'
18
18
  const HELP = `rookie — the Rookie Skill/Hook 설치 관리자
19
19
 
20
20
  사용:
21
- rookie init [--local] [--force] [--yes] [--deep-scan] [--claude-md | --no-claude-md]
22
- rookie login [--local] [--claude-md | --no-claude-md]
23
- rookie update [--force] [--claude-md | --no-claude-md]
21
+ rookie init [--local] [--force] [--yes] [--deep-scan] [--claude-md | --no-claude-md] [--codex-md | --no-codex-md]
22
+ rookie login [--local] [--claude-md | --no-claude-md] [--codex-md | --no-codex-md]
23
+ rookie update [--force] [--claude-md | --no-claude-md] [--codex-md | --no-codex-md]
24
24
  rookie link [--local]
25
25
  rookie save [--file <body.json>] [--json] # 또는 stdin. --json = 서버 raw 응답 출력
26
26
  rookie status
@@ -28,7 +28,7 @@ const HELP = `rookie — the Rookie Skill/Hook 설치 관리자
28
28
  rookie unlink [--local | --global]
29
29
  rookie token list | revoke <id> | rotate
30
30
  rookie sweep-recovery [--force]
31
- rookie codex-setup [--yes]
31
+ rookie codex-setup [--yes] [--codex-md | --no-codex-md]
32
32
  rookie done-scan [--force]
33
33
  rookie context-diet [--json] [--force]
34
34
  rookie orphan-extract [--batch=5]
@@ -40,6 +40,8 @@ const HELP = `rookie — the Rookie Skill/Hook 설치 관리자
40
40
  --deep-scan 홈 디렉토리에서 로컬 Skill 전수 스캔
41
41
  --claude-md 글로벌 ~/.claude/CLAUDE.md 에 더루키 관리 블록을 넣는다(심링크·이미 같은 규칙이 있어도, update 에서도)
42
42
  --no-claude-md 글로벌 CLAUDE.md 를 건드리지 않는다 (환경 변수 ROOKIE_NO_CLAUDE_MD=1 과 같다)
43
+ --codex-md Codex 전역 지시 파일(AGENTS.override.md 가 있으면 그것, 없으면 AGENTS.md)에 더루키 관리 블록을 넣는다
44
+ --no-codex-md Codex 전역 지시 파일을 건드리지 않는다 (환경 변수 ROOKIE_NO_CODEX_MD=1 과 같다)
43
45
 
44
46
  명령:
45
47
  init 첫 설치. 비밀번호 입력 받아 PAT 발급 + Skill/Hook 배포.
@@ -4,6 +4,8 @@ import { existsSync } from 'node:fs'
4
4
  import { confirm } from '../prompt.js'
5
5
  import { loadConfig } from '../config.js'
6
6
  import { applyCodexSetup } from '../lib/deploy-artifacts.js'
7
+ import { runCodexMdStep } from '../lib/claude-md-block.js'
8
+ import { codexHomeDir } from '../lib/codex-home.js'
7
9
 
8
10
  // Codex config.toml 에 더루키 SessionStart hook 을 동의 기반으로 등록한다(managed 블록만).
9
11
  // 명시적 Codex-only 재설정 진입점 — 내부 write 는 applyCodexSetup(3층 adapter) 공유(spec §4 item 6).
@@ -18,7 +20,7 @@ export async function runCodexSetup(flags) {
18
20
  console.error(`✗ hook 스크립트가 없습니다(${hookScript}). \`rookie login\` 또는 \`rookie update\` 를 먼저 실행하세요.`)
19
21
  process.exit(1)
20
22
  }
21
- const configPath = join(homedir(), '.codex', 'config.toml')
23
+ const configPath = join(codexHomeDir(), 'config.toml')
22
24
 
23
25
  console.log('\nCodex 설정 파일에 더루키 SessionStart hook 을 등록합니다:')
24
26
  console.log(` ${configPath}`)
@@ -33,4 +35,13 @@ export async function runCodexSetup(flags) {
33
35
  process.exit(1)
34
36
  }
35
37
  console.log('✓ 등록 완료. 새 Codex 세션부터 자동 적용됩니다.')
38
+
39
+ // spec 2026-09-29 codex-agents-md — Codex 전역 지시 파일에 "더루키 먼저" 관리 블록(동의 기반).
40
+ const { lines } = await runCodexMdStep({
41
+ mode: 'connect',
42
+ flags,
43
+ interactive: !!process.stdin.isTTY && !flags.yes,
44
+ confirmFn: confirm,
45
+ })
46
+ for (const line of lines) console.log(line)
36
47
  }
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
- import { claudeMdStatusLine } from '../lib/claude-md-block.js'
2
+ import { claudeMdStatusLine, codexMdStatusLine } from '../lib/claude-md-block.js'
3
3
  import { homedir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
  import { execSync } from 'node:child_process'
@@ -45,6 +45,8 @@ export async function runDoctor(flags) {
45
45
  console.log(hasRookieHook(lSettings) ? '✓ 로컬 hook 등록됨' : ' 로컬 hook 없음')
46
46
  const claudeMdLine = claudeMdStatusLine()
47
47
  if (claudeMdLine) console.log(claudeMdLine)
48
+ const codexMdLine = codexMdStatusLine()
49
+ if (codexMdLine) console.log(codexMdLine)
48
50
 
49
51
  if (g && l && g.version !== l.version) {
50
52
  console.log(`⚠ 버전 불일치: 글로벌 v${g.version} vs 로컬 v${l.version}`)
@@ -12,7 +12,7 @@ import { apiGet, apiPostNoAuth } from '../api.js'
12
12
  import { applyToolSetup, pendingToolSetup } from '../lib/apply-tool-setup.js'
13
13
  import { renderDetected, renderConnectOutcome, toolName } from '../lib/connect-ui.js'
14
14
  import { resolveEndpoint } from '../lib/resolve-endpoint.js'
15
- import { runClaudeMdStep } from '../lib/claude-md-block.js'
15
+ import { runClaudeMdStep, runCodexMdStep } from '../lib/claude-md-block.js'
16
16
 
17
17
  async function pickChoice(prompt, options, autoValue) {
18
18
  console.log(prompt)
@@ -179,4 +179,9 @@ export async function runInit(flags) {
179
179
  const { lines } = await runClaudeMdStep({ mode: 'connect', flags, interactive, confirmFn: confirm })
180
180
  for (const line of lines) console.log(line)
181
181
  }
182
+ // 2026-09-29 두목 결정 — Codex 전역 지시 파일도 첫 연결에서 기본 적용(끄기 ROOKIE_NO_CODEX_MD=1).
183
+ if (scope === 'global' && report.applied.some((a) => a.id === 'codex')) {
184
+ const { lines } = await runCodexMdStep({ mode: 'connect', flags, interactive, confirmFn: confirm })
185
+ for (const line of lines) console.log(line)
186
+ }
182
187
  }
@@ -10,7 +10,7 @@ import { renderDetected, renderConnectOutcome, toolName } from '../lib/connect-u
10
10
  import { confirm } from '../prompt.js'
11
11
  import { resolveEndpoint } from '../lib/resolve-endpoint.js'
12
12
  import { accountLine, accountWarning } from '../lib/account-ui.js'
13
- import { runClaudeMdStep } from '../lib/claude-md-block.js'
13
+ import { runClaudeMdStep, runCodexMdStep } from '../lib/claude-md-block.js'
14
14
 
15
15
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
16
16
 
@@ -170,4 +170,14 @@ export async function runLogin(flags) {
170
170
  })
171
171
  for (const line of lines) console.log(line)
172
172
  }
173
+ // 2026-09-29 두목 결정 — Codex 전역 지시 파일도 첫 연결에서 기본 적용(끄기 ROOKIE_NO_CODEX_MD=1).
174
+ if (scope === 'global' && report.applied.some((a) => a.id === 'codex')) {
175
+ const { lines } = await runCodexMdStep({
176
+ mode: 'connect',
177
+ flags,
178
+ interactive: !!process.stdin.isTTY && !flags.yes,
179
+ confirmFn: confirm,
180
+ })
181
+ for (const line of lines) console.log(line)
182
+ }
173
183
  }
@@ -13,7 +13,7 @@ import { semverGt, fetchReleaseManifest, selfUpdateAndRerun, isNpmManagedInstall
13
13
  import { installSkill } from '../lib/skill-installer.js'
14
14
  import { claudeHookCommand, escapeJsonStringValue } from '../lib/hook-command.js'
15
15
  import { mergeHooks } from '../lib/merge-hooks.js'
16
- import { runClaudeMdStep } from '../lib/claude-md-block.js'
16
+ import { runClaudeMdStep, runCodexMdStep } from '../lib/claude-md-block.js'
17
17
  import { confirm } from '../prompt.js'
18
18
 
19
19
  const __filename = fileURLToPath(import.meta.url)
@@ -58,13 +58,11 @@ export async function runUpdate(flags) {
58
58
  // manifest "최신" 조기 종료·서버 실패보다 앞에 둔다. 표식이 있으면 갱신, 없으면 대화형 확인 또는
59
59
  // --claude-md 일 때만 붙인다 — update 는 모델이 비대화로 돌리는 경로가 있다.
60
60
  {
61
- const { lines } = await runClaudeMdStep({
62
- mode: 'update',
63
- flags,
64
- interactive: !!process.stdin.isTTY && !flags.yes,
65
- confirmFn: confirm,
66
- })
67
- for (const line of lines) console.log(line)
61
+ const interactive = !!process.stdin.isTTY && !flags.yes
62
+ for (const step of [runClaudeMdStep, runCodexMdStep]) {
63
+ const { lines } = await step({ mode: 'update', flags, interactive, confirmFn: confirm })
64
+ for (const line of lines) console.log(line)
65
+ }
68
66
  }
69
67
 
70
68
  let serverManifest
package/src/detect.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, existsSync, readdirSync, accessSync, constants } from 'node:fs'
2
2
  import { homedir } from 'node:os'
3
3
  import { join } from 'node:path'
4
+ import { codexHomeDir } from './lib/codex-home.js'
4
5
 
5
6
  // 통일 연결 흐름(spec 2026-06-29-unified-connect-flow §3 D1): 이 컴퓨터에 깔린 AI 도구를 감지한다.
6
7
  // registry 형 — Cursor/Gemini 등은 entry 추가만으로 확장. boolean 반환 금지.
@@ -44,7 +45,7 @@ function defaultHasCli(name, env = process.env, platform = process.platform) {
44
45
  export function detectInstalledTools({ home = homedir(), env = process.env, hasCli = defaultHasCli } = {}) {
45
46
  return TOOL_REGISTRY.map((t) => {
46
47
  const cli = t.cliNames.some((n) => hasCli(n, env))
47
- const dir = existsSync(join(home, t.homeDir))
48
+ const dir = existsSync(t.id === 'codex' ? codexHomeDir({ home, env }) : join(home, t.homeDir))
48
49
  const present = cli || dir
49
50
  const signal = cli ? 'strong' : dir ? 'weak' : 'none'
50
51
  const reason = cli ? `${t.id} CLI 발견` : dir ? `~/${t.homeDir} 존재` : '없음'
@@ -3,13 +3,14 @@ import { homedir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
  import { installSharedHookRuntime, applyClaudeSetup, applyCodexSetup } from './deploy-artifacts.js'
5
5
  import { ROOKIE_BEGIN } from './codex-config.js'
6
+ import { codexHomeDir } from './codex-home.js'
6
7
 
7
8
  // 도구가 이미 rookie 로 설정됐는지. init 의 도구-추가 모드(early-return 우회) 판정용.
8
- export function isToolConfigured(id, { scope, home = homedir(), cwd = process.cwd() } = {}) {
9
+ export function isToolConfigured(id, { scope, home = homedir(), cwd = process.cwd(), env = process.env } = {}) {
9
10
  const base = scope === 'global' ? home : cwd
10
11
  if (id === 'claude') return existsSync(join(base, '.claude', 'skills', 'rookie', 'SKILL.md'))
11
12
  if (id === 'codex') {
12
- const cfg = join(home, '.codex', 'config.toml')
13
+ const cfg = join(codexHomeDir({ home, env }), 'config.toml')
13
14
  return existsSync(cfg) && readFileSync(cfg, 'utf8').includes(ROOKIE_BEGIN)
14
15
  }
15
16
  return false
@@ -18,9 +18,11 @@ import {
18
18
  import { homedir } from 'node:os'
19
19
  import { join, dirname, basename } from 'node:path'
20
20
  import { fileURLToPath } from 'node:url'
21
+ import { codexHomeDir } from './codex-home.js'
21
22
 
22
23
  const __dirname = dirname(fileURLToPath(import.meta.url))
23
24
  export const DEFAULT_TEMPLATE_PATH = join(__dirname, '..', '..', 'templates', 'claude-md', 'rookie-block.md')
25
+ export const CODEX_TEMPLATE_PATH = join(__dirname, '..', '..', 'templates', 'codex-md', 'rookie-block.md')
24
26
 
25
27
  const BEGIN_RE = /<!-- rookie:begin v(\d+) -->/g
26
28
  const END_MARK = '<!-- rookie:end -->'
@@ -58,28 +60,61 @@ export function upsertRookieBlock(content, blockText) {
58
60
  return content + sep + blockText + '\n'
59
61
  }
60
62
 
61
- // 무엇을 할지 정한다(쓰지 않는다). force = --claude-md.
63
+ // 대상 서술자 — 어느 폴더의 어느 파일을 관리하는가(spec 2026-09-29 codex-agents-md §3).
64
+ // candidates: 앞에서부터 먼저 존재하는 파일이 Codex/Claude 가 실제로 읽는 파일이다.
65
+ export function claudeTarget({ home = homedir() } = {}) {
66
+ return {
67
+ dir: join(home, '.claude'),
68
+ candidates: ['CLAUDE.md'],
69
+ createName: 'CLAUDE.md',
70
+ needles: [SAME_INTENT_NEEDLE],
71
+ noDirAction: 'no_claude_dir',
72
+ templatePath: DEFAULT_TEMPLATE_PATH,
73
+ }
74
+ }
75
+
76
+ // Codex 는 $CODEX_HOME(없으면 ~/.codex)의 AGENTS.override.md 가 있으면 그것만 읽고 AGENTS.md 는 무시한다
77
+ // (2026-09-29 codex 0.153.4 바이너리 strings + codex exec 실측).
78
+ export function codexTarget({ home = homedir(), env = process.env } = {}) {
79
+ return {
80
+ dir: codexHomeDir({ home, env }),
81
+ candidates: ['AGENTS.override.md', 'AGENTS.md'],
82
+ createName: 'AGENTS.md',
83
+ needles: [SAME_INTENT_NEEDLE, 'rookie_recall'],
84
+ noDirAction: 'no_codex_dir',
85
+ templatePath: CODEX_TEMPLATE_PATH,
86
+ }
87
+ }
88
+
89
+ function pickTargetFile(dir, candidates) {
90
+ for (const name of candidates) {
91
+ const p = join(dir, name)
92
+ try { return { path: p, lst: lstatSync(p) } } catch { /* 다음 후보 */ }
93
+ }
94
+ return null
95
+ }
96
+
97
+ // 무엇을 할지 정한다(쓰지 않는다). force = --claude-md / --codex-md.
62
98
  // action: create | append | replace | noop | noop_edited | skip_same_intent |
63
- // skip_symlink_unmanaged | skip_broken_symlink | skip_newer | malformed | no_claude_dir
64
- export function planClaudeMd({ home = homedir(), force = false, template } = {}) {
65
- const tpl = template ?? readTemplateBlock()
66
- const claudeDir = join(home, '.claude')
67
- const path = join(claudeDir, 'CLAUDE.md')
68
- const base = { path, target: path, isSymlink: false, template: tpl }
69
- if (!existsSync(claudeDir)) return { ...base, action: 'no_claude_dir' }
70
-
71
- let lst = null
72
- try { lst = lstatSync(path) } catch { lst = null }
73
- if (!lst) return { ...base, action: 'create' }
74
-
75
- let target = path
76
- const isSymlink = lst.isSymbolicLink()
99
+ // skip_symlink_unmanaged | skip_broken_symlink | skip_newer | malformed | no_claude_dir | no_codex_dir
100
+ export function planManagedMd({ target, force = false, template } = {}) {
101
+ const tpl = template ?? readTemplateBlock(target.templatePath)
102
+ const createPath = join(target.dir, target.createName)
103
+ const base = { path: createPath, target: createPath, isSymlink: false, template: tpl, needles: target.needles }
104
+ if (!existsSync(target.dir)) return { ...base, action: target.noDirAction }
105
+
106
+ const found = pickTargetFile(target.dir, target.candidates)
107
+ if (!found) return { ...base, action: 'create' }
108
+
109
+ const path = found.path
110
+ let real = path
111
+ const isSymlink = found.lst.isSymbolicLink()
77
112
  if (isSymlink) {
78
- try { target = realpathSync(path) } catch { return { ...base, isSymlink, action: 'skip_broken_symlink' } }
113
+ try { real = realpathSync(path) } catch { return { ...base, path, target: path, isSymlink, action: 'skip_broken_symlink' } }
79
114
  }
80
- const content = readFileSync(target, 'utf8')
115
+ const content = readFileSync(real, 'utf8')
81
116
  const p = parseRookieBlock(content)
82
- const ctx = { ...base, target, isSymlink, content, existing: p }
117
+ const ctx = { ...base, path, target: real, isSymlink, content, existing: p }
83
118
 
84
119
  if (p.state === 'malformed') return { ...ctx, action: 'malformed' }
85
120
  if (p.state === 'ok') {
@@ -90,10 +125,18 @@ export function planClaudeMd({ home = homedir(), force = false, template } = {})
90
125
  }
91
126
  // 표식 없음
92
127
  if (isSymlink && !force) return { ...ctx, action: 'skip_symlink_unmanaged' }
93
- if (content.includes(SAME_INTENT_NEEDLE) && !force) return { ...ctx, action: 'skip_same_intent' }
128
+ if (target.needles.some((n) => content.includes(n)) && !force) return { ...ctx, action: 'skip_same_intent' }
94
129
  return { ...ctx, action: 'append' }
95
130
  }
96
131
 
132
+ export function planClaudeMd({ home = homedir(), force = false, template } = {}) {
133
+ return planManagedMd({ target: claudeTarget({ home }), force, template })
134
+ }
135
+
136
+ export function planCodexMd({ home = homedir(), env = process.env, force = false, template } = {}) {
137
+ return planManagedMd({ target: codexTarget({ home, env }), force, template })
138
+ }
139
+
97
140
  export const WRITING_ACTIONS = new Set(['create', 'append', 'replace'])
98
141
 
99
142
  // plan 대로 쓴다. 대상 파일 폴더에 임시 파일을 만들고 rename — 심링크 경로 자체는 건드리지 않는다.
@@ -121,9 +164,29 @@ export function applyClaudeMd(plan) {
121
164
  return { written: true, action: plan.action, target, backup: mode !== null ? `${target}.rookie-bak` : null }
122
165
  }
123
166
 
124
- // 명령(init·login·update)이 공유하는 한 단계. 출력 줄 목록을 돌려준다(테스트 가능하게).
125
- // mode 'connect'(login·init): 기본 적용. 'update': 표식이 있을 때만 갱신, 없으면 대화형 확인 또는 --claude-md.
126
- export async function runClaudeMdStep({
167
+ // 대상별 문구·플래그. CLAUDE.md 문구는 0.4.38 과 글자 그대로다(테스트가 고정한다).
168
+ export const KINDS = {
169
+ claude: {
170
+ label: () => '글로벌 CLAUDE.md',
171
+ forceFlag: 'claude-md',
172
+ optOutFlag: 'no-claude-md',
173
+ optOutEnv: 'ROOKIE_NO_CLAUDE_MD',
174
+ plan: ({ home, force, template }) => planClaudeMd({ home, force, template }),
175
+ },
176
+ codex: {
177
+ // Codex 는 override 가 있으면 그 파일을 읽으므로 실제 파일 이름을 그대로 보인다.
178
+ label: (plan) => `Codex ${plan ? basename(plan.path) : 'AGENTS.md'}`,
179
+ forceFlag: 'codex-md',
180
+ optOutFlag: 'no-codex-md',
181
+ optOutEnv: 'ROOKIE_NO_CODEX_MD',
182
+ plan: ({ home, env, force, template }) => planCodexMd({ home, env, force, template }),
183
+ },
184
+ }
185
+
186
+ // 명령(init·login·update·codex-setup)이 공유하는 한 단계. 출력 줄 목록을 돌려준다(테스트 가능하게).
187
+ // mode 'connect'(login·init·codex-setup): 기본 적용. 'update': 표식이 있을 때만 갱신, 없으면 대화형 확인 또는 강제 플래그.
188
+ export async function runManagedMdStep({
189
+ kind = 'claude',
127
190
  mode,
128
191
  flags = {},
129
192
  env = process.env,
@@ -132,40 +195,44 @@ export async function runClaudeMdStep({
132
195
  home = homedir(),
133
196
  template,
134
197
  } = {}) {
198
+ const K = KINDS[kind]
135
199
  const lines = []
136
- if (flags['no-claude-md'] || env.ROOKIE_NO_CLAUDE_MD === '1') return { lines, result: { written: false, action: 'opted_out' } }
200
+ if (flags[K.optOutFlag] || env[K.optOutEnv] === '1') return { lines, result: { written: false, action: 'opted_out' } }
137
201
 
138
- const force = !!flags['claude-md']
202
+ const force = !!flags[K.forceFlag]
203
+ const hintCmd = `rookie update --${K.forceFlag}`
139
204
  let plan
140
205
  try {
141
- plan = planClaudeMd({ home, force, template })
206
+ plan = K.plan({ home, env, force, template })
142
207
  } catch (e) {
143
- lines.push(`⚠ 글로벌 CLAUDE.md 점검 실패: ${e.message}`)
208
+ lines.push(`⚠ ${K.label()} 점검 실패: ${e.message}`)
144
209
  return { lines, result: { written: false, action: 'error' } }
145
210
  }
211
+ const label = K.label(plan)
146
212
  const where = plan.isSymlink ? `${plan.path} → ${plan.target}` : plan.path
147
213
 
148
214
  switch (plan.action) {
149
215
  case 'no_claude_dir':
216
+ case 'no_codex_dir':
150
217
  case 'noop':
151
218
  return { lines, result: { written: false, action: plan.action } }
152
219
  case 'noop_edited':
153
- lines.push(` 글로벌 CLAUDE.md 의 더루키 블록이 수정돼 있어 그대로 둡니다 — 템플릿으로 되돌리려면 rookie update --claude-md`)
220
+ lines.push(` ${label} 의 더루키 블록이 수정돼 있어 그대로 둡니다 — 템플릿으로 되돌리려면 ${hintCmd}`)
154
221
  return { lines, result: { written: false, action: plan.action } }
155
222
  case 'skip_same_intent':
156
- lines.push(` 글로벌 CLAUDE.md 에 더루키 회상 규칙이 이미 있어 블록을 넣지 않았습니다 — 관리 블록으로 두려면 --claude-md`)
223
+ lines.push(` ${label} 에 더루키 회상 규칙이 이미 있어 블록을 넣지 않았습니다 — 관리 블록으로 두려면 --${K.forceFlag}`)
157
224
  return { lines, result: { written: false, action: plan.action } }
158
225
  case 'skip_symlink_unmanaged':
159
- lines.push(` 글로벌 CLAUDE.md 가 심링크라 블록을 넣지 않았습니다(${where}) — 넣으려면 --claude-md`)
226
+ lines.push(` ${label} 가 심링크라 블록을 넣지 않았습니다(${where}) — 넣으려면 --${K.forceFlag}`)
160
227
  return { lines, result: { written: false, action: plan.action } }
161
228
  case 'skip_broken_symlink':
162
- lines.push(`⚠ 글로벌 CLAUDE.md 심링크의 대상이 없습니다(${plan.path}) — 건드리지 않았습니다`)
229
+ lines.push(`⚠ ${label} 심링크의 대상이 없습니다(${plan.path}) — 건드리지 않았습니다`)
163
230
  return { lines, result: { written: false, action: plan.action } }
164
231
  case 'skip_newer':
165
- lines.push(` 글로벌 CLAUDE.md 의 더루키 블록(v${plan.existing.version})이 이 CLI(v${plan.template.version})보다 새 버전이라 그대로 둡니다`)
232
+ lines.push(` ${label} 의 더루키 블록(v${plan.existing.version})이 이 CLI(v${plan.template.version})보다 새 버전이라 그대로 둡니다`)
166
233
  return { lines, result: { written: false, action: plan.action } }
167
234
  case 'malformed':
168
- lines.push(`⚠ 글로벌 CLAUDE.md 의 더루키 블록 표식이 손상돼 건드리지 않았습니다 — 수동 점검: ${plan.target}`)
235
+ lines.push(`⚠ ${label} 의 더루키 블록 표식이 손상돼 건드리지 않았습니다 — 수동 점검: ${plan.target}`)
169
236
  return { lines, result: { written: false, action: plan.action } }
170
237
  default:
171
238
  break
@@ -175,18 +242,18 @@ export async function runClaudeMdStep({
175
242
  const isNewBlock = plan.action === 'create' || plan.action === 'append'
176
243
  if (mode === 'update' && isNewBlock && !force) {
177
244
  if (!interactive) {
178
- lines.push(` 글로벌 CLAUDE.md 에 더루키 블록이 없습니다 — 넣으려면 rookie update --claude-md (rookie doctor 로 확인)`)
245
+ lines.push(` ${label} 에 더루키 블록이 없습니다 — 넣으려면 ${hintCmd} (rookie doctor 로 확인)`)
179
246
  return { lines, result: { written: false, action: 'needs_consent' } }
180
247
  }
181
- console.log(describePlan(plan))
182
- if (!(await confirmFn('글로벌 CLAUDE.md 에 이 블록을 넣을까요?', { defaultYes: false }))) {
183
- lines.push(' 글로벌 CLAUDE.md 블록 추가를 건너뛰었습니다')
248
+ console.log(describePlan(plan, label))
249
+ if (!(await confirmFn(`${label} 에 이 블록을 넣을까요?`, { defaultYes: false }))) {
250
+ lines.push(` ${label} 블록 추가를 건너뛰었습니다`)
184
251
  return { lines, result: { written: false, action: 'declined' } }
185
252
  }
186
253
  } else if (mode === 'connect' && isNewBlock && interactive) {
187
- console.log(describePlan(plan))
188
- if (!(await confirmFn('글로벌 CLAUDE.md 에 이 블록을 넣을까요?', { defaultYes: true }))) {
189
- lines.push(' 글로벌 CLAUDE.md 블록 추가를 건너뛰었습니다 (나중에: rookie update --claude-md)')
254
+ console.log(describePlan(plan, label))
255
+ if (!(await confirmFn(`${label} 에 이 블록을 넣을까요?`, { defaultYes: true }))) {
256
+ lines.push(` ${label} 블록 추가를 건너뛰었습니다 (나중에: ${hintCmd})`)
190
257
  return { lines, result: { written: false, action: 'declined' } }
191
258
  }
192
259
  }
@@ -195,49 +262,69 @@ export async function runClaudeMdStep({
195
262
  try {
196
263
  result = applyClaudeMd(plan)
197
264
  } catch (e) {
198
- lines.push(`⚠ 글로벌 CLAUDE.md 쓰기 실패(원본 보존): ${e.message}`)
265
+ lines.push(`⚠ ${label} 쓰기 실패(원본 보존): ${e.message}`)
199
266
  return { lines, result: { written: false, action: 'error' } }
200
267
  }
201
268
  if (result.written) {
202
269
  const verb = plan.action === 'replace' ? `갱신(v${plan.existing.version} → v${plan.template.version})` : `추가(v${plan.template.version})`
203
- lines.push(`✓ 글로벌 CLAUDE.md 더루키 블록 ${verb}: ${where}`)
270
+ lines.push(`✓ ${label} 더루키 블록 ${verb}: ${where}`)
204
271
  if (result.backup) lines.push(` · 이전 본문 backup → ${result.backup}`)
205
- if (mode === 'connect' && !interactive && isNewBlock) lines.push(' (넣지 않으려면 표식 사이를 지우고 ROOKIE_NO_CLAUDE_MD=1 로 두세요)')
272
+ if (mode === 'connect' && !interactive && isNewBlock) lines.push(` (넣지 않으려면 표식 사이를 지우고 ${K.optOutEnv}=1 로 두세요)`)
206
273
  }
207
274
  return { lines, result }
208
275
  }
209
276
 
277
+ export async function runClaudeMdStep(opts = {}) {
278
+ return runManagedMdStep({ ...opts, kind: 'claude' })
279
+ }
280
+
281
+ export async function runCodexMdStep(opts = {}) {
282
+ return runManagedMdStep({ ...opts, kind: 'codex' })
283
+ }
284
+
210
285
  // 쓰기 전에 보여 줄 설명.
211
- export function describePlan(plan) {
286
+ export function describePlan(plan, label = '글로벌 CLAUDE.md') {
212
287
  const where = plan.isSymlink ? `${plan.path} → ${plan.target}` : plan.path
213
288
  const what = plan.action === 'create' ? '새 파일로 만들고' : plan.action === 'append' ? '파일 끝에' : '기존 블록을 바꿔'
214
289
  return [
215
- `\n글로벌 CLAUDE.md 에 더루키 관리 블록을 둡니다 — ${where}`,
290
+ `\n${label} 에 더루키 관리 블록을 둡니다 — ${where}`,
216
291
  ` ${what} 아래 블록을 넣습니다(표식 밖의 글은 건드리지 않습니다):`,
217
292
  ...plan.template.text.split('\n').map((l) => ` | ${l}`),
218
293
  ].join('\n')
219
294
  }
220
295
 
221
296
  // doctor 용 한 줄(읽기만).
222
- export function claudeMdStatusLine({ home = homedir(), template } = {}) {
297
+ export function managedMdStatusLine({ kind = 'claude', home = homedir(), env = process.env, template } = {}) {
298
+ const K = KINDS[kind]
299
+ const hintCmd = `rookie update --${K.forceFlag}`
223
300
  let plan
224
- try { plan = planClaudeMd({ home, force: false, template }) } catch (e) { return `⚠ 글로벌 CLAUDE.md 점검 실패: ${e.message}` }
301
+ try { plan = K.plan({ home, env, force: false, template }) } catch (e) { return `⚠ ${K.label()} 점검 실패: ${e.message}` }
302
+ const label = K.label(plan)
225
303
  const v = plan.template.version
226
304
  switch (plan.action) {
227
- case 'no_claude_dir': return null
228
- case 'noop': return `✓ 글로벌 CLAUDE.md 더루키 블록 v${v}`
229
- case 'replace': return `⚠ 글로벌 CLAUDE.md 더루키 블록 v${plan.existing.version} → v${v} — rookie update`
230
- case 'noop_edited': return '⚠ 글로벌 CLAUDE.md 더루키 블록이 수정됨 — 되돌리려면 rookie update --claude-md'
231
- case 'skip_newer': return `✓ 글로벌 CLAUDE.md 더루키 블록 v${plan.existing.version} (이 CLI 보다 새 버전)`
232
- case 'skip_same_intent': return '✓ 글로벌 CLAUDE.md 에 더루키 회상 규칙 있음(관리 블록 아님)'
305
+ case 'no_claude_dir':
306
+ case 'no_codex_dir': return null
307
+ case 'noop': return `✓ ${label} 더루키 블록 v${v}`
308
+ case 'replace': return `⚠ ${label} 더루키 블록 v${plan.existing.version} → v${v} — rookie update`
309
+ case 'noop_edited': return `⚠ ${label} 더루키 블록이 수정됨 — 되돌리려면 ${hintCmd}`
310
+ case 'skip_newer': return `✓ ${label} 더루키 블록 v${plan.existing.version} (이 CLI 보다 새 버전)`
311
+ case 'skip_same_intent': return `✓ ${label} 에 더루키 회상 규칙 있음(관리 블록 아님)`
233
312
  case 'skip_symlink_unmanaged':
234
- return plan.content.includes(SAME_INTENT_NEEDLE)
235
- ? '✓ 글로벌 CLAUDE.md(심링크)에 더루키 회상 규칙 있음(관리 블록 아님)'
236
- : '⚠ 글로벌 CLAUDE.md(심링크)에 더루키 블록 없음 — rookie update --claude-md'
237
- case 'skip_broken_symlink': return `⚠ 글로벌 CLAUDE.md 심링크 대상 없음 — ${plan.path}`
238
- case 'malformed': return `✗ 글로벌 CLAUDE.md 더루키 블록 손상(begin/end 불일치) — 수동 점검: ${plan.target}`
239
- case 'create': return '⚠ 글로벌 CLAUDE.md 없음 — rookie update --claude-md'
240
- case 'append': return '⚠ 글로벌 CLAUDE.md 에 더루키 블록 없음 — rookie update --claude-md'
313
+ return plan.needles.some((n) => plan.content.includes(n))
314
+ ? `✓ ${label}(심링크)에 더루키 회상 규칙 있음(관리 블록 아님)`
315
+ : `⚠ ${label}(심링크)에 더루키 블록 없음 — ${hintCmd}`
316
+ case 'skip_broken_symlink': return `⚠ ${label} 심링크 대상 없음 — ${plan.path}`
317
+ case 'malformed': return `✗ ${label} 더루키 블록 손상(begin/end 불일치) — 수동 점검: ${plan.target}`
318
+ case 'create': return `⚠ ${label} 없음 — ${hintCmd}`
319
+ case 'append': return `⚠ ${label} 에 더루키 블록 없음 — ${hintCmd}`
241
320
  default: return null
242
321
  }
243
322
  }
323
+
324
+ export function claudeMdStatusLine({ home = homedir(), template } = {}) {
325
+ return managedMdStatusLine({ kind: 'claude', home, template })
326
+ }
327
+
328
+ export function codexMdStatusLine({ home = homedir(), env = process.env, template } = {}) {
329
+ return managedMdStatusLine({ kind: 'codex', home, env, template })
330
+ }
@@ -0,0 +1,8 @@
1
+ import { homedir } from 'node:os'
2
+ import { join } from 'node:path'
3
+
4
+ // Codex 설정 폴더. Codex 는 $CODEX_HOME 이 있으면 그 폴더를 쓴다(2026-09-29 바이너리 strings 실측).
5
+ // config.toml·AGENTS.md·감지가 모두 이 한 곳을 보게 한다 — 갈리면 hook 과 지시 블록이 서로 다른 폴더에 들어간다.
6
+ export function codexHomeDir({ home = homedir(), env = process.env } = {}) {
7
+ return env.CODEX_HOME ? env.CODEX_HOME : join(home, '.codex')
8
+ }
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, cpSync } from 'node:fs'
2
2
  import { homedir } from 'node:os'
3
+ import { codexHomeDir } from './codex-home.js'
3
4
  import { join, dirname } from 'node:path'
4
5
  import { fileURLToPath } from 'node:url'
5
6
  import { apiGet } from '../api.js'
@@ -163,8 +164,8 @@ export async function applyClaudeSetup({ scope, cfg, home, cwd } = {}) {
163
164
 
164
165
  // ── 3층: Codex adapter — ~/.codex/config.toml 의 managed 블록(upsertCodexHookBlock).
165
166
  // scope=global 전제(D3): Codex config 는 항상 전역이고 전역 hook 을 문다. 호출 게이트는 오케스트레이터 책임.
166
- export function applyCodexSetup({ home = homedir() } = {}) {
167
- const codexDir = join(home, '.codex')
167
+ export function applyCodexSetup({ home = homedir(), env = process.env } = {}) {
168
+ const codexDir = codexHomeDir({ home, env })
168
169
  const configPath = join(codexDir, 'config.toml')
169
170
  const hookScript = join(home, '.rookie', 'bin', 'session-start.sh')
170
171
  const existing = existsSync(configPath) ? readFileSync(configPath, 'utf8') : ''
@@ -0,0 +1,7 @@
1
+ <!-- rookie:begin v1 -->
2
+ ## 더루키(the Rookie) 기억 — 로컬보다 먼저
3
+ - 전에 한 일·결정·맥락이 필요하면(어제·전에·이어서 같은 말이 없어도) 로컬 메모·Codex 자체 기억을 뒤지기 **전에** 더루키 회상을 먼저 부른다 — rookie MCP 도구 `rookie_recall` 이 있으면 그것을, 없으면 셸에서 `GET /api/rookie/recall`(rookie CLI 설정의 endpoint·토큰). 로컬 기억은 보조다.
4
+ - 새로 기억할 것(운영 사실·결정·규칙·취향)은 **더루키에 먼저** 저장한다 — `rookie_save` 도구 또는 셸의 `rookie save`. 로컬 메모는 필요할 때만 보조로 남긴다.
5
+ - 예외: 작업 폴더에 `.rookie-ignore` 가 있거나, `ROOKIE_DISABLED=1` 이거나, `~/.rookie/config.json` 이 없으면 위 두 줄을 따르지 않는다.
6
+ - 세부 절차는 세션 시작 주입(더루키 hook)이 정본이다 — 여기에 옮겨 적지 않는다.
7
+ <!-- rookie:end -->
package/version.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "cli_version": "0.4.38",
2
+ "cli_version": "0.4.40",
3
3
  "skill_version": "1.40.1"
4
4
  }