@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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code skills + commands — QA, audit, docs, architecture, dependency setup, and wiki; the banker plugin marketplace.",
9
- "version": "0.19.3"
9
+ "version": "0.19.4"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "banker",
3
- "version": "0.19.3",
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(fs.readFileSync(tc.UPDATE_CHECK_PATH, 'utf8')); } catch { /* 캐시 없음/malformed */ }
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
- // config.json 을 읽어 객체로 반환. 파일 없음·malformed JSON·권한 오류 등 어떤 예외에도 던지지 않고 {} 반환.
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 = fs.readFileSync(path.join(configDir(), 'config.json'), 'utf8');
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 → temp 파일 쓰기 → rename(원자적)
213
- // 으로 기록한다. 테스트 격리를 위해 경로는 configDir() 로 매 호출 재계산한다(UPDATE_CHECK_PATH 상수
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(fs.readFileSync(cachePath, 'utf8'));
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
- const tmp = path.join(dir, `update-check.json.${process.pid}.tmp`);
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(fs.readFileSync(path.join(configDir(), 'used-skills.json'), 'utf8'));
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
- // 매 호출 재계산(테스트 격리) + temp→rename 원자적 기록. 모든 오류를 삼켜 신규 기록 시 true, 그 외 false.
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
- const target = path.join(dir, 'used-skills.json');
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 { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
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
- record(name); // 서버 카운팅용 usage-log append (스킬별 x 시간대 버킷).
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 { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
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
- record(name); // 서버 카운팅용 usage-log append (스킬별 x 시간대 버킷).
63
+ appendUsageLog(name); // 서버 카운팅용 usage-log 한 줄 (스킬별 x 시간대 버킷). 상한이면 건너뛴다.
87
64
  recordUsedSkill(name); // 개인화 알림용 로컬 집합 union (전송 안 함; update-notify 만 로컬에서 읽는다).
88
65
  }
89
66
 
@@ -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 는 새 파일에 쓴 뒤 rename 으로 놓아, 그 이름 자리의 링크를 따라 다른 파일을 덮지 않는다.
247
+ // last-flush 는 writeByRename 으로 놓아, 그 이름 자리나 임시 이름의 링크를 따라 다른 파일을 덮지 않는다.
261
248
  function finishClaim(claim) {
262
249
  if (claim) try { unlinkSync(claim); } catch { /* best-effort */ }
263
- const tmp = `${LAST_FLUSH_PATH}.${process.pid}.tmp`;
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() {
@@ -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 { UPDATE_CHECK_PATH, UPDATE_THROTTLE_MS, NPM_LATEST_URL, writeUpdateCache, collectBody, netDeadlineMs } from '../bin/lib/telemetry-config.mjs';
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(readFileSync(UPDATE_CHECK_PATH, 'utf8'));
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 {};
@@ -14,14 +14,14 @@
14
14
  * - 어떤 오류도 삼키고 항상 exit 0. 세션을 절대 막지 않는다. 비TTY 안전.
15
15
  */
16
16
  import { spawn } from 'node:child_process';
17
- import { readFileSync, realpathSync } from 'node:fs';
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·배열 → null(무고지·throttle 경과 취급).
41
+ // 캐시({latest,checkedAt,notified})를 객체로 반환. 없음·malformed·배열·일반 파일 아님(FIFO 를 기다리지 않음) → null
42
+ // (무고지·throttle 경과 취급).
42
43
  function readCache() {
43
44
  try {
44
- const obj = JSON.parse(readFileSync(UPDATE_CHECK_PATH, 'utf8'));
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",
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"