therookie 0.4.37 → 0.4.39

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