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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "therookie",
3
- "version": "0.4.24",
3
+ "version": "0.4.26",
4
4
  "description": "the Rookie — the AI new hire that remembers what you teach it. A CLI that connects a personal AI memory to AI tools like Claude and Codex.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -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(hookScriptPath, platform = process.platform) {
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
- const path = toPosixPath(hookScriptPath, platform)
12
- return [
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 "${path}" codex_cli'`,
30
+ `command = 'bash "${sessionStart}" codex_cli'`,
22
31
  'timeout = 15',
23
- ROOKIE_END,
24
- ].join('\n')
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
- export function upsertCodexHookBlock(existingToml, hookScriptPath, platform = process.platform) {
29
- const fresh = block(hookScriptPath, platform)
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
- return before + fresh + after
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 { upsertCodexHookBlock } from './codex-config.js'
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
- next = upsertCodexHookBlock(existing, hookScript)
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
@@ -1,4 +1,4 @@
1
1
  {
2
- "cli_version": "0.4.24",
2
+ "cli_version": "0.4.26",
3
3
  "skill_version": "1.39.0"
4
4
  }