@kaydash9999/banker-plugins 0.19.3 → 0.19.4
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 +10 -0
- package/bin/banker.js +1 -1
- package/bin/lib/telemetry-config.mjs +100 -18
- package/hooks/telemetry-count-skill.mjs +4 -27
- package/hooks/telemetry-count.mjs +4 -27
- package/hooks/update-checkin.mjs +5 -24
- package/hooks/update-fetch.mjs +6 -4
- package/hooks/update-notify.mjs +5 -4
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "banker",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.4",
|
|
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,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.19.4] - 2026-10-17
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- 유지보수자용: 체크인 훅 시험이 `last-flush` 임시 이름의 링크, 폴더나 FIFO 인 차지 이름을 덮는다.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- 업데이트 확인 캐시(`update-check.json`), 써 본 스킬 집합(`used-skills.json`), 체크인의 `last-flush` 는 이름을 미리 알 수 없는 새 임시 파일에 써서 옮긴다. 예전에는 캐시와 집합을 `<이름>.<pid>.tmp` 에 그냥 써서, 그 이름에 놓인 링크를 따라 다른 파일을 덮었다. `last-flush` 는 0.19.3 부터 그 이름에 무엇이 있으면 만들지 않았으나, Windows 는 끊어진 링크면 그 너머에 새 파일을 만들었다. 캐시와 집합 자리가 FIFO 여도 읽으며 기다리지 않는다.
|
|
10
|
+
- 사용량 기록 훅(`/banker:*` 입력, 모델의 Skill 호출): 사용 기록 자리가 FIFO 면 기다리지 않고, 링크면 그 너머에 쓰지 않으며, 끊어진 링크로 새 파일을 만들지 않는다. 같은 파일에 다른 이름이 있으면(하드 링크) 쓰지 않는다. 예전에는 FIFO 에서 훅 제한(5초)까지 멈췄고, 링크가 가리키는 파일 끝에 기록 줄을 더했다. 링크인 기록 자리는 다음 체크인이 링크를 옮겨 지운 뒤 일반 파일로 다시 쌓인다. Windows 는 링크를 따르지 않고 여는 방법이 없어 열기 전에 본다.
|
|
11
|
+
- 설정(`config.json`)과 업데이트 확인 캐시를 읽는 훅과 `banker telemetry status` 는 그 자리가 FIFO 여도 기다리지 않는다. 예전에는 세션 시작 알림과 사용량 기록 훅은 훅 제한(5초)까지 멈췄고, 백그라운드 갱신 확인과 `banker telemetry status` 는 끝나지 않았다.
|
|
12
|
+
|
|
3
13
|
## [0.19.3] - 2026-10-17
|
|
4
14
|
|
|
5
15
|
### Fixed
|
package/bin/banker.js
CHANGED
|
@@ -342,7 +342,7 @@ async function telemetryCmd(sub) {
|
|
|
342
342
|
log(' 참고: 엔드포인트가 설정되어 있고 opt-out 이 아닐 때(env·config 둘 다 아님)만 실제로 전송됩니다.');
|
|
343
343
|
|
|
344
344
|
let cache = {};
|
|
345
|
-
try { cache = JSON.parse(
|
|
345
|
+
try { cache = JSON.parse(tc.readRegular(tc.UPDATE_CHECK_PATH)); } catch { /* 캐시 없음/malformed/일반 파일 아님(FIFO 를 기다리지 않음) */ }
|
|
346
346
|
const installed = tc.installedVersion();
|
|
347
347
|
const cmp = (cache.latest && installed) ? tc.compareVersions(cache.latest, installed) : null;
|
|
348
348
|
log('[업데이트 체크] 새 버전 알림 (기본값: 켜짐)');
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* telemetry-config - banker 텔레메트리 config/consent 공용 모듈 (bin·hook 공유).
|
|
3
|
-
* 의존성 0 (Node 내장 모듈만: node:fs, node:path, node:os). ESM(.mjs).
|
|
3
|
+
* 의존성 0 (Node 내장 모듈만: node:fs, node:path, node:os, node:crypto). ESM(.mjs).
|
|
4
4
|
* import 시 부작용 없음: 경로는 resolver 로 호출 시 계산하고, 파일시스템 접근·mkdir 은
|
|
5
5
|
* 각 함수 호출 시에만 일어난다. readConfig/isEnabled/endpoint 는 어떤 예외도 밖으로 던지지 않는다.
|
|
6
6
|
*/
|
|
7
|
+
import crypto from 'node:crypto';
|
|
7
8
|
import fs from 'node:fs';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
import os from 'node:os';
|
|
@@ -73,10 +74,95 @@ export function collectBody(res, maxBytes, done) {
|
|
|
73
74
|
res.on('error', () => done(null));
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
//
|
|
77
|
+
// 일반 파일의 글. 그 밖(FIFO, 폴더)과 오류는 ''. 막지 않고 열어 FIFO 를 기다리지 않는다.
|
|
78
|
+
export function readRegular(file) {
|
|
79
|
+
let fd;
|
|
80
|
+
try {
|
|
81
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0));
|
|
82
|
+
return fs.fstatSync(fd).isFile() ? fs.readFileSync(fd, 'utf8') : '';
|
|
83
|
+
} catch {
|
|
84
|
+
return '';
|
|
85
|
+
} finally {
|
|
86
|
+
if (fd !== undefined) try { fs.closeSync(fd); } catch { /* best-effort */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// target 자리에 data 를 놓는다. 새 임시 파일을 만들어(wx: 그 이름에 무엇이 있으면 만들지 않음) 쓰고 rename 으로
|
|
91
|
+
// 옮겨, 임시 이름이나 target 자리의 링크를 따라 다른 파일을 덮지 않는다. 임시 이름에 난수를 붙이는 것은 Windows 의
|
|
92
|
+
// wx 가 끊어진 링크를 따라가 그 너머에 새 파일을 만들기 때문이다: 이름을 미리 알 수 없어야 링크를 심어 둘 수 없다.
|
|
93
|
+
// 실패하면 던진다. 지우는 것은 이 호출이 만든 임시 파일뿐이고, 그 이름에 이미 있던 것은 둔다.
|
|
94
|
+
export function writeByRename(target, data) {
|
|
95
|
+
const tmp = `${target}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
96
|
+
let made = false;
|
|
97
|
+
try {
|
|
98
|
+
const fd = fs.openSync(tmp, 'wx');
|
|
99
|
+
made = true;
|
|
100
|
+
try {
|
|
101
|
+
fs.writeFileSync(fd, data);
|
|
102
|
+
} finally {
|
|
103
|
+
fs.closeSync(fd);
|
|
104
|
+
}
|
|
105
|
+
fs.renameSync(tmp, target);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
if (made) try { fs.unlinkSync(tmp); } catch { /* best-effort */ }
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 사용 기록을 덧붙이기로 여는 깃발. 같은 fd 로 줄 수도 센다. 링크를 따르지 않고(O_NOFOLLOW) FIFO 를 기다리지
|
|
113
|
+
// 않는다(O_NONBLOCK). Windows 에는 둘 다 없어 열기 전에 자리를 보고 링크면 두며, 그 뒤에 바뀌어 든 링크는 거기서
|
|
114
|
+
// 따라간다(obsidize 훅과 같은 한계. Windows 는 wx 도 끊어진 링크를 따라가 원자적으로 막을 수단이 없다).
|
|
115
|
+
const NO_FOLLOW = fs.constants.O_NOFOLLOW ?? 0;
|
|
116
|
+
const APPEND_FLAGS = fs.constants.O_RDWR | fs.constants.O_APPEND | fs.constants.O_CREAT | NO_FOLLOW
|
|
117
|
+
| (fs.constants.O_NONBLOCK ?? 0);
|
|
118
|
+
|
|
119
|
+
function isLink(file) {
|
|
120
|
+
try { return fs.lstatSync(file).isSymbolicLink(); } catch { return false; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 사용 기록을 덧붙이기로 연 fd. 일반 파일이고 이름이 하나(nlink 1)일 때만 돌려주고, 그 밖(링크, 하드 링크, FIFO,
|
|
124
|
+
// 폴더)은 연 것을 닫고 null. 없으면 새로 만든다. 열기 자체의 오류는 던진다.
|
|
125
|
+
function openLogForAppend(file) {
|
|
126
|
+
if (!NO_FOLLOW && isLink(file)) return null;
|
|
127
|
+
const fd = fs.openSync(file, APPEND_FLAGS);
|
|
128
|
+
let ok = false;
|
|
129
|
+
try {
|
|
130
|
+
const st = fs.fstatSync(fd);
|
|
131
|
+
ok = st.isFile() && st.nlink <= 1;
|
|
132
|
+
} finally {
|
|
133
|
+
if (!ok) fs.closeSync(fd);
|
|
134
|
+
}
|
|
135
|
+
return ok ? fd : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 사용 기록(usage-log)에 `<name>\t<hour>` 한 줄을 더한다(hour = 로컬 시각 0-23, name 은 SKILL_NAME_RE 꼴만).
|
|
139
|
+
// 줄이 LOG_MAX_LINES 에 이르렀으면 더하지 않는다(best-effort, 체크인이 비우면 다시 쌓인다). 기록 자리가 FIFO 면
|
|
140
|
+
// 기다리지 않고, 링크나 하드 링크면 그 너머 파일에 쓰지 않으며, 끊어진 링크로 새 파일을 만들지 않는다(링크인 자리는
|
|
141
|
+
// 다음 체크인이 링크를 옮겨 지운다). configDir() 를 매 호출 재계산한다(테스트 격리). 더했으면 true, 그 밖과 모든
|
|
142
|
+
// 오류는 false.
|
|
143
|
+
export function appendUsageLog(name) {
|
|
144
|
+
if (typeof name !== 'string' || !SKILL_NAME_RE.test(name)) return false;
|
|
145
|
+
let fd = null;
|
|
146
|
+
try {
|
|
147
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
148
|
+
fd = openLogForAppend(path.join(configDir(), 'usage-log'));
|
|
149
|
+
if (fd === null) return false;
|
|
150
|
+
const lines = (fs.readFileSync(fd, 'utf8').match(/\n/g) || []).length;
|
|
151
|
+
if (lines >= LOG_MAX_LINES) return false;
|
|
152
|
+
fs.writeSync(fd, `${name}\t${new Date().getHours()}\n`);
|
|
153
|
+
return true;
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
} finally {
|
|
157
|
+
if (fd !== null) try { fs.closeSync(fd); } catch { /* best-effort */ }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// config.json 을 읽어 객체로 반환. 파일 없음·malformed JSON·권한 오류·일반 파일 아님(FIFO 를 기다리지 않음) 등
|
|
162
|
+
// 어떤 예외에도 던지지 않고 {} 반환.
|
|
77
163
|
export function readConfig() {
|
|
78
164
|
try {
|
|
79
|
-
const raw =
|
|
165
|
+
const raw = readRegular(path.join(configDir(), 'config.json'));
|
|
80
166
|
const obj = JSON.parse(raw);
|
|
81
167
|
return (obj && typeof obj === 'object' && !Array.isArray(obj)) ? obj : {};
|
|
82
168
|
} catch {
|
|
@@ -209,9 +295,9 @@ export function compareVersions(a, b) {
|
|
|
209
295
|
}
|
|
210
296
|
|
|
211
297
|
// update-check 캐시를 read-modify-write 로 병합 기록한다: 기존 JSON 을 읽어({} on 실패) patch 의
|
|
212
|
-
// 필드만 덮고 나머지 형제 필드는 보존한 뒤, configDir() mkdir -p →
|
|
213
|
-
// 으로 기록한다. 테스트 격리를 위해 경로는 configDir() 로 매 호출
|
|
214
|
-
// 비의존). 모든 오류를 삼켜 best-effort: 성공 true, 실패 false.
|
|
298
|
+
// 필드만 덮고 나머지 형제 필드는 보존한 뒤, configDir() mkdir -p → writeByRename(새 임시 파일, rename)
|
|
299
|
+
// 으로 기록한다. 캐시 자리가 FIFO 여도 읽으며 기다리지 않는다. 테스트 격리를 위해 경로는 configDir() 로 매 호출
|
|
300
|
+
// 재계산한다(UPDATE_CHECK_PATH 상수 비의존). 모든 오류를 삼켜 best-effort: 성공 true, 실패 false.
|
|
215
301
|
export function writeUpdateCache(patch) {
|
|
216
302
|
try {
|
|
217
303
|
const dir = configDir();
|
|
@@ -219,13 +305,11 @@ export function writeUpdateCache(patch) {
|
|
|
219
305
|
const cachePath = path.join(dir, 'update-check.json');
|
|
220
306
|
let existing = {};
|
|
221
307
|
try {
|
|
222
|
-
const obj = JSON.parse(
|
|
308
|
+
const obj = JSON.parse(readRegular(cachePath));
|
|
223
309
|
if (obj && typeof obj === 'object' && !Array.isArray(obj)) existing = obj;
|
|
224
|
-
} catch { /* 없음·malformed → {} */ }
|
|
310
|
+
} catch { /* 없음·malformed·일반 파일 아님 → {} */ }
|
|
225
311
|
const merged = { ...existing, ...(patch && typeof patch === 'object' && !Array.isArray(patch) ? patch : {}) };
|
|
226
|
-
|
|
227
|
-
fs.writeFileSync(tmp, JSON.stringify(merged, null, 2));
|
|
228
|
-
fs.renameSync(tmp, cachePath);
|
|
312
|
+
writeByRename(cachePath, JSON.stringify(merged, null, 2));
|
|
229
313
|
return true;
|
|
230
314
|
} catch {
|
|
231
315
|
return false;
|
|
@@ -236,10 +320,11 @@ export function writeUpdateCache(patch) {
|
|
|
236
320
|
// 이 집합은 절대 서버로 보내지 않는다: update-checkin 페이로드는 usage-log 집계(counts)만 담는다.
|
|
237
321
|
// update-notify 가 로컬에서 읽어, 새 버전에서 바뀐 스킬 중 "사용자가 써 본 것"만 골라 알림에 이름을 넣는다.
|
|
238
322
|
|
|
239
|
-
// 로컬 "써 본 스킬" 배열을 반환. 없음·malformed
|
|
323
|
+
// 로컬 "써 본 스킬" 배열을 반환. 없음·malformed·비배열·일반 파일 아님(FIFO 를 기다리지 않음) → [].
|
|
324
|
+
// 문자열 원소만 남긴다. 어떤 오류에도 [].
|
|
240
325
|
export function readUsedSkills() {
|
|
241
326
|
try {
|
|
242
|
-
const arr = JSON.parse(
|
|
327
|
+
const arr = JSON.parse(readRegular(path.join(configDir(), 'used-skills.json')));
|
|
243
328
|
return Array.isArray(arr) ? arr.filter((x) => typeof x === 'string' && x) : [];
|
|
244
329
|
} catch {
|
|
245
330
|
return [];
|
|
@@ -248,7 +333,7 @@ export function readUsedSkills() {
|
|
|
248
333
|
|
|
249
334
|
// name 을 로컬 집합에 union 한다(개인화 알림용, 전송 안 함). 이미 있으면 파일을 쓰지 않는다(핫패스 절약).
|
|
250
335
|
// 상한(USED_SKILLS_MAX) 도달 시 새 이름은 무시한다(오래된 것 보존; 개인화는 근사여도 무방). configDir() 를
|
|
251
|
-
// 매 호출 재계산(테스트 격리) +
|
|
336
|
+
// 매 호출 재계산(테스트 격리) + writeByRename 으로 기록. 모든 오류를 삼켜 신규 기록 시 true, 그 외 false.
|
|
252
337
|
export function recordUsedSkill(name) {
|
|
253
338
|
try {
|
|
254
339
|
if (typeof name !== 'string' || !name) return false;
|
|
@@ -258,10 +343,7 @@ export function recordUsedSkill(name) {
|
|
|
258
343
|
set.push(name);
|
|
259
344
|
const dir = configDir();
|
|
260
345
|
fs.mkdirSync(dir, { recursive: true });
|
|
261
|
-
|
|
262
|
-
const tmp = path.join(dir, `used-skills.json.${process.pid}.tmp`);
|
|
263
|
-
fs.writeFileSync(tmp, JSON.stringify(set));
|
|
264
|
-
fs.renameSync(tmp, target);
|
|
346
|
+
writeByRename(path.join(dir, 'used-skills.json'), JSON.stringify(set));
|
|
265
347
|
return true;
|
|
266
348
|
} catch {
|
|
267
349
|
return false;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* fail-closed 규약(각 항목은 telemetry-count-skill.test.mjs 가 검증한다):
|
|
7
7
|
* - countingActive() 가 아니면 로컬 쓰기조차 하지 않는다. 파일도 만들지 않는다.
|
|
8
|
+
* - 기록 자리가 FIFO 면 기다리지 않고, 링크면 그 너머에 쓰지 않는다(Windows 는 열기 전에 본다). 하드 링크에 쓰지
|
|
9
|
+
* 않는 것은 공용 appendUsageLog 의 시험(bin/lib/telemetry-config.test.mjs)이 검증한다.
|
|
8
10
|
* - 어떤 오류도 삼키고 항상 exit 0. 세션/툴호출을 절대 막지 않는다.
|
|
9
11
|
* - PII/식별자를 다루지 않는다. 로그 한 줄 = 이름 문자열 + TAB + hour(0-23) 뿐이다.
|
|
10
12
|
*
|
|
@@ -20,9 +22,7 @@
|
|
|
20
22
|
*
|
|
21
23
|
* 배선: hooks.json 의 PostToolUse(matcher Skill)에 등록되어 모델 자동 스킬 호출 시 발화한다.
|
|
22
24
|
*/
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
import { configDir, countingActive, recordUsedSkill, USAGE_LOG_PATH, LOG_MAX_LINES, SKILL_NAME_RE } from '../bin/lib/telemetry-config.mjs';
|
|
25
|
+
import { appendUsageLog, countingActive, recordUsedSkill, SKILL_NAME_RE } from '../bin/lib/telemetry-config.mjs';
|
|
26
26
|
|
|
27
27
|
function readStdin() {
|
|
28
28
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
@@ -46,29 +46,6 @@ function extractName(payload) {
|
|
|
46
46
|
return null;
|
|
47
47
|
}
|
|
48
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
49
|
async function main() {
|
|
73
50
|
let raw;
|
|
74
51
|
try {
|
|
@@ -85,7 +62,7 @@ async function main() {
|
|
|
85
62
|
if (!countingActive()) return; // 카운팅 비활성 - 로컬 쓰기조차 하지 않는다.
|
|
86
63
|
const name = extractName(payload);
|
|
87
64
|
if (!name) return; // banker:<name> 아님 - 무동작.
|
|
88
|
-
|
|
65
|
+
appendUsageLog(name); // 서버 카운팅용 usage-log 한 줄 (스킬별 x 시간대 버킷). 상한이면 건너뛴다.
|
|
89
66
|
recordUsedSkill(name); // 개인화 알림용 로컬 집합 union (전송 안 함; update-notify 만 로컬에서 읽는다).
|
|
90
67
|
}
|
|
91
68
|
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* fail-closed 규약(각 항목은 telemetry-count.test.mjs 가 검증한다):
|
|
7
7
|
* - countingActive() 가 아니면 로컬 쓰기조차 하지 않는다. 파일도 만들지 않는다.
|
|
8
|
+
* - 기록 자리가 FIFO 면 기다리지 않고, 링크면 그 너머에 쓰지 않는다(Windows 는 열기 전에 본다). 하드 링크에 쓰지
|
|
9
|
+
* 않는 것은 공용 appendUsageLog 의 시험(bin/lib/telemetry-config.test.mjs)이 검증한다.
|
|
8
10
|
* - 어떤 오류도 삼키고 항상 exit 0. 세션/프롬프트를 절대 막지 않는다.
|
|
9
11
|
* - PII/식별자를 다루지 않는다. 로그 한 줄 = 이름 문자열 + TAB + hour(0-23) 뿐이다.
|
|
10
12
|
*
|
|
@@ -20,9 +22,7 @@
|
|
|
20
22
|
*
|
|
21
23
|
* 배선: hooks.json 의 UserPromptExpansion(matcher banker:.*)에 등록되어 사용자 직접 /banker:* 호출 시 발화한다.
|
|
22
24
|
*/
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
import { configDir, countingActive, recordUsedSkill, USAGE_LOG_PATH, LOG_MAX_LINES, SKILL_NAME_RE } from '../bin/lib/telemetry-config.mjs';
|
|
25
|
+
import { appendUsageLog, countingActive, recordUsedSkill, SKILL_NAME_RE } from '../bin/lib/telemetry-config.mjs';
|
|
26
26
|
|
|
27
27
|
function readStdin() {
|
|
28
28
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
@@ -44,29 +44,6 @@ function extractName(payload) {
|
|
|
44
44
|
return (typeof name === 'string' && SKILL_NAME_RE.test(name)) ? name : null;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// 파일의 개행 개수 = 줄 수(각 append 가 끝에 개행을 붙이므로). 없거나 읽기 실패면 0.
|
|
48
|
-
function lineCount(path) {
|
|
49
|
-
try {
|
|
50
|
-
const raw = readFileSync(path, 'utf8');
|
|
51
|
-
return (raw.match(/\n/g) || []).length;
|
|
52
|
-
} catch {
|
|
53
|
-
return 0;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// usage-log 에 `<name>\t<hour>` 한 줄을 append 한다(hour = 로컬 시각 0-23). 상한 도달 시
|
|
58
|
-
// 스킵(best-effort; 체크인이 비우면 재개). flush/checkin 은 여기서 spawn 하지 않는다
|
|
59
|
-
// (트리거는 SessionStart update-notify 소관).
|
|
60
|
-
function record(name) {
|
|
61
|
-
try { mkdirSync(configDir(), { recursive: true }); } catch { /* best-effort */ }
|
|
62
|
-
try {
|
|
63
|
-
if (lineCount(USAGE_LOG_PATH) < LOG_MAX_LINES) {
|
|
64
|
-
const hour = new Date().getHours();
|
|
65
|
-
appendFileSync(USAGE_LOG_PATH, `${name}\t${hour}\n`);
|
|
66
|
-
}
|
|
67
|
-
} catch { /* best-effort */ }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
47
|
async function main() {
|
|
71
48
|
let raw;
|
|
72
49
|
try {
|
|
@@ -83,7 +60,7 @@ async function main() {
|
|
|
83
60
|
if (!countingActive()) return; // 카운팅 비활성 - 로컬 쓰기조차 하지 않는다.
|
|
84
61
|
const name = extractName(payload);
|
|
85
62
|
if (!name) return; // banker:<name> 아님 - 무동작.
|
|
86
|
-
|
|
63
|
+
appendUsageLog(name); // 서버 카운팅용 usage-log 한 줄 (스킬별 x 시간대 버킷). 상한이면 건너뛴다.
|
|
87
64
|
recordUsedSkill(name); // 개인화 알림용 로컬 집합 union (전송 안 함; update-notify 만 로컬에서 읽는다).
|
|
88
65
|
}
|
|
89
66
|
|
package/hooks/update-checkin.mjs
CHANGED
|
@@ -17,9 +17,7 @@
|
|
|
17
17
|
* count-default-on: 이 훅은 hooks.json 에 배선되지 않는다(스탠드얼론 .mjs). update-notify 가
|
|
18
18
|
* countingActive + 엔드포인트 설정 시 fetcher 로 detached spawn 한다(응답 latest 가 알림 캐시를 겸함).
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
21
|
-
closeSync, constants, fstatSync, lstatSync, openSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync,
|
|
22
|
-
} from 'node:fs';
|
|
20
|
+
import { lstatSync, readdirSync, renameSync, unlinkSync } from 'node:fs';
|
|
23
21
|
import { randomBytes } from 'node:crypto';
|
|
24
22
|
import http from 'node:http';
|
|
25
23
|
import https from 'node:https';
|
|
@@ -37,6 +35,8 @@ import {
|
|
|
37
35
|
SKILL_NAME_RE,
|
|
38
36
|
collectBody,
|
|
39
37
|
netDeadlineMs,
|
|
38
|
+
readRegular,
|
|
39
|
+
writeByRename,
|
|
40
40
|
} from '../bin/lib/telemetry-config.mjs';
|
|
41
41
|
|
|
42
42
|
const NET_TIMEOUT_MS = 10_000;
|
|
@@ -182,19 +182,6 @@ const RENAME_TRIES = 3;
|
|
|
182
182
|
const RENAME_REST_MS = 50;
|
|
183
183
|
const SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
184
184
|
|
|
185
|
-
// 일반 파일의 글. 그 밖(FIFO, 폴더)과 오류는 ''. 막지 않고 열어 FIFO 를 기다리지 않는다.
|
|
186
|
-
function readRegular(path) {
|
|
187
|
-
let fd;
|
|
188
|
-
try {
|
|
189
|
-
fd = openSync(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
190
|
-
return fstatSync(fd).isFile() ? readFileSync(fd, 'utf8') : '';
|
|
191
|
-
} catch {
|
|
192
|
-
return '';
|
|
193
|
-
} finally {
|
|
194
|
-
if (fd !== undefined) try { closeSync(fd); } catch { /* best-effort */ }
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
185
|
// 보내기 전에 로그 이름을 바꿔 차지한다. rename 은 원자적이라 함께 시작한 체크인 가운데 하나만 그 줄을 읽고,
|
|
199
186
|
// 보내는 동안 더해진 줄은 새 로그로 가 다음 체크인이 보낸다. 링크면 링크 자체가 옮겨진다(대상은 그대로).
|
|
200
187
|
// 로그가 없거나 폴더, FIFO 이거나 이름을 바꾸지 못하면(다른 체크인이 먼저 차지, Windows 에서 백신이 쥠)
|
|
@@ -257,16 +244,10 @@ async function refreshUpdateCache(responseBody) {
|
|
|
257
244
|
}
|
|
258
245
|
|
|
259
246
|
// 전송 시도 후(성공/실패 무관) 차지한 파일을 지워 24h 윈도우로 상한(배치 유실 허용)하고 last-flush 를 남긴다.
|
|
260
|
-
// last-flush 는
|
|
247
|
+
// last-flush 는 writeByRename 으로 놓아, 그 이름 자리나 임시 이름의 링크를 따라 다른 파일을 덮지 않는다.
|
|
261
248
|
function finishClaim(claim) {
|
|
262
249
|
if (claim) try { unlinkSync(claim); } catch { /* best-effort */ }
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
writeFileSync(tmp, new Date().toISOString(), { flag: 'wx' });
|
|
266
|
-
renameSync(tmp, LAST_FLUSH_PATH);
|
|
267
|
-
} catch {
|
|
268
|
-
try { unlinkSync(tmp); } catch { /* best-effort */ }
|
|
269
|
-
}
|
|
250
|
+
try { writeByRename(LAST_FLUSH_PATH, new Date().toISOString()); } catch { /* best-effort */ }
|
|
270
251
|
}
|
|
271
252
|
|
|
272
253
|
async function main() {
|
package/hooks/update-fetch.mjs
CHANGED
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
*
|
|
16
16
|
* 이 훅은 hooks.json 에 배선되지 않는다(스탠드얼론 .mjs). update-notify 가 detached spawn 한다.
|
|
17
17
|
*/
|
|
18
|
-
import { readFileSync } from 'node:fs';
|
|
19
18
|
import http from 'node:http';
|
|
20
19
|
import https from 'node:https';
|
|
21
20
|
|
|
22
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
UPDATE_CHECK_PATH, UPDATE_THROTTLE_MS, NPM_LATEST_URL, writeUpdateCache, collectBody, netDeadlineMs, readRegular,
|
|
23
|
+
} from '../bin/lib/telemetry-config.mjs';
|
|
23
24
|
|
|
24
25
|
const NET_TIMEOUT_MS = 10_000;
|
|
25
26
|
const NET_DEADLINE_MS = netDeadlineMs(); // 교환 전체 마감(위 값은 무활동 기준).
|
|
@@ -32,10 +33,11 @@ function isLoopback(hostname) {
|
|
|
32
33
|
return h === 'localhost' || h === '127.0.0.1' || h === '::1';
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
// update-check 캐시를 읽어 객체로 반환. 없음·malformed·권한
|
|
36
|
+
// update-check 캐시를 읽어 객체로 반환. 없음·malformed·권한 오류·일반 파일 아님(FIFO 를 기다리지 않음) 등 어떤
|
|
37
|
+
// 예외에도 던지지 않고 {} 반환.
|
|
36
38
|
function readCache() {
|
|
37
39
|
try {
|
|
38
|
-
const obj = JSON.parse(
|
|
40
|
+
const obj = JSON.parse(readRegular(UPDATE_CHECK_PATH));
|
|
39
41
|
return (obj && typeof obj === 'object' && !Array.isArray(obj)) ? obj : {};
|
|
40
42
|
} catch {
|
|
41
43
|
return {};
|
package/hooks/update-notify.mjs
CHANGED
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
* - 어떤 오류도 삼키고 항상 exit 0. 세션을 절대 막지 않는다. 비TTY 안전.
|
|
15
15
|
*/
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
17
|
-
import {
|
|
17
|
+
import { realpathSync } from 'node:fs';
|
|
18
18
|
import { dirname, join } from 'node:path';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
22
|
UPDATE_CHECK_PATH, UPDATE_THROTTLE_MS,
|
|
23
23
|
noUpdateCheck, countingActive, endpoint,
|
|
24
|
-
installedVersion, compareVersions, writeUpdateCache, readUsedSkills,
|
|
24
|
+
installedVersion, compareVersions, writeUpdateCache, readUsedSkills, readRegular,
|
|
25
25
|
} from '../bin/lib/telemetry-config.mjs';
|
|
26
26
|
|
|
27
27
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -38,10 +38,11 @@ function readStdin() {
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// 캐시({latest,checkedAt,notified})를 객체로 반환. 없음·malformed
|
|
41
|
+
// 캐시({latest,checkedAt,notified})를 객체로 반환. 없음·malformed·배열·일반 파일 아님(FIFO 를 기다리지 않음) → null
|
|
42
|
+
// (무고지·throttle 경과 취급).
|
|
42
43
|
function readCache() {
|
|
43
44
|
try {
|
|
44
|
-
const obj = JSON.parse(
|
|
45
|
+
const obj = JSON.parse(readRegular(UPDATE_CHECK_PATH));
|
|
45
46
|
return (obj && typeof obj === 'object' && !Array.isArray(obj)) ? obj : null;
|
|
46
47
|
} catch {
|
|
47
48
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaydash9999/banker-plugins",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.4",
|
|
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"
|