@kaydash9999/banker-plugins 0.7.0 → 0.8.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +21 -0
- package/PRIVACY.md +94 -0
- package/README.md +76 -20
- package/bin/banker.js +111 -4
- package/bin/lib/telemetry-config.mjs +198 -0
- package/hooks/hooks.json +35 -1
- package/hooks/telemetry-count-skill.mjs +95 -0
- package/hooks/telemetry-count.mjs +93 -0
- package/hooks/update-checkin.mjs +147 -0
- package/hooks/update-fetch.mjs +111 -0
- package/hooks/update-notify.mjs +113 -0
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "banker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Claude Code plugin: QA/audit/docs/architecture skills + commands, a /banker:setup installer for OS dependencies (playwright, etc.), and wiki compaction.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "smallOpenSource",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- README 에 npm 월간 다운로드 배지와 GitHub stars 배지를 추가했다.
|
|
7
|
+
- `banker setup` 최초 실행 시 GitHub 저장소 별(star)을 요청하는 프롬프트를 추가했다(양 런타임 공통·홍보 목적·카운팅 opt-out 여부와 무관).
|
|
8
|
+
- **업데이트-체크 서비스**: 이전 초안(opt-in 익명 텔레메트리 클라이언트, 기본값 미수집)은 동의자가 거의 없어 사용량 가시성이라는 목표를 달성하지 못한다고 판단해 두 기능으로 재구성(피벗)했다.
|
|
9
|
+
- **업데이트 알림**: 공개 npm 레지스트리(`registry.npmjs.org/@kaydash9999/banker-plugins/latest`)를 조회해 세션 시작 시 새 버전이 있으면 알린다. 조회 요청의 수신자는 npm(GitHub/Microsoft)이고, banker 유지보수자는 이 조회를 받지 않는다. 기본값 켜짐, `BANKER_NO_UPDATE_CHECK` 환경변수 또는 `config.updateCheck=false` 로 끌 수 있다.
|
|
10
|
+
- **사용량 카운팅(count-default-on)**: 하루 1회 스킬/커맨드별 호출 수를 시간대(hour-of-day)별로 버킷화해 플러그인 버전·OS 종류와 함께 유지보수자가 운영하는 외부 엔드포인트로 익명 체크인한다. 집계는 사용자가 직접 입력한 `/banker:*` 커맨드와 모델이 자동 호출한 스킬 양쪽을 서로 다른 두 훅으로 겹침 없이 포착한다. 기본값 켜짐, `BANKER_NO_TELEMETRY` 환경변수·`config.telemetry=false`·`banker telemetry off` 로 끌 수 있다.
|
|
11
|
+
- **기본 카운팅 엔드포인트 내장**: 배포에 기본 엔드포인트(`banker.banker-plugins.workers.dev`, 유지보수자의 Cloudflare Worker)를 박아 `endpoint()` 가 env·config 미설정이어도 이를 반환한다. 그 결과 `countingActive()` 가 실제로 default-on 이 되어 opt-out 하지 않은 모든 설치에서 카운팅이 활성이다(이전엔 엔드포인트 미설정이라 사실상 유지보수자 셸만 잡혔다). 자가호스팅/override 는 `BANKER_TELEMETRY_ENDPOINT` 환경변수로 한다.
|
|
12
|
+
- 두 기능 모두 Claude Code 전용이다. Codex CLI는 샌드박스가 네트워크 접근을 차단하므로 알림·카운팅 둘 다 배제된다.
|
|
13
|
+
- `PRIVACY.md` 를 이 설계에 맞게 재작성했다: 알림/카운팅을 분리 기술하고 각각의 수신자를 이름으로 명시했다(알림=npm, 카운팅=유지보수자). 익명성이 페이로드의 속성이지 시스템 전체의 속성은 아니라는 점(카운팅 서버는 소스 IP를 수신하되 기록하지 않도록 설계)을 정직하게 밝혔다. EU ePrivacy Art.5(3)·Planet49 판례는 표준 관행 근거와 반대 근거를 양면으로 제시했고, 미국 CCPA·한국 PIPA는 이 익명 체크인에 재적용하지 않는다는 점(실무 판단 기준인 IP 지속 보유·프로파일링·판매에 해당하지 않음)을 명기했다.
|
|
14
|
+
|
|
15
|
+
### Notes
|
|
16
|
+
- **정직 명시**: 이 릴리스는 아직 게시(publish)되지 않았다. 실제 공개는 EU ePrivacy 대상 opt-in 전환 여부를 포함한 별도 법률 검토를 통과한 뒤 진행한다.
|
|
17
|
+
|
|
18
|
+
## [0.7.1] - 2026-07-16
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- README 정비(문서 전용 — 코드·스킬·배포 표면 무변경). 최상단에 총 구성요소 수(스킬 48 + 커맨드 2)를 명시하고, `라이선스 / 서드파티`의 의존 라이브러리·연동 참조 나열을 표(분류·항목·라이선스·사용 스킬)로 재구성했다. 사실은 코드와 대조해 확인했다 — banker 자체는 MIT, 코드로 재배포하는 서드파티는 `humanizer`(MIT © 2025 Siqi Chen) 단독이며 나머지는 런타임 의존·연동일 뿐이다. 도구별 사용 스킬 매핑은 `/banker:setup` 표와 교차검증했고, 원본에 라이선스가 없던 항목은 지어내지 않고 `각 프로젝트`로 표기했다.
|
|
22
|
+
- README 산문 가독성. 여러 문장이 한 줄에 붙던 문단을 Markdown hard break 로 문장마다 개행(뷰어에서 실제 줄바꿈)하고, Codex 설치 안내 문장을 분리했다.
|
|
23
|
+
|
|
3
24
|
## [0.7.0] - 2026-07-16
|
|
4
25
|
|
|
5
26
|
### Added
|
package/PRIVACY.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# 프라이버시 고지
|
|
2
|
+
|
|
3
|
+
> banker 플러그인의 업데이트-체크 서비스(업데이트 알림 + 익명 사용량 카운팅)에 대한 프라이버시 고지입니다.
|
|
4
|
+
|
|
5
|
+
## 개요
|
|
6
|
+
|
|
7
|
+
이 문서는 두 개의 독립된 기능을 설명합니다.\
|
|
8
|
+
하나는 새 버전이 나왔을 때 알려주는 **업데이트 알림**이고, 다른 하나는 유지보수자가 사용량을 파악하기 위한 **익명 사용량 카운팅**입니다.\
|
|
9
|
+
두 기능 모두 기본값은 켜짐(default-on)이며, 각각 독립적으로 끌 수 있습니다.\
|
|
10
|
+
이 기능은 Claude Code 런타임에서만 동작합니다. Codex CLI는 샌드박스가 네트워크 접근을 차단하므로 알림과 카운팅 둘 다 동작하지 않습니다.
|
|
11
|
+
|
|
12
|
+
## 현재 상태
|
|
13
|
+
|
|
14
|
+
버전 0.8.0은 아직 게시(publish)되지 않았습니다.\
|
|
15
|
+
공개는 아래 "법적 상태"에서 설명하는 검토를 통과한 뒤 진행합니다.\
|
|
16
|
+
이 문서의 문구는 그 검토 결과에 따라 더 정교해질 수 있습니다.
|
|
17
|
+
|
|
18
|
+
## 업데이트 알림
|
|
19
|
+
|
|
20
|
+
공개 npm 레지스트리(`https://registry.npmjs.org/@kaydash9999/banker-plugins/latest`)에서 최신 버전을 조회하고, 설치된 버전보다 최신 버전이 있으면 세션 시작 시 알립니다.\
|
|
21
|
+
이 조회 요청의 수신자는 **npm(GitHub/Microsoft가 운영)**입니다. banker 유지보수자는 이 조회를 전혀 수신하지 않습니다.\
|
|
22
|
+
update-notifier·Homebrew 등 다른 CLI 도구가 쓰는 것과 같은 방식입니다.
|
|
23
|
+
|
|
24
|
+
기본값은 켜짐입니다.\
|
|
25
|
+
환경변수 `BANKER_NO_UPDATE_CHECK` 를 설정하거나 config의 `updateCheck` 를 `false` 로 두면 끌 수 있습니다.
|
|
26
|
+
|
|
27
|
+
## 사용량 카운팅
|
|
28
|
+
|
|
29
|
+
하루 1회, 유지보수자가 운영하는 외부 엔드포인트(banker 패키지 밖에서 별도로 운영되는 serverless)로 익명 체크인을 전송합니다.\
|
|
30
|
+
플러그인은 기본 카운팅 엔드포인트(`banker.banker-plugins.workers.dev`)를 내장하므로, count-default-on 이라 설치 즉시 활성입니다.\
|
|
31
|
+
`BANKER_TELEMETRY_ENDPOINT` 환경변수로 자가호스팅 엔드포인트를 지정하거나 override 할 수 있습니다.
|
|
32
|
+
|
|
33
|
+
### 수집 항목
|
|
34
|
+
|
|
35
|
+
- 스킬/커맨드별 호출 횟수를 시간대(hour-of-day, 0시~23시)별로 버킷화한 값
|
|
36
|
+
- 플러그인 버전
|
|
37
|
+
- OS 종류(linux/darwin/win32 수준)
|
|
38
|
+
|
|
39
|
+
개별 호출의 시각은 전송하지 않고 시간대 버킷까지만 뭉쳐서 보냅니다.\
|
|
40
|
+
집계 대상은 사용자가 직접 입력한 `/banker:*` 커맨드와 모델이 자동으로 호출한 스킬 양쪽 모두이며, 서로 다른 두 훅이 겹치지 않게 나누어 포착합니다.
|
|
41
|
+
|
|
42
|
+
### 수집하지 않는 항목
|
|
43
|
+
|
|
44
|
+
- 이름, 이메일 등 개인식별정보(PII)
|
|
45
|
+
- 코드 및 파일 내용, 커맨드 인자
|
|
46
|
+
- 저장소·계정 정보
|
|
47
|
+
- 지속적인 사용자 또는 기기 식별자(UUID·머신ID 등을 생성하지 않습니다)
|
|
48
|
+
- 페이로드 안의 IP 주소
|
|
49
|
+
|
|
50
|
+
기본값은 켜짐(count-default-on)입니다.\
|
|
51
|
+
환경변수 `BANKER_NO_TELEMETRY` 를 설정하거나, config의 `telemetry` 를 `false` 로 두거나, `banker telemetry off` 를 실행하면 끌 수 있습니다.\
|
|
52
|
+
`banker setup` 첫 실행 시 이 기본값과 끄는 방법을 투명하게 고지합니다. 진행을 막는 opt-in 동의 프롬프트가 아니라 정보 제공입니다.
|
|
53
|
+
|
|
54
|
+
## 익명성에 대한 정직한 고지
|
|
55
|
+
|
|
56
|
+
익명성은 전송되는 데이터, 즉 페이로드 자체의 속성이지 시스템 전체의 속성이 아닙니다.\
|
|
57
|
+
카운팅 엔드포인트는 네트워크 요청의 시스템 경계에서 TCP 소스 IP 주소를 봅니다. IP 주소는 GDPR과 한국 개인정보보호법(PIPA) 모두에서 개인정보로 취급됩니다.\
|
|
58
|
+
이 엔드포인트는 banker 유지보수자가 운영하며, 수신 IP를 기록하지 않도록 설계되어 있습니다. banker 플러그인(클라이언트)과 카운팅 서버는 같은 운영자가 관리합니다.\
|
|
59
|
+
반면 업데이트 알림 조회의 IP는 npm만 보며, banker 유지보수자는 어느 경로로도 보지 않습니다.\
|
|
60
|
+
클라이언트인 banker가 보장하는 범위는 페이로드 안에 IP나 그 밖의 식별자를 넣지 않는다는 것까지이며, 서버가 수신 IP를 기록하지 않는 것은 서버를 운영하는 유지보수자의 설계 책임입니다.
|
|
61
|
+
|
|
62
|
+
## 상시 훅 비용
|
|
63
|
+
|
|
64
|
+
알림과 카운팅이 기본값으로 켜져 있기 때문에 다음 훅이 banker 활성화 중 항상 등록되어 있습니다.
|
|
65
|
+
|
|
66
|
+
- `SessionStart`: 세션 시작 시 1회, 캐시된 업데이트 알림을 확인합니다(이 훅 자체는 네트워크 요청을 하지 않습니다).
|
|
67
|
+
- `PostToolUse`(Skill 매처): 모델이 자동으로 호출한 스킬을 집계합니다.
|
|
68
|
+
- `UserPromptExpansion`: 사용자가 직접 입력한 `/banker:*` 커맨드를 집계합니다.
|
|
69
|
+
|
|
70
|
+
세 훅 모두 트리거될 때마다 짧게 실행되는 node 프로세스를 하나씩 기동합니다(수십 ms 수준).\
|
|
71
|
+
opt-out 하더라도 훅의 등록 자체는 남고, 각 스크립트 내부의 게이트가 즉시 무동작으로 반환합니다.
|
|
72
|
+
|
|
73
|
+
## 전송 안전
|
|
74
|
+
|
|
75
|
+
- 업데이트 알림 조회는 GET 요청이며 페이로드가 없습니다.
|
|
76
|
+
- 카운팅 체크인은 HTTPS만 사용합니다. 평문 http는 로컬 테스트용 loopback(localhost/127.0.0.1/::1) 엔드포인트에만 허용되고, 그 외 엔드포인트에는 거부됩니다.
|
|
77
|
+
- 네트워크 조회·전송은 세션을 막지 않도록 분리 실행되며, 포그라운드 훅은 캐시 읽기만 하고 즉시 반환합니다.
|
|
78
|
+
- 어떤 네트워크 오류도 삼키며(fail-closed), 항상 정상 종료합니다.
|
|
79
|
+
|
|
80
|
+
## 법적 상태
|
|
81
|
+
|
|
82
|
+
**EU ePrivacy는 양면을 그대로 남깁니다.**
|
|
83
|
+
|
|
84
|
+
표준 관행이라는 근거: 익명 체크인, 요청 카운트, IP 미보유, 사전 고지와 opt-out의 조합은 사실상 업계 표준 관행에 해당합니다. 지속 식별자나 사용자별 프로파일을 만들지 않는 무상태 체크인이라는 점이 이 판단의 근거입니다.
|
|
85
|
+
|
|
86
|
+
반대 근거: EU ePrivacy Directive Art.5(3)은 개인식별정보 여부와 무관하게 단말기(device) 접근 자체에 이용자 동의를 요구할 수 있습니다. Planet49 판례에 따르면 분석 목적의 opt-out은 유효한 동의로 인정되지 않을 수 있습니다. 즉 무상태 체크인이라도 접근 자체가 EU에서는 동의를 유발할 가능성이 있습니다.
|
|
87
|
+
|
|
88
|
+
실무 결론: 실제로 EU 사용자를 대상으로 배포하기 전에는 opt-in 전환 여부를 별도로 검토해야 합니다. 게시(publish)는 이 검토를 포함한 별도의 법률 게이트이며, 아직 통과하지 않았습니다.
|
|
89
|
+
|
|
90
|
+
**미국 CCPA·한국 PIPA는 이 익명 업데이트-체크에 재적용하지 않습니다.** 두 법의 실무 판단 기준은 IP 주소의 지속적 보유, 사용자별 프로파일 구축, 데이터 판매 여부입니다.\
|
|
91
|
+
이 기능의 익명 체크인은 이 중 어느 것도 하지 않습니다. IP를 기록하지 않고, 지속 식별자가 없고, 데이터를 판매하지 않습니다.\
|
|
92
|
+
따라서 CCPA·PIPA를 이 기능의 별도 미해결 법적 쟁점으로 표기하지 않습니다. 위에서 다룬 EU ePrivacy는 이와 무관한 별건입니다.
|
|
93
|
+
|
|
94
|
+
이 문구는 향후 더 정교해질 수 있습니다.
|
package/README.md
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@kaydash9999/banker-plugins)
|
|
6
6
|
[](LICENSE)
|
|
7
|
+
[](https://www.npmjs.com/package/@kaydash9999/banker-plugins) [](https://github.com/smallOpenSource/banker_plugins)
|
|
7
8
|
|
|
8
|
-
[빠른 시작](#빠른-시작) · [구성](#구성) · [설치 상세](#설치-상세-npm--codex) · [요구사항](#요구사항) · [업데이트 / 제거](#업데이트--제거) · [라이선스 / 서드파티](#라이선스--서드파티)
|
|
9
|
+
[빠른 시작](#빠른-시작) · [구성](#구성) · [설치 상세](#설치-상세-npm--codex) · [요구사항](#요구사항) · [업데이트 / 제거](#업데이트--제거) · [업데이트 확인](#업데이트-확인-및-사용량-카운팅) · [라이선스 / 서드파티](#라이선스--서드파티)
|
|
9
10
|
|
|
10
|
-
banker는 QA
|
|
11
|
+
banker는 QA·보안 감사·문서·아키텍처·위키 작업과 의존성·개발환경(OS별) 설치를 아우르는 **스킬 48개 + 커맨드 2개**(총 50개 구성요소)를 묶은 Claude Code 플러그인입니다.\
|
|
12
|
+
설치하면 스킬과 커맨드가 `/banker:<이름>` 네임스페이스로 노출됩니다.\
|
|
13
|
+
이 저장소 자체가 Claude Code 마켓플레이스(`.claude-plugin/marketplace.json`)이자 플러그인(`.claude-plugin/plugin.json`, name `banker`)이며, 도구에 무관한 스킬은 Codex CLI에도 설치됩니다.
|
|
11
14
|
|
|
12
15
|
> npm 패키지(`@kaydash9999/banker-plugins`)와 GitHub 저장소(`smallOpenSource/banker_plugins`)는 같은 메인테이너가 관리합니다.
|
|
13
16
|
|
|
@@ -69,7 +72,9 @@ playwright(브라우저 QA)나 oh-my-claudecode(OMC/OMX)처럼 별도 의존성
|
|
|
69
72
|
| `obsidizer` | AI 위키를 의미보존 Obsidian 그래프로 정규화·상호링크·백링크 |
|
|
70
73
|
| `deep-init` | 코드베이스 전체에 계층형 AGENTS.md 문서 생성/갱신 |
|
|
71
74
|
|
|
72
|
-
> `obsidizer` 는 banker 최초의 플러그인 선언 hook(`hooks/hooks.json`)을 함께
|
|
75
|
+
> `obsidizer` 는 banker 최초의 플러그인 선언 hook(`hooks/hooks.json`)을 함께 설치합니다.\
|
|
76
|
+
> banker가 활성화되어 있으면 이 hook은 **항상 등록**되어 위키 쓰기(`wiki_ingest`/`wiki_add`)마다 node 프로세스를 띄웁니다(플래그 확인이 그 안에서 일어나므로 `--enable` 전에도 쓰기당 약 30ms).\
|
|
77
|
+
> `--enable` 전에는 아무것도 쓰지 않는 순수 no-op입니다.
|
|
73
78
|
|
|
74
79
|
### 스킬: 워크플로 · 유틸
|
|
75
80
|
|
|
@@ -94,7 +99,8 @@ playwright(브라우저 QA)나 oh-my-claudecode(OMC/OMX)처럼 별도 의존성
|
|
|
94
99
|
|
|
95
100
|
### 스킬: 개발환경 setup (OS별 · 런타임별)
|
|
96
101
|
|
|
97
|
-
USER_RESOURCES 가이드의 공통 요소를 OS별·런타임별(Claude Code/Codex)로 구성하는 얇은 실행
|
|
102
|
+
USER_RESOURCES 가이드의 공통 요소를 OS별·런타임별(Claude Code/Codex)로 구성하는 얇은 실행 유닛.\
|
|
103
|
+
`/banker:setup` 오케스트레이터가 의존 순서(런타임 먼저)로 조합한다.
|
|
98
104
|
|
|
99
105
|
| 스킬 | 설명 |
|
|
100
106
|
|---|---|
|
|
@@ -121,7 +127,8 @@ USER_RESOURCES 가이드의 공통 요소를 OS별·런타임별(Claude Code/Cod
|
|
|
121
127
|
|
|
122
128
|
## 설치 상세 (npm · Codex)
|
|
123
129
|
|
|
124
|
-
마켓플레이스 대신 npm으로 전역 설치할 수 있고, Codex CLI에도 스킬을 설치할 수
|
|
130
|
+
마켓플레이스 대신 npm으로 전역 설치할 수 있고, Codex CLI에도 스킬을 설치할 수 있습니다.\
|
|
131
|
+
Codex 설치는 npm 전역 설치가 선행되어야 합니다.
|
|
125
132
|
|
|
126
133
|
```bash
|
|
127
134
|
npm i -g @kaydash9999/banker-plugins
|
|
@@ -134,8 +141,10 @@ banker uninstall # 제거
|
|
|
134
141
|
|
|
135
142
|
- `--scope project` 로 프로젝트 로컬(`./.codex`)에 설치하고, `--dry-run` 으로 미리 볼 수 있습니다.
|
|
136
143
|
- non-root 전용입니다(전역 sudo 설치 시 root 소유 파일을 방지). postinstall이 없으므로 `banker setup` 을 직접 실행합니다.
|
|
137
|
-
- Codex에는 스킬 48개가 `~/.codex/skills/banker-<name>/` 에, 커맨드 2개가 `~/.codex/prompts/banker-<name>.md` 에 설치됩니다(`codex/manifest.json`).
|
|
138
|
-
|
|
144
|
+
- Codex에는 스킬 48개가 `~/.codex/skills/banker-<name>/` 에, 커맨드 2개가 `~/.codex/prompts/banker-<name>.md` 에 설치됩니다(`codex/manifest.json`). \
|
|
145
|
+
디렉터리명과 일치하도록 프론트매터 `name:` 이 `banker-<name>` 으로 재작성되어 Codex가 `banker-<name>` 으로 인식합니다.
|
|
146
|
+
- OMC/Claude 에 결합됐던 오케스트레이터·설치·유틸 표면(all-in-one·ultra-init·front-qa·setup·setup-omc·setup-omc-hud·setup-stitch·omc-reference·compact-copy)은 본문이 **런타임 인식**이라 Codex에서도 동작합니다.
|
|
147
|
+
- Codex에선 OMC 대신 **oh-my-codex(OMX)** 의 동명 스킬(ralplan·ralph·ultraqa·hud 등)과 `codex mcp`·내장 `/copy` 를 사용합니다(Codex는 `omx setup` 전제).
|
|
139
148
|
- `~/.codex/AGENTS.md` 는 건드리지 않습니다(omx가 재생성하므로 `~/.codex/skills/` 자동 검색에 의존).
|
|
140
149
|
|
|
141
150
|
## 요구사항
|
|
@@ -166,24 +175,71 @@ banker uninstall # 제거
|
|
|
166
175
|
|
|
167
176
|
Codex는 재설치할 때마다 기존 `banker-*` 를 먼저 정리하므로 옛 버전이 중복으로 남지 않습니다.
|
|
168
177
|
|
|
178
|
+
## 업데이트 확인 및 사용량 카운팅
|
|
179
|
+
|
|
180
|
+
banker는 기본값(default-on)으로 두 가지를 합니다.\
|
|
181
|
+
새 버전이 나오면 세션 시작 시 알려주는 **업데이트 알림**과, 유지보수자가 사용량을 파악하는 **익명 사용량 카운팅**입니다.\
|
|
182
|
+
이 때문에 `SessionStart`(알림 확인)·`PostToolUse`(모델이 자동 호출한 스킬 집계)·`UserPromptExpansion`(사용자가 직접 입력한 `/banker:*` 집계) 훅이 항상 등록되어 있고, 트리거될 때마다 짧게 node 프로세스를 하나씩 띄웁니다(수십 ms 수준).
|
|
183
|
+
|
|
184
|
+
끄기:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
export BANKER_NO_UPDATE_CHECK=1 # 업데이트 알림만 끄기
|
|
188
|
+
export BANKER_NO_TELEMETRY=1 # 사용량 카운팅만 끄기
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
카운팅은 `banker telemetry off` 로도 끌 수 있습니다.\
|
|
192
|
+
플러그인은 기본 카운팅 엔드포인트(`banker.banker-plugins.workers.dev`)를 내장하므로 count-default-on 이라 설치 즉시 활성이며, `BANKER_TELEMETRY_ENDPOINT` 환경변수로 자가호스팅/override 할 수 있습니다.\
|
|
193
|
+
Codex CLI는 샌드박스가 네트워크를 차단하므로 알림·카운팅 둘 다 동작하지 않습니다.
|
|
194
|
+
|
|
195
|
+
무엇을 수집하고 누가 수신하는지는 [PRIVACY.md](PRIVACY.md)에 정리했습니다.
|
|
196
|
+
|
|
169
197
|
## 라이선스 / 서드파티
|
|
170
198
|
|
|
171
199
|
banker 자체는 **MIT** ([LICENSE](LICENSE)). Owner: [smallOpenSource](https://github.com/smallOpenSource).
|
|
172
200
|
|
|
173
201
|
**번들된 코드 (이 패키지가 재배포)**
|
|
174
|
-
- `skills/humanizer`: [blader/humanizer](https://github.com/blader/humanizer) 기반, **MIT License**(© 2025 Siqi Chen)
|
|
202
|
+
- `skills/humanizer`: [blader/humanizer](https://github.com/blader/humanizer) 기반, **MIT License**(© 2025 Siqi Chen).\
|
|
203
|
+
원본 라이선스 고지는 `skills/humanizer/LICENSE` 에 포함.\
|
|
204
|
+
패턴 목록은 Wikipedia [*Signs of AI writing*](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing)(WikiProject AI Cleanup, **CC BY-SA**)에 기반합니다.
|
|
175
205
|
|
|
176
|
-
코드로 포함(재배포)하는 서드파티는 `humanizer`
|
|
206
|
+
코드로 포함(재배포)하는 서드파티는 `humanizer` 뿐입니다.\
|
|
207
|
+
아래는 스킬이 **런타임에 의존하거나 연동**하는 외부 요소로, banker 가 재배포하지 않으며 각 라이선스·상표는 소유자에게 있습니다.
|
|
177
208
|
|
|
178
209
|
**의존 라이브러리·도구 (사용 시 별도 설치, 라이선스는 각 프로젝트 소유)**
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
210
|
+
|
|
211
|
+
| 분류 | 도구 | 라이선스 | 사용 스킬 |
|
|
212
|
+
|---|---|---|---|
|
|
213
|
+
| Python | python-pptx | MIT | `arch-diagram` |
|
|
214
|
+
| Python | python-docx | MIT | `rfp-author` |
|
|
215
|
+
| Python | Playwright | Apache-2.0 | `audit-web-page`·`play-qa`·`ultra-ui-qa` |
|
|
216
|
+
| Python | detect-secrets | Apache-2.0 | `lineage`(시크릿 스캔) |
|
|
217
|
+
| 브라우저·렌더링 | Chromium | BSD-3-Clause | `setup-playwright`·브라우저 QA |
|
|
218
|
+
| 브라우저·렌더링 | SwiftShader | Apache-2.0 | `setup-playwright`·브라우저 QA |
|
|
219
|
+
| 브라우저·렌더링 | Xvfb·X.Org | MIT | `setup-playwright`·브라우저 QA |
|
|
220
|
+
| 문서·다이어그램 | PlantUML | GPL 계열·다중 라이선스 | `arch-diagram` |
|
|
221
|
+
| 문서·다이어그램 | Poppler(`pdftoppm`) | GPL | `pdf-vision-extract` |
|
|
222
|
+
| 문서·다이어그램 | ImageMagick | ImageMagick License | `pdf-vision-extract` |
|
|
223
|
+
| 개발환경·LSP | Node.js·nvm | MIT | `setup-node` |
|
|
224
|
+
| 개발환경·LSP | uv | Apache-2.0/MIT | `setup-python` |
|
|
225
|
+
| 개발환경·LSP | OpenJDK·Temurin | GPL+CE | `setup-java` |
|
|
226
|
+
| 개발환경·LSP | jdtls | EPL-2.0 | `setup-lsp` |
|
|
227
|
+
| 개발환경·LSP | basedpyright·bash-language-server | MIT | `setup-lsp` |
|
|
228
|
+
| 개발환경·LSP | vtsls·vscode-langservers-extracted | 각 프로젝트 | `setup-lsp` |
|
|
229
|
+
| 개발환경·LSP | lsp-mcp([CesarPetrescu/lsp-mcp](https://github.com/CesarPetrescu/lsp-mcp)) | 각 프로젝트 | `setup-lsp` |
|
|
230
|
+
| 개발환경·LSP | tmux | ISC | `setup-tmux` |
|
|
231
|
+
| 개발환경·LSP | psmux | 각 프로젝트 | `setup-tmux` |
|
|
232
|
+
| 개발환경·LSP | bubblewrap | LGPL-2.0 | `setup-sandbox` |
|
|
233
|
+
| 개발환경·LSP | rustup | 각 프로젝트 | `setup-sandbox` |
|
|
234
|
+
|
|
235
|
+
**연동·참조 (외부 프레임워크·서비스·브랜드, banker 는 재배포하지 않음)**
|
|
236
|
+
|
|
237
|
+
| 분류 | 항목 | 라이선스 | 사용/설치 스킬 |
|
|
238
|
+
|---|---|---|---|
|
|
239
|
+
| 프레임워크 | oh-my-claudecode(OMC)·oh-my-codex(OMX), by [Yeachan-Heo](https://github.com/Yeachan-Heo) | MIT | `all-in-one`·`ultra-init`·`/banker:front-qa`·`setup-omc` |
|
|
240
|
+
| 플러그인 | insane-search (© fivetaku, [fivetaku/gptaku_plugins](https://github.com/fivetaku/gptaku_plugins)) | MIT | `setup-insane-search` |
|
|
241
|
+
| 팀 아키텍처 팩토리 | revfactory/harness (© Minho Hwang, [revfactory/harness](https://github.com/revfactory/harness)) · Codex 포트 SaehwanPark/meta-harness | Apache-2.0 | `harness-factory`(설치·구성만 안내) |
|
|
242
|
+
| 서비스(독점·상표) | Notion | 독점 | `make-notion-guide` |
|
|
243
|
+
| 서비스(독점·상표) | Google Stitch | 독점 | `setup-stitch` |
|
|
244
|
+
| 디자인(상표) | Nothing 디자인 언어 | 상표 | `nothing-design` |
|
|
245
|
+
| QA 엔진(예시) | Godot·Phaser 등 | MIT | `play-qa` |
|
package/bin/banker.js
CHANGED
|
@@ -11,12 +11,14 @@ const fs = require('fs');
|
|
|
11
11
|
const os = require('os');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const cp = require('child_process');
|
|
14
|
+
const { pathToFileURL } = require('url');
|
|
14
15
|
|
|
15
16
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
16
17
|
const MANIFEST = path.join(PKG_ROOT, 'codex', 'manifest.json');
|
|
17
18
|
const PLUGIN_JSON = path.join(PKG_ROOT, '.claude-plugin', 'plugin.json');
|
|
18
19
|
const MARKETPLACE = 'banker-plugins';
|
|
19
20
|
const PLUGIN_NAME = 'banker';
|
|
21
|
+
const REPO_URL = 'https://github.com/smallOpenSource/banker_plugins';
|
|
20
22
|
|
|
21
23
|
function version() {
|
|
22
24
|
try { return require(path.join(PKG_ROOT, 'package.json')).version; } catch { return '0.0.0'; }
|
|
@@ -25,9 +27,10 @@ function log(...a) { console.log(...a); }
|
|
|
25
27
|
function warn(...a) { console.error(...a); }
|
|
26
28
|
|
|
27
29
|
function parseArgs(argv) {
|
|
28
|
-
const a = { cmd: null, claude: false, codex: false, scope: 'user', scopeExplicit: false, dryRun: false, help: false, version: false };
|
|
30
|
+
const a = { cmd: null, sub: null, claude: false, codex: false, scope: 'user', scopeExplicit: false, dryRun: false, help: false, version: false };
|
|
29
31
|
for (const t of argv) {
|
|
30
|
-
if (t === 'setup' || t === 'doctor' || t === 'uninstall' || t === 'help') { if (!a.cmd) a.cmd = t; }
|
|
32
|
+
if (t === 'setup' || t === 'doctor' || t === 'uninstall' || t === 'help' || t === 'telemetry') { if (!a.cmd) a.cmd = t; }
|
|
33
|
+
else if (t === 'on' || t === 'off' || t === 'status') { if (!a.sub) a.sub = t; } // telemetry subcommand
|
|
31
34
|
else if (t === '--claude') a.claude = true;
|
|
32
35
|
else if (t === '--codex') a.codex = true;
|
|
33
36
|
else if (t === '--dry-run' || t === '-n') a.dryRun = true;
|
|
@@ -228,11 +231,111 @@ function uninstallCodex(dryRun, scopes) {
|
|
|
228
231
|
}
|
|
229
232
|
}
|
|
230
233
|
|
|
234
|
+
/* ---------- telemetry config bridge ---------- */
|
|
235
|
+
// bin/banker.js is CommonJS; telemetry-config is ESM (.mjs). Load it lazily via dynamic import
|
|
236
|
+
// (pathToFileURL keeps the specifier valid on Windows). Only the interactive setup path and the
|
|
237
|
+
// telemetry subcommand call this, so the non-TTY / dry-run setup path never imports it.
|
|
238
|
+
async function loadTelemetry() {
|
|
239
|
+
return import(pathToFileURL(path.join(__dirname, 'lib', 'telemetry-config.mjs')).href);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* ---------- setup first-run prompts (US-6) ---------- */
|
|
243
|
+
// Interactive-only. ABSOLUTE rule: never prompt or persist in a non-TTY / CI / dry-run context.
|
|
244
|
+
// scripts/smoke-test.js runs `setup --claude --dry-run` non-interactively; a blocking read here
|
|
245
|
+
// would hang CI forever. The isTTY + dryRun guard below is the single gate that prevents that.
|
|
246
|
+
async function maybeFirstRunPrompts(a) {
|
|
247
|
+
if (!process.stdin.isTTY || a.dryRun) return; // non-TTY / CI / dry-run: do nothing
|
|
248
|
+
const tc = await loadTelemetry();
|
|
249
|
+
const cfg = tc.readConfig();
|
|
250
|
+
const askStar = cfg.promptedStarV1 !== true;
|
|
251
|
+
// count-default-on: 카운팅은 opt-in 질문이 아니라 투명 고지(차단 프롬프트 아님). --claude 관여 시에만 1회 출력.
|
|
252
|
+
const showTelemetryNotice = a.claude && cfg.promptedTelemetryV2 !== true;
|
|
253
|
+
if (!askStar && !showTelemetryNotice) return; // already shown before - never re-show
|
|
254
|
+
|
|
255
|
+
const readline = require('readline');
|
|
256
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
257
|
+
const ask = (q) => new Promise((res) => rl.question(q, res));
|
|
258
|
+
const next = { ...cfg };
|
|
259
|
+
try {
|
|
260
|
+
if (askStar) {
|
|
261
|
+
const ans = (await ask('banker가 유용했다면 GitHub 별(star)을 눌러 주세요. [Y/n] ')).trim().toLowerCase();
|
|
262
|
+
if (ans === '' || ans === 'y' || ans === 'yes') {
|
|
263
|
+
log('감사합니다. 아래 주소에서 직접 별을 눌러 주세요 (자동으로 누르지 않고 안내만 합니다):');
|
|
264
|
+
log(` ${REPO_URL}`);
|
|
265
|
+
}
|
|
266
|
+
next.promptedStarV1 = true;
|
|
267
|
+
}
|
|
268
|
+
if (showTelemetryNotice) {
|
|
269
|
+
log('banker 는 익명 사용량 카운트를 기본 수집합니다(스킬별 호출수·버전·OS·식별자 없음).');
|
|
270
|
+
log('끄려면 BANKER_NO_TELEMETRY=1 또는 `banker telemetry off`. 자세히: PRIVACY.md');
|
|
271
|
+
next.promptedTelemetryV2 = true;
|
|
272
|
+
}
|
|
273
|
+
} finally {
|
|
274
|
+
rl.close();
|
|
275
|
+
}
|
|
276
|
+
tc.writeConfig(next);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* ---------- telemetry subcommand (US-08, count-default-on) ---------- */
|
|
280
|
+
const TELEMETRY_USAGE = `Usage: banker telemetry <on|off|status>
|
|
281
|
+
on 카운팅 opt-out 을 해제 (기본값인 켜짐으로 복귀)
|
|
282
|
+
off 카운팅을 끔 (config.telemetry=false 로 opt-out)
|
|
283
|
+
status 카운팅 / 업데이트-체크의 opt-out · 엔드포인트 · 캐시 상태를 정직하게 출력`;
|
|
284
|
+
|
|
285
|
+
async function telemetryCmd(sub) {
|
|
286
|
+
const tc = await loadTelemetry();
|
|
287
|
+
if (sub === 'on' || sub === 'off') {
|
|
288
|
+
const cfg = tc.readConfig();
|
|
289
|
+
if (sub === 'off') cfg.telemetry = false; // opt-out 저장
|
|
290
|
+
else delete cfg.telemetry; // opt-out 해제: 기본-on 으로 복귀
|
|
291
|
+
if (!tc.writeConfig(cfg)) {
|
|
292
|
+
warn('banker: 설정을 저장하지 못했습니다 (config 쓰기 실패).');
|
|
293
|
+
process.exitCode = 1;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (sub === 'off') {
|
|
297
|
+
log('카운팅을 껐습니다 (off). config.telemetry=false 로 opt-out 을 저장했습니다.');
|
|
298
|
+
} else {
|
|
299
|
+
log('카운팅 opt-out 을 해제했습니다 (on). 기본값(켜짐)으로 복귀합니다.');
|
|
300
|
+
if (!tc.endpoint()) log('참고: 엔드포인트가 설정되어 있지 않아 지금은 아무것도 전송하지 않습니다.');
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (sub === 'status') {
|
|
305
|
+
const cfg = tc.readConfig();
|
|
306
|
+
const v = process.env.BANKER_NO_TELEMETRY;
|
|
307
|
+
const envOptOut = !(v === undefined || v === '' || v === '0' || v === 'false');
|
|
308
|
+
const cfgOptOut = cfg.telemetry === false;
|
|
309
|
+
log('banker 상태:');
|
|
310
|
+
log('[카운팅] 익명 사용량 카운팅 (기본값: 켜짐 · count-default-on)');
|
|
311
|
+
log(` 엔드포인트 설정: ${tc.endpoint() != null ? '설정됨' : '미설정'}`);
|
|
312
|
+
log(` opt-out(BANKER_NO_TELEMETRY): ${envOptOut ? '적용됨' : '미적용'}`);
|
|
313
|
+
log(` opt-out(config.telemetry=false): ${cfgOptOut ? '적용됨' : '미적용'}`);
|
|
314
|
+
log(` 실효 상태(effective): ${tc.countingActive() ? 'enabled' : 'disabled'}`);
|
|
315
|
+
log(' 참고: 엔드포인트가 설정되어 있고 opt-out 이 아닐 때(env·config 둘 다 아님)만 실제로 전송됩니다.');
|
|
316
|
+
|
|
317
|
+
let cache = {};
|
|
318
|
+
try { cache = JSON.parse(fs.readFileSync(tc.UPDATE_CHECK_PATH, 'utf8')); } catch { /* 캐시 없음/malformed */ }
|
|
319
|
+
const installed = tc.installedVersion();
|
|
320
|
+
const cmp = (cache.latest && installed) ? tc.compareVersions(cache.latest, installed) : null;
|
|
321
|
+
log('[업데이트 체크] 새 버전 알림 (기본값: 켜짐)');
|
|
322
|
+
log(` opt-out(BANKER_NO_UPDATE_CHECK/config.updateCheck=false): ${tc.noUpdateCheck() ? '적용됨' : '미적용'}`);
|
|
323
|
+
log(` 마지막 체크: ${cache.checkedAt ? new Date(cache.checkedAt).toISOString() : '없음'}`);
|
|
324
|
+
log(` 캐시된 최신 버전: ${cache.latest || '없음'} / 설치된 버전: ${installed || '알 수 없음'}`);
|
|
325
|
+
log(cmp === null ? ' 비교 불가 (캐시 없음 또는 malformed)'
|
|
326
|
+
: cmp > 0 ? ` 업데이트 있음 (${installed} → ${cache.latest})`
|
|
327
|
+
: ' 최신 버전 사용 중');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
log(TELEMETRY_USAGE); // 알 수 없는 하위명령: 사용법 출력
|
|
331
|
+
}
|
|
332
|
+
|
|
231
333
|
const HELP = `banker v${version()} — install the banker plugin into Claude Code and/or Codex CLI
|
|
232
334
|
|
|
233
335
|
Usage:
|
|
234
336
|
banker setup [--claude] [--codex] [--scope user|project] [--dry-run]
|
|
235
337
|
banker uninstall [--claude] [--codex] [--scope user|project] [--dry-run]
|
|
338
|
+
banker telemetry <on|off|status>
|
|
236
339
|
banker doctor
|
|
237
340
|
banker help
|
|
238
341
|
|
|
@@ -240,18 +343,22 @@ Notes:
|
|
|
240
343
|
- setup/uninstall with no target flag applies to BOTH tools.
|
|
241
344
|
- --claude registers the marketplace + installs banker@banker-plugins (skills as /banker:*).
|
|
242
345
|
- --codex copies codex-eligible skills -> ~/.codex/skills/banker-* and commands -> ~/.codex/prompts/banker-*.md.
|
|
346
|
+
- telemetry on|off toggles anonymous usage counting opt-out (default-on; config only); status prints counting + update-check state. See PRIVACY.md.
|
|
243
347
|
- Never runs as root. --dry-run prints planned actions without writing.`;
|
|
244
348
|
|
|
245
|
-
function main() {
|
|
349
|
+
async function main() {
|
|
246
350
|
const a = parseArgs(process.argv.slice(2));
|
|
247
351
|
if (a.version) { log(version()); return; }
|
|
248
352
|
if (a.help || a.cmd === 'help' || !a.cmd) { log(HELP); return; }
|
|
249
353
|
if (a.cmd === 'doctor') { doctor(); return; }
|
|
354
|
+
if (a.cmd === 'telemetry') { await telemetryCmd(a.sub); return; }
|
|
250
355
|
assertNotRoot();
|
|
251
356
|
if (a.cmd === 'setup') {
|
|
252
357
|
log(`banker setup${a.dryRun ? ' (dry-run)' : ''}: claude=${a.claude} codex=${a.codex}`);
|
|
253
358
|
if (a.claude) setupClaude(a.dryRun);
|
|
254
359
|
if (a.codex) setupCodex(a.dryRun, a.scope);
|
|
360
|
+
// Best-effort first-run prompts (interactive TTY only). A prompt failure must never fail setup.
|
|
361
|
+
try { await maybeFirstRunPrompts(a); } catch { /* non-fatal */ }
|
|
255
362
|
log('done.');
|
|
256
363
|
return;
|
|
257
364
|
}
|
|
@@ -262,4 +369,4 @@ function main() {
|
|
|
262
369
|
return;
|
|
263
370
|
}
|
|
264
371
|
}
|
|
265
|
-
main();
|
|
372
|
+
main().catch((e) => { warn('banker:', e && e.message ? e.message : e); process.exitCode = 1; });
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* telemetry-config - banker 텔레메트리 config/consent 공용 모듈 (bin·hook 공유).
|
|
3
|
+
* 의존성 0 (Node 내장 모듈만: node:fs, node:path, node:os). ESM(.mjs).
|
|
4
|
+
* import 시 부작용 없음: 경로는 resolver 로 호출 시 계산하고, 파일시스템 접근·mkdir 은
|
|
5
|
+
* 각 함수 호출 시에만 일어난다. readConfig/isEnabled/endpoint 는 어떤 예외도 밖으로 던지지 않는다.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
// 단일 config-path resolver. 호출 시점의 env 를 읽어 계산한다 (테스트는 XDG_CONFIG_HOME 으로 격리).
|
|
13
|
+
export function configDir() {
|
|
14
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
15
|
+
if (xdg) return path.join(xdg, 'banker');
|
|
16
|
+
if (process.platform === 'win32') {
|
|
17
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'banker');
|
|
18
|
+
}
|
|
19
|
+
return path.join(os.homedir(), '.config', 'banker');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 공유 경로 상수: import 시점의 resolver 결과를 캡처한다 (bin·hook 은 단명 프로세스라 env 안정적).
|
|
23
|
+
// 순수 경로 계산이라 파일시스템을 건드리지 않는다. 내부 함수는 configDir() 를 매 호출 재계산하므로
|
|
24
|
+
// 테스트가 XDG_CONFIG_HOME 을 바꿔도 올바르게 격리된다.
|
|
25
|
+
export const USAGE_LOG_PATH = path.join(configDir(), 'usage-log'); // 사용량 로그 절대경로
|
|
26
|
+
export const LAST_FLUSH_PATH = path.join(configDir(), 'last-flush');
|
|
27
|
+
// USAGE_LOG 계약: 한 줄 = `<skill-name>\t<hour 0-23>` (스킬 이름 + TAB + 기록 시각의 로컬 hour).
|
|
28
|
+
// flush 는 이 줄들을 스킬별 x 시간대별로 집계한다. LOG_MAX_LINES 는 (필드 아닌) 줄 수 상한이며,
|
|
29
|
+
// 초과 시 오래된 줄부터 잘라내 24h 윈도우를 근사한다.
|
|
30
|
+
export const LOG_MAX_LINES = 5000;
|
|
31
|
+
|
|
32
|
+
// update-check 캐시 파일({latest,checkedAt,notified})의 절대경로. USAGE_LOG_PATH 와 동일하게
|
|
33
|
+
// import 시점 resolver 결과를 캡처한다(단명 hook 프로세스용). writeUpdateCache 는 테스트 격리를 위해
|
|
34
|
+
// configDir() 를 매 호출 재계산하므로 이 상수에 의존하지 않는다.
|
|
35
|
+
export const UPDATE_CHECK_PATH = path.join(configDir(), 'update-check.json');
|
|
36
|
+
// 업데이트 체크/체크인 스로틀 창(24h). 이 간격 미경과면 재조회하지 않는다.
|
|
37
|
+
export const UPDATE_THROTTLE_MS = 24 * 60 * 60 * 1000;
|
|
38
|
+
// 최신 버전 조회용 공개 npm 레지스트리 URL. 본문 .version 을 직접 반환한다(실측 확정).
|
|
39
|
+
export const NPM_LATEST_URL = 'https://registry.npmjs.org/@kaydash9999/banker-plugins/latest';
|
|
40
|
+
|
|
41
|
+
// config.json 을 읽어 객체로 반환. 파일 없음·malformed JSON·권한 오류 등 어떤 예외에도 던지지 않고 {} 반환.
|
|
42
|
+
export function readConfig() {
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(path.join(configDir(), 'config.json'), 'utf8');
|
|
45
|
+
const obj = JSON.parse(raw);
|
|
46
|
+
return (obj && typeof obj === 'object' && !Array.isArray(obj)) ? obj : {};
|
|
47
|
+
} catch {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// configDir 을 mkdir -p 후 config.json 에 기록. 오류는 삼켜 false, 성공 true.
|
|
53
|
+
export function writeConfig(obj) {
|
|
54
|
+
try {
|
|
55
|
+
const target = configDir();
|
|
56
|
+
fs.mkdirSync(target, { recursive: true });
|
|
57
|
+
fs.writeFileSync(path.join(target, 'config.json'), JSON.stringify(obj, null, 2));
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 배포에 내장하는 기본 카운팅 엔드포인트(유지보수자가 운영하는 Cloudflare Worker). 이 상수를 내장해서
|
|
65
|
+
// count-default-on 이 모든 설치에서 활성이다: endpoint() 가 (거의) 항상 값을 반환하므로 countingActive()
|
|
66
|
+
// 가 기본 켜짐이 되고, opt-out(BANKER_NO_TELEMETRY / banker telemetry off / config.telemetry=false)으로만
|
|
67
|
+
// 끈다. 자가호스팅/override 는 BANKER_TELEMETRY_ENDPOINT env 로 한다.
|
|
68
|
+
export const DEFAULT_ENDPOINT = 'https://banker.banker-plugins.workers.dev/';
|
|
69
|
+
|
|
70
|
+
// 카운팅 엔드포인트 문자열. 우선순위: env(BANKER_TELEMETRY_ENDPOINT) > config.endpoint > DEFAULT_ENDPOINT.
|
|
71
|
+
// 기본 엔드포인트를 내장하므로 env·config 미설정이어도 DEFAULT_ENDPOINT 를 반환한다(= count-default-on 이
|
|
72
|
+
// 모든 설치에서 활성, opt-out 으로만 끔). env 빈문자열('')은 falsy 라 여전히 config/DEFAULT 로 흐른다.
|
|
73
|
+
// 예외가 난 경우에만 null 을 반환한다(에러 시 보수적으로 전송 안 함).
|
|
74
|
+
export function endpoint() {
|
|
75
|
+
try {
|
|
76
|
+
const env = process.env.BANKER_TELEMETRY_ENDPOINT;
|
|
77
|
+
if (env) return env;
|
|
78
|
+
const ep = readConfig().endpoint;
|
|
79
|
+
return (typeof ep === 'string' && ep) ? ep : DEFAULT_ENDPOINT;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// env 값이 opt-out 을 의미하는지. unset·''·'0'·'false' 는 opt-out 이 아니고, 그 외 모든 값이 opt-out.
|
|
86
|
+
// BANKER_NO_TELEMETRY 와 BANKER_NO_UPDATE_CHECK 가 동일 규칙을 공유한다.
|
|
87
|
+
function isOptOut(v) {
|
|
88
|
+
return !(v === undefined || v === '' || v === '0' || v === 'false');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// BANKER_NO_TELEMETRY 가 opt-out 을 의미하는지.
|
|
92
|
+
function noTelemetry() {
|
|
93
|
+
return isOptOut(process.env.BANKER_NO_TELEMETRY);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// [예약] opt-in 게이트. 현재 카운팅은 countingActive(default-on)를 쓰므로 프로덕션 호출자는 없다.
|
|
97
|
+
// 향후 EU 등에서 opt-in 전환이 필요할 때(PRIVACY.md 법적 상태 참조) 재도입할 수 있게 보존한다.
|
|
98
|
+
// 다음이 전부 참일 때만 true: (1) readConfig().telemetry===true, (2) 엔드포인트 설정됨,
|
|
99
|
+
// (3) BANKER_NO_TELEMETRY 가 opt-out 아님. 전체를 try/catch 로 감싸 어떤 예외에도 false.
|
|
100
|
+
export function isEnabled() {
|
|
101
|
+
try {
|
|
102
|
+
if (readConfig().telemetry !== true) return false;
|
|
103
|
+
if (!endpoint()) return false;
|
|
104
|
+
if (noTelemetry()) return false;
|
|
105
|
+
return true;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 업데이트 체크를 opt-out 했는지. env BANKER_NO_UPDATE_CHECK 가 opt-out 이거나
|
|
112
|
+
// readConfig().updateCheck===false 이면 true. 전체를 try/catch 로 감싸 어떤 예외에도 false.
|
|
113
|
+
export function noUpdateCheck() {
|
|
114
|
+
try {
|
|
115
|
+
if (isOptOut(process.env.BANKER_NO_UPDATE_CHECK)) return true;
|
|
116
|
+
if (readConfig().updateCheck === false) return true;
|
|
117
|
+
return false;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 익명 사용량 카운팅이 활성인지 (default-on). isEnabled 의 opt-in(telemetry===true) 과 달리
|
|
124
|
+
// telemetry 미설정도 활성으로 본다: (1) 엔드포인트 설정됨, (2) BANKER_NO_TELEMETRY 가 opt-out 아님,
|
|
125
|
+
// (3) readConfig().telemetry!==false. 셋 다 참이면 true. 전체를 try/catch 로 감싸 어떤 예외에도 false.
|
|
126
|
+
export function countingActive() {
|
|
127
|
+
try {
|
|
128
|
+
if (!endpoint()) return false;
|
|
129
|
+
if (noTelemetry()) return false;
|
|
130
|
+
if (readConfig().telemetry === false) return false;
|
|
131
|
+
return true;
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 이 모듈 파일 기준 플러그인 루트의 plugin.json 절대경로 (bin/lib -> 루트 = 두 단계 위).
|
|
138
|
+
// fileURLToPath·path 는 순수 계산이라 import 부작용이 없다(파일 읽기는 installedVersion 호출 시에만).
|
|
139
|
+
const PLUGIN_JSON_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '.claude-plugin', 'plugin.json');
|
|
140
|
+
|
|
141
|
+
// 설치된 플러그인 버전 문자열. 기본 경로는 플러그인 루트의 .claude-plugin/plugin.json 이며,
|
|
142
|
+
// 테스트가 임의 경로를 주입할 수 있게 파라미터로도 받는다. 파일없음·malformed·비문자열·빈문자열 → null.
|
|
143
|
+
export function installedVersion(pluginJsonPath = PLUGIN_JSON_PATH) {
|
|
144
|
+
try {
|
|
145
|
+
const v = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8')).version;
|
|
146
|
+
return (typeof v === 'string' && v) ? v : null;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 'x.y.z' 두 버전을 major.minor.patch 정수 3튜플로 비교한다. a>b 면 양수, a<b 면 음수, 같으면 0.
|
|
153
|
+
// prerelease 접미사('1.2.3-rc1')·토큰 누락('1.2')·비수치('a.b.c')가 있으면 null(미확정) 반환.
|
|
154
|
+
// 순수 함수이며 어떤 입력에도 throw 하지 않는다.
|
|
155
|
+
export function compareVersions(a, b) {
|
|
156
|
+
const parse = (s) => {
|
|
157
|
+
if (typeof s !== 'string') return null;
|
|
158
|
+
const parts = s.split('.');
|
|
159
|
+
if (parts.length !== 3) return null;
|
|
160
|
+
const nums = [];
|
|
161
|
+
for (const part of parts) {
|
|
162
|
+
if (!/^\d+$/.test(part)) return null; // 빈·비수치·prerelease('3-rc1') 토큰 배제
|
|
163
|
+
nums.push(Number(part));
|
|
164
|
+
}
|
|
165
|
+
return nums;
|
|
166
|
+
};
|
|
167
|
+
const pa = parse(a);
|
|
168
|
+
const pb = parse(b);
|
|
169
|
+
if (pa === null || pb === null) return null;
|
|
170
|
+
for (let i = 0; i < 3; i++) {
|
|
171
|
+
if (pa[i] !== pb[i]) return pa[i] - pb[i];
|
|
172
|
+
}
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// update-check 캐시를 read-modify-write 로 병합 기록한다: 기존 JSON 을 읽어({} on 실패) patch 의
|
|
177
|
+
// 필드만 덮고 나머지 형제 필드는 보존한 뒤, configDir() mkdir -p → temp 파일 쓰기 → rename(원자적)
|
|
178
|
+
// 으로 기록한다. 테스트 격리를 위해 경로는 configDir() 로 매 호출 재계산한다(UPDATE_CHECK_PATH 상수
|
|
179
|
+
// 비의존). 모든 오류를 삼켜 best-effort: 성공 true, 실패 false.
|
|
180
|
+
export function writeUpdateCache(patch) {
|
|
181
|
+
try {
|
|
182
|
+
const dir = configDir();
|
|
183
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
184
|
+
const cachePath = path.join(dir, 'update-check.json');
|
|
185
|
+
let existing = {};
|
|
186
|
+
try {
|
|
187
|
+
const obj = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
188
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj)) existing = obj;
|
|
189
|
+
} catch { /* 없음·malformed → {} */ }
|
|
190
|
+
const merged = { ...existing, ...(patch && typeof patch === 'object' && !Array.isArray(patch) ? patch : {}) };
|
|
191
|
+
const tmp = path.join(dir, `update-check.json.${process.pid}.tmp`);
|
|
192
|
+
fs.writeFileSync(tmp, JSON.stringify(merged, null, 2));
|
|
193
|
+
fs.renameSync(tmp, cachePath);
|
|
194
|
+
return true;
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
package/hooks/hooks.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "banker
|
|
2
|
+
"description": "banker hooks: obsidizer wiki canonicalization (PostToolUse, inert until `obsidizer --enable`) + anonymous usage counting (PostToolUse Skill / UserPromptExpansion, count-default-on, opt-out BANKER_NO_TELEMETRY, no-op without endpoint) + update-check notification (SessionStart, opt-out BANKER_NO_UPDATE_CHECK).",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PostToolUse": [
|
|
5
5
|
{
|
|
@@ -11,6 +11,40 @@
|
|
|
11
11
|
"timeout": 5
|
|
12
12
|
}
|
|
13
13
|
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Skill",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node \"$CLAUDE_PLUGIN_ROOT\"/hooks/run.cjs \"$CLAUDE_PLUGIN_ROOT\"/hooks/telemetry-count-skill.mjs",
|
|
21
|
+
"timeout": 5
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"UserPromptExpansion": [
|
|
27
|
+
{
|
|
28
|
+
"matcher": "banker:.*",
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "node \"$CLAUDE_PLUGIN_ROOT\"/hooks/run.cjs \"$CLAUDE_PLUGIN_ROOT\"/hooks/telemetry-count.mjs",
|
|
33
|
+
"timeout": 5
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"SessionStart": [
|
|
39
|
+
{
|
|
40
|
+
"matcher": "*",
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "node \"$CLAUDE_PLUGIN_ROOT\"/hooks/run.cjs \"$CLAUDE_PLUGIN_ROOT\"/hooks/update-notify.mjs",
|
|
45
|
+
"timeout": 5
|
|
46
|
+
}
|
|
47
|
+
]
|
|
14
48
|
}
|
|
15
49
|
]
|
|
16
50
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* banker 텔레메트리 count 훅 (PostToolUse, matcher: Skill) - 모델이 자동 호출한
|
|
4
|
+
* banker:<name> 스킬을 로컬 usage-log 에 `<name>\t<hour>` 한 줄로 기록한다.
|
|
5
|
+
*
|
|
6
|
+
* fail-closed 규약(각 항목은 telemetry-count-skill.test.mjs 가 검증한다):
|
|
7
|
+
* - countingActive() 가 아니면 로컬 쓰기조차 하지 않는다. 파일도 만들지 않는다.
|
|
8
|
+
* - 어떤 오류도 삼키고 항상 exit 0. 세션/툴호출을 절대 막지 않는다.
|
|
9
|
+
* - PII/식별자를 다루지 않는다. 로그 한 줄 = 이름 문자열 + TAB + hour(0-23) 뿐이다.
|
|
10
|
+
*
|
|
11
|
+
* payload 필드 확정(US-4 라이브 스모크테스트): PostToolUse(Skill) stdin payload 는
|
|
12
|
+
* `tool_input.skill`(예: "banker:compact-copy")을 담는다. 보조 소스로
|
|
13
|
+
* `tool_response.commandName` 도 시도한다. 사용자가 /banker:* 를 직접 호출한 경우는
|
|
14
|
+
* UserPromptExpansion 쪽(telemetry-count.mjs)이 담당한다 - 두 경로는 disjoint 로 실측
|
|
15
|
+
* 확정되어 dedup 이 불요하다.
|
|
16
|
+
*
|
|
17
|
+
* 체크인(usage-log 집계 후 서버 전송)은 이 훅의 책임이 아니다. 트리거는 SessionStart
|
|
18
|
+
* update-notify 가 throttle 경과 시 별도로 연다 - 이 훅은 append 만 하고 flush/checkin 을
|
|
19
|
+
* spawn 하지 않는다.
|
|
20
|
+
*
|
|
21
|
+
* 배선: hooks.json 의 PostToolUse(matcher Skill)에 등록되어 모델 자동 스킬 호출 시 발화한다.
|
|
22
|
+
*/
|
|
23
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
24
|
+
|
|
25
|
+
import { configDir, countingActive, USAGE_LOG_PATH, LOG_MAX_LINES } from '../bin/lib/telemetry-config.mjs';
|
|
26
|
+
|
|
27
|
+
// banker:<name> 전체 일치 - 이름 문자열 자체를 검증한다(TAB/개행 등 이질 문자가 섞여
|
|
28
|
+
// usage-log 의 `name\thour` 형식을 깨는 것을 막는다). i 플래그는 매칭만 관대하게(실제 이름은 소문자).
|
|
29
|
+
const NAME_RE = /^banker:[a-z0-9_-]+$/i;
|
|
30
|
+
|
|
31
|
+
function readStdin() {
|
|
32
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
33
|
+
let data = '';
|
|
34
|
+
process.stdin.setEncoding('utf8');
|
|
35
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
36
|
+
process.stdin.on('end', () => resolvePromise(data));
|
|
37
|
+
process.stdin.on('error', rejectPromise);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// PostToolUse(Skill) payload 에서 모델이 자동 호출한 banker:<name> 스킬 이름을 추출한다.
|
|
42
|
+
// 주 소스는 tool_input.skill, 없거나 미매치면 보조로 tool_response.commandName 을 시도한다.
|
|
43
|
+
function extractName(payload) {
|
|
44
|
+
const candidates = [payload?.tool_input?.skill, payload?.tool_response?.commandName];
|
|
45
|
+
for (const c of candidates) {
|
|
46
|
+
if (typeof c === 'string' && NAME_RE.test(c)) return c;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 파일의 개행 개수 = 줄 수(각 append 가 끝에 개행을 붙이므로). 없거나 읽기 실패면 0.
|
|
52
|
+
function lineCount(path) {
|
|
53
|
+
try {
|
|
54
|
+
const raw = readFileSync(path, 'utf8');
|
|
55
|
+
return (raw.match(/\n/g) || []).length;
|
|
56
|
+
} catch {
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// usage-log 에 `<name>\t<hour>` 한 줄을 append 한다(hour = 로컬 시각 0-23). 상한 도달 시
|
|
62
|
+
// 스킵(best-effort; 체크인이 비우면 재개). flush/checkin 은 여기서 spawn 하지 않는다
|
|
63
|
+
// (트리거는 SessionStart update-notify 소관).
|
|
64
|
+
function record(name) {
|
|
65
|
+
try { mkdirSync(configDir(), { recursive: true }); } catch { /* best-effort */ }
|
|
66
|
+
try {
|
|
67
|
+
if (lineCount(USAGE_LOG_PATH) < LOG_MAX_LINES) {
|
|
68
|
+
const hour = new Date().getHours();
|
|
69
|
+
appendFileSync(USAGE_LOG_PATH, `${name}\t${hour}\n`);
|
|
70
|
+
}
|
|
71
|
+
} catch { /* best-effort */ }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function main() {
|
|
75
|
+
let raw;
|
|
76
|
+
try {
|
|
77
|
+
raw = await readStdin();
|
|
78
|
+
} catch {
|
|
79
|
+
return; // stdin 읽기 실패 - 무동작.
|
|
80
|
+
}
|
|
81
|
+
let payload;
|
|
82
|
+
try {
|
|
83
|
+
payload = JSON.parse(raw);
|
|
84
|
+
} catch {
|
|
85
|
+
return; // malformed stdin - 무동작.
|
|
86
|
+
}
|
|
87
|
+
if (!countingActive()) return; // 카운팅 비활성 - 로컬 쓰기조차 하지 않는다.
|
|
88
|
+
const name = extractName(payload);
|
|
89
|
+
if (!name) return; // banker:<name> 아님 - 무동작.
|
|
90
|
+
record(name);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
main()
|
|
94
|
+
.catch(() => {}) // 어떤 예외도 세션을 막지 않는다.
|
|
95
|
+
.finally(() => process.exit(0));
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* banker 텔레메트리 count 훅 (UserPromptExpansion) - 사용자가 직접 호출한 /banker:<name>
|
|
4
|
+
* 커맨드를 로컬 usage-log 에 `<name>\t<hour>` 한 줄로 기록한다.
|
|
5
|
+
*
|
|
6
|
+
* fail-closed 규약(각 항목은 telemetry-count.test.mjs 가 검증한다):
|
|
7
|
+
* - countingActive() 가 아니면 로컬 쓰기조차 하지 않는다. 파일도 만들지 않는다.
|
|
8
|
+
* - 어떤 오류도 삼키고 항상 exit 0. 세션/프롬프트를 절대 막지 않는다.
|
|
9
|
+
* - PII/식별자를 다루지 않는다. 로그 한 줄 = 이름 문자열 + TAB + hour(0-23) 뿐이다.
|
|
10
|
+
*
|
|
11
|
+
* payload 필드 확정(US-4 라이브 스모크테스트): UserPromptExpansion stdin payload 는
|
|
12
|
+
* `command_name`(예: "banker:setup-lsp")과 `command_source`("plugin")를 담는다. 사용자가
|
|
13
|
+
* /banker:* 를 직접 호출한 경우만 이 이벤트가 발화하고, 모델이 자동 호출하는 스킬은
|
|
14
|
+
* PostToolUse(Skill) 쪽(telemetry-count-skill.mjs)이 담당한다 - 두 경로는 disjoint 로 실측
|
|
15
|
+
* 확정되어 dedup 이 불요하다. 더 이상 방어적 다중필드 추출이 필요 없다.
|
|
16
|
+
*
|
|
17
|
+
* 체크인(usage-log 집계 후 서버 전송)은 이 훅의 책임이 아니다. 트리거는 SessionStart
|
|
18
|
+
* update-notify 가 throttle 경과 시 별도로 연다 - 이 훅은 append 만 하고 flush/checkin 을
|
|
19
|
+
* spawn 하지 않는다.
|
|
20
|
+
*
|
|
21
|
+
* 배선: hooks.json 의 UserPromptExpansion(matcher banker:.*)에 등록되어 사용자 직접 /banker:* 호출 시 발화한다.
|
|
22
|
+
*/
|
|
23
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
24
|
+
|
|
25
|
+
import { configDir, countingActive, USAGE_LOG_PATH, LOG_MAX_LINES } from '../bin/lib/telemetry-config.mjs';
|
|
26
|
+
|
|
27
|
+
// banker:<name> 전체 일치 - 이름 문자열 자체를 검증한다(TAB/개행 등 이질 문자가 섞여
|
|
28
|
+
// usage-log 의 `name\thour` 형식을 깨는 것을 막는다). i 플래그는 매칭만 관대하게(실제 이름은 소문자).
|
|
29
|
+
const NAME_RE = /^banker:[a-z0-9_-]+$/i;
|
|
30
|
+
|
|
31
|
+
function readStdin() {
|
|
32
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
33
|
+
let data = '';
|
|
34
|
+
process.stdin.setEncoding('utf8');
|
|
35
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
36
|
+
process.stdin.on('end', () => resolvePromise(data));
|
|
37
|
+
process.stdin.on('error', rejectPromise);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// UserPromptExpansion payload 에서 사용자가 직접 호출한 banker:<name> 이름을 추출한다.
|
|
42
|
+
// command_source 가 'plugin' 이고 command_name 이 banker:<name> 형식일 때만 반환, 그 외 null.
|
|
43
|
+
function extractName(payload) {
|
|
44
|
+
if (payload?.command_source !== 'plugin') return null;
|
|
45
|
+
const name = payload?.command_name;
|
|
46
|
+
return (typeof name === 'string' && NAME_RE.test(name)) ? name : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 파일의 개행 개수 = 줄 수(각 append 가 끝에 개행을 붙이므로). 없거나 읽기 실패면 0.
|
|
50
|
+
function lineCount(path) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = readFileSync(path, 'utf8');
|
|
53
|
+
return (raw.match(/\n/g) || []).length;
|
|
54
|
+
} catch {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// usage-log 에 `<name>\t<hour>` 한 줄을 append 한다(hour = 로컬 시각 0-23). 상한 도달 시
|
|
60
|
+
// 스킵(best-effort; 체크인이 비우면 재개). flush/checkin 은 여기서 spawn 하지 않는다
|
|
61
|
+
// (트리거는 SessionStart update-notify 소관).
|
|
62
|
+
function record(name) {
|
|
63
|
+
try { mkdirSync(configDir(), { recursive: true }); } catch { /* best-effort */ }
|
|
64
|
+
try {
|
|
65
|
+
if (lineCount(USAGE_LOG_PATH) < LOG_MAX_LINES) {
|
|
66
|
+
const hour = new Date().getHours();
|
|
67
|
+
appendFileSync(USAGE_LOG_PATH, `${name}\t${hour}\n`);
|
|
68
|
+
}
|
|
69
|
+
} catch { /* best-effort */ }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function main() {
|
|
73
|
+
let raw;
|
|
74
|
+
try {
|
|
75
|
+
raw = await readStdin();
|
|
76
|
+
} catch {
|
|
77
|
+
return; // stdin 읽기 실패 - 무동작.
|
|
78
|
+
}
|
|
79
|
+
let payload;
|
|
80
|
+
try {
|
|
81
|
+
payload = JSON.parse(raw);
|
|
82
|
+
} catch {
|
|
83
|
+
return; // malformed stdin - 무동작.
|
|
84
|
+
}
|
|
85
|
+
if (!countingActive()) return; // 카운팅 비활성 - 로컬 쓰기조차 하지 않는다.
|
|
86
|
+
const name = extractName(payload);
|
|
87
|
+
if (!name) return; // banker:<name> 아님 - 무동작.
|
|
88
|
+
record(name);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main()
|
|
92
|
+
.catch(() => {}) // 어떤 예외도 세션을 막지 않는다.
|
|
93
|
+
.finally(() => process.exit(0));
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* banker 카운팅 체크인 훅 - usage-log 를 스킬별 x 시간대(hour 0-23)별 count 로 집계해 엔드포인트로
|
|
4
|
+
* 한 번 POST 하고, 응답의 latest 로 update-check 캐시를 겸해 갱신한다. 전송 시도 후(성공/실패 무관)
|
|
5
|
+
* 로그를 truncate 해 24h 윈도우로 상한한다. telemetry-flush 를 대체(evolve)한다.
|
|
6
|
+
*
|
|
7
|
+
* fail-closed 규약(각 항목은 update-checkin.test.mjs 가 검증한다):
|
|
8
|
+
* - 미활성(countingActive) 또는 엔드포인트 없음이면 무동작(로그도 건드리지 않는다).
|
|
9
|
+
* - PII/IP/지속ID 절대 미포함. 페이로드는 {version, os, counts} 세 키뿐(os=플랫폼 계열,
|
|
10
|
+
* counts=스킬별 x 시간대별 호출수).
|
|
11
|
+
* - 자체 네트워크 timeout 으로 hang 방지. 어떤 오류도 삼키고 항상 exit 0. 어떤 경로도 reject 안 함.
|
|
12
|
+
* - 비-loopback 평문 http 는 전송하지 않는다(https-only). loopback http 만 로컬 테스트 mock 에 허용.
|
|
13
|
+
*
|
|
14
|
+
* count-default-on: 이 훅은 hooks.json 에 배선되지 않는다(스탠드얼론 .mjs). update-notify 가
|
|
15
|
+
* countingActive + 엔드포인트 설정 시 fetcher 로 detached spawn 한다(응답 latest 가 알림 캐시를 겸함).
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import http from 'node:http';
|
|
19
|
+
import https from 'node:https';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
countingActive,
|
|
23
|
+
endpoint,
|
|
24
|
+
USAGE_LOG_PATH,
|
|
25
|
+
LAST_FLUSH_PATH,
|
|
26
|
+
installedVersion,
|
|
27
|
+
writeUpdateCache,
|
|
28
|
+
} from '../bin/lib/telemetry-config.mjs';
|
|
29
|
+
|
|
30
|
+
const NET_TIMEOUT_MS = 10_000;
|
|
31
|
+
|
|
32
|
+
// usage-log 각 줄 `<name>\t<hour 0-23>` 를 스킬별 x 시간대별 count 로 집계한다.
|
|
33
|
+
// 빈 줄·TAB 없는 줄·이름 없는 줄·hour 가 0-23 정수가 아닌 줄은 무시한다.
|
|
34
|
+
// 반환 예: { 'banker:compact-copy': { '9': 3, '14': 1 }, 'banker:setup': { '9': 1 } }.
|
|
35
|
+
function aggregate(raw) {
|
|
36
|
+
const counts = {};
|
|
37
|
+
for (const line of raw.split('\n')) {
|
|
38
|
+
if (!line.trim()) continue; // 빈 줄 무시.
|
|
39
|
+
const tab = line.indexOf('\t');
|
|
40
|
+
if (tab < 0) continue; // TAB 없는 malformed 줄 무시.
|
|
41
|
+
const name = line.slice(0, tab).trim();
|
|
42
|
+
const hourStr = line.slice(tab + 1).trim();
|
|
43
|
+
if (!name) continue; // 이름 없는 줄 무시.
|
|
44
|
+
if (!/^\d+$/.test(hourStr)) continue; // 정수 아닌 hour 무시(음수·소수·비수치 배제).
|
|
45
|
+
const hour = Number(hourStr);
|
|
46
|
+
if (hour > 23) continue; // 0-23 범위 밖 무시(하한은 \d+ 가 보장).
|
|
47
|
+
const key = String(hour); // '09' -> '9' 정규화.
|
|
48
|
+
if (!counts[name]) counts[name] = {};
|
|
49
|
+
counts[name][key] = (counts[name][key] || 0) + 1;
|
|
50
|
+
}
|
|
51
|
+
return counts;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// loopback 호스트만 평문 http 를 허용한다(로컬 테스트 mock 전용). update-fetch.mjs 와 동일 판별:
|
|
55
|
+
// URL.hostname 은 IPv6 를 대괄호로 감싸므로([::1]) 벗겨서 비교한다.
|
|
56
|
+
function isLoopback(hostname) {
|
|
57
|
+
const h = (hostname || '').replace(/^\[|\]$/g, '').toLowerCase();
|
|
58
|
+
return h === 'localhost' || h === '127.0.0.1' || h === '::1';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 엔드포인트로 JSON POST. 어떤 경로에서도 reject 하지 않고, 전송 시도가 끝나면 응답 본문(문자열)
|
|
62
|
+
// 또는 null 로 resolve 한다. 자체 timeout(NET_TIMEOUT_MS)으로 무응답 엔드포인트에도 hang 하지 않는다.
|
|
63
|
+
function post(url, body) {
|
|
64
|
+
return new Promise((resolvePromise) => {
|
|
65
|
+
let done = false;
|
|
66
|
+
const finish = (responseBody = null) => { if (!done) { done = true; resolvePromise(responseBody); } };
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = new URL(url);
|
|
70
|
+
} catch {
|
|
71
|
+
return finish(); // 잘못된 엔드포인트 - 전송 스킵.
|
|
72
|
+
}
|
|
73
|
+
// PRIVACY.md 정합: 비-loopback 엔드포인트에는 https 만 허용한다. 평문 http 는
|
|
74
|
+
// loopback(localhost/127.0.0.1/::1) 로컬 테스트 mock 에만, http/https 외 스킴은
|
|
75
|
+
// 전송하지 않고 no-op(fail-closed). 평문 비-loopback 유출을 원천 차단한다.
|
|
76
|
+
if (parsed.protocol === 'http:') {
|
|
77
|
+
if (!isLoopback(parsed.hostname)) return finish();
|
|
78
|
+
} else if (parsed.protocol !== 'https:') {
|
|
79
|
+
return finish();
|
|
80
|
+
}
|
|
81
|
+
const transport = parsed.protocol === 'http:' ? http : https;
|
|
82
|
+
try {
|
|
83
|
+
const req = transport.request(parsed, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) },
|
|
86
|
+
}, (res) => {
|
|
87
|
+
let data = '';
|
|
88
|
+
res.on('data', (c) => { data += c; });
|
|
89
|
+
res.on('end', () => finish(data));
|
|
90
|
+
res.on('error', () => finish());
|
|
91
|
+
});
|
|
92
|
+
req.on('error', () => finish()); // 연결 거부/리셋 등은 즉시 여기로.
|
|
93
|
+
req.setTimeout(NET_TIMEOUT_MS, () => req.destroy());
|
|
94
|
+
req.write(body);
|
|
95
|
+
req.end();
|
|
96
|
+
} catch {
|
|
97
|
+
finish();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 응답 본문에서 latest 버전 문자열만 방어 추출. null·비문자열·빈·malformed·비문자열 latest 값 → null.
|
|
103
|
+
function extractLatest(responseBody) {
|
|
104
|
+
if (typeof responseBody !== 'string' || !responseBody) return null;
|
|
105
|
+
try {
|
|
106
|
+
const obj = JSON.parse(responseBody);
|
|
107
|
+
const v = (obj && typeof obj === 'object') ? obj.latest : undefined;
|
|
108
|
+
return (typeof v === 'string' && v) ? v : null;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function main() {
|
|
115
|
+
if (!countingActive()) return; // 미활성 - 무동작(로그 미변경).
|
|
116
|
+
const url = endpoint();
|
|
117
|
+
if (!url) return; // 엔드포인트 없음 - 무동작(로그 미변경).
|
|
118
|
+
|
|
119
|
+
// 로그 없음도 유효한 체크인(설치 펄스 + 알림 fetch 겸함): 빈 counts 로 진행한다.
|
|
120
|
+
let raw = '';
|
|
121
|
+
try { raw = readFileSync(USAGE_LOG_PATH, 'utf8'); } catch { raw = ''; }
|
|
122
|
+
|
|
123
|
+
// PII/IP/지속ID 없음: version + os(플랫폼 계열) + counts(스킬별 x 시간대)뿐.
|
|
124
|
+
const payload = JSON.stringify({
|
|
125
|
+
version: installedVersion() || 'unknown',
|
|
126
|
+
os: process.platform,
|
|
127
|
+
counts: aggregate(raw),
|
|
128
|
+
});
|
|
129
|
+
const responseBody = await post(url, payload);
|
|
130
|
+
|
|
131
|
+
// 응답의 latest 로 공유 update-check 캐시를 갱신(알림 겸함). 없으면 캐시 미변경.
|
|
132
|
+
// update-fetch 처럼 notified:null 재무장을 하지 않아도 등가다: dedupe 판정이 notified===latest
|
|
133
|
+
// (update-notify)라 전진 버전에서 구 notified 는 항상 신 latest 와 달라 재고지가 정상 발화하고,
|
|
134
|
+
// count-default-on 은 이 훅만·npm 배포는 update-fetch 만 써 같은 캐시를 겹쳐 쓰지 않는다.
|
|
135
|
+
const latest = extractLatest(responseBody);
|
|
136
|
+
if (latest) {
|
|
137
|
+
try { writeUpdateCache({ latest, checkedAt: Date.now() }); } catch { /* best-effort */ }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 전송 시도 후(성공/실패 무관) 로그를 비워 24h 윈도우로 상한(배치 유실 허용).
|
|
141
|
+
try { writeFileSync(USAGE_LOG_PATH, ''); } catch { /* best-effort */ }
|
|
142
|
+
try { writeFileSync(LAST_FLUSH_PATH, new Date().toISOString()); } catch { /* best-effort */ }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
main()
|
|
146
|
+
.catch(() => {}) // 어떤 예외도 삼킨다.
|
|
147
|
+
.finally(() => process.exit(0));
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* banker 업데이트 알림 fetcher 훅 - npm 레지스트리에서 최신 버전을 조회해 update-check 캐시를 갱신한다.
|
|
4
|
+
* SessionStart 알림 훅(update-notify)이 이 fetcher 를 detached 로 기동해 캐시를 채우고, 알림 렌더링
|
|
5
|
+
* 자체는 캐시만 읽는 별도 훅이 담당한다(이 파일은 네트워크 조회만 - 화면 출력 없음).
|
|
6
|
+
*
|
|
7
|
+
* fail-closed 규약(각 항목은 update-fetch.test.mjs 가 검증한다):
|
|
8
|
+
* - throttle(UPDATE_THROTTLE_MS) 미경과면 무동작(캐시가 없으면 진행).
|
|
9
|
+
* - 비-https 는 loopback(localhost/127.0.0.1/::1) 에서만 http 허용, 그 외 비-https 는 거부.
|
|
10
|
+
* - 3xx 리다이렉트는 따라가지 않고 no-op.
|
|
11
|
+
* - 응답 JSON 에서 version 문자열만 방어 추출 - 없거나 비문자열이면 무동작(캐시 미기록).
|
|
12
|
+
* - 캐시는 writeUpdateCache 로 병합 기록: fetch 된 latest 가 기존 캐시와 다르면 notified 를 초기화
|
|
13
|
+
* (재무장)하고, 같으면 형제 필드(notified 등)를 patch 에서 생략해 그대로 보존한다.
|
|
14
|
+
* - 어떤 오류도 삼키고 항상 exit 0. GET 뿐이라 페이로드/식별자 전송이 없다.
|
|
15
|
+
*
|
|
16
|
+
* 이 훅은 hooks.json 에 배선되지 않는다(스탠드얼론 .mjs). update-notify 가 detached spawn 한다.
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import http from 'node:http';
|
|
20
|
+
import https from 'node:https';
|
|
21
|
+
|
|
22
|
+
import { UPDATE_CHECK_PATH, UPDATE_THROTTLE_MS, NPM_LATEST_URL, writeUpdateCache } from '../bin/lib/telemetry-config.mjs';
|
|
23
|
+
|
|
24
|
+
const NET_TIMEOUT_MS = 10_000;
|
|
25
|
+
|
|
26
|
+
// loopback 호스트만 평문 http 를 허용한다(로컬 테스트 mock 전용). update-checkin.mjs 와 동일 판별:
|
|
27
|
+
// URL.hostname 은 IPv6 를 대괄호로 감싸므로([::1]) 벗겨서 비교한다.
|
|
28
|
+
function isLoopback(hostname) {
|
|
29
|
+
const h = (hostname || '').replace(/^\[|\]$/g, '').toLowerCase();
|
|
30
|
+
return h === 'localhost' || h === '127.0.0.1' || h === '::1';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// update-check 캐시를 읽어 객체로 반환. 없음·malformed·권한 오류 등 어떤 예외에도 던지지 않고 {} 반환.
|
|
34
|
+
function readCache() {
|
|
35
|
+
try {
|
|
36
|
+
const obj = JSON.parse(readFileSync(UPDATE_CHECK_PATH, 'utf8'));
|
|
37
|
+
return (obj && typeof obj === 'object' && !Array.isArray(obj)) ? obj : {};
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// url 에 GET 요청해 응답 JSON 의 version 문자열만 추출한다. 실패·거부·리다이렉트·malformed 는 전부
|
|
44
|
+
// null. reject 하지 않고 항상 resolve 한다(hang 방지). GET 뿐이라 페이로드·식별자 헤더가 없다.
|
|
45
|
+
function fetchLatestVersion(url) {
|
|
46
|
+
return new Promise((resolvePromise) => {
|
|
47
|
+
let done = false;
|
|
48
|
+
const finish = (result) => { if (!done) { done = true; resolvePromise(result); } };
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = new URL(url);
|
|
52
|
+
} catch {
|
|
53
|
+
return finish(null); // 잘못된 URL - 조회 스킵.
|
|
54
|
+
}
|
|
55
|
+
// 비-https 는 loopback 로컬 테스트 mock 에만 허용, 그 외 스킴/호스트는 no-op(fail-closed).
|
|
56
|
+
if (parsed.protocol === 'http:') {
|
|
57
|
+
if (!isLoopback(parsed.hostname)) return finish(null);
|
|
58
|
+
} else if (parsed.protocol !== 'https:') {
|
|
59
|
+
return finish(null);
|
|
60
|
+
}
|
|
61
|
+
const transport = parsed.protocol === 'http:' ? http : https;
|
|
62
|
+
try {
|
|
63
|
+
const req = transport.request(parsed, { method: 'GET' }, (res) => {
|
|
64
|
+
const status = res.statusCode || 0;
|
|
65
|
+
// 3xx 리다이렉트는 따라가지 않는다: 응답을 흘려보내고 no-op 으로 종료.
|
|
66
|
+
if (status >= 300 && status < 400) {
|
|
67
|
+
res.resume();
|
|
68
|
+
return finish(null);
|
|
69
|
+
}
|
|
70
|
+
let data = '';
|
|
71
|
+
res.on('data', (c) => { data += c; });
|
|
72
|
+
res.on('end', () => {
|
|
73
|
+
try {
|
|
74
|
+
const obj = JSON.parse(data);
|
|
75
|
+
const v = obj && typeof obj === 'object' ? obj.version : undefined;
|
|
76
|
+
finish((typeof v === 'string' && v) ? v : null);
|
|
77
|
+
} catch {
|
|
78
|
+
finish(null); // 빈/malformed 본문.
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
res.on('error', () => finish(null));
|
|
82
|
+
});
|
|
83
|
+
req.on('error', () => finish(null)); // 연결 거부/리셋 등은 즉시 여기로.
|
|
84
|
+
req.setTimeout(NET_TIMEOUT_MS, () => req.destroy());
|
|
85
|
+
req.end(); // GET - 요청 본문 없음.
|
|
86
|
+
} catch {
|
|
87
|
+
finish(null);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
const cache = readCache();
|
|
94
|
+
const checkedAt = typeof cache.checkedAt === 'number' ? cache.checkedAt : null;
|
|
95
|
+
if (checkedAt !== null && (Date.now() - checkedAt) < UPDATE_THROTTLE_MS) return; // throttle 미경과 - no-op.
|
|
96
|
+
|
|
97
|
+
const url = process.env.BANKER_NPM_URL || NPM_LATEST_URL; // 테스트 주입용 override.
|
|
98
|
+
const latest = await fetchLatestVersion(url);
|
|
99
|
+
if (!latest) return; // 조회 실패·거부·malformed - 캐시 미기록.
|
|
100
|
+
|
|
101
|
+
const cachedLatest = typeof cache.latest === 'string' ? cache.latest : null;
|
|
102
|
+
if (cachedLatest !== latest) {
|
|
103
|
+
writeUpdateCache({ latest, checkedAt: Date.now(), notified: null }); // 신버전 - notified 재무장.
|
|
104
|
+
} else {
|
|
105
|
+
writeUpdateCache({ latest, checkedAt: Date.now() }); // 동일 버전 - notified 등 형제 필드는 병합 보존.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main()
|
|
110
|
+
.catch(() => {}) // 어떤 예외도 삼킨다.
|
|
111
|
+
.finally(() => process.exit(0));
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* banker 업데이트-체크 알림 훅 (SessionStart) - 캐시된 최신 버전을 확인해 사용자에게
|
|
4
|
+
* 한 번 고지하고, throttle 경과 시 백그라운드 fetcher 를 detached 로 기동한다.
|
|
5
|
+
*
|
|
6
|
+
* 알림 소스는 형제 fetcher(update-fetch/update-checkin)가 채운 로컬 캐시뿐이다. 이 훅 자신은
|
|
7
|
+
* 네트워크를 건드리지 않는다: 포그라운드는 캐시 읽기만 하고, 갱신은 detached 자식에게 위임한다.
|
|
8
|
+
*
|
|
9
|
+
* fail-closed 규약(update-notify.test.mjs 가 검증):
|
|
10
|
+
* - noUpdateCheck() opt-out(env BANKER_NO_UPDATE_CHECK 또는 config.updateCheck===false)이면 무동작.
|
|
11
|
+
* - 캐시 없음·구버전·미확정(malformed) 버전 → 무고지.
|
|
12
|
+
* - 알림 채널 = stdout JSON {systemMessage} (SessionStart 공통 필드·상호작용 세션 사용자 가시·실측 확정).
|
|
13
|
+
* - 동일 latest 재고지 방지: 고지 직후 writeUpdateCache({notified: latest}) 로 기록.
|
|
14
|
+
* - 어떤 오류도 삼키고 항상 exit 0. 세션을 절대 막지 않는다. 비TTY 안전.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
UPDATE_CHECK_PATH, UPDATE_THROTTLE_MS,
|
|
23
|
+
noUpdateCheck, countingActive, endpoint,
|
|
24
|
+
installedVersion, compareVersions, writeUpdateCache,
|
|
25
|
+
} from '../bin/lib/telemetry-config.mjs';
|
|
26
|
+
|
|
27
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
// 업데이트 안내에 쓰는 npm 패키지 이름(고지 문구·이모지/em-dash 없음).
|
|
29
|
+
const NPM_PKG = '@kaydash9999/banker-plugins';
|
|
30
|
+
|
|
31
|
+
function readStdin() {
|
|
32
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
33
|
+
let data = '';
|
|
34
|
+
process.stdin.setEncoding('utf8');
|
|
35
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
36
|
+
process.stdin.on('end', () => resolvePromise(data));
|
|
37
|
+
process.stdin.on('error', rejectPromise);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 캐시({latest,checkedAt,notified})를 객체로 반환. 없음·malformed·배열 → null(무고지·throttle 경과 취급).
|
|
42
|
+
function readCache() {
|
|
43
|
+
try {
|
|
44
|
+
const obj = JSON.parse(readFileSync(UPDATE_CHECK_PATH, 'utf8'));
|
|
45
|
+
return (obj && typeof obj === 'object' && !Array.isArray(obj)) ? obj : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 캐시 latest 가 설치 버전보다 엄격히 크고 그 버전을 아직 고지하지 않았으면 systemMessage 로 1회 고지.
|
|
52
|
+
// compareVersions null(미확정·malformed)·구버전·동일 → 무고지. 고지 후 notified 를 기록해 재고지를 막는다.
|
|
53
|
+
function maybeNotify(cached) {
|
|
54
|
+
if (!cached) return;
|
|
55
|
+
const installed = installedVersion();
|
|
56
|
+
const cmp = compareVersions(cached.latest, installed);
|
|
57
|
+
if (cmp === null || cmp <= 0) return; // 미확정/구/동일 → 무고지
|
|
58
|
+
if (cached.notified === cached.latest) return; // 이미 고지한 버전 → dedupe
|
|
59
|
+
const msg = `banker ${cached.latest} 사용 가능 (현재 ${installed}). 업데이트: /banker:update-banker 또는 npm i -g ${NPM_PKG}`;
|
|
60
|
+
process.stdout.write(JSON.stringify({ systemMessage: msg }));
|
|
61
|
+
writeUpdateCache({ notified: cached.latest }); // 형제 필드(latest/checkedAt) 보존하며 notified 만 병합.
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// checkedAt 기준 throttle 경과 여부. 캐시 없음·checkedAt 미기록/비수치 → 경과로 본다(첫 조회 유도).
|
|
65
|
+
function throttleElapsed(cached) {
|
|
66
|
+
const checkedAt = cached?.checkedAt;
|
|
67
|
+
if (typeof checkedAt !== 'number') return true;
|
|
68
|
+
return (Date.now() - checkedAt) >= UPDATE_THROTTLE_MS;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 갱신 fetcher 절대경로 결정: count-default-on 활성(countingActive) + 엔드포인트 설정 시
|
|
72
|
+
// update-checkin.mjs(체크인이 알림 조회를 겸함), 그 외에는 update-fetch.mjs(공개 npm GET 폴백).
|
|
73
|
+
// 경로는 이 파일 기준으로 resolve 한다. (형제 스토리가 생성 중 - 런타임에 존재하면 된다.)
|
|
74
|
+
export function chooseFetcher() {
|
|
75
|
+
const useCheckin = countingActive() && !!endpoint();
|
|
76
|
+
return join(HERE, useCheckin ? 'update-checkin.mjs' : 'update-fetch.mjs');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 갱신 fetcher 를 detached 로 기동(포그라운드 비차단). 부재/실패는 자식에서 무해히 종료된다.
|
|
80
|
+
function spawnFetcher() {
|
|
81
|
+
try {
|
|
82
|
+
spawn(process.execPath, [chooseFetcher()], { detached: true, stdio: 'ignore' }).unref();
|
|
83
|
+
} catch {
|
|
84
|
+
// best-effort: 기동 실패는 삼킨다(다음 세션이 재시도).
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function main() {
|
|
89
|
+
// SessionStart payload 를 드레인(파이프 위생). 내용을 쓰지 않으므로 파싱 실패해도 무해.
|
|
90
|
+
try { await readStdin(); } catch { /* stdin 없음/오류 무해 */ }
|
|
91
|
+
|
|
92
|
+
if (noUpdateCheck()) return; // opt-out - 무동작.
|
|
93
|
+
|
|
94
|
+
const cached = readCache(); // 포그라운드는 캐시 읽기만(네트워크 없음).
|
|
95
|
+
maybeNotify(cached);
|
|
96
|
+
if (throttleElapsed(cached)) spawnFetcher(); // 갱신은 백그라운드로 위임.
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 직접 실행(훅)일 때만 main() 을 돌린다. import(테스트)시엔 chooseFetcher 만 노출하고 main 은 미실행.
|
|
100
|
+
// run.cjs 가 realpath 로 resolve 한 뒤 argv[1] 로 넘겨도(심링크 배포) 양쪽을 realpath 비교해 일치시킨다.
|
|
101
|
+
function invokedDirectly() {
|
|
102
|
+
try {
|
|
103
|
+
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (invokedDirectly()) {
|
|
110
|
+
main()
|
|
111
|
+
.catch(() => {}) // 어떤 예외도 세션을 막지 않는다.
|
|
112
|
+
.finally(() => process.exit(0));
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaydash9999/banker-plugins",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "banker — Claude Code + Codex CLI plugin (QA, audit, docs, architecture, wiki skills). Install via Claude Code marketplace, or `npm i -g @kaydash9999/banker-plugins` then `banker setup`.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"banker": "bin/banker.js"
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"!**/*.test.mjs",
|
|
22
22
|
"LICENSE",
|
|
23
23
|
"README.md",
|
|
24
|
-
"CHANGELOG.md"
|
|
24
|
+
"CHANGELOG.md",
|
|
25
|
+
"PRIVACY.md"
|
|
25
26
|
],
|
|
26
27
|
"engines": {
|
|
27
28
|
"node": ">=16.7.0"
|