@kaydash9999/banker-plugins 0.8.0 → 0.9.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 +13 -1
- package/PRIVACY.md +18 -6
- package/README.md +5 -0
- package/bin/lib/telemetry-config.mjs +73 -0
- package/hooks/telemetry-count-skill.mjs +3 -2
- package/hooks/telemetry-count.mjs +3 -2
- package/hooks/update-checkin.mjs +53 -1
- package/hooks/update-fetch.mjs +3 -0
- package/hooks/update-notify.mjs +30 -3
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "banker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.0] - 2026-07-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **개인화 업데이트 알림**: 새 버전이 나왔을 때, 그 버전에서 바뀐 스킬 중 이 설치에서 실제로 써 본 스킬이 있으면 알림에 그 스킬 이름을 넣어 보여준다("자주 쓰는 스킬이 이번 업데이트에서 바뀌었습니다: ..."). 교집합이 없으면 기존 일반 알림으로 폴백한다.
|
|
7
|
+
- **로컬 "써 본 스킬" 집합**: 카운팅 훅(PostToolUse Skill / UserPromptExpansion)이 스킬 이름을 config 폴더의 `used-skills.json` 에 union 으로 모은다. 이 집합은 절대 전송하지 않는다 — 체크인 페이로드는 여전히 `{version, os, counts}` 뿐이다. 상한(300)·중복 무기록·원자적 기록.
|
|
8
|
+
- **바뀐 스킬 목록(공개 GitHub raw)**: 버전별 바뀐 스킬을 담은 `skill-changes.json` 을 저장소에서 GET 으로 조회한다(카운팅 활성 경로의 update-checkin 이 best-effort; 개인화 표시가 카운팅에 게이트되므로 카운팅 opt-out 경로의 update-fetch 는 조회하지 않는다). 무페이로드 GET 이라 식별자 전송이 없고, 실패하면 일반 알림으로 폴백한다. 릴리스마다 `scripts/gen-skill-changes.js`(git diff 기반·배포 제외)로 채운다.
|
|
9
|
+
- 두 opt-out(`BANKER_NO_UPDATE_CHECK`·`BANKER_NO_TELEMETRY`) 중 어느 쪽을 켜도 개인화는 동작하지 않는다. Codex CLI는 네트워크 차단으로 배제된다.
|
|
10
|
+
- `PRIVACY.md` 에 개인화 항목을 추가했다(로컬 집합 미전송·매니페스트 GET 수신자=GitHub).
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `PRIVACY.md`·CHANGELOG 의 "0.8.0 아직 게시 안 됨" 상태 문구를 실제(게시됨)에 맞게 정정했다. EU ePrivacy opt-in 전환 여부는 열린 검토 항목으로 표기를 유지한다.
|
|
14
|
+
|
|
3
15
|
## [0.8.0] - 2026-07-17
|
|
4
16
|
|
|
5
17
|
### Added
|
|
@@ -13,7 +25,7 @@
|
|
|
13
25
|
- `PRIVACY.md` 를 이 설계에 맞게 재작성했다: 알림/카운팅을 분리 기술하고 각각의 수신자를 이름으로 명시했다(알림=npm, 카운팅=유지보수자). 익명성이 페이로드의 속성이지 시스템 전체의 속성은 아니라는 점(카운팅 서버는 소스 IP를 수신하되 기록하지 않도록 설계)을 정직하게 밝혔다. EU ePrivacy Art.5(3)·Planet49 판례는 표준 관행 근거와 반대 근거를 양면으로 제시했고, 미국 CCPA·한국 PIPA는 이 익명 체크인에 재적용하지 않는다는 점(실무 판단 기준인 IP 지속 보유·프로파일링·판매에 해당하지 않음)을 명기했다.
|
|
14
26
|
|
|
15
27
|
### Notes
|
|
16
|
-
- **정직 명시**: 이 릴리스는
|
|
28
|
+
- **정직 명시**: 이 릴리스는 npm·GitHub·마켓플레이스에 게시되었다. 초안 당시 계획했던 "EU ePrivacy opt-in 전환 검토 통과 후 게시" 게이트는 게시 결정으로 우회되었고, EU ePrivacy opt-in 전환 여부는 `PRIVACY.md` 의 열린 검토 항목으로 남아 있다.
|
|
17
29
|
|
|
18
30
|
## [0.7.1] - 2026-07-16
|
|
19
31
|
|
package/PRIVACY.md
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
## 현재 상태
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
이
|
|
14
|
+
이 문서가 기술하는 업데이트-체크 서비스(업데이트 알림 + 익명 사용량 카운팅 + 개인화)는 게시된 버전에 포함되어 동작합니다.\
|
|
15
|
+
각 버전의 실제 게시 여부는 npm 레지스트리(`@kaydash9999/banker-plugins`)가 정본입니다. 아래 "법적 상태"에서 다루는 EU ePrivacy opt-in 검토는 여전히 열린 항목이며, 현재 배포 방침은 사전 고지 + opt-out 입니다.\
|
|
16
|
+
이 문구는 추가 검토 결과에 따라 더 정교해질 수 있습니다.
|
|
17
17
|
|
|
18
18
|
## 업데이트 알림
|
|
19
19
|
|
|
@@ -24,6 +24,18 @@ update-notifier·Homebrew 등 다른 CLI 도구가 쓰는 것과 같은 방식
|
|
|
24
24
|
기본값은 켜짐입니다.\
|
|
25
25
|
환경변수 `BANKER_NO_UPDATE_CHECK` 를 설정하거나 config의 `updateCheck` 를 `false` 로 두면 끌 수 있습니다.
|
|
26
26
|
|
|
27
|
+
### 개인화 (선택적 강화)
|
|
28
|
+
|
|
29
|
+
새 버전이 있을 때, 그 버전에서 바뀐 스킬 중 이 설치에서 실제로 써 본 스킬이 있으면 알림에 그 스킬 이름을 넣어 보여줍니다("자주 쓰는 스킬이 이번 업데이트에서 바뀌었습니다: ...").\
|
|
30
|
+
이를 위해 두 가지가 쓰이며, 어느 것도 새로운 개인식별정보를 서버로 보내지 않습니다.
|
|
31
|
+
|
|
32
|
+
- **로컬 "써 본 스킬" 집합**: 이 기기의 config 폴더에 `used-skills.json` 로 스킬 이름만 모아 둡니다. 이 파일은 **절대 전송하지 않습니다**. 사용량 카운팅 체크인의 페이로드는 여전히 `{version, os, counts}` 세 키뿐이며 이 집합을 포함하지 않습니다. 오직 세션 시작 시 로컬에서 읽어 알림 문구를 개인화하는 데만 씁니다.
|
|
33
|
+
- **바뀐 스킬 목록 조회**: 어떤 스킬이 바뀌었는지는 공개 GitHub raw(저장소의 `skill-changes.json`)에서 GET 으로 조회합니다. 이 요청의 수신자는 **GitHub(Microsoft가 운영)**이고, GET 이라 페이로드나 식별자를 보내지 않습니다. 업데이트 버전 조회(npm)와 같은 성격의 무페이로드 조회입니다.
|
|
34
|
+
|
|
35
|
+
개인화는 두 opt-out 중 어느 쪽으로도 완전히 꺼집니다.\
|
|
36
|
+
`BANKER_NO_UPDATE_CHECK`(또는 config `updateCheck=false`)는 업데이트 알림 자체를 끄므로 개인화도 함께 사라집니다.\
|
|
37
|
+
`BANKER_NO_TELEMETRY`(또는 config `telemetry=false`·`banker telemetry off`)는 개인화가 기반하는 사용량 카운팅을 끄므로, 로컬 "써 본 스킬" 집합의 **기록**과 알림에서의 **표시**가 모두 중단됩니다(이미 쌓인 로컬 집합이 있어도 표시하지 않으며, 이 경우 일반 업데이트 알림은 계속 표시됩니다).
|
|
38
|
+
|
|
27
39
|
## 사용량 카운팅
|
|
28
40
|
|
|
29
41
|
하루 1회, 유지보수자가 운영하는 외부 엔드포인트(banker 패키지 밖에서 별도로 운영되는 serverless)로 익명 체크인을 전송합니다.\
|
|
@@ -64,8 +76,8 @@ update-notifier·Homebrew 등 다른 CLI 도구가 쓰는 것과 같은 방식
|
|
|
64
76
|
알림과 카운팅이 기본값으로 켜져 있기 때문에 다음 훅이 banker 활성화 중 항상 등록되어 있습니다.
|
|
65
77
|
|
|
66
78
|
- `SessionStart`: 세션 시작 시 1회, 캐시된 업데이트 알림을 확인합니다(이 훅 자체는 네트워크 요청을 하지 않습니다).
|
|
67
|
-
- `PostToolUse`(Skill 매처): 모델이 자동으로 호출한 스킬을
|
|
68
|
-
- `UserPromptExpansion`: 사용자가 직접 입력한 `/banker:*` 커맨드를
|
|
79
|
+
- `PostToolUse`(Skill 매처): 모델이 자동으로 호출한 스킬을 집계하고, 개인화용 로컬 "써 본 스킬" 집합을 갱신합니다(집계는 전송, 로컬 집합은 미전송).
|
|
80
|
+
- `UserPromptExpansion`: 사용자가 직접 입력한 `/banker:*` 커맨드를 집계하고, 개인화용 로컬 "써 본 스킬" 집합을 갱신합니다.
|
|
69
81
|
|
|
70
82
|
세 훅 모두 트리거될 때마다 짧게 실행되는 node 프로세스를 하나씩 기동합니다(수십 ms 수준).\
|
|
71
83
|
opt-out 하더라도 훅의 등록 자체는 남고, 각 스크립트 내부의 게이트가 즉시 무동작으로 반환합니다.
|
|
@@ -85,7 +97,7 @@ opt-out 하더라도 훅의 등록 자체는 남고, 각 스크립트 내부의
|
|
|
85
97
|
|
|
86
98
|
반대 근거: EU ePrivacy Directive Art.5(3)은 개인식별정보 여부와 무관하게 단말기(device) 접근 자체에 이용자 동의를 요구할 수 있습니다. Planet49 판례에 따르면 분석 목적의 opt-out은 유효한 동의로 인정되지 않을 수 있습니다. 즉 무상태 체크인이라도 접근 자체가 EU에서는 동의를 유발할 가능성이 있습니다.
|
|
87
99
|
|
|
88
|
-
실무 결론: 실제로 EU 사용자를 대상으로 배포하기 전에는 opt-in 전환 여부를 별도로 검토해야 합니다.
|
|
100
|
+
실무 결론: 실제로 EU 사용자를 대상으로 배포하기 전에는 opt-in 전환 여부를 별도로 검토해야 합니다. 현재 배포는 사전 고지 + opt-out 방식으로 운영되고 있으며, 이 EU ePrivacy opt-in 전환 여부는 열린 검토 항목으로 남아 있습니다.
|
|
89
101
|
|
|
90
102
|
**미국 CCPA·한국 PIPA는 이 익명 업데이트-체크에 재적용하지 않습니다.** 두 법의 실무 판단 기준은 IP 주소의 지속적 보유, 사용자별 프로파일 구축, 데이터 판매 여부입니다.\
|
|
91
103
|
이 기능의 익명 체크인은 이 중 어느 것도 하지 않습니다. IP를 기록하지 않고, 지속 식별자가 없고, 데이터를 판매하지 않습니다.\
|
package/README.md
CHANGED
|
@@ -181,6 +181,11 @@ banker는 기본값(default-on)으로 두 가지를 합니다.\
|
|
|
181
181
|
새 버전이 나오면 세션 시작 시 알려주는 **업데이트 알림**과, 유지보수자가 사용량을 파악하는 **익명 사용량 카운팅**입니다.\
|
|
182
182
|
이 때문에 `SessionStart`(알림 확인)·`PostToolUse`(모델이 자동 호출한 스킬 집계)·`UserPromptExpansion`(사용자가 직접 입력한 `/banker:*` 집계) 훅이 항상 등록되어 있고, 트리거될 때마다 짧게 node 프로세스를 하나씩 띄웁니다(수십 ms 수준).
|
|
183
183
|
|
|
184
|
+
업데이트 알림은 **개인화**됩니다.\
|
|
185
|
+
새 버전에서 바뀐 스킬 중 당신이 실제로 써 본 스킬이 있으면 그 이름을 알림에 넣어 보여줍니다("자주 쓰는 스킬이 이번 업데이트에서 바뀌었습니다: ...").\
|
|
186
|
+
이를 위해 써 본 스킬 이름만 로컬 파일(`used-skills.json`)에 모으며, 이 목록은 전송하지 않습니다(바뀐 스킬 목록만 공개 GitHub raw 에서 무페이로드 GET 으로 조회).\
|
|
187
|
+
개인화는 아래 두 opt-out 중 어느 쪽으로도 함께 꺼집니다.
|
|
188
|
+
|
|
184
189
|
끄기:
|
|
185
190
|
|
|
186
191
|
```bash
|
|
@@ -29,6 +29,11 @@ export const LAST_FLUSH_PATH = path.join(configDir(), 'last-flush');
|
|
|
29
29
|
// 초과 시 오래된 줄부터 잘라내 24h 윈도우를 근사한다.
|
|
30
30
|
export const LOG_MAX_LINES = 5000;
|
|
31
31
|
|
|
32
|
+
// 개인화 알림용 로컬 "써 본 스킬" 집합 경로 + 상한. 이 파일은 절대 전송하지 않는다(로컬 전용).
|
|
33
|
+
// update-checkin 페이로드는 usage-log 집계만 담고 이 집합은 건드리지 않는다. update-notify 만 읽는다.
|
|
34
|
+
export const USED_SKILLS_PATH = path.join(configDir(), 'used-skills.json');
|
|
35
|
+
export const USED_SKILLS_MAX = 300;
|
|
36
|
+
|
|
32
37
|
// update-check 캐시 파일({latest,checkedAt,notified})의 절대경로. USAGE_LOG_PATH 와 동일하게
|
|
33
38
|
// import 시점 resolver 결과를 캡처한다(단명 hook 프로세스용). writeUpdateCache 는 테스트 격리를 위해
|
|
34
39
|
// configDir() 를 매 호출 재계산하므로 이 상수에 의존하지 않는다.
|
|
@@ -37,6 +42,11 @@ export const UPDATE_CHECK_PATH = path.join(configDir(), 'update-check.json');
|
|
|
37
42
|
export const UPDATE_THROTTLE_MS = 24 * 60 * 60 * 1000;
|
|
38
43
|
// 최신 버전 조회용 공개 npm 레지스트리 URL. 본문 .version 을 직접 반환한다(실측 확정).
|
|
39
44
|
export const NPM_LATEST_URL = 'https://registry.npmjs.org/@kaydash9999/banker-plugins/latest';
|
|
45
|
+
// 버전별 "바뀐 스킬 목록" 매니페스트(공개 GitHub raw JSON: {"x.y.z": ["banker:skill", ...]}). update-fetch/
|
|
46
|
+
// update-checkin 이 best-effort GET 해 changedSkills 를 캐시에 채우고, update-notify 가 로컬 "써 본 스킬"과
|
|
47
|
+
// 교차해 개인화 알림을 만든다. GET 뿐이라 페이로드/식별자 전송이 없고, 실패해도 무해하다(일반 알림으로 폴백).
|
|
48
|
+
// 테스트는 BANKER_SKILL_CHANGES_URL 로 loopback mock 을 주입해 실제 네트워크를 때리지 않는다.
|
|
49
|
+
export const SKILL_CHANGES_URL = 'https://raw.githubusercontent.com/smallOpenSource/banker_plugins/main/skill-changes.json';
|
|
40
50
|
|
|
41
51
|
// config.json 을 읽어 객체로 반환. 파일 없음·malformed JSON·권한 오류 등 어떤 예외에도 던지지 않고 {} 반환.
|
|
42
52
|
export function readConfig() {
|
|
@@ -196,3 +206,66 @@ export function writeUpdateCache(patch) {
|
|
|
196
206
|
return false;
|
|
197
207
|
}
|
|
198
208
|
}
|
|
209
|
+
|
|
210
|
+
// ---- 개인화 알림용 로컬 "써 본 스킬" 집합 (전송하지 않음) ----
|
|
211
|
+
// 이 집합은 절대 서버로 보내지 않는다: update-checkin 페이로드는 usage-log 집계(counts)만 담는다.
|
|
212
|
+
// update-notify 가 로컬에서 읽어, 새 버전에서 바뀐 스킬 중 "사용자가 써 본 것"만 골라 알림에 이름을 넣는다.
|
|
213
|
+
|
|
214
|
+
// 로컬 "써 본 스킬" 배열을 반환. 없음·malformed·비배열 → []. 문자열 원소만 남긴다. 어떤 오류에도 [].
|
|
215
|
+
export function readUsedSkills() {
|
|
216
|
+
try {
|
|
217
|
+
const arr = JSON.parse(fs.readFileSync(path.join(configDir(), 'used-skills.json'), 'utf8'));
|
|
218
|
+
return Array.isArray(arr) ? arr.filter((x) => typeof x === 'string' && x) : [];
|
|
219
|
+
} catch {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// name 을 로컬 집합에 union 한다(개인화 알림용, 전송 안 함). 이미 있으면 파일을 쓰지 않는다(핫패스 절약).
|
|
225
|
+
// 상한(USED_SKILLS_MAX) 도달 시 새 이름은 무시한다(오래된 것 보존; 개인화는 근사여도 무방). configDir() 를
|
|
226
|
+
// 매 호출 재계산(테스트 격리) + temp→rename 원자적 기록. 모든 오류를 삼켜 신규 기록 시 true, 그 외 false.
|
|
227
|
+
export function recordUsedSkill(name) {
|
|
228
|
+
try {
|
|
229
|
+
if (typeof name !== 'string' || !name) return false;
|
|
230
|
+
const set = readUsedSkills();
|
|
231
|
+
if (set.includes(name)) return false; // 이미 기록됨 - 쓰지 않는다.
|
|
232
|
+
if (set.length >= USED_SKILLS_MAX) return false; // 상한 - 무시.
|
|
233
|
+
set.push(name);
|
|
234
|
+
const dir = configDir();
|
|
235
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
236
|
+
const target = path.join(dir, 'used-skills.json');
|
|
237
|
+
const tmp = path.join(dir, `used-skills.json.${process.pid}.tmp`);
|
|
238
|
+
fs.writeFileSync(tmp, JSON.stringify(set));
|
|
239
|
+
fs.renameSync(tmp, target);
|
|
240
|
+
return true;
|
|
241
|
+
} catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 순수 함수: 버전->바뀐 스킬 배열 map 에서 installed < v <= latest 인 버전들의 스킬을 합쳐 중복 제거해 반환.
|
|
247
|
+
// map 비객체·installed/latest 비문자열 → []. malformed 버전 키(compareVersions null)·비배열 값은 건너뛴다.
|
|
248
|
+
// 어떤 입력에도 throw 하지 않는다. 개인화가 "설치~최신 사이에 바뀐 스킬"을 계산하는 데 쓴다.
|
|
249
|
+
export function changedSkillsBetween(map, installed, latest) {
|
|
250
|
+
try {
|
|
251
|
+
if (!map || typeof map !== 'object' || Array.isArray(map)) return [];
|
|
252
|
+
if (typeof installed !== 'string' || typeof latest !== 'string') return [];
|
|
253
|
+
const seen = new Set();
|
|
254
|
+
const out = [];
|
|
255
|
+
for (const v of Object.keys(map)) {
|
|
256
|
+
const gt = compareVersions(v, installed);
|
|
257
|
+
const le = compareVersions(v, latest);
|
|
258
|
+
if (gt === null || le === null) continue; // malformed 버전 키 무시.
|
|
259
|
+
if (gt > 0 && le <= 0) {
|
|
260
|
+
const skills = map[v];
|
|
261
|
+
if (!Array.isArray(skills)) continue;
|
|
262
|
+
for (const s of skills) {
|
|
263
|
+
if (typeof s === 'string' && s && !seen.has(s)) { seen.add(s); out.push(s); }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
} catch {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
24
24
|
|
|
25
|
-
import { configDir, countingActive, USAGE_LOG_PATH, LOG_MAX_LINES } from '../bin/lib/telemetry-config.mjs';
|
|
25
|
+
import { configDir, countingActive, recordUsedSkill, USAGE_LOG_PATH, LOG_MAX_LINES } from '../bin/lib/telemetry-config.mjs';
|
|
26
26
|
|
|
27
27
|
// banker:<name> 전체 일치 - 이름 문자열 자체를 검증한다(TAB/개행 등 이질 문자가 섞여
|
|
28
28
|
// usage-log 의 `name\thour` 형식을 깨는 것을 막는다). i 플래그는 매칭만 관대하게(실제 이름은 소문자).
|
|
@@ -87,7 +87,8 @@ async function main() {
|
|
|
87
87
|
if (!countingActive()) return; // 카운팅 비활성 - 로컬 쓰기조차 하지 않는다.
|
|
88
88
|
const name = extractName(payload);
|
|
89
89
|
if (!name) return; // banker:<name> 아님 - 무동작.
|
|
90
|
-
record(name);
|
|
90
|
+
record(name); // 서버 카운팅용 usage-log append (스킬별 x 시간대 버킷).
|
|
91
|
+
recordUsedSkill(name); // 개인화 알림용 로컬 집합 union (전송 안 함; update-notify 만 로컬에서 읽는다).
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
main()
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
24
24
|
|
|
25
|
-
import { configDir, countingActive, USAGE_LOG_PATH, LOG_MAX_LINES } from '../bin/lib/telemetry-config.mjs';
|
|
25
|
+
import { configDir, countingActive, recordUsedSkill, USAGE_LOG_PATH, LOG_MAX_LINES } from '../bin/lib/telemetry-config.mjs';
|
|
26
26
|
|
|
27
27
|
// banker:<name> 전체 일치 - 이름 문자열 자체를 검증한다(TAB/개행 등 이질 문자가 섞여
|
|
28
28
|
// usage-log 의 `name\thour` 형식을 깨는 것을 막는다). i 플래그는 매칭만 관대하게(실제 이름은 소문자).
|
|
@@ -85,7 +85,8 @@ async function main() {
|
|
|
85
85
|
if (!countingActive()) return; // 카운팅 비활성 - 로컬 쓰기조차 하지 않는다.
|
|
86
86
|
const name = extractName(payload);
|
|
87
87
|
if (!name) return; // banker:<name> 아님 - 무동작.
|
|
88
|
-
record(name);
|
|
88
|
+
record(name); // 서버 카운팅용 usage-log append (스킬별 x 시간대 버킷).
|
|
89
|
+
recordUsedSkill(name); // 개인화 알림용 로컬 집합 union (전송 안 함; update-notify 만 로컬에서 읽는다).
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
main()
|
package/hooks/update-checkin.mjs
CHANGED
|
@@ -23,8 +23,10 @@ import {
|
|
|
23
23
|
endpoint,
|
|
24
24
|
USAGE_LOG_PATH,
|
|
25
25
|
LAST_FLUSH_PATH,
|
|
26
|
+
SKILL_CHANGES_URL,
|
|
26
27
|
installedVersion,
|
|
27
28
|
writeUpdateCache,
|
|
29
|
+
changedSkillsBetween,
|
|
28
30
|
} from '../bin/lib/telemetry-config.mjs';
|
|
29
31
|
|
|
30
32
|
const NET_TIMEOUT_MS = 10_000;
|
|
@@ -111,6 +113,51 @@ function extractLatest(responseBody) {
|
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
// 변경-스킬 매니페스트(공개 GitHub raw JSON: {"x.y.z": ["banker:skill", ...]})를 GET 해 객체로 반환한다.
|
|
117
|
+
// update-fetch 의 동일 함수와 self-contained 로 나란히 둔다(훅별 자기완결 관례·isLoopback 도 이미 각자 보유).
|
|
118
|
+
// post() 와 동일한 안전 가드: 비-https 는 loopback 만 허용·3xx no-op·자체 timeout·malformed→null. 어떤
|
|
119
|
+
// 실패에도 reject 하지 않고 null 로 resolve 한다(개인화는 best-effort). 비배열 객체만 반환.
|
|
120
|
+
function fetchSkillChangesMap(url) {
|
|
121
|
+
return new Promise((resolvePromise) => {
|
|
122
|
+
let done = false;
|
|
123
|
+
const finish = (result) => { if (!done) { done = true; resolvePromise(result); } };
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = new URL(url);
|
|
127
|
+
} catch {
|
|
128
|
+
return finish(null);
|
|
129
|
+
}
|
|
130
|
+
if (parsed.protocol === 'http:') {
|
|
131
|
+
if (!isLoopback(parsed.hostname)) return finish(null);
|
|
132
|
+
} else if (parsed.protocol !== 'https:') {
|
|
133
|
+
return finish(null);
|
|
134
|
+
}
|
|
135
|
+
const transport = parsed.protocol === 'http:' ? http : https;
|
|
136
|
+
try {
|
|
137
|
+
const req = transport.request(parsed, { method: 'GET' }, (res) => {
|
|
138
|
+
const status = res.statusCode || 0;
|
|
139
|
+
if (status >= 300 && status < 400) { res.resume(); return finish(null); } // 리다이렉트 미추적.
|
|
140
|
+
let data = '';
|
|
141
|
+
res.on('data', (c) => { data += c; });
|
|
142
|
+
res.on('end', () => {
|
|
143
|
+
try {
|
|
144
|
+
const obj = JSON.parse(data);
|
|
145
|
+
finish((obj && typeof obj === 'object' && !Array.isArray(obj)) ? obj : null);
|
|
146
|
+
} catch {
|
|
147
|
+
finish(null);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
res.on('error', () => finish(null));
|
|
151
|
+
});
|
|
152
|
+
req.on('error', () => finish(null));
|
|
153
|
+
req.setTimeout(NET_TIMEOUT_MS, () => req.destroy());
|
|
154
|
+
req.end(); // GET - 요청 본문 없음.
|
|
155
|
+
} catch {
|
|
156
|
+
finish(null);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
114
161
|
async function main() {
|
|
115
162
|
if (!countingActive()) return; // 미활성 - 무동작(로그 미변경).
|
|
116
163
|
const url = endpoint();
|
|
@@ -134,7 +181,12 @@ async function main() {
|
|
|
134
181
|
// count-default-on 은 이 훅만·npm 배포는 update-fetch 만 써 같은 캐시를 겹쳐 쓰지 않는다.
|
|
135
182
|
const latest = extractLatest(responseBody);
|
|
136
183
|
if (latest) {
|
|
137
|
-
|
|
184
|
+
// 개인화용: 변경-스킬 매니페스트를 best-effort GET(실패 시 changedSkills 생략, 기존 값 보존).
|
|
185
|
+
const changesUrl = process.env.BANKER_SKILL_CHANGES_URL || SKILL_CHANGES_URL; // 테스트 주입용 override.
|
|
186
|
+
const map = await fetchSkillChangesMap(changesUrl);
|
|
187
|
+
const patch = { latest, checkedAt: Date.now() };
|
|
188
|
+
if (map) patch.changedSkills = changedSkillsBetween(map, installedVersion(), latest);
|
|
189
|
+
try { writeUpdateCache(patch); } catch { /* best-effort */ }
|
|
138
190
|
}
|
|
139
191
|
|
|
140
192
|
// 전송 시도 후(성공/실패 무관) 로그를 비워 24h 윈도우로 상한(배치 유실 허용).
|
package/hooks/update-fetch.mjs
CHANGED
|
@@ -98,6 +98,9 @@ async function main() {
|
|
|
98
98
|
const latest = await fetchLatestVersion(url);
|
|
99
99
|
if (!latest) return; // 조회 실패·거부·malformed - 캐시 미기록.
|
|
100
100
|
|
|
101
|
+
// 개인화 매니페스트는 여기서 조회하지 않는다: update-fetch 는 카운팅 opt-out(countingActive false) 경로의
|
|
102
|
+
// fetcher 이고, 개인화 표시도 countingActive() 로 게이트되므로 이 경로에선 표시될 일이 없다. 매니페스트
|
|
103
|
+
// 조회는 카운팅 활성 경로의 update-checkin 만 수행한다(중복·죽은 조회 제거).
|
|
101
104
|
const cachedLatest = typeof cache.latest === 'string' ? cache.latest : null;
|
|
102
105
|
if (cachedLatest !== latest) {
|
|
103
106
|
writeUpdateCache({ latest, checkedAt: Date.now(), notified: null }); // 신버전 - notified 재무장.
|
package/hooks/update-notify.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
21
21
|
import {
|
|
22
22
|
UPDATE_CHECK_PATH, UPDATE_THROTTLE_MS,
|
|
23
23
|
noUpdateCheck, countingActive, endpoint,
|
|
24
|
-
installedVersion, compareVersions, writeUpdateCache,
|
|
24
|
+
installedVersion, compareVersions, writeUpdateCache, readUsedSkills,
|
|
25
25
|
} from '../bin/lib/telemetry-config.mjs';
|
|
26
26
|
|
|
27
27
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -48,17 +48,44 @@ function readCache() {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// 캐시의 changedSkills(새 버전에서 바뀐 스킬)와 로컬 "써 본 스킬"의 교집합을 반환한다(순서=changedSkills 기준).
|
|
52
|
+
// changedSkills 없음/비배열·써 본 스킬 없음이면 [](일반 알림으로 폴백). 로컬 파일만 읽고 네트워크는 없다.
|
|
53
|
+
// 개인화는 사용량 카운팅에 기반하므로 countingActive() 가 아니면(BANKER_NO_TELEMETRY 등) 이미 쌓인 로컬
|
|
54
|
+
// 집합이 있어도 표시하지 않는다 — 기록과 표시가 같은 텔레메트리 게이트를 따른다(PRIVACY.md 정합).
|
|
55
|
+
function personalizedSkills(cached) {
|
|
56
|
+
if (!countingActive()) return []; // 텔레메트리 opt-out - 개인화 표시 안 함(일반 알림으로 폴백).
|
|
57
|
+
const changed = Array.isArray(cached?.changedSkills) ? cached.changedSkills : [];
|
|
58
|
+
if (!changed.length) return [];
|
|
59
|
+
const used = new Set(readUsedSkills());
|
|
60
|
+
return changed.filter((s) => typeof s === 'string' && used.has(s));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 고지 문구를 만든다. 개인화 교집합이 있으면 바뀐 스킬 이름을 넣고(최대 5개 + "외 N개"), 없으면 일반 문구
|
|
64
|
+
// (기존 동작). banker: 접두는 떼서 짧게. 이모지/em-dash 없음(사용자 가시 채널).
|
|
65
|
+
function buildMessage(latest, installed, cached) {
|
|
66
|
+
const head = `banker ${latest} 사용 가능 (현재 ${installed}).`;
|
|
67
|
+
const tail = `업데이트: /banker:update-banker 또는 npm i -g ${NPM_PKG}`;
|
|
68
|
+
const hits = personalizedSkills(cached);
|
|
69
|
+
if (hits.length) {
|
|
70
|
+
const names = hits.slice(0, 5).map((s) => s.replace(/^banker:/, '')).join(', ');
|
|
71
|
+
const more = hits.length > 5 ? ` 외 ${hits.length - 5}개` : '';
|
|
72
|
+
return `${head} 자주 쓰는 스킬이 이번 업데이트에서 바뀌었습니다: ${names}${more}. ${tail}`;
|
|
73
|
+
}
|
|
74
|
+
return `${head} ${tail}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
// 캐시 latest 가 설치 버전보다 엄격히 크고 그 버전을 아직 고지하지 않았으면 systemMessage 로 1회 고지.
|
|
52
78
|
// compareVersions null(미확정·malformed)·구버전·동일 → 무고지. 고지 후 notified 를 기록해 재고지를 막는다.
|
|
79
|
+
// 고지 문구는 buildMessage 가 만든다(로컬 changedSkills∩써본스킬이 있으면 개인화, 없으면 일반).
|
|
53
80
|
function maybeNotify(cached) {
|
|
54
81
|
if (!cached) return;
|
|
55
82
|
const installed = installedVersion();
|
|
56
83
|
const cmp = compareVersions(cached.latest, installed);
|
|
57
84
|
if (cmp === null || cmp <= 0) return; // 미확정/구/동일 → 무고지
|
|
58
85
|
if (cached.notified === cached.latest) return; // 이미 고지한 버전 → dedupe
|
|
59
|
-
const msg =
|
|
86
|
+
const msg = buildMessage(cached.latest, installed, cached);
|
|
60
87
|
process.stdout.write(JSON.stringify({ systemMessage: msg }));
|
|
61
|
-
writeUpdateCache({ notified: cached.latest }); // 형제 필드(latest/checkedAt) 보존하며 notified 만 병합.
|
|
88
|
+
writeUpdateCache({ notified: cached.latest }); // 형제 필드(latest/checkedAt/changedSkills) 보존하며 notified 만 병합.
|
|
62
89
|
}
|
|
63
90
|
|
|
64
91
|
// checkedAt 기준 throttle 경과 여부. 캐시 없음·checkedAt 미기록/비수치 → 경과로 본다(첫 조회 유도).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaydash9999/banker-plugins",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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"
|