geobuke-code 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -53,27 +53,30 @@ gbc init
53
53
 
54
54
  </details>
55
55
 
56
- `gbc init`은 **프로젝트 로컬 `.claude/settings.json`만** 머지한다(append·멱등·백업). 전역 `~/.claude`는 건드리지 않는다. `~/.gbc/api-key`가 있으면 hook 명령에 주입까지 자동화한다(아래 [빠른 게이트 활성화](#빠른-게이트-활성화-api-키--선택)).
56
+ `gbc init`은 **프로젝트 로컬 `.claude/settings.json`만** 머지한다(append·멱등·백업). 전역 `~/.claude`는 건드리지 않는다. hook 명령은 **셸 무관 순수 형태**(`node "<path>" hook pre-tool-use`)라 Windows(cmd.exe)/bash/zsh/Mac에서 동일하게 동작한다.
57
57
 
58
58
  ## 빠른 게이트 활성화 (API 키 — 선택)
59
59
 
60
- `gbc init`은 키 주입 **없는** hook을 설치한다 → 기본은 `claude -p` 폴백(~13–20s). **haiku 직접 API(~1–3s)**를 쓰려면 키를 **hook 명령에만** 주입한다.
60
+ 키가 없으면 `claude -p` 폴백(~13–20s)으로 무설정 동작한다. **haiku 직접 API(~1–3s)**를 쓰려면 **키 파일만 만들면 된다** — gbc가 실행 시 직접 읽으므로 settings.json 수정이나 셸 주입은 불필요하다(0.2.1+).
61
61
 
62
- > ⚠️ **`export ANTHROPIC_API_KEY=…` 전역 설정 금지.** Claude Code 본체가 키로 **과금 전환**된다(구독 대신과금). 게이트 hook 서브프로세스에만 주입해야 안전하다.
62
+ > ⚠️ **native Windows에선 API 키가 사실상 필수다.** `claude -p` 폴백은 `claude.cmd`(배치 shim)를 셸 없이 실행하지 못해(ENOENT) fail-open으로 빠질 수 있다. Windows에선 위 파일을 만들어 API 경로로 쓰는 것을 권장한다(WSL/Mac/Linux는 폴백 정상).
63
+
64
+ 키 해석 순서: `ANTHROPIC_API_KEY` 환경변수 > `~/.gbc/api-key` 파일.
63
65
 
64
66
  ```bash
65
- # 1) 키를 파일에 저장 (권한 600)
67
+ # bash / zsh / WSL / Mac
66
68
  mkdir -p ~/.gbc && printf '%s' 'sk-ant-...' > ~/.gbc/api-key && chmod 600 ~/.gbc/api-key
67
69
  ```
68
70
 
69
- ```jsonc
70
- // 2) 대상 프로젝트 .claude/settings.json의 PreToolUse command 앞에 주입을 추가:
71
- // "command": "node \"…/dist/cli.js\" hook pre-tool-use"
72
- //
73
- "command": "ANTHROPIC_API_KEY=\"$(cat ~/.gbc/api-key)\" node \"…/dist/cli.js\" hook pre-tool-use"
71
+ ```powershell
72
+ # native Windows (PowerShell) -NoNewline 필수(끝에 개행 붙으면오염)
73
+ New-Item -ItemType Directory -Force -Path "$HOME\.gbc" | Out-Null
74
+ Set-Content -Path "$HOME\.gbc\api-key" -Value "sk-ant-..." -NoNewline
74
75
  ```
75
76
 
76
- `gbc status`엔 `트랜스포트: cli`로 보일 있다(status 명령 자체엔 키가 없어서). 무관하다 실제 hook 발동 시엔 주입으로 `api(haiku)` 경로로 동작한다.
77
+ > ⚠️ **`export ANTHROPIC_API_KEY=…` 전역 설정(또는 settings.json top-level `env`) 금지.** Claude Code 본체가 키로 **과금 전환**된다(구독 대신 과금). 파일 방식은 gbc 판정 호출에만 키가 쓰이므로 함정을 구조적으로 피한다.
78
+
79
+ `gbc status`는 키 파일/환경변수를 반영해 `트랜스포트: api`로 표시한다(0.2.1+).
77
80
 
78
81
  ## 동작 원리
79
82
 
@@ -103,12 +106,12 @@ phase-protocol/계획 → /plan(SubTask) → 【게이트: 구현 직전 케이
103
106
 
104
107
  | 조건 | 트랜스포트 | 지연 |
105
108
  |---|---|---|
106
- | `ANTHROPIC_API_KEY` 설정됨 | Anthropic API 직접 (haiku, 최소 시스템프롬프트) | ~1–3s (목표) |
107
- | 미설정 | `claude -p` 폴백 (CC 인증 재사용, 무설정) | ~13–20s |
109
+ | 키 있음 (`ANTHROPIC_API_KEY` env 또는 `~/.gbc/api-key` 파일) | Anthropic API 직접 (haiku, 최소 시스템프롬프트) | ~1–3s (목표) |
110
+ | 없음 | `claude -p` 폴백 (CC 인증 재사용, 무설정 / ⚠️native Windows 미지원) | ~13–20s |
108
111
 
109
112
  **작업단위 1회**: 게이트는 작업단위(계획 명세 해시)당 한 번만 발동한다. 명세가 바뀌거나 명세 밖 파일을 편집할 때만 재발동 → 매 편집 지연을 피한다.
110
113
 
111
- > 빠른 게이트를 원하면 `ANTHROPIC_API_KEY`를 설정하라(설정법·과금 주의: 위 [「빠른 게이트 활성화」](#빠른-게이트-활성화-api-키--선택)). 없으면 `claude -p` 폴백으로 무설정 동작하되 작업단위당 한 번 느리다.
114
+ > 빠른 게이트를 원하면 `~/.gbc/api-key` 키 파일을 만들어라(설정법·과금 주의: 위 [「빠른 게이트 활성화」](#빠른-게이트-활성화-api-키--선택)). 없으면 `claude -p` 폴백으로 무설정 동작하되 작업단위당 한 번 느리다.
112
115
 
113
116
  ## 명령
114
117
 
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { loadPlanSpec, computeSpecHash, addSpecCase, readSpecCases, clearSpec }
9
9
  import { loadState, resetGate } from "./state.js";
10
10
  import { addDefer, loadDefers, resolveDefer } from "./defer.js";
11
11
  import { selectedTransport } from "./judge.js";
12
- import { buildPreCommand, upgradeKeylessHooks } from "./install.js";
12
+ import { buildPreCommand, normalizeHooks } from "./install.js";
13
13
  import { logEvent, parseEvents, computeMetrics } from "./metrics.js";
14
14
  const CLI_PATH = fileURLToPath(import.meta.url);
15
15
  const PKG_ROOT = join(dirname(CLI_PATH), ".."); // dist/cli.js → 패키지 루트
@@ -63,8 +63,10 @@ function cmdInit(args) {
63
63
  1) ${settingsPath} 에 PreToolUse(Edit|Write) + Stop hook 추가 (머지·멱등)
64
64
  - 기존 settings.json 있으면 백업: settings.json.bak-<시각>
65
65
  2) ${join(skillDestDir, "SKILL.md")} 에 /gate 스킬 설치
66
- 3) hook 명령: ${buildPreCommand(CLI_PATH, hasApiKey())}
67
-
66
+ 3) hook 명령: ${buildPreCommand(CLI_PATH)}
67
+ ${hasApiKey()
68
+ ? " (~/.gbc/api-key 감지됨 → 빠른 haiku API 경로로 동작)"
69
+ : " (~/.gbc/api-key 없음 → claude -p 폴백. 빠른 경로 원하면 키 파일 생성)"}
68
70
  실행하려면: gbc init --yes
69
71
  `);
70
72
  return;
@@ -86,21 +88,20 @@ function cmdInit(args) {
86
88
  }
87
89
  const hooks = (settings.hooks ??= {});
88
90
  const serialized = JSON.stringify(settings);
89
- const useKey = hasApiKey();
90
- // PreToolUse (멱등). 신규면 추가, 이미 있으면 keyless→키주입 업그레이드(skip만 하지 않음).
91
+ // PreToolUse (멱등). 신규면 추가, 이미 있으면 옛 명령(keyless·bash 키주입)을 pure로 정규화.
91
92
  if (!serialized.includes("hook pre-tool-use")) {
92
93
  (hooks.PreToolUse ??= []).push({
93
94
  matcher: "Edit|Write|MultiEdit",
94
- hooks: [{ type: "command", command: buildPreCommand(CLI_PATH, useKey) }],
95
+ hooks: [{ type: "command", command: buildPreCommand(CLI_PATH) }],
95
96
  });
96
- console.log(` + PreToolUse hook 추가${useKey ? " (API 키 주입)" : ""}`);
97
+ console.log(` + PreToolUse hook 추가`);
97
98
  }
98
99
  else {
99
- const n = useKey ? upgradeKeylessHooks(settings, CLI_PATH, true) : 0;
100
+ const n = normalizeHooks(settings, CLI_PATH);
100
101
  if (n > 0)
101
- console.log(` ↑ PreToolUse hook 키주입 업그레이드 (${n})`);
102
+ console.log(` ↑ PreToolUse hook 정규화 (${n}건, 셸 무관 명령으로)`);
102
103
  else
103
- console.log(` = PreToolUse hook 이미 존재 (skip)`);
104
+ console.log(` = PreToolUse hook 이미 표준 (skip)`);
104
105
  }
105
106
  // Stop (멱등)
106
107
  if (!serialized.includes("hook stop")) {
package/dist/install.js CHANGED
@@ -1,39 +1,33 @@
1
1
  // gbc init 설치 로직 (순수함수 — cli.ts main() 부작용 없이 단위테스트 가능).
2
- // 키 주입은 확장($HOME)으로 머신 독립적이게 한다(하드코딩 홈경로 금지).
2
+ // 키 주입은 셸이 아니라 gbc 코드(judge.ts resolveApiKey) 처리한다 hook 명령은
3
+ // 셸 무관 순수 형태라 native Windows(cmd.exe)/bash/zsh/Mac에서 동일하게 동작한다.
3
4
  /**
4
- * PreToolUse hook 명령 생성.
5
- * useKey=true면 ANTHROPIC_API_KEY를 $HOME/.gbc/api-key에서 읽어 주입(빠른 haiku 경로).
6
- * 경로는 셸이 확장하는 $HOME을 머신 독립적이게 한다.
5
+ * PreToolUse hook 명령 생성 — 셸 무관 순수 명령.
6
+ * `node "<cliPath>" hook pre-tool-use` 형태만 생성한다. 키 주입( prefix)·셸 확장 없음.
7
+ * - cliPath는 큰따옴표로만 감싼다(공백 포함 경로 안전). 큰따옴표는 cmd.exe·POSIX sh 공통.
8
+ * - 백슬래시를 이스케이프하지 않는다: Windows 경로(C:\...)의 구분자이며, settings.json에
9
+ * 기록될 때 cli.ts의 JSON.stringify가 `\`→`\\` 처리를 담당한다(여기서 또 하면 이중).
10
+ * - cliPath는 import.meta.url 기반 설치 경로(사용자 입력 아님)라 셸 인젝션 위험이 실질적으로
11
+ * 없어 별도 메타문자 이스케이프를 두지 않는다(이전 shDquote 방어 제거 — 보안 재검토 반영).
7
12
  */
8
- /**
9
- * 더블쿼트 컨텍스트용 이스케이프. 셸이 확장하거나 따옴표를 벗어나지 못하도록
10
- * `"` 백틱 `$` `\` 를 백슬래시 처리한다(settings.json 명령 인젝션 방지).
11
- */
12
- function shDquote(s) {
13
- return s.replace(/(["`$\\])/g, "\\$1");
14
- }
15
- export function buildPreCommand(cliPath, useKey) {
16
- const base = `node "${shDquote(cliPath)}" hook pre-tool-use`;
17
- // $HOME·$(...)는 의도된 셸 확장이므로 이스케이프하지 않는다.
18
- return useKey ? `ANTHROPIC_API_KEY="$(cat "$HOME/.gbc/api-key")" ${base}` : base;
13
+ export function buildPreCommand(cliPath) {
14
+ return `node "${cliPath}" hook pre-tool-use`;
19
15
  }
20
16
  /**
21
- * 이미 설치된 keyless PreToolUse hook command를 주입 버전으로 업그레이드.
22
- * settings를 제자리 수정하고, 업그레이드한 건수를 반환한다(멱등: 이미 키주입된 건너뜀).
17
+ * 기존 PreToolUse hook 명령을 현재 표준(셸 무관 pure 명령)으로 정규화한다.
18
+ * keyless 명령·옛 bash 키주입 prefix 명령을 모두 pure로 교체 → "모든 OS 동일 명령" 목표 달성.
19
+ * settings를 제자리 수정하고 변경 건수를 반환한다(멱등: 이미 표준이면 0건).
23
20
  */
24
- export function upgradeKeylessHooks(settings, cliPath, useKey) {
25
- if (!useKey)
26
- return 0;
27
- const target = buildPreCommand(cliPath, true);
28
- let upgraded = 0;
21
+ export function normalizeHooks(settings, cliPath) {
22
+ const target = buildPreCommand(cliPath);
23
+ let changed = 0;
29
24
  for (const entry of settings.hooks?.PreToolUse ?? []) {
30
25
  for (const h of entry.hooks ?? []) {
31
- // pre-tool-use hook인데 아직 주입이 없으면(keyless) 교체
32
- if (h.command.includes("hook pre-tool-use") && !h.command.includes("ANTHROPIC_API_KEY")) {
26
+ if (h.command.includes("hook pre-tool-use") && h.command !== target) {
33
27
  h.command = target;
34
- upgraded++;
28
+ changed++;
35
29
  }
36
30
  }
37
31
  }
38
- return upgraded;
32
+ return changed;
39
33
  }
package/dist/judge.js CHANGED
@@ -1,7 +1,31 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
+ import { readFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
3
6
  const execFileAsync = promisify(execFile);
4
7
  const MODEL = process.env.GBC_MODEL ?? "claude-haiku-4-5";
8
+ /**
9
+ * API 키 해석 (크로스플랫폼, 셸 무관).
10
+ * 1) ANTHROPIC_API_KEY 환경변수 우선, 2) 없으면 ~/.gbc/api-key 파일.
11
+ * STUB
12
+ */
13
+ export function resolveApiKey(opts = {}) {
14
+ const env = opts.env ?? process.env;
15
+ const fromEnv = env.ANTHROPIC_API_KEY;
16
+ if (fromEnv && fromEnv.trim())
17
+ return fromEnv;
18
+ const home = opts.homeDir ?? homedir();
19
+ const read = opts.readFile ?? ((p) => readFileSync(p, "utf8"));
20
+ try {
21
+ // bash `$(cat)`는 trailing newline을 벗기지만 readFileSync는 안 벗긴다 → 명시 trim 필수.
22
+ const key = read(join(home, ".gbc", "api-key")).trim();
23
+ return key || null;
24
+ }
25
+ catch {
26
+ return null; // 파일 부재/읽기 실패 → 키 없음(claude -p 폴백)
27
+ }
28
+ }
5
29
  /**
6
30
  * 최소 게이트 시스템 프롬프트.
7
31
  * 의미론(핵심): 한 편집이 모든 케이스를 *완전 구현*할 필요는 없다.
@@ -53,15 +77,16 @@ function parseVerdict(raw) {
53
77
  reason: typeof j.reason === "string" ? j.reason : "",
54
78
  };
55
79
  }
56
- /** 트랜스포트 선택 결과 (디버그/리포트용) */
80
+ /** 트랜스포트 선택 결과 (디버그/리포트용). env 또는 키파일에 키가 있으면 api. */
57
81
  export function selectedTransport() {
58
- return process.env.ANTHROPIC_API_KEY ? "api" : "cli";
82
+ return resolveApiKey() ? "api" : "cli";
59
83
  }
60
84
  /** 직접 Anthropic API (haiku). SDK는 여기서만 lazy import → hook 핫패스 보호. */
61
85
  async function judgeViaApi(system, user) {
62
86
  const mod = await import("@anthropic-ai/sdk");
63
87
  const Anthropic = mod.default;
64
- const client = new Anthropic(); // ANTHROPIC_API_KEY 환경변수 사용
88
+ // 키를 코드에서 해석(env 또는 ~/.gbc/api-key) 명시 전달 셸 주입 불필요(크로스플랫폼).
89
+ const client = new Anthropic({ apiKey: resolveApiKey() ?? undefined });
65
90
  const resp = await client.messages.create({
66
91
  model: MODEL,
67
92
  max_tokens: 1024,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geobuke-code",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "거북이코드 — 구현 직전 강제 게이트. Claude Code PreToolUse hook으로 코드 변경 전 계획 케이스 누락·시나리오 미지정을 차단한다.",
5
5
  "type": "module",
6
6
  "bin": {