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 +1 -1
- package/src/cli.js +6 -4
- package/src/commands/codex-setup.js +12 -1
- package/src/commands/doctor.js +3 -1
- package/src/commands/init.js +6 -1
- package/src/commands/login.js +11 -1
- package/src/commands/update.js +6 -8
- package/src/detect.js +2 -1
- package/src/lib/apply-tool-setup.js +3 -2
- package/src/lib/claude-md-block.js +146 -59
- package/src/lib/codex-home.js +8 -0
- package/src/lib/deploy-artifacts.js +3 -2
- package/templates/codex-md/rookie-block.md +7 -0
- package/version.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "therookie",
|
|
3
|
-
"version": "0.4.
|
|
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(
|
|
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
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -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}`)
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -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
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
|
62
|
-
|
|
63
|
-
flags,
|
|
64
|
-
|
|
65
|
-
|
|
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,
|
|
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
|
-
//
|
|
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
|
|
65
|
-
const tpl = template ?? readTemplateBlock()
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 {
|
|
113
|
+
try { real = realpathSync(path) } catch { return { ...base, path, target: path, isSymlink, action: 'skip_broken_symlink' } }
|
|
79
114
|
}
|
|
80
|
-
const content = readFileSync(
|
|
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(
|
|
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
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
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[
|
|
200
|
+
if (flags[K.optOutFlag] || env[K.optOutEnv] === '1') return { lines, result: { written: false, action: 'opted_out' } }
|
|
137
201
|
|
|
138
|
-
const force = !!flags[
|
|
202
|
+
const force = !!flags[K.forceFlag]
|
|
203
|
+
const hintCmd = `rookie update --${K.forceFlag}`
|
|
139
204
|
let plan
|
|
140
205
|
try {
|
|
141
|
-
plan =
|
|
206
|
+
plan = K.plan({ home, env, force, template })
|
|
142
207
|
} catch (e) {
|
|
143
|
-
lines.push(`⚠
|
|
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(`
|
|
220
|
+
lines.push(` ${label} 의 더루키 블록이 수정돼 있어 그대로 둡니다 — 템플릿으로 되돌리려면 ${hintCmd}`)
|
|
154
221
|
return { lines, result: { written: false, action: plan.action } }
|
|
155
222
|
case 'skip_same_intent':
|
|
156
|
-
lines.push(`
|
|
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(`
|
|
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(`⚠
|
|
229
|
+
lines.push(`⚠ ${label} 심링크의 대상이 없습니다(${plan.path}) — 건드리지 않았습니다`)
|
|
163
230
|
return { lines, result: { written: false, action: plan.action } }
|
|
164
231
|
case 'skip_newer':
|
|
165
|
-
lines.push(`
|
|
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(`⚠
|
|
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(`
|
|
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(
|
|
183
|
-
lines.push(
|
|
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(
|
|
189
|
-
lines.push(
|
|
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(`⚠
|
|
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(`✓
|
|
270
|
+
lines.push(`✓ ${label} 더루키 블록 ${verb}: ${where}`)
|
|
204
271
|
if (result.backup) lines.push(` · 이전 본문 backup → ${result.backup}`)
|
|
205
|
-
if (mode === 'connect' && !interactive && isNewBlock) lines.push(
|
|
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
|
|
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
|
|
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 =
|
|
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':
|
|
228
|
-
case '
|
|
229
|
-
case '
|
|
230
|
-
case '
|
|
231
|
-
case '
|
|
232
|
-
case '
|
|
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(
|
|
235
|
-
?
|
|
236
|
-
:
|
|
237
|
-
case 'skip_broken_symlink': return `⚠
|
|
238
|
-
case 'malformed': return `✗
|
|
239
|
-
case 'create': return
|
|
240
|
-
case 'append': return
|
|
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 =
|
|
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