therookie 0.4.3
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/LICENSE +21 -0
- package/README.md +63 -0
- package/bin/rookie.js +6 -0
- package/package.json +40 -0
- package/src/api.js +67 -0
- package/src/cli.js +139 -0
- package/src/commands/cluster-todos.js +74 -0
- package/src/commands/codex-setup.js +36 -0
- package/src/commands/context-diet.js +80 -0
- package/src/commands/doctor.js +710 -0
- package/src/commands/done-scan.js +162 -0
- package/src/commands/init.js +175 -0
- package/src/commands/link.js +53 -0
- package/src/commands/login.js +160 -0
- package/src/commands/orphan-extract.js +44 -0
- package/src/commands/save.js +91 -0
- package/src/commands/scan-specs.js +153 -0
- package/src/commands/status.js +44 -0
- package/src/commands/sweep-recovery.js +33 -0
- package/src/commands/token.js +105 -0
- package/src/commands/unlink.js +40 -0
- package/src/commands/update.js +220 -0
- package/src/config.js +65 -0
- package/src/detect.js +109 -0
- package/src/lib/account-ui.js +28 -0
- package/src/lib/apply-tool-setup.js +89 -0
- package/src/lib/codex-config.js +46 -0
- package/src/lib/connect-ui.js +59 -0
- package/src/lib/context-diet.js +628 -0
- package/src/lib/dead-letter.js +73 -0
- package/src/lib/deploy-artifacts.js +185 -0
- package/src/lib/hook-command.js +31 -0
- package/src/lib/manifest.js +87 -0
- package/src/lib/merge-hooks.js +31 -0
- package/src/lib/orphan-extract-core.js +113 -0
- package/src/lib/orphan-pending.js +49 -0
- package/src/lib/parse-hook-cwd.js +11 -0
- package/src/lib/poll-state.js +14 -0
- package/src/lib/redact-lite.js +55 -0
- package/src/lib/resolve-endpoint.js +8 -0
- package/src/lib/self-update.js +79 -0
- package/src/lib/skill-installer.js +55 -0
- package/src/lib/spec-scanner.js +85 -0
- package/src/lib/sweep-core.mjs +423 -0
- package/src/lib/task-path-match.js +122 -0
- package/src/prompt.js +60 -0
- package/templates/hook/bin/posttool-recall-nudge.sh +96 -0
- package/templates/hook/bin/pretool-md-read.sh +107 -0
- package/templates/hook/flush-session.sh +314 -0
- package/templates/hook/guard-destructive-db.sh +24 -0
- package/templates/hook/lib/config-resolve.sh +106 -0
- package/templates/hook/lib/cross-runtime-sweep.sh +202 -0
- package/templates/hook/lib/dispatcher.sh +423 -0
- package/templates/hook/lib/orphan-recovery.sh +491 -0
- package/templates/hook/lib/queue.sh +314 -0
- package/templates/hook/lib/skill-gate.sh +28 -0
- package/templates/hook/lib/transcript.sh +166 -0
- package/templates/hook/refresh-context.sh +163 -0
- package/templates/hook/session-start.sh +417 -0
- package/templates/hook/settings.partial.json +55 -0
- package/templates/hook/sweep-recovery.sh +43 -0
- package/templates/hook/user-prompt-skill-gate.sh +90 -0
- package/templates/launchd/com.rollingmango.rookie-sweep.plist +38 -0
- package/templates/skill/SKILL.md +214 -0
- package/version.json +4 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rolling Mango Corp. (주식회사 롤링망고)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# therookie
|
|
2
|
+
|
|
3
|
+
**the Rookie** — the AI new hire that remembers what you teach it. A CLI that connects a personal AI memory ("the Rookie") to AI tools like Claude and Codex. It remembers your company, projects, context, and preferences across sessions.
|
|
4
|
+
|
|
5
|
+
- Homepage: https://therookie.kr
|
|
6
|
+
- Guide: https://therookie.kr/guide
|
|
7
|
+
- Maintained by: Rolling Mango Corp.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm i -g therookie
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Two steps to connect after install:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
rookie init # Register Skill & Hook (attaches the Rookie to your AI tools)
|
|
19
|
+
rookie login # Link your Rookie account in the browser (issues a PAT)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`rookie login` opens your browser. Log in, click **"Allow"**, and you're done. From then on, every time you start a Claude Code or Codex session, the Rookie shows up automatically with its memory.
|
|
23
|
+
|
|
24
|
+
## What gets installed
|
|
25
|
+
|
|
26
|
+
- CLI: npm global path (`npm root -g`)
|
|
27
|
+
- Skill & Hook: `~/.claude/` (Claude Code), `~/.codex/` (Codex) — these power the "shows up every session" behavior
|
|
28
|
+
- Shared hook runtime & config: `~/.rookie/`
|
|
29
|
+
|
|
30
|
+
Nothing is installed system-wide — everything lives under your home directory. Hooks you registered yourself are preserved and distinguished by a `__rookie_managed` marker.
|
|
31
|
+
|
|
32
|
+
## Access scope
|
|
33
|
+
|
|
34
|
+
The **"Allow"** in `rookie login` grants permission to read and write **your own Rookie account memory** — nothing else. It does not touch your local filesystem or anyone else's account. What you teach it is stored only in your account.
|
|
35
|
+
|
|
36
|
+
Sensitive data (API keys, passwords, PII, `.env` contents, etc.) is filtered out at the code level before it ever reaches storage.
|
|
37
|
+
|
|
38
|
+
## Update & remove
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm i -g therookie@latest # update
|
|
42
|
+
npm rm -g therookie # remove (your ~/.rookie account config is preserved)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
To also clean up hooks and the Skill, run `rookie unlink` first.
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- Node.js 20+
|
|
50
|
+
- Zero dependencies (Node built-in modules only)
|
|
51
|
+
|
|
52
|
+
## Commands
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
rookie init First install — deploy Skill & Hook
|
|
56
|
+
rookie login Connect your account via browser approval
|
|
57
|
+
rookie link Register the current project with the Rookie
|
|
58
|
+
rookie doctor Check install & connection status
|
|
59
|
+
rookie update Sync templates
|
|
60
|
+
rookie unlink Remove hooks & Skill
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Run `rookie help` for the full command list.
|
package/bin/rookie.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "therookie",
|
|
3
|
+
"version": "0.4.3",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"ai",
|
|
8
|
+
"memory",
|
|
9
|
+
"claude",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"codex",
|
|
12
|
+
"mcp",
|
|
13
|
+
"rookie",
|
|
14
|
+
"the-rookie"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://therookie.kr",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://therookie.kr"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"rookie": "bin/rookie.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin",
|
|
25
|
+
"src",
|
|
26
|
+
"templates",
|
|
27
|
+
"version.json",
|
|
28
|
+
"!**/*.test.js",
|
|
29
|
+
"!**/*.test.mjs",
|
|
30
|
+
"!**/*.test.ts",
|
|
31
|
+
"!**/*.test.sh"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"type": "module",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export async function apiGet(endpoint, token, path, opts = {}) {
|
|
2
|
+
// opts.timeoutMs — AbortController 로 fetch 를 바운드한다. hook(SessionStart) 안에서 도는 호출은
|
|
3
|
+
// 네트워크 지연 시 무한 대기하면 hook timeout(Codex 15s)을 터뜨리므로 반드시 타임아웃을 준다.
|
|
4
|
+
const { timeoutMs } = opts
|
|
5
|
+
let signal
|
|
6
|
+
let timer
|
|
7
|
+
if (timeoutMs) {
|
|
8
|
+
const ac = new AbortController()
|
|
9
|
+
signal = ac.signal
|
|
10
|
+
timer = setTimeout(() => ac.abort(), timeoutMs)
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch(`${endpoint}${path}`, {
|
|
14
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
15
|
+
signal,
|
|
16
|
+
})
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const body = await res.text()
|
|
19
|
+
throw new Error(`GET ${path} failed: ${res.status} ${body}`)
|
|
20
|
+
}
|
|
21
|
+
return res.json()
|
|
22
|
+
} finally {
|
|
23
|
+
if (timer) clearTimeout(timer)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function apiPost(endpoint, token, path, body, opts = {}) {
|
|
28
|
+
// opts.headers — 추가 헤더 병합 (B5: link 의 x-rookie-surface 등). 기존 호출자 무영향.
|
|
29
|
+
const res = await fetch(`${endpoint}${path}`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Authorization': `Bearer ${token}`,
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
...(opts.headers || {}),
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
})
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const text = await res.text()
|
|
40
|
+
throw new Error(`POST ${path} failed: ${res.status} ${text}`)
|
|
41
|
+
}
|
|
42
|
+
return res.json()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function apiPostNoAuth(endpoint, path, body) {
|
|
46
|
+
const res = await fetch(`${endpoint}${path}`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify(body),
|
|
50
|
+
})
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
const text = await res.text()
|
|
53
|
+
throw new Error(`POST ${path} failed: ${res.status} ${text}`)
|
|
54
|
+
}
|
|
55
|
+
return res.json()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 폴링 전용: 비2xx 도 throw 하지 않고 {ok,status,body} 반환.
|
|
59
|
+
export async function apiPostNoAuthRaw(endpoint, path, body) {
|
|
60
|
+
const res = await fetch(`${endpoint}${path}`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify(body),
|
|
64
|
+
})
|
|
65
|
+
const json = await res.json().catch(() => ({}))
|
|
66
|
+
return { ok: res.ok, status: res.status, body: json }
|
|
67
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { runInit } from './commands/init.js'
|
|
2
|
+
import { runLogin } from './commands/login.js'
|
|
3
|
+
import { runLink } from './commands/link.js'
|
|
4
|
+
import { runStatus } from './commands/status.js'
|
|
5
|
+
import { runUnlink } from './commands/unlink.js'
|
|
6
|
+
import { runDoctor } from './commands/doctor.js'
|
|
7
|
+
import { runSave } from './commands/save.js'
|
|
8
|
+
import { runToken } from './commands/token.js'
|
|
9
|
+
import { runUpdate } from './commands/update.js'
|
|
10
|
+
import { runSweepRecovery } from './commands/sweep-recovery.js'
|
|
11
|
+
import { runCodexSetup } from './commands/codex-setup.js'
|
|
12
|
+
import { runScanSpecs } from './commands/scan-specs.js'
|
|
13
|
+
import { runClusterTodos } from './commands/cluster-todos.js'
|
|
14
|
+
import { runDoneScan } from './commands/done-scan.js'
|
|
15
|
+
import { runContextDiet } from './commands/context-diet.js'
|
|
16
|
+
import { runOrphanExtract } from './commands/orphan-extract.js'
|
|
17
|
+
|
|
18
|
+
const HELP = `rookie — the Rookie Skill/Hook 설치 관리자
|
|
19
|
+
|
|
20
|
+
사용:
|
|
21
|
+
rookie init [--local] [--force] [--yes] [--deep-scan]
|
|
22
|
+
rookie login [--local]
|
|
23
|
+
rookie update [--force]
|
|
24
|
+
rookie link [--local]
|
|
25
|
+
rookie save [--file <body.json>] # 또는 stdin
|
|
26
|
+
rookie status
|
|
27
|
+
rookie doctor [--deep-scan] [--dead-letters]
|
|
28
|
+
rookie unlink [--local | --global]
|
|
29
|
+
rookie token list | revoke <id> | rotate
|
|
30
|
+
rookie sweep-recovery [--force]
|
|
31
|
+
rookie codex-setup [--yes]
|
|
32
|
+
rookie done-scan [--force]
|
|
33
|
+
rookie context-diet [--json] [--force]
|
|
34
|
+
rookie orphan-extract [--batch=5]
|
|
35
|
+
|
|
36
|
+
옵션:
|
|
37
|
+
--local 프로젝트 로컬 스코프 — login 은 토큰도 <dir>/.rookie/config.json 에 저장(전역 무변경)
|
|
38
|
+
--force 경고 무시, 재설치/덮어쓰기
|
|
39
|
+
--yes 비대화형 자동 승인
|
|
40
|
+
--deep-scan 홈 디렉토리에서 로컬 Skill 전수 스캔
|
|
41
|
+
|
|
42
|
+
명령:
|
|
43
|
+
init 첫 설치. 비밀번호 입력 받아 PAT 발급 + Skill/Hook 배포.
|
|
44
|
+
login 브라우저 승인으로 PAT 발급(비밀번호 입력 없음). 권장 인증 경로.
|
|
45
|
+
⚠ 다계정 운영 규칙(B6): 테스트 계정은 반드시 login --local 로 해당 디렉토리에만 binding.
|
|
46
|
+
전역 login 으로 다른 계정 전환 시 확인 게이트가 뜬다(비대화형은 --yes 필요).
|
|
47
|
+
update templates 동기화 전용. PAT 재발급 안 함. CI/자동화 친화.
|
|
48
|
+
save 기억 저장(batch-mutate). **cwd·git remote·hostname 을 CLI 가 자동 주입**하므로
|
|
49
|
+
호출자는 본문(working_cards/evidence/self_rules/verifications)만 주면 된다.
|
|
50
|
+
전역 fact 는 본문에 project_id:null 을 명시한다(생략은 거부됨 — 조용한 누수 차단).
|
|
51
|
+
sweep-recovery 비정상 종료 세션 사후 복구 sweep (하루 1회, --force 로 강제 실행).
|
|
52
|
+
codex-setup Codex config.toml 에 SessionStart hook 등록(동의 기반). 자동 회상 주입.
|
|
53
|
+
scan-specs 현재 프로젝트 spec 의 후속 섹션을 대시보드 proposal 로 동기화(하루 1회, --force 강제).
|
|
54
|
+
cluster-todos 미분류 todo 를 임베딩 유사도로 묶어 트랙 후보 proposal 생성(하루 1회, --force 강제).
|
|
55
|
+
done-scan doing task 를 최근 커밋과 경로 강매칭해 완료 후보를 세션 시작 시 surface(하루 1회, --force 강제).
|
|
56
|
+
context-diet 세션 주입 컨텍스트 비대 감지(5항목 proxy 측정). hook 이 주1회 권고 블록 주입, --force 는 usage 재스캔.
|
|
57
|
+
orphan-extract 비정상 종료 세션 tail 추출 초안 생성(오래된 순, 기본 5건). 저장·완료 marker 는 세션 LLM/사용자 판단.
|
|
58
|
+
`
|
|
59
|
+
|
|
60
|
+
export async function run(argv) {
|
|
61
|
+
const [cmd, ...rest] = argv
|
|
62
|
+
const flags = parseFlags(rest)
|
|
63
|
+
switch (cmd) {
|
|
64
|
+
case 'init': return runInit(flags)
|
|
65
|
+
case 'login': return runLogin(flags)
|
|
66
|
+
case 'update': return runUpdate(flags)
|
|
67
|
+
case 'link': return runLink(flags)
|
|
68
|
+
case 'save': return runSave(flags)
|
|
69
|
+
case 'status': return runStatus(flags)
|
|
70
|
+
case 'unlink': return runUnlink(flags)
|
|
71
|
+
case 'doctor': return runDoctor(flags)
|
|
72
|
+
case 'sweep-recovery': return runSweepRecovery(flags)
|
|
73
|
+
case 'codex-setup': return runCodexSetup(flags)
|
|
74
|
+
case 'scan-specs': return runScanSpecs(flags)
|
|
75
|
+
case 'cluster-todos': return runClusterTodos(flags)
|
|
76
|
+
case 'done-scan': {
|
|
77
|
+
// done-scan 은 SessionStart hook 안에서 돈다. 네트워크 타임아웃(apiGet 4s) 외에도 git·node startup 등
|
|
78
|
+
// 예기치 못한 hang 이 hook timeout(Codex 15s)을 터뜨리지 않도록 프로세스 하드 워치독(6s)을 건다.
|
|
79
|
+
// unref 라 정상 완료 시엔 대기 없이 종료되고, 무언가 hang 하면 6s 에 best-effort 종료(후보 미출력).
|
|
80
|
+
const _watchdog = setTimeout(() => process.exit(0), 6000)
|
|
81
|
+
if (typeof _watchdog.unref === 'function') _watchdog.unref()
|
|
82
|
+
return runDoneScan(flags)
|
|
83
|
+
}
|
|
84
|
+
case 'context-diet': {
|
|
85
|
+
// done-scan 과 동일 사유 — SessionStart hook 안에서 돌 수 있어 하드 워치독.
|
|
86
|
+
// transcript 스캔은 lib 내부 time budget(10s)이 1차 방어, 워치독 15s 는 최후 방어선.
|
|
87
|
+
const _dietWatchdog = setTimeout(() => process.exit(0), 15000)
|
|
88
|
+
if (typeof _dietWatchdog.unref === 'function') _dietWatchdog.unref()
|
|
89
|
+
return runContextDiet(flags)
|
|
90
|
+
}
|
|
91
|
+
case 'orphan-extract': return runOrphanExtract(flags)
|
|
92
|
+
case 'token': return runToken(flags)
|
|
93
|
+
case 'help':
|
|
94
|
+
case '--help':
|
|
95
|
+
case undefined:
|
|
96
|
+
console.log(HELP)
|
|
97
|
+
return
|
|
98
|
+
default:
|
|
99
|
+
console.error(`Unknown command: ${cmd}`)
|
|
100
|
+
console.log(HELP)
|
|
101
|
+
process.exit(2)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 값을 받는 플래그. 이 목록만 공백형(`--file body.json`)에서 다음 토큰을 값으로 소비한다.
|
|
106
|
+
// 나머지(force·yes·batch 등)는 boolean 유지 — batch 는 orphan-extract 가 positional 폴백에 의존.
|
|
107
|
+
const VALUE_FLAGS = new Set([
|
|
108
|
+
'file',
|
|
109
|
+
'endpoint',
|
|
110
|
+
'project-id',
|
|
111
|
+
'projectId',
|
|
112
|
+
'repo-path',
|
|
113
|
+
'repo-url',
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
export function parseFlags(args) {
|
|
117
|
+
const out = { positional: [] }
|
|
118
|
+
for (let i = 0; i < args.length; i++) {
|
|
119
|
+
const a = args[i]
|
|
120
|
+
if (a.startsWith('--')) {
|
|
121
|
+
const eq = a.indexOf('=')
|
|
122
|
+
if (eq !== -1) {
|
|
123
|
+
out[a.slice(2, eq)] = a.slice(eq + 1)
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
const key = a.slice(2)
|
|
127
|
+
const next = args[i + 1]
|
|
128
|
+
if (VALUE_FLAGS.has(key) && next !== undefined && !next.startsWith('--')) {
|
|
129
|
+
out[key] = next
|
|
130
|
+
i++
|
|
131
|
+
} else {
|
|
132
|
+
out[key] = true
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
out.positional.push(a)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out
|
|
139
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { statSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { loadConfig } from '../config.js'
|
|
6
|
+
import { apiGet, apiPost } from '../api.js'
|
|
7
|
+
|
|
8
|
+
const THROTTLE_MS = 24 * 60 * 60 * 1000
|
|
9
|
+
|
|
10
|
+
function markerPath(projectId) {
|
|
11
|
+
return join(homedir(), '.rookie', `cluster-todos-${projectId}.last`)
|
|
12
|
+
}
|
|
13
|
+
function throttled(projectId) {
|
|
14
|
+
try {
|
|
15
|
+
return Date.now() - statSync(markerPath(projectId)).mtimeMs < THROTTLE_MS
|
|
16
|
+
} catch {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function touchMarker(projectId) {
|
|
21
|
+
try {
|
|
22
|
+
writeFileSync(markerPath(projectId), String(Date.now()))
|
|
23
|
+
} catch {
|
|
24
|
+
/* best-effort */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function runClusterTodos(flags) {
|
|
29
|
+
const cfg = loadConfig()
|
|
30
|
+
if (!cfg) {
|
|
31
|
+
console.error('✗ 먼저 `rookie init` 을 실행하세요.')
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
const repoPath = process.cwd()
|
|
35
|
+
let repoUrl = ''
|
|
36
|
+
try {
|
|
37
|
+
repoUrl = execSync('git remote get-url origin', { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
38
|
+
.toString()
|
|
39
|
+
.trim()
|
|
40
|
+
} catch {
|
|
41
|
+
/* git repo 아님 */
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const q = `repo_url=${encodeURIComponent(repoUrl)}&repo_path=${encodeURIComponent(repoPath)}`
|
|
45
|
+
let ctx
|
|
46
|
+
try {
|
|
47
|
+
ctx = await apiGet(cfg.endpoint, cfg.token, `/api/rookie/context?${q}`)
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.error(`✗ context 조회 실패: ${e.message}`)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
const project = ctx && ctx.current_project
|
|
53
|
+
if (!project || !project.id) {
|
|
54
|
+
console.log('이 프로젝트는 미link — cluster 생략')
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
if (!flags.force && throttled(project.id)) return
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const r = await apiPost(cfg.endpoint, cfg.token, '/api/rookie/todo-clusters/scan', {
|
|
61
|
+
project_id: project.id,
|
|
62
|
+
})
|
|
63
|
+
touchMarker(project.id)
|
|
64
|
+
if (r && r.ok) {
|
|
65
|
+
console.log(
|
|
66
|
+
`[cluster-todos] created ${r.created}, bumped ${r.bumped}, rejected ${r.rejected_same}, stale ${r.stale_swept}, singletons ${r.skipped_singletons}`
|
|
67
|
+
)
|
|
68
|
+
} else if (r && r.error) {
|
|
69
|
+
console.log(`[cluster-todos] skip — ${r.error}`)
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error(`✗ cluster scan 전송 실패: ${e.message}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { confirm } from '../prompt.js'
|
|
5
|
+
import { loadConfig } from '../config.js'
|
|
6
|
+
import { applyCodexSetup } from '../lib/deploy-artifacts.js'
|
|
7
|
+
|
|
8
|
+
// Codex config.toml 에 더루키 SessionStart hook 을 동의 기반으로 등록한다(managed 블록만).
|
|
9
|
+
// 명시적 Codex-only 재설정 진입점 — 내부 write 는 applyCodexSetup(3층 adapter) 공유(spec §4 item 6).
|
|
10
|
+
export async function runCodexSetup(flags) {
|
|
11
|
+
const cfg = loadConfig()
|
|
12
|
+
if (!cfg?.token) {
|
|
13
|
+
console.error('✗ 먼저 `rookie login` 으로 연결하세요.')
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
const hookScript = join(homedir(), '.rookie', 'bin', 'session-start.sh')
|
|
17
|
+
if (!existsSync(hookScript)) {
|
|
18
|
+
console.error(`✗ hook 스크립트가 없습니다(${hookScript}). \`rookie login\` 또는 \`rookie update\` 를 먼저 실행하세요.`)
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
const configPath = join(homedir(), '.codex', 'config.toml')
|
|
22
|
+
|
|
23
|
+
console.log('\nCodex 설정 파일에 더루키 SessionStart hook 을 등록합니다:')
|
|
24
|
+
console.log(` ${configPath}`)
|
|
25
|
+
console.log(' → Codex 세션 시작 시 차차 컨텍스트가 자동 주입됩니다(managed 블록만 추가, 기존 설정 보존).')
|
|
26
|
+
if (!flags.yes && !(await confirm('진행할까요?', { defaultYes: true }))) {
|
|
27
|
+
console.log('취소했습니다.')
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
const r = applyCodexSetup({ home: homedir() })
|
|
31
|
+
if (r.error === 'malformed_marker') {
|
|
32
|
+
console.error('✗ Codex config 의 더루키 marker 가 손상됐습니다(begin 만 있고 end 없음). 사용자 설정 보호를 위해 중단합니다 — config.toml 의 managed 블록을 수동 점검하세요.')
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
console.log('✓ 등록 완료. 새 Codex 세션부터 자동 적용됩니다.')
|
|
36
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { computeReport, throttleMarkerPath, THRESHOLDS } from '../lib/context-diet.js'
|
|
4
|
+
|
|
5
|
+
// spec 2026-07-10-context-diet-design.md §2.1 — 측정 5항목 + 임계 판정.
|
|
6
|
+
// 주1회 throttle 은 hook(session-start.sh) 이 marker 나이로 검사하고, 여기선
|
|
7
|
+
// 판정이 성공적으로 끝난 뒤에만 marker 를 기록한다(실패 세션 미기록 → 다음 재시도, codex 2차 P2).
|
|
8
|
+
export async function runContextDiet(flags) {
|
|
9
|
+
const home = homedir()
|
|
10
|
+
const report = computeReport({
|
|
11
|
+
cwd: process.cwd(),
|
|
12
|
+
home,
|
|
13
|
+
usageMode: flags.force ? 'force-scan' : 'cache-or-scan',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
if (report.ok) {
|
|
17
|
+
try {
|
|
18
|
+
writeFileSync(throttleMarkerPath(home), String(Date.now()))
|
|
19
|
+
} catch {
|
|
20
|
+
/* best-effort — 미기록이면 중복 권고 (누락보다 낫다, §4) */
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (flags.json) {
|
|
25
|
+
console.log(JSON.stringify(report))
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log('== 컨텍스트 다이어트 (설치 자산 proxy — 실제 주입 토큰과 다를 수 있음) ==')
|
|
30
|
+
printHuman(report)
|
|
31
|
+
if (!report.ok) process.exitCode = 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fmtNum(n) {
|
|
35
|
+
return typeof n === 'number' ? n.toLocaleString('en-US') : '?'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function printHuman(report, log = console.log) {
|
|
39
|
+
const it = report.items
|
|
40
|
+
const line = (flag, text) => log(`${flag} ${text}`)
|
|
41
|
+
const mark = (item) => (item.exceeded ? '⚠️' : '✓')
|
|
42
|
+
|
|
43
|
+
if (it.claude_md_project)
|
|
44
|
+
line(
|
|
45
|
+
mark(it.claude_md_project),
|
|
46
|
+
`claude_md_project: ${fmtNum(it.claude_md_project.chars)}자 / ${fmtNum(it.claude_md_project.lines)}줄 (임계 ${fmtNum(THRESHOLDS.claude_md_project)})${it.claude_md_project.snoozed ? ' [snooze]' : ''}`
|
|
47
|
+
)
|
|
48
|
+
else line('✗', 'claude_md_project: 측정 실패')
|
|
49
|
+
|
|
50
|
+
if (it.claude_md_total)
|
|
51
|
+
line(
|
|
52
|
+
mark(it.claude_md_total),
|
|
53
|
+
`claude_md_total: ${fmtNum(it.claude_md_total.chars)}자 / ${it.claude_md_total.files}파일 (@import 포함, 임계 ${fmtNum(THRESHOLDS.claude_md_total)})${it.claude_md_total.snoozed ? ' [snooze]' : ''}`
|
|
54
|
+
)
|
|
55
|
+
else line('✗', 'claude_md_total: 측정 실패')
|
|
56
|
+
|
|
57
|
+
if (it.skills_agents_desc) {
|
|
58
|
+
line(
|
|
59
|
+
mark(it.skills_agents_desc),
|
|
60
|
+
`skills_agents_desc: ${fmtNum(it.skills_agents_desc.chars)}자 / ${it.skills_agents_desc.count}개 자산 (임계 ${fmtNum(THRESHOLDS.skills_agents_desc)})${it.skills_agents_desc.snoozed ? ' [snooze]' : ''}`
|
|
61
|
+
)
|
|
62
|
+
if (it.skills_agents_desc.parse_errors > 0)
|
|
63
|
+
log(` frontmatter parse_error ${it.skills_agents_desc.parse_errors}건 (측정 제외분 있음)`)
|
|
64
|
+
} else line('✗', 'skills_agents_desc: 측정 실패')
|
|
65
|
+
|
|
66
|
+
if (it.rookie_self)
|
|
67
|
+
line(
|
|
68
|
+
mark(it.rookie_self),
|
|
69
|
+
`rookie_self: SKILL.md ${fmtNum(it.rookie_self.skill_chars)}자 (임계 ${fmtNum(THRESHOLDS.rookie_self_skill)}) / 주입 ${it.rookie_self.injected_chars == null ? '미기록' : fmtNum(it.rookie_self.injected_chars) + '자'}${it.rookie_self.injection_stale ? ' (stale — 판정 제외)' : ` (임계 ${fmtNum(THRESHOLDS.rookie_self_injected)})`}${it.rookie_self.snoozed ? ' [snooze]' : ''}`
|
|
70
|
+
)
|
|
71
|
+
else line('✗', 'rookie_self: 측정 실패')
|
|
72
|
+
|
|
73
|
+
if (it.unused_skills) {
|
|
74
|
+
line(
|
|
75
|
+
mark(it.unused_skills),
|
|
76
|
+
`unused_skills: ${it.unused_skills.count}개 ${it.unused_skills.window_days}일간 0회 (임계 ≥${THRESHOLDS.unused_skills_min})${it.unused_skills.usage_partial ? ' [partial 스캔 — 판정 제외]' : ''}${it.unused_skills.usage_cached ? ' [캐시]' : ''}${it.unused_skills.snoozed ? ' [snooze]' : ''}`
|
|
77
|
+
)
|
|
78
|
+
for (const name of it.unused_skills.names) log(` - ${name}`)
|
|
79
|
+
} else line('✗', 'unused_skills: 측정 실패 (usage 캐시 없음이면 rookie context-diet --force)')
|
|
80
|
+
}
|