therookie 0.4.24 → 0.4.26
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
package/src/lib/codex-config.js
CHANGED
|
@@ -5,11 +5,20 @@ import { toPosixPath } from './hook-command.js'
|
|
|
5
5
|
export const ROOKIE_BEGIN = '# >>> rookie codex hook (managed) — 수동 편집 금지 >>>'
|
|
6
6
|
export const ROOKIE_END = '# <<< rookie codex hook (managed) <<<'
|
|
7
7
|
|
|
8
|
-
function block(
|
|
9
|
-
// Codex SessionStart command handler. additionalContext 출력 형식은 Claude 와 동일.
|
|
8
|
+
function block(paths, platform = process.platform) {
|
|
9
|
+
// Codex SessionStart / UserPromptSubmit command handler. additionalContext 출력 형식은 Claude 와 동일.
|
|
10
10
|
// Windows 는 backslash 경로를 forward-slash 로 변환(Git Bash 가 실행) — macOS/Linux 무변경.
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
//
|
|
12
|
+
// spec 2026-09-02 (2단계 로드 강제): UserPromptSubmit 이 없으면 SessionStart 가 만든
|
|
13
|
+
// 인격 대기열을 아무도 소비하지 않는다 — 인격은 안 들어가고 파일만 쌓인다(2026-09-02 실사고).
|
|
14
|
+
// 둘은 같이 등록해야 짝이 맞는다. paths 는 문자열(SessionStart 만) 또는 {sessionStart, promptSubmit}.
|
|
15
|
+
const sessionStart = toPosixPath(
|
|
16
|
+
typeof paths === 'string' ? paths : paths.sessionStart,
|
|
17
|
+
platform
|
|
18
|
+
)
|
|
19
|
+
const promptSubmitRaw = typeof paths === 'string' ? null : paths.promptSubmit
|
|
20
|
+
const promptSubmit = promptSubmitRaw ? toPosixPath(promptSubmitRaw, platform) : null
|
|
21
|
+
const lines = [
|
|
13
22
|
ROOKIE_BEGIN,
|
|
14
23
|
'[[hooks.SessionStart]]',
|
|
15
24
|
'matcher = "startup|resume"',
|
|
@@ -18,19 +27,55 @@ function block(hookScriptPath, platform = process.platform) {
|
|
|
18
27
|
'type = "command"',
|
|
19
28
|
// B-3: codex_cli 표면을 인자로 전달 → session-start.sh 가 x-rookie-surface 로 비콘.
|
|
20
29
|
// (Codex Desktop 도 같은 config.toml 을 읽어 codex_cli 로 집계 — desktop 세분화는 후속.)
|
|
21
|
-
`command = 'bash "${
|
|
30
|
+
`command = 'bash "${sessionStart}" codex_cli'`,
|
|
22
31
|
'timeout = 15',
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
]
|
|
33
|
+
if (promptSubmit) {
|
|
34
|
+
lines.push(
|
|
35
|
+
'',
|
|
36
|
+
'[[hooks.UserPromptSubmit]]',
|
|
37
|
+
'',
|
|
38
|
+
'[[hooks.UserPromptSubmit.hooks]]',
|
|
39
|
+
'type = "command"',
|
|
40
|
+
`command = 'bash "${promptSubmit}"'`,
|
|
41
|
+
// 파일만 읽고 끝나는 경로라 짧게 — Claude 쪽도 3초다.
|
|
42
|
+
'timeout = 5'
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
lines.push(ROOKIE_END)
|
|
46
|
+
return lines.join('\n')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// rookie 가 managed 블록 안에 쓰는 TOML 테이블. 이 밖의 테이블이 블록 안에서
|
|
50
|
+
// 발견되면 사용자 설정이 흘러들어온 것이다 — 덮지 않고 마커 밖으로 옮긴다.
|
|
51
|
+
// (2026-09-02 실사고: 두목의 [shell_environment_policy.set] 4줄이 마커 안에 있어
|
|
52
|
+
// 블록 교체와 함께 지워졌다. "수동 편집 금지" 주석만으로는 못 막는다.)
|
|
53
|
+
const ROOKIE_TABLE = /^\[\[?hooks\.(SessionStart|UserPromptSubmit)(\.hooks)?\]\]?$/
|
|
54
|
+
|
|
55
|
+
function splitForeignLines(inner) {
|
|
56
|
+
const own = []
|
|
57
|
+
const foreign = []
|
|
58
|
+
let inForeign = false
|
|
59
|
+
for (const line of inner.split('\n')) {
|
|
60
|
+
const t = line.trim()
|
|
61
|
+
if (/^\[.*\]$/.test(t)) inForeign = !ROOKIE_TABLE.test(t)
|
|
62
|
+
;(inForeign ? foreign : own).push(line)
|
|
63
|
+
}
|
|
64
|
+
return foreign.join('\n').trim()
|
|
25
65
|
}
|
|
26
66
|
|
|
27
67
|
// 기존 managed 블록이 있으면 교체, 없으면 끝에 append. 마커 밖은 보존.
|
|
28
|
-
|
|
29
|
-
|
|
68
|
+
// 마커 **안**에 있던 남의 설정도 잃지 않는다 — 마커 밖으로 옮기고 preserved 로 알린다.
|
|
69
|
+
export function upsertCodexHookBlockDetailed(
|
|
70
|
+
existingToml,
|
|
71
|
+
hookPaths,
|
|
72
|
+
platform = process.platform
|
|
73
|
+
) {
|
|
74
|
+
const fresh = block(hookPaths, platform)
|
|
30
75
|
const begin = existingToml.indexOf(ROOKIE_BEGIN)
|
|
31
76
|
if (begin === -1) {
|
|
32
77
|
const base = existingToml.replace(/\s*$/, '')
|
|
33
|
-
return (base ? base + '\n\n' : '') + fresh + '\n'
|
|
78
|
+
return { toml: (base ? base + '\n\n' : '') + fresh + '\n', preserved: '' }
|
|
34
79
|
}
|
|
35
80
|
const endIdx = existingToml.indexOf(ROOKIE_END, begin)
|
|
36
81
|
if (endIdx === -1) {
|
|
@@ -40,7 +85,18 @@ export function upsertCodexHookBlock(existingToml, hookScriptPath, platform = pr
|
|
|
40
85
|
err.code = 'MALFORMED_CODEX_MARKER'
|
|
41
86
|
throw err
|
|
42
87
|
}
|
|
88
|
+
const inner = existingToml.slice(begin + ROOKIE_BEGIN.length, endIdx)
|
|
89
|
+
const preserved = splitForeignLines(inner)
|
|
43
90
|
const before = existingToml.slice(0, begin)
|
|
44
91
|
const after = existingToml.slice(endIdx + ROOKIE_END.length)
|
|
45
|
-
|
|
92
|
+
let toml = before + fresh + after
|
|
93
|
+
if (preserved) {
|
|
94
|
+
// 마커 **뒤**에 둔다 — 다음 재실행 때 다시 삼켜지지 않는다.
|
|
95
|
+
toml = toml.replace(/\s*$/, '') + '\n\n' + preserved + '\n'
|
|
96
|
+
}
|
|
97
|
+
return { toml, preserved }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function upsertCodexHookBlock(existingToml, hookPaths, platform = process.platform) {
|
|
101
|
+
return upsertCodexHookBlockDetailed(existingToml, hookPaths, platform).toml
|
|
46
102
|
}
|
|
@@ -4,7 +4,7 @@ import { join, dirname } from 'node:path'
|
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { apiGet } from '../api.js'
|
|
6
6
|
import { saveClientManifest } from './manifest.js'
|
|
7
|
-
import {
|
|
7
|
+
import { upsertCodexHookBlockDetailed } from './codex-config.js'
|
|
8
8
|
import { claudeHookCommand, escapeJsonStringValue } from './hook-command.js'
|
|
9
9
|
import { mergeHooks } from './merge-hooks.js'
|
|
10
10
|
import { syncSkillAssets } from './skill-installer.js'
|
|
@@ -170,7 +170,19 @@ export function applyCodexSetup({ home = homedir() } = {}) {
|
|
|
170
170
|
const existing = existsSync(configPath) ? readFileSync(configPath, 'utf8') : ''
|
|
171
171
|
let next
|
|
172
172
|
try {
|
|
173
|
-
|
|
173
|
+
const upserted = upsertCodexHookBlockDetailed(existing, {
|
|
174
|
+
sessionStart: hookScript,
|
|
175
|
+
// 인격 대기열 소비자 — 짝이 없으면 파일만 쌓인다(spec 2026-09-02).
|
|
176
|
+
promptSubmit: join(homedir(), '.rookie', 'bin', 'user-prompt-skill-gate.sh'),
|
|
177
|
+
})
|
|
178
|
+
next = upserted.toml
|
|
179
|
+
// 마커 안에 흘러든 사용자 설정을 밖으로 옮겼으면 조용히 넘기지 않는다.
|
|
180
|
+
if (upserted.preserved) {
|
|
181
|
+
console.log(
|
|
182
|
+
'⚠ managed 블록 안에 있던 설정을 블록 밖으로 옮겼습니다(지우지 않았습니다) — 위치를 확인하세요:'
|
|
183
|
+
)
|
|
184
|
+
for (const line of upserted.preserved.split('\n')) console.log(` ${line}`)
|
|
185
|
+
}
|
|
174
186
|
} catch (e) {
|
|
175
187
|
// marker 손상(begin 만 있고 end 없음) → 파일을 건드리지 않고 경고 신호만 반환(사용자 config 보호).
|
|
176
188
|
if (e?.code === 'MALFORMED_CODEX_MARKER') return { configPath, hookScript, error: 'malformed_marker' }
|
|
@@ -61,3 +61,34 @@ pending_inject_write() {
|
|
|
61
61
|
' 2>/dev/null || true
|
|
62
62
|
return 0
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
# pending_inject_sweep — 소비되지 않고 남은 대기열 청소.
|
|
66
|
+
# 짝이 되는 UserPromptSubmit 훅이 없는 환경(구 Codex 등록본 등)에서는 대기열이
|
|
67
|
+
# 만들어지기만 하고 아무도 읽지 않는다. 그대로 두면 파일이 쌓이고, 그 자체가
|
|
68
|
+
# 조용한 실패가 된다(2026-09-02 실사고 — Codex 는 SessionStart 만 등록돼 있었다).
|
|
69
|
+
# 24시간 넘게 남은 것만 지운다 — 진행 중인 세션의 대기열은 건드리지 않는다.
|
|
70
|
+
pending_inject_sweep() {
|
|
71
|
+
local dir
|
|
72
|
+
dir="$(pending_inject_dir)"
|
|
73
|
+
[ -d "$dir" ] || return 0
|
|
74
|
+
ROOKIE_PI_DIR="$dir" ROOKIE_DROP_LOG="${ROOKIE_DROP_LOG:-$HOME/.rookie/dropped-mutations.log}" node -e '
|
|
75
|
+
try {
|
|
76
|
+
const fs=require("fs"), path=require("path");
|
|
77
|
+
const dir=process.env.ROOKIE_PI_DIR, MAX=24*3600*1000;
|
|
78
|
+
let swept=0;
|
|
79
|
+
for(const f of fs.readdirSync(dir)){
|
|
80
|
+
if(!f.endsWith(".json") && !f.endsWith(".lock")) continue;
|
|
81
|
+
const p=path.join(dir,f);
|
|
82
|
+
try{
|
|
83
|
+
const st=fs.lstatSync(p);
|
|
84
|
+
if(!st.isFile()){ fs.unlinkSync(p); continue; }
|
|
85
|
+
if(Date.now()-st.mtimeMs>MAX){ fs.unlinkSync(p); swept+=1; }
|
|
86
|
+
}catch{}
|
|
87
|
+
}
|
|
88
|
+
if(swept>0){
|
|
89
|
+
try{ fs.appendFileSync(process.env.ROOKIE_DROP_LOG, new Date().toISOString()+" pending_inject swept_unconsumed="+swept+"\n"); }catch{}
|
|
90
|
+
}
|
|
91
|
+
} catch {}
|
|
92
|
+
' 2>/dev/null || true
|
|
93
|
+
return 0
|
|
94
|
+
}
|
|
@@ -261,6 +261,8 @@ if [ "$STATUS_CODE" = "200" ]; then
|
|
|
261
261
|
# spec 2026-09-02 (2단계 로드 강제) — 예산에서 생략된 섹션 본문을 대기열에 둔다.
|
|
262
262
|
# UserPromptSubmit 이 첫 발화에 직접 주입한다. resume 은 컨텍스트가 살아 있어 제외.
|
|
263
263
|
if command -v pending_inject_write >/dev/null 2>&1; then
|
|
264
|
+
# 소비자 없는 환경에서 쌓인 옛 대기열부터 치운다(24시간 초과분만).
|
|
265
|
+
command -v pending_inject_sweep >/dev/null 2>&1 && pending_inject_sweep || true
|
|
264
266
|
pending_inject_write "$SESSION_ID" "$RESP_FILE" "$SOURCE" || true
|
|
265
267
|
fi
|
|
266
268
|
|
package/version.json
CHANGED