@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +12 -0
- package/hooks/update-checkin.mjs +1 -1
- package/package.json +1 -1
- package/skills/anti-drm/SKILL.md +1 -1
- package/skills/anti-drm/scripts/instruction-toggle.mjs +2 -1
- package/skills/lineage/SKILL.md +1 -1
- package/skills/lineage/lineage.py +64 -12
- package/skills/tone-compact/scripts/instruction-toggle.mjs +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "banker",
|
|
3
|
-
"version": "0.19.
|
|
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
|
package/hooks/update-checkin.mjs
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.
|
|
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"
|
package/skills/anti-drm/SKILL.md
CHANGED
|
@@ -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"
|
|
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;
|
package/skills/lineage/SKILL.md
CHANGED
|
@@ -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.
|
|
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"
|
|
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;
|