@kaydash9999/banker-plugins 0.19.1 → 0.19.2

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.1"
9
+ "version": "0.19.2"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "banker",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
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.19.2] - 2026-10-16
4
+
5
+ ### Changed
6
+ - `anti-drm` SKILL: 종료 코드 1 행에 `앞서 바꾼 파일:` 줄이 있으면 그 파일은 이미 바뀌었다고 적는다(tone-compact 와 같게).
7
+ - 유지보수자용: SKILL 과 명령 문서의 PowerShell 블록이 Windows PowerShell 5.1 에서 구문 오류 없이 읽히는지 Windows 에서 시험한다. 자리 표시는 괄호 안쪽에 공백이 없는 꼴만 낱말로 바꿔 읽는다(공백 낀 `< in.txt >` 는 5.1 이 거부하는 리디렉션이라 그대로 둠). 알려진 오류 두 줄을 시험이 잡는지 먼저 보고, 파싱하며 코드를 불러오는 블록(`using module`, `using assembly`)은 파싱하지 않고 실패로 알린다. Windows PowerShell 5.1 은 제 경로로 부른다. pwsh 7 은 5.1 보다 넓게 받아 Windows 가 아닌 OS 에서는 건너뛴다.
8
+
9
+ ### Fixed
10
+ - `lineage` 3.0.7: 짝 없는 서로게이트(U+D800~U+DFFF 가 짝 없이 든 JSON 이스케이프)가 든 전사 줄은 그 글자를 U+FFFD 로 바꾸고 줄마다 한 번 경고한 뒤 계속한다. 검토 묶음, 검토 결정, 결정 캐시를 읽을 때와 페이지와 묶음을 쓸 때(제목이나 파일 이름에서 온 것)도 경고 없이 같은 글자로 바꾼다. 예전에는 기본 흐름의 검토 묶음(`--emit-review`)과 `--rulebase` 페이지가 트레이스백으로 멈췄다. uuid 가 글이 아닌 줄(목록, 객체, 숫자, 참과 거짓)은 경고 뒤 줄 번호로 id 를 만든다(빈 글과 없는 uuid 는 예전처럼 경고 없이). 예전에는 검토 묶음이 멈췄다. 역할이 user, assistant 가 아닌 줄은 기록 종류로 읽는다. 예전에는 묶음 적용이 `malformed turns` 로 멈췄다.
11
+ - `lineage`: 검토자가 쓴 결정 id 와 묶음의 턴 id 는 한 줄로 찍히지 않는 글자가 있으면 이스케이프해 보인다. 예전에는 줄바꿈이나 제어 문자가 든 id 가 `[lineage] PASS:` 같은 가짜 줄을 만들 수 있었다.
12
+ - 업데이트 확인 체크인 훅: 사용 기록을 보낸 직후 비운다. 예전에는 바뀐 스킬 목록을 받는 동안(최대 30초) 비우지 않아, 그 사이 시작한 세션이 같은 기록을 다시 보냈다.
13
+ - `tone-compact`, `anti-drm`: 잠금 자리가 ESTALE 을 계속 내면 쉬면서 다시 보고, 기다림이 끝나면 거부한다. 예전에는 기다리는 동안 쉬지 않고 lstat 을 되풀이해 코어 하나를 썼다. ESTALE 이 한 번만 나는 죽은 잠금은 예전처럼 정리한다.
14
+
3
15
  ## [0.19.1] - 2026-10-16
4
16
 
5
17
  ### Changed
@@ -193,8 +193,8 @@ async function main() {
193
193
  const url = endpoint();
194
194
  if (!url) return; // 엔드포인트 없음 - 무동작(로그 미변경).
195
195
  const responseBody = await post(url, checkinBody());
196
+ resetLog(); // 보낸 직후 비운다. 매니페스트를 받는 동안(최대 30초) 시작한 세션이 같은 로그를 다시 보내지 않게.
196
197
  await refreshUpdateCache(responseBody);
197
- resetLog();
198
198
  }
199
199
 
200
200
  main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaydash9999/banker-plugins",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
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"
@@ -130,7 +130,7 @@ argument-hint: "on | off | status | config"
130
130
  | 코드 | 뜻 | 처리 |
131
131
  |---|---|---|
132
132
  | 0 | 완료. 이미 그 상태였던 경우 포함 | 결과 전달 |
133
- | 1 | 거부 또는 사용법 오류. 설정 값을 준 `on` 과 `config` 는 설정을 저장한 뒤 갱신하지 못한 런타임이 있어도 1 | 메시지 그대로 전달 |
133
+ | 1 | 거부 또는 사용법 오류. 설정 값을 준 `on` 과 `config` 는 설정을 저장한 뒤 갱신하지 못한 런타임이 있어도 1. 메시지에 `앞서 바꾼 파일:` 이 있으면 그 파일은 이미 바뀜 | 메시지 그대로 전달 |
134
134
  | 2 | 예기치 못한 오류 | 메시지 전달 후 `status` 로 확인 |
135
135
 
136
136
  - 출력이 비어 있으면 실행 실패. 성공으로 보고하지 않음
@@ -549,7 +549,8 @@ function dropStale(path) {
549
549
  try {
550
550
  st = lstatSync(path);
551
551
  } catch (e) {
552
- if (e.code === "ENOENT" || e.code === "ESTALE") return true; /* gone in between (ESTALE: a network share's word for it) */
552
+ if (e.code === "ENOENT") return true; /* gone in between */
553
+ if (e.code === "ESTALE") return false; /* a network share's stale handle: rest and look again, not spin */
553
554
  throw e;
554
555
  }
555
556
  if (st.isDirectory() || Math.abs(Date.now() - st.mtimeMs) <= LOCK_STALE_MS) return false;
@@ -2,7 +2,7 @@
2
2
  name: lineage
3
3
  description: "현재 세션 대화를 카카오톡 스타일의 단일 HTML 로 내보냄. Claude 답변은 한 줄 요약 아래에 원문을 접어 두고, 펼치면 마크다운으로 렌더해 보여 줌. 기본은 세션 모델이 요약을 쓰고 노이즈 턴을 걸러 내며, `--rulebase` 는 모델 없이 규칙만 씀. 여러 세션을 합칠 수 있음. 'lineage'/'대화 export'/'카톡 스타일 html' 시 사용."
4
4
  invocation: /lineage
5
- version: 3.0.6
5
+ version: 3.0.7
6
6
  schema_version: 1
7
7
  ---
8
8
 
@@ -23,6 +23,7 @@ import sys
23
23
  import tempfile
24
24
  import unicodedata
25
25
  from html.parser import HTMLParser
26
+ from typing import Any
26
27
 
27
28
  if sys.version_info < (3, 7):
28
29
  sys.stderr.write("lineage requires Python 3.7+ (found %s)\n"
@@ -424,6 +425,56 @@ def session_start(path):
424
425
  KNOWN_TYPES = {"user", "assistant", "agent-setting", "permission-mode", "summary"}
425
426
  KNOWN_CONTENT_ITEMS = {"text", "tool_use", "tool_result", "thinking"}
426
427
  _CTRL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]") # strip control chars (keeps \t\n)
428
+ # What a JSON escape for a lone surrogate (U+D800 to U+DFFF with no pair) leaves in a str. UTF-8 cannot hold one,
429
+ # so a page, a review pack or a cache key that has it stops with UnicodeEncodeError.
430
+ _LONE_SURROGATE_RE = re.compile("[" + chr(0xD800) + "-" + chr(0xDFFF) + "]")
431
+
432
+
433
+ def _without_lone_surrogates(value: Any) -> Any:
434
+ """value with each lone surrogate in its strings as U+FFFD, through lists and dicts (keys too). Any, as the
435
+ JSON it takes is: the callers check the shape after."""
436
+ if isinstance(value, str):
437
+ return _LONE_SURROGATE_RE.sub(chr(0xFFFD), value)
438
+ if isinstance(value, list):
439
+ return [_without_lone_surrogates(v) for v in value]
440
+ if isinstance(value, dict):
441
+ return {_without_lone_surrogates(k): _without_lone_surrogates(v) for k, v in value.items()}
442
+ return value
443
+
444
+
445
+ def _lone_to_fffd(value):
446
+ """A str with each lone surrogate as U+FFFD; anything else as it is (no walk into it)."""
447
+ return _LONE_SURROGATE_RE.sub(chr(0xFFFD), value) if isinstance(value, str) else value
448
+
449
+
450
+ def _shown_id(value):
451
+ """An id from a transcript or a reviewer as a message shows it: as it is when it prints as one plain line, else its
452
+ repr, so a line break or a control character in it cannot forge a line of lineage's own."""
453
+ return value if isinstance(value, str) and value.isprintable() else repr(value)
454
+
455
+
456
+ def _clean_strings(line_no, role, text, tools, ts, uuid_):
457
+ """A record's role, text, tool counts, timestamp and uuid with each lone surrogate as U+FFFD, after one WARN for
458
+ the line. Only text is touched: a uuid of another shape is replaced later, whole."""
459
+ if not any(isinstance(s, str) and _LONE_SURROGATE_RE.search(s) for s in (role, text, ts, uuid_, *tools)):
460
+ return role, text, tools, ts, uuid_
461
+ print(f"[WARN] line {line_no}: lone surrogate replaced with U+FFFD", file=sys.stderr)
462
+ counts = {}
463
+ for name, n in tools.items():
464
+ name = _lone_to_fffd(name)
465
+ counts[name] = counts.get(name, 0) + n
466
+ clean = _lone_to_fffd
467
+ return clean(role), clean(text), counts, clean(ts), clean(uuid_)
468
+
469
+
470
+ def _turn_id(uuid_, line_no, ts):
471
+ """The record's uuid when it is text, else an id from the line and the timestamp: a missing or empty one as
472
+ before, another shape (a list, an object, a number) after a WARN, since ids are dict keys and joined text."""
473
+ if isinstance(uuid_, str) and uuid_:
474
+ return uuid_
475
+ if uuid_ is not None and uuid_ != "":
476
+ print(f"[WARN] line {line_no}: uuid that is not text replaced by an id from the line", file=sys.stderr)
477
+ return hashlib.sha256(f"{line_no}:{ts}".encode()).hexdigest()[:16]
427
478
 
428
479
 
429
480
  def _json_object(raw, line_no):
@@ -534,10 +585,6 @@ def parse_turns(stream, unsafe_schema: bool = False, session_id=None,
534
585
  if obj is None or not _is_turn_record(obj, seen_unknown_type, unsafe_schema, line_no):
535
586
  continue
536
587
  msg = obj.get("message") or {}
537
- role = msg.get("role") or obj.get("type")
538
- ts = _stamp(obj.get("timestamp"))
539
- uuid_ = obj.get("uuid") or hashlib.sha256(
540
- f"{line_no}:{ts}".encode()).hexdigest()[:16]
541
588
  got = _content_parts(msg.get("content"), line_no)
542
589
  if got is None:
543
590
  continue
@@ -545,6 +592,11 @@ def parse_turns(stream, unsafe_schema: bool = False, session_id=None,
545
592
  text = _CTRL_RE.sub("", "\n".join(p for p in text_parts if p).strip())
546
593
  if not text and not tools:
547
594
  continue
595
+ role = msg.get("role")
596
+ role = role if role in ("user", "assistant") else obj.get("type") # the type _is_turn_record let through
597
+ role, text, tools, ts, uuid_ = _clean_strings(
598
+ line_no, role, text, tools, _stamp(obj.get("timestamp")), obj.get("uuid"))
599
+ uuid_ = _turn_id(uuid_, line_no, ts)
548
600
 
549
601
  yield {"role": role, "text": text, "ts": ts, "uuid": uuid_,
550
602
  "tools": tools, "line_no": line_no,
@@ -1477,7 +1529,7 @@ def _write_private(path, text):
1477
1529
  fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp")
1478
1530
  try:
1479
1531
  with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as f:
1480
- f.write(text)
1532
+ f.write(_lone_to_fffd(text)) # a title or a file name may still bring one: UTF-8 cannot hold it
1481
1533
  os.replace(tmp, str(path))
1482
1534
  except BaseException:
1483
1535
  try:
@@ -1491,7 +1543,7 @@ def _write_text_lf(path, text):
1491
1543
  """Write `text` to `path` as UTF-8 with LF line ends on every OS. Text mode on Windows
1492
1544
  would write CRLF, so the same transcript would give a different page there."""
1493
1545
  with open(str(path), "w", encoding="utf-8", newline="\n") as f:
1494
- f.write(text)
1546
+ f.write(_lone_to_fffd(text)) # a title or a file name may still bring one: UTF-8 cannot hold it
1495
1547
 
1496
1548
 
1497
1549
  def _safe_name(s):
@@ -1537,7 +1589,7 @@ def read_llm_cache(turn_id, session, digest):
1537
1589
  keep, summary = got.get("keep"), got.get("summary")
1538
1590
  if not (_is_keep(keep) and _is_summary(summary)):
1539
1591
  return None
1540
- return {"keep": keep, "summary": summary}
1592
+ return {"keep": keep, "summary": _lone_to_fffd(summary)}
1541
1593
 
1542
1594
 
1543
1595
  def write_llm_cache(turn_id, session, digest, keep, summary):
@@ -1811,7 +1863,7 @@ def _is_pack_turn(x):
1811
1863
 
1812
1864
  def _load_pack(path):
1813
1865
  try:
1814
- pack = json.loads(path.read_text(encoding="utf-8"))
1866
+ pack = _without_lone_surrogates(json.loads(path.read_text(encoding="utf-8")))
1815
1867
  except (OSError, ValueError, UnicodeDecodeError, RecursionError) as e:
1816
1868
  return _bad(f"cannot read review pack {path}: {e}")
1817
1869
  if not isinstance(pack, dict) or pack.get("schema") != REVIEW_SCHEMA:
@@ -1914,10 +1966,10 @@ def _load_decisions(files):
1914
1966
  why = _decision_problem(d)
1915
1967
  if why:
1916
1968
  return _bad(f"{f}[{k}]: {why}")
1917
- cur = merged.setdefault(d["id"], {})
1969
+ cur = merged.setdefault(_lone_to_fffd(d["id"]), {}) # a model's JSON may hold a lone surrogate
1918
1970
  for key in ("keep", "summary"):
1919
1971
  if d.get(key) is not None:
1920
- cur[key] = d[key]
1972
+ cur[key] = _lone_to_fffd(d[key])
1921
1973
  if items and all(d.get("keep") is None and d.get("summary") is None for d in items):
1922
1974
  print(f"[lineage] WARN: {f}: all {len(items)} decisions are null; the rules "
1923
1975
  "decide those turns", file=sys.stderr)
@@ -2044,7 +2096,7 @@ def _check_pack(pack, merged, args):
2044
2096
  unknown = [i for i in merged if i not in ids]
2045
2097
  if unknown:
2046
2098
  print(f"[lineage] WARN: decisions name {len(unknown)} turn(s) not in the pack, "
2047
- f"ignored: {', '.join(unknown[:5])}", file=sys.stderr)
2099
+ f"ignored: {', '.join(_shown_id(i) for i in unknown[:5])}", file=sys.stderr)
2048
2100
  _warn_coverage(pack, set(merged) | {x["id"] for x in pack["turns"]
2049
2101
  if x["llm"].get("cached")})
2050
2102
  if pack.get("redact_mode") in ("full", "mask"):
@@ -2109,7 +2161,7 @@ def _report_review(stats, turns, pack, typed):
2109
2161
  print(f"[lineage] note: {stats['flag-kept']} turn(s) a reviewer marked keep: false "
2110
2162
  "stay, as --keep-trivia or --keep-tool-only asked", file=sys.stderr)
2111
2163
  if typed:
2112
- print(f"[lineage] WARN: reviewers dropped {len(typed)} typed user turn(s): {', '.join(typed[:10])}"
2164
+ print(f"[lineage] WARN: reviewers dropped {len(typed)} typed user turn(s): {', '.join(_shown_id(i) for i in typed[:10])}"
2113
2165
  f"{' ...' if len(typed) > 10 else ''}; tell the user", file=sys.stderr)
2114
2166
 
2115
2167
 
@@ -549,7 +549,8 @@ function dropStale(path) {
549
549
  try {
550
550
  st = lstatSync(path);
551
551
  } catch (e) {
552
- if (e.code === "ENOENT" || e.code === "ESTALE") return true; /* gone in between (ESTALE: a network share's word for it) */
552
+ if (e.code === "ENOENT") return true; /* gone in between */
553
+ if (e.code === "ESTALE") return false; /* a network share's stale handle: rest and look again, not spin */
553
554
  throw e;
554
555
  }
555
556
  if (st.isDirectory() || Math.abs(Date.now() - st.mtimeMs) <= LOCK_STALE_MS) return false;