@kaydash9999/banker-plugins 0.20.2 → 0.20.5
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 +179 -1
- package/PRIVACY.md +13 -3
- package/README.md +25 -2
- package/bin/banker.js +84 -40
- package/bin/lib/telemetry-config.mjs +31 -16
- package/codex/manifest.json +1 -1
- package/codex/transform-matrix.md +1 -1
- package/commands/setup.md +23 -0
- package/hooks/progress.mjs +143 -38
- package/hooks/register.mjs +13 -3
- package/hooks/update-checkin.mjs +15 -7
- package/hooks/update-fetch.mjs +15 -9
- package/hooks/update-notify.mjs +2 -9
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +11 -5
- package/skills/3d-intro-build/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-build/references/curate.mjs +48 -14
- package/skills/3d-intro-build/references/preview-lib.mjs +51 -15
- package/skills/3d-intro-build/references/prompts.md +4 -4
- package/skills/3d-intro-build/references/serve.mjs +6 -1
- package/skills/3d-intro-build/references/verify-intro.mjs +16 -9
- package/skills/3d-intro-build/references/video-pool.mjs +250 -135
- package/skills/3d-intro-setup/SKILL.md +41 -10
- package/skills/3d-intro-setup/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-setup/references/video-pool.mjs +250 -135
- package/skills/anti-drm/SKILL.md +6 -3
- package/skills/anti-drm/scripts/anti-drm.mjs +49 -12
- package/skills/anti-drm/scripts/instruction-toggle.mjs +105 -20
- package/skills/audit-mock/SKILL.md +1 -1
- package/skills/audit-security/SKILL.md +64 -2
- package/skills/deep-research/SKILL.md +5 -0
- package/skills/docs-setup/SKILL.md +37 -7
- package/skills/lineage/SKILL.md +12 -12
- package/skills/lineage/lineage.py +399 -300
- package/skills/motion-graphic-make/SKILL.md +1 -1
- package/skills/motion-graphic-make/references/curate.mjs +48 -14
- package/skills/motion-graphic-make/references/preview-lib.mjs +51 -15
- package/skills/motion-graphic-make/references/serve.mjs +6 -1
- package/skills/motion-graphic-setup/SKILL.md +58 -13
- package/skills/obsidizer/SKILL.md +1 -1
- package/skills/obsidizer/obsidize.mjs +8 -1
- package/skills/omc-patch/SKILL.md +80 -23
- package/skills/omc-patch/scripts/omc-patch.mjs +460 -116
- package/skills/payload-mon/SKILL.md +4 -2
- package/skills/payload-mon/scripts/payload-mon.mjs +88 -12
- package/skills/payload-mon/scripts/payload-size.mjs +4 -1
- package/skills/pdf-vision-extract/SKILL.md +11 -6
- package/skills/ralph-qa/SKILL.md +125 -151
- package/skills/ralph-qa/references/gemini-seat.mjs +67 -19
- package/skills/ralph-qa/references/payload-scan.mjs +22 -7
- package/skills/ralph-qa/references/seat-payload.mjs +646 -0
- package/skills/ralph-qa/references/seat-run.mjs +956 -0
- package/skills/ralph-qa/references/seat-spawn.mjs +205 -0
- package/skills/ralph-qa/references/verifier-probe.mjs +57 -16
- package/skills/setup-harness-factory/SKILL.md +17 -1
- package/skills/setup-insane-search/SKILL.md +16 -0
- package/skills/setup-java/SKILL.md +20 -1
- package/skills/setup-lsp/SKILL.md +56 -13
- package/skills/setup-mcp/SKILL.md +73 -4
- package/skills/setup-node/SKILL.md +31 -6
- package/skills/setup-omc/SKILL.md +24 -0
- package/skills/setup-omc-hud/SKILL.md +9 -6
- package/skills/setup-omc-hud/assets/apply-hud.mjs +242 -27
- package/skills/setup-omc-hud/assets/omc-hud-custom.mjs +81 -14
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +5 -3
- package/skills/setup-playwright/SKILL.md +45 -2
- package/skills/setup-pwsh/SKILL.md +19 -0
- package/skills/setup-python/SKILL.md +45 -7
- package/skills/setup-sandbox/SKILL.md +30 -5
- package/skills/setup-stitch/SKILL.md +22 -0
- package/skills/setup-tmux/SKILL.md +24 -4
- package/skills/tone-compact/SKILL.md +2 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +105 -20
- package/skills/ultra-chain/SKILL.md +4 -3
- package/skills/ultra-chain/references/preview-lib.mjs +51 -15
- package/skills/ultra-chain/references/serve.mjs +6 -1
- package/skills/ultra-interview/SKILL.md +1 -0
- package/skills/update-banker/SKILL.md +19 -3
- package/skills/vertical-pptx/SKILL.md +6 -3
- package/skills/vertical-pptx-setup/SKILL.md +76 -20
|
@@ -27,7 +27,7 @@ The generator (`banker setup --codex`) applies the rules below for `target: both
|
|
|
27
27
|
- `visual-ralph` → OMX `$ralph`·`$imagegen`(+`omx imagegen continuation`)·Visual Verdict; Claude은 `ralph`+`visual-verdict`+Stitch/ccg imagegen.
|
|
28
28
|
- `deep-init` → 서브에이전트 Claude=OMC explore/architect/writer, Codex=OMX worker/explore(부재 시 직접 수행). 순수 fs+doc.
|
|
29
29
|
- `deep-research` → Claude 번들 워크플로/`WebSearch`, Codex OMX `autoresearch`/`best-practice-research`.
|
|
30
|
-
- `ralph-qa` → 백본은 세션 모델로 띄운 검토 에이전트 3개 이상이다(Claude=Agent 의 `Plan` 을 `model` 없이, Codex=`spawn_agent` 를 역할 없이, 저자 대화를 넘기지 않고). 두 런타임 모두 Bash 가 있어 읽기 전용은 지시로 선다. 외부 좌석은 `--codex`·`--gemini`·`--opencode` 를 줄 때만 앉고, 저자 계열은 앉지 않는다(Codex 런타임의 `--codex` 는 자기 자신이라 부적격). Codex 의 `workspace-write` 샌드박스는 HOME 쓰기를 막아 gemini, opencode 가 시작부터 실패하고, Linux 에서는 페이로드 기준 폴더(`$XDG_RUNTIME_DIR`, `~/.cache`)도 막는다. 그래서 외부 좌석 전송의 명령 전부(
|
|
30
|
+
- `ralph-qa` → 백본은 세션 모델로 띄운 검토 에이전트 3개 이상이다(Claude=Agent 의 `Plan` 을 `model` 없이, Codex=`spawn_agent` 를 역할 없이, 저자 대화를 넘기지 않고). 두 런타임 모두 Bash 가 있어 읽기 전용은 지시로 선다. 외부 좌석은 `--codex`·`--gemini`·`--opencode` 를 줄 때만 앉고, 저자 계열은 앉지 않는다(Codex 런타임의 `--codex` 는 자기 자신이라 부적격). Codex 의 `workspace-write` 샌드박스는 HOME 쓰기를 막아 gemini, opencode 가 시작부터 실패하고, Linux 에서는 페이로드 기준 폴더(`$XDG_RUNTIME_DIR`, `~/.cache`)도 막는다. 그래서 외부 좌석 전송의 실행기 명령 전부(`seat-run.mjs` 의 init, untracked, prepare, check, send, cleanup, put)를 승인을 받아 샌드박스 밖에서 실행한다.
|
|
31
31
|
- `lineage` → 기본 흐름의 검토자는 세션 모델이다(Claude=Agent 의 `Plan` 을 `model` 없이, Codex=`spawn_agent` 를 역할 없이, 저자 대화를 넘기지 않고). 띄울 수 없으면 세션이 파트를 직접 검토한다. Codex 검토자에게는 파일 읽기 도구가 없어 파트 파일을 읽는 셸 명령만 쓰게 한다. `workspace-write` 샌드박스에서는 `~/.cache` 에 쓸 수 없어 이번 실행의 검토 결정을 캐시에 쓰지 못한다(WARN). 앞선 실행이 남긴 결정은 읽는다. 읽는 기록은 Claude Code 형식뿐이라 Codex 에서는 Claude Code 기록을 내보낼 때만 쓴다. `--rulebase` 는 모델 호출 없이 양쪽 같다.
|
|
32
32
|
- `smart-compact` → Claude statusLine `context_window.used_percentage`+hook; Codex 신호 미확인 시 휴리스틱 폴백. TUI 3단(`/copy`·`/compact`·paste)은 유저.
|
|
33
33
|
- `curation` → 런타임 무관(외부 의존 0, 양쪽 동일).
|
package/commands/setup.md
CHANGED
|
@@ -10,6 +10,29 @@ banker 플러그인이 제공하는 설치 스킬들을 **multi-select**로 골
|
|
|
10
10
|
|
|
11
11
|
## 절차
|
|
12
12
|
|
|
13
|
+
0. **네트워크 확인**: 항목을 고르기 전에 원천에 닿는지 한 번 본다(curl 은 10초, npm 은 15초 안). 결과는 알림으로만 쓰고, 이것만으로 항목을 건너뛰지 않는다.
|
|
14
|
+
```bash
|
|
15
|
+
[ "${CODEX_SANDBOX_NETWORK_DISABLED:-}" = 1 ] && echo "Codex sandbox 가 네트워크를 막음(아래 실패는 오프라인이 아님)"
|
|
16
|
+
in15() { "$@" & p=$!; ( sleep 15 && kill "$p" ) > /dev/null 2>&1 & k=$!; wait "$p" 2>/dev/null; r=$?; kill "$k" 2>/dev/null; wait "$k" 2>/dev/null; [ "$r" = 143 ] && echo "15초 안에 끝나지 않아 멈춤(패킷을 버리는 망일 수 있음)"; return "$r"; }
|
|
17
|
+
if command -v npm >/dev/null; then in15 sh -c 'cd ~ && exec npm ping --fetch-retries=0 --fetch-timeout=10000 --no-update-notifier'; else echo "npm 없음(확인하지 못함)"; fi
|
|
18
|
+
curl --ssl-no-revoke -sSI -o /dev/null --connect-timeout 5 --max-time 10 https://github.com
|
|
19
|
+
```
|
|
20
|
+
- `curl -sSI` 는 머리만 받는다. 확인 줄은 `--ssl-no-revoke` 를 준다. Windows 의 Git Bash curl 도 Schannel 이라, 폐지 목록 서버에 닿지 못하는 망에서 폐지 확인이 실패하면 종료 60 이다(Windows 박스 실측). 다른 curl 은 이 깃발을 받고 무시한다(macOS curl 8.7.1, Rocky 8.10 curl 7.61.1 실측).
|
|
21
|
+
- 둘 다 실패하면 "네트워크가 없거나 막힌 것으로 보임. 항목마다 자기 원천을 다시 확인하고, 닿지 않는 원천에서 받는 단계는 하지 않음" 으로 먼저 알린다.
|
|
22
|
+
- 항목을 건너뛸지는 각 스킬의 0단계가 그 스킬의 원천으로 정한다. 사내 미러만 열린 망(`npm ping` 에 답하지 않는 npm 미러, PyPI 미러)에서도 그 원천을 쓰는 항목은 진행된다. 닿지 않는 원천에서 받는 단계만 "오프라인이라 설치하지 않음(필요한 것: <항목>)" 으로 보고된다.
|
|
23
|
+
- `npm` 이 아직 없으면(새 PC) npm 쪽은 오프라인이 아니라 확인하지 못한 것이다. `setup-node` 가 npm 을 깐 뒤 각 스킬의 0단계가 다시 확인한다.
|
|
24
|
+
- `npm ping` 은 홈 폴더에서 부른다. 지금 폴더에 있는 프로젝트의 `.npmrc` 가 정한 레지스트리와 토큰을 따르지 않게 하려는 것이다.
|
|
25
|
+
- `npm ping` 은 15초 감시(`in15`) 안에서 부른다. 패킷을 버리는 망에서 npm 은 연결에 시간 제한을 두지 않아(`--fetch-timeout` 은 받는 중의 쉼만 잰다) OS 의 연결 재시도가 끝날 때까지 기다린다(Rocky 8.10 실측 130초). 15초 안에 끝나지 않으면 `15초 안에 끝나지 않아 멈춤(패킷을 버리는 망일 수 있음)` 과 종료 143 이다. `--no-update-notifier` 는 npm 이 제 판을 묻는 요청을 하나 더 내지 않게 한다.
|
|
26
|
+
- OS 패키지 관리자(dnf, apt, brew, winget)만 쓰는 단계는 자기 설정의 미러를 쓰므로 이 확인과 관계없이 각 스킬의 규칙대로 시도한다.
|
|
27
|
+
- 패킷을 버리거나 답하지 않는 망에서 dnf 는 저장소마다 약 2분(SYN 을 버리는 망 실측 121초), apt 는 원천마다 약 4분(답하지 않는 원천 실측 247초) 기다린 뒤 실패로 끝난다. 시간 제한은 관리자 설정을 따르므로 이 스킬이 바꾸지 않는다.
|
|
28
|
+
- `apt-get update` 는 받지 못해도 종료 0 이다. 출력의 `W: Failed to fetch` 가 있으면 그 단계를 실패로 보고한다.
|
|
29
|
+
- 내려받기가 없는 항목(`setup-omc-hud`)과 이미 설치된 것의 확인은 그대로 진행한다.
|
|
30
|
+
- Windows PowerShell 에서는 `curl.exe --ssl-no-revoke -sSI -o NUL --connect-timeout 5 --max-time 10 https://github.com` 의 `$LASTEXITCODE` 로 본다. `curl.exe` 는 Windows 시스템 프록시를 따르지 않고 `HTTPS_PROXY` 만 따른다. `--ssl-no-revoke` 는 폐지 목록 서버에 닿지 못하는 망에서 Schannel 의 폐지 확인 실패를 오프라인으로 읽지 않게 한다. 닿는지만 보고 받은 내용은 쓰지 않으므로 이 확인에만 준다. `curl.exe` 가 없으면 `$LASTEXITCODE` 에 앞 명령의 값이 남으므로 `Get-Command curl.exe -ErrorAction SilentlyContinue` 로 먼저 보고, 없으면 확인하지 못한 것으로 적는다. `npm ping` 은 `Push-Location ~; npm ping --fetch-retries=0 --fetch-timeout=10000 --no-update-notifier; Pop-Location` 처럼 홈에서 부르고 돌아온다(돌아오지 않으면 뒤 단계가 프로젝트가 아니라 홈에서 돈다). PowerShell 에는 15초 감시를 두지 않는다(Windows 의 연결 시간 제한은 약 21초로 알려져 있다. 미실측).
|
|
31
|
+
- Codex sandbox 가 네트워크를 막는 모드(기본 `workspace-write`, `read-only`)에서는 `CODEX_SANDBOX_NETWORK_DISABLED=1` 이 서고 확인이 모두 실패한다(curl 종료 7, npm `EPERM`). 블록의 첫 줄이 그렇다고 알린다. 그때는 '오프라인' 이 아니라 'sandbox 가 네트워크를 막음' 으로 보고하고, 승인을 받아 sandbox 밖에서 확인부터 다시 한다. Claude Code sandbox 안(`SANDBOX_RUNTIME=1`)에서는 허용 목록 밖 주소를 sandbox 프록시가 막는다. Windows PowerShell 에서는 `$env:CODEX_SANDBOX_NETWORK_DISABLED -eq '1'` 로 본다.
|
|
32
|
+
- 확인이 실패하면 보고에 그 명령, 종료 코드, stderr 첫 줄을 붙인다(예: `npm error code ETIMEDOUT`, `curl: (28) ...`). 원인을 '오프라인' 하나로 짐작하지 않는다.
|
|
33
|
+
- curl 의 종료 60(인증서 검증 실패)도 오프라인이 아니다. 망은 열려 있고, TLS 를 가로채는 사내 프록시의 인증서를 OS 가 믿지 않는 것이다. '인증서 문제로 설치하지 않음' 으로 보고한다.
|
|
34
|
+
- curl 은 닿는데 npm 만 인증서 오류(`UNABLE_TO_VERIFY_LEAF_SIGNATURE`, `SELF_SIGNED_CERT_IN_CHAIN`, `UNABLE_TO_GET_ISSUER_CERT_LOCALLY`)로 실패하면 망은 열려 있고 npm 이 사내 인증서를 믿지 않는 것이다(npm 은 OS 인증서 저장소를 쓰지 않는다). '인증서 문제로 설치하지 않음' 으로 보고하고 `NODE_EXTRA_CA_CERTS=<사내 CA 파일>` 을 안내한다. Node 24 는 `NODE_OPTIONS=--use-system-ca` 도 된다.
|
|
35
|
+
- 이 실행에서 부르는 스킬의 0단계는 /banker:setup 0단계와 앞 스킬이 이미 돌린 같은 확인 줄(같은 명령과 같은 주소)의 결과를 쓰고 다시 돌리지 않는다. 처음 보는 원천만 새로 본다. 결과는 이 실행 안에서만 쓴다(그 사이 망이 바뀌면 옛 결과일 수 있다). 막힌 망에서 항목마다 같은 확인을 다시 기다리지 않게 하려는 것이다.
|
|
13
36
|
1. **선택 받기**: `$ARGUMENTS` 에 컴포넌트명이 명시돼 있으면 그것만 실행(질문 생략).
|
|
14
37
|
비어 있으면 아래를 `AskUserQuestion`(**multiSelect: true**)으로 제시한다(**Codex 런타임엔 AskUserQuestion 이 없으므로 목록을 제시하고 설치할 항목을 사용자에게 물어 받는다**). 항목은 **계층/의존 순서**로 묶여 있으니, 뒤 계층을 고르면 앞 계층 전제를 먼저 채운다:
|
|
15
38
|
- 질문 하나에 선택지는 4개까지다. 5개 이상인 묶음은 두 질문으로 나눈다.
|
package/hooks/progress.mjs
CHANGED
|
@@ -122,11 +122,14 @@ const looseStep = () => ({ key: 'loose', title: '작업 목록 밖의 작업', .
|
|
|
122
122
|
// list last came from.
|
|
123
123
|
// `plan` is the plan last read from `planPath`, `planShut` whether the whole plan is folded and
|
|
124
124
|
// `planOpen` the ids of its nodes unfolded. `touch` counts the calls that touched a plan file and
|
|
125
|
-
// `taken` is the touch
|
|
125
|
+
// `taken` is the newest touch whose read was taken in, to tell a read that a newer one overtook (a weak plan's touch does
|
|
126
|
+
// not count). `shown` is the touch, or the read again's count of touches, that the words on the pane came from: a later
|
|
127
|
+
// read of the file shown that began before them does not take their place; `shownOrder` is the order a read again of
|
|
128
|
+
// theirs began in (0 for a touch's read), so of two begun after the same touches the later one stays. `followed` is the step in
|
|
126
129
|
// progress the pane last asked to bring into view.
|
|
127
130
|
export const freshState = () => ({ turns: [], tasks: new Map(), todos: null, loose: looseStep(), open: new Set(), selected: null, seq: 0, rev: 0,
|
|
128
131
|
order: 0, marks: new Map(), listAt: 0, plan: null, planPath: null, planShut: false, planOpen: new Set(), touch: 0, taken: 0,
|
|
129
|
-
followed: null });
|
|
132
|
+
shown: 0, shownOrder: 0, followed: null, reads: [], base: null, folded: 0 });
|
|
130
133
|
|
|
131
134
|
const tail = (path) => String(path).split(/[\\/]/).filter(Boolean).slice(-2).join('/');
|
|
132
135
|
const seconds = (ms) => {
|
|
@@ -262,33 +265,51 @@ function stamp(t, status, now) {
|
|
|
262
265
|
|
|
263
266
|
// A list longer than the pane holds loses its oldest finished item first, else its oldest.
|
|
264
267
|
const overflow = (items) => items.find((t) => t.status === 'completed') ?? items[0];
|
|
268
|
+
// A list cut to `max` items as overflow would cut it one by one, in one pass: the oldest finished ones go first, then
|
|
269
|
+
// the oldest.
|
|
270
|
+
function trimList(items, max) {
|
|
271
|
+
let extra = items.length - max;
|
|
272
|
+
if (extra <= 0) return items;
|
|
273
|
+
const left = items.filter((t) => t.status !== 'completed' || extra-- <= 0);
|
|
274
|
+
return left.slice(left.length - max);
|
|
275
|
+
}
|
|
265
276
|
|
|
266
277
|
// One item of a TodoWrite list: while its text stays it is the same step (the first unused one of
|
|
267
278
|
// that text), with its key, calls and times.
|
|
268
279
|
function todoItem(s, before, d, now) {
|
|
269
|
-
const
|
|
280
|
+
const words = kept(d.content);
|
|
281
|
+
const t = before.get(words)?.shift() ?? { key: `d-${++s.seq}`, description: words, ...tally() };
|
|
270
282
|
t.title = clip(d.content, 60);
|
|
271
|
-
t.activeForm = d.activeForm;
|
|
283
|
+
t.activeForm = kept(d.activeForm);
|
|
272
284
|
stamp(t, d.status, now);
|
|
273
285
|
return t;
|
|
274
286
|
}
|
|
275
287
|
|
|
288
|
+
// A task's or an item's words as the state keeps them: a string cut to what any description reads of it (clip reads
|
|
289
|
+
// 4n + 1024 characters), anything else as it came.
|
|
290
|
+
const MAX_WORDS = 4096;
|
|
291
|
+
const kept = (v) => (typeof v === 'string' ? v.slice(0, MAX_WORDS) : v);
|
|
292
|
+
|
|
276
293
|
// TodoWrite sends the whole list each time. A subagent keeps a list of its own, not the session's.
|
|
277
294
|
function writeTodos(s, e, now) {
|
|
278
295
|
if (e.agentId || !Array.isArray(e.todos)) return;
|
|
279
296
|
const before = new Map();
|
|
280
297
|
for (const t of s.todos ?? []) before.set(t.description, [...(before.get(t.description) ?? []), t]);
|
|
281
|
-
s.todos = e.todos.map((d) => todoItem(s, before, d, now));
|
|
282
|
-
while (s.todos.length > MAX_STEPS) s.todos.splice(s.todos.indexOf(overflow(s.todos)), 1);
|
|
298
|
+
s.todos = trimList(e.todos.map((d) => todoItem(s, before, d, now)), MAX_STEPS);
|
|
283
299
|
}
|
|
284
300
|
|
|
301
|
+
// A task's key on the pane: `k-` and its id where the id is a plain one (the store hands out numbers), else its first
|
|
302
|
+
// 1,024 characters as code points, so ids that differ there never share a key (longer ones alike that far do) and none
|
|
303
|
+
// holds what the engine drops a Box key for (a control character, more than 10,000 characters).
|
|
304
|
+
const taskKey = (id) => (/^[\w.-]{1,64}$/.test(id) ? `k-${id}` : `k~${[...id.slice(0, 1024)].map((c) => c.codePointAt(0).toString(36)).join('.')}`);
|
|
305
|
+
|
|
285
306
|
// Adds a task or brings it up to date; a task keeps its key, calls and times. One known only by
|
|
286
307
|
// its id is named by it until a TaskGet or a TaskList says more.
|
|
287
308
|
function putTask(s, t, now) {
|
|
288
|
-
const held = s.tasks.get(t.id) ?? { key:
|
|
309
|
+
const held = s.tasks.get(t.id) ?? { key: taskKey(t.id), id: t.id, title: clip(`작업 #${t.id}`, 60), description: '', ...tally() };
|
|
289
310
|
const title = clip(t.subject, 60);
|
|
290
311
|
if (title) held.title = title;
|
|
291
|
-
for (const k of ['description', 'activeForm']) if (t[k] !== undefined) held[k] = t[k];
|
|
312
|
+
for (const k of ['description', 'activeForm']) if (t[k] !== undefined) held[k] = kept(t[k]);
|
|
292
313
|
if (t.status !== undefined) stamp(held, t.status, now);
|
|
293
314
|
s.tasks.set(t.id, held);
|
|
294
315
|
while (s.tasks.size > MAX_STEPS) s.tasks.delete(overflow([...s.tasks.values()]).id);
|
|
@@ -297,15 +318,29 @@ function putTask(s, t, now) {
|
|
|
297
318
|
// The marks of a task id, made on first use. They live in the state, not on the task, so a task deleted, or not
|
|
298
319
|
// created yet, keeps them: `changedRev` the change count after its last TaskCreate or TaskUpdate (a delete
|
|
299
320
|
// included), `statusAt` and `status` the order and the status of the last status update begun, `seenAt` the order
|
|
300
|
-
// of the last read taken in.
|
|
321
|
+
// of the last read taken in. Past MAX_MARKS the oldest marks of tasks off the pane go (trimMarks), so reads of ids the
|
|
322
|
+
// store does not have cannot grow them without end.
|
|
323
|
+
const MAX_MARKS = 1000;
|
|
324
|
+
// A task id longer than this is no id the store hands out: a read that misses it leaves no mark.
|
|
325
|
+
const MAX_ID = 64;
|
|
301
326
|
function markOf(s, id) {
|
|
302
327
|
if (!s.marks.has(id)) s.marks.set(id, {});
|
|
303
328
|
return s.marks.get(id);
|
|
304
329
|
}
|
|
305
330
|
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
331
|
+
// Drops the oldest marks of tasks off the pane down to MAX_MARKS. It runs once a call is taken in, when every task the
|
|
332
|
+
// pane holds is in place again: a TaskList fills a new list, and a task not moved yet must not count as off the pane.
|
|
333
|
+
function trimMarks(s) {
|
|
334
|
+
for (const k of s.marks.keys()) {
|
|
335
|
+
if (s.marks.size <= MAX_MARKS) return;
|
|
336
|
+
if (!s.tasks.has(k)) s.marks.delete(k);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Whether a list or a read begun at (since, order) is older than what the pane holds of a task: a list begun after this
|
|
341
|
+
// one began was already taken in (a list names the whole list, so this holds for a task it left out that the pane never
|
|
342
|
+
// held, too), the task changed after this one began (a delete too), or a later-begun read already brought it. The first
|
|
343
|
+
// takes the order calls began in for the order they read the list in, which holds where a call reads soon after it begins.
|
|
309
344
|
function stale(s, id, since, order) {
|
|
310
345
|
if (order < s.listAt) return true;
|
|
311
346
|
const m = s.marks.get(id);
|
|
@@ -389,10 +424,7 @@ function listTasks(s, _e, r, now, { since, order }) {
|
|
|
389
424
|
function getTask(s, e, r, now, { since, order }) {
|
|
390
425
|
if (stale(s, String(e.taskId), since, order)) return;
|
|
391
426
|
const t = r?.result?.task;
|
|
392
|
-
if (t === null)
|
|
393
|
-
s.tasks.delete(String(e.taskId));
|
|
394
|
-
markOf(s, String(e.taskId)).seenAt = order;
|
|
395
|
-
}
|
|
427
|
+
if (t === null) noTask(s, String(e.taskId), order);
|
|
396
428
|
if (!t) return;
|
|
397
429
|
const id = String(t.id);
|
|
398
430
|
const status = readStatus(s, id, order, t.status);
|
|
@@ -401,6 +433,13 @@ function getTask(s, e, r, now, { since, order }) {
|
|
|
401
433
|
markOf(s, id).seenAt = order;
|
|
402
434
|
}
|
|
403
435
|
|
|
436
|
+
// The store has no such task: it leaves the pane and the read is marked, unless the id is longer than any the store
|
|
437
|
+
// hands out.
|
|
438
|
+
function noTask(s, id, order) {
|
|
439
|
+
s.tasks.delete(id);
|
|
440
|
+
if (id.length <= MAX_ID) markOf(s, id).seenAt = order;
|
|
441
|
+
}
|
|
442
|
+
|
|
404
443
|
const TASK_NOTES = {
|
|
405
444
|
TaskCreate: createTask,
|
|
406
445
|
TaskUpdate: updateTask,
|
|
@@ -422,10 +461,14 @@ const changedId = (e, r) => (e.tool === 'TaskCreate' ? r?.result?.task?.id : e.t
|
|
|
422
461
|
export function noteTaskTool(s, e, r, now, since = s.rev, order = ++s.order) {
|
|
423
462
|
if (refused(r)) return;
|
|
424
463
|
TASK_NOTES[e.tool]?.(s, e, r, now, { since, order });
|
|
425
|
-
if (
|
|
464
|
+
if (CHANGES.has(e.tool)) noteChange(s, e, r);
|
|
465
|
+
trimMarks(s);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Only a change the pane took in marks the task: one it holds after the change, or a delete. An update of a task the
|
|
469
|
+
// pane does not hold leaves the lists alone (its status still counts, through readStatus).
|
|
470
|
+
function noteChange(s, e, r) {
|
|
426
471
|
s.rev += 1;
|
|
427
|
-
// Only a change the pane took in marks the task: one it holds after the change, or a delete. An update of a task the
|
|
428
|
-
// pane does not hold leaves the lists alone (its status still counts, through readStatus).
|
|
429
472
|
const id = changedId(e, r);
|
|
430
473
|
if (id !== undefined && (s.tasks.has(String(id)) || updatedStatus(e, r) === 'deleted')) markOf(s, String(id)).changedRev = s.rev;
|
|
431
474
|
}
|
|
@@ -477,11 +520,25 @@ function parsed(path, text) {
|
|
|
477
520
|
}
|
|
478
521
|
|
|
479
522
|
// Whether a read may change the plan: a read for a touch newer than the one the plan came from, or a
|
|
480
|
-
// read again of the plan's own file (begun when `since` touches were counted)
|
|
481
|
-
// in
|
|
482
|
-
function current(s, path, { touch, since }) {
|
|
523
|
+
// read again of the plan's own file (begun when `since` touches were counted) begun after the words on the pane
|
|
524
|
+
// came in (`shown`), so neither an older touch nor an older read again takes their place.
|
|
525
|
+
function current(s, path, { touch, since, order = 0 }) {
|
|
483
526
|
if (touch !== undefined) return touch > s.taken;
|
|
484
|
-
return path === s.planPath && s.
|
|
527
|
+
return path === s.planPath && (s.shown < since || (s.shown === since && order >= (s.shownOrder ?? 0)));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// What a read taken in leaves, and whether its words go on the pane. `taken` makes older touches late (a weak plan's
|
|
531
|
+
// touch does not), `shown` is the touch or the read again's since the words came from: a touch's read of the file
|
|
532
|
+
// shown, begun before the read whose words are shown, leaves them.
|
|
533
|
+
function noteTouch(s, read, moving) {
|
|
534
|
+
if (read.touch === undefined) {
|
|
535
|
+
[s.shown, s.shownOrder] = [read.since, read.order ?? 0];
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
if (!read.weak) s.taken = read.touch;
|
|
539
|
+
if (!moving && read.touch <= s.shown) return false;
|
|
540
|
+
[s.shown, s.shownOrder] = [read.touch, 0];
|
|
541
|
+
return true;
|
|
485
542
|
}
|
|
486
543
|
|
|
487
544
|
// A file with no steps does not push out a plan that has some, unless the session wrote it itself; a weak plan holds no
|
|
@@ -505,20 +562,60 @@ function treeIds(nodes, out = new Set()) {
|
|
|
505
562
|
return out;
|
|
506
563
|
}
|
|
507
564
|
|
|
565
|
+
// The plan as reads leave it: the plan, its path, `taken` and `shown` (see freshState), with nothing of the pane.
|
|
566
|
+
const planOf = (s) => ({ plan: s.plan, planPath: s.planPath, taken: s.taken, shown: s.shown, shownOrder: s.shownOrder });
|
|
567
|
+
|
|
568
|
+
// What one read leaves of a plan state, as takePlan always did when reads landed in the order their touches came.
|
|
569
|
+
function applyRead(st, e) {
|
|
570
|
+
if (!current(st, e.path, e.read) || keepsOff(st, e.path, e.plan, e.read)) return st;
|
|
571
|
+
const next = { ...st };
|
|
572
|
+
if (!noteTouch(next, e.read, e.path !== st.planPath)) return next;
|
|
573
|
+
return { ...next, planPath: e.path, plan: { ...e.plan, path: e.path } };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Where a read goes among the touches: a touch's read at its touch, a read again just after the touches counted when
|
|
577
|
+
// it began.
|
|
578
|
+
const placeOf = (read) => (read.touch !== undefined ? read.touch : read.since + 0.5);
|
|
579
|
+
// Reads kept past this wait for a touch whose read never came: that touch then counts as a read of nothing. Each
|
|
580
|
+
// kept read holds a parsed plan, so the bound keeps the worst at about 8 MB.
|
|
581
|
+
const MAX_READS = 16;
|
|
582
|
+
|
|
583
|
+
// Folds into the base the reads whose earlier touches have all landed, so the base is the plan of those touches in their
|
|
584
|
+
// order; a read again goes in once the touches it began after are in.
|
|
585
|
+
function foldReads(s) {
|
|
586
|
+
if (s.reads.length > MAX_READS) s.folded = Math.max(s.folded, Math.ceil(placeOf(s.reads[0].read)) - 1);
|
|
587
|
+
while (s.reads.length) {
|
|
588
|
+
const { read } = s.reads[0];
|
|
589
|
+
const ready = read.touch !== undefined ? read.touch === s.folded + 1 : read.since <= s.folded;
|
|
590
|
+
if (!ready) return;
|
|
591
|
+
s.base = applyRead(s.base, s.reads.shift());
|
|
592
|
+
if (read.touch !== undefined) s.folded = read.touch;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
508
596
|
// What a read of a plan file found (`read` is `{ touch }` for a touch's read, `{ since }` for a read
|
|
509
|
-
// again).
|
|
510
|
-
//
|
|
597
|
+
// again). Reads land in any order: each is kept in its touch's place, and the pane shows the plan the touches
|
|
598
|
+
// would leave in their order, as far as their reads have landed. A file that cannot be read or is no plan leaves
|
|
599
|
+
// the plan as it was. Another plan starts folded; the same plan read again keeps the nodes unfolded that it still holds.
|
|
511
600
|
export function takePlan(s, path, text, read = {}) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
601
|
+
s.base ??= planOf(s);
|
|
602
|
+
if (read.touch !== undefined && read.touch <= s.folded) return; // its place went by as a read of nothing (MAX_READS)
|
|
603
|
+
const entry = { path, plan: parsed(path, text), read };
|
|
604
|
+
const at = s.reads.findIndex((r) => placeOf(r.read) > placeOf(read));
|
|
605
|
+
s.reads.splice(at < 0 ? s.reads.length : at, 0, entry);
|
|
606
|
+
foldReads(s);
|
|
607
|
+
showPlan(s, s.reads.reduce(applyRead, s.base));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Puts a plan state on the pane: another plan starts folded, and the same plan keeps the nodes unfolded that it holds.
|
|
611
|
+
function showPlan(s, st) {
|
|
612
|
+
const moving = st.planPath !== s.planPath;
|
|
517
613
|
if (moving && s.selected === 'plan') s.selected = null;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
614
|
+
if (st.plan && st.plan !== s.plan) {
|
|
615
|
+
const ids = treeIds(st.plan.tree);
|
|
616
|
+
s.planOpen = moving ? new Set() : new Set([...s.planOpen].filter((id) => ids.has(id)));
|
|
617
|
+
}
|
|
618
|
+
Object.assign(s, st);
|
|
522
619
|
}
|
|
523
620
|
|
|
524
621
|
// ── what the pane shows ──────────────────────────────────────────────────────
|
|
@@ -551,9 +648,12 @@ function stepState(step, now) {
|
|
|
551
648
|
const status = statusOf(step);
|
|
552
649
|
const doing = status === 'in_progress' && step.activeForm ? `(${clip(step.activeForm, 40)})` : '';
|
|
553
650
|
const time = Number.isFinite(step.startedAt) ? `, ${seconds((step.endedAt ?? now) - step.startedAt)}` : '';
|
|
554
|
-
return `${
|
|
651
|
+
return `${stateName(status)}${doing}${time}`;
|
|
555
652
|
}
|
|
556
653
|
|
|
654
|
+
// A state's word: the pane's own, else the state cleaned (a name such as `constructor` is no word of the table).
|
|
655
|
+
const stateName = (status) => pick(STATUS, status) ?? clip(status, 20);
|
|
656
|
+
|
|
557
657
|
function stepWords(step, now) {
|
|
558
658
|
if (step.key === 'idle') return '아직 시작한 작업이 없습니다. 요청을 보내면 여기에 단계가 쌓입니다.';
|
|
559
659
|
const words = step.text !== undefined ? `요청: ${clip(step.text, 160) || '(이어서 진행)'}` : clip(step.description || step.title, 160);
|
|
@@ -627,7 +727,7 @@ function stepRow(ui, s, step, act) {
|
|
|
627
727
|
? Button({ key: `arrow-${step.key}`, label: isOpen ? '▾' : '▸', plain: true, onPress: () => act.fold(step.key) })
|
|
628
728
|
: Text({ children: [' '] });
|
|
629
729
|
const status = statusOf(step);
|
|
630
|
-
const state =
|
|
730
|
+
const state = stateName(status);
|
|
631
731
|
return Box({ key: `row-${step.key}`, flexDirection: 'row', columnGap: 1, children: [
|
|
632
732
|
arrow,
|
|
633
733
|
Button({ key: step.key, label: room(step.title, act, 2, state), plain: true, onPress: () => act.select(step.key) }),
|
|
@@ -901,10 +1001,15 @@ export function progressTurnCompleted(e) {
|
|
|
901
1001
|
|
|
902
1002
|
// The plan file to read again as a turn starts and ends (register.mjs reads it), with the touches
|
|
903
1003
|
// counted when the read begins; null without a plan.
|
|
1004
|
+
// A read again begins with the touches counted and its place in the order reads begin, so of two begun after the same
|
|
1005
|
+
// touches the later one wins, whichever lands last (the file may change between them without a touch: a shell edit).
|
|
1006
|
+
export const planTarget = (s) => (s.planPath ? { path: s.planPath, since: s.touch, order: ++s.order } : null);
|
|
1007
|
+
export const planRead = (s, target, text) => takePlan(s, target.path, text, { since: target.since, order: target.order });
|
|
1008
|
+
|
|
904
1009
|
export function progressPlanTarget() {
|
|
905
|
-
return ref.s
|
|
1010
|
+
return planTarget(ref.s);
|
|
906
1011
|
}
|
|
907
1012
|
|
|
908
1013
|
export function progressPlanRead(target, text) {
|
|
909
|
-
|
|
1014
|
+
planRead(ref.s, target, text);
|
|
910
1015
|
}
|
package/hooks/register.mjs
CHANGED
|
@@ -83,6 +83,16 @@ async function voidLateRequest($, late) {
|
|
|
83
83
|
$.ui.log(late.log);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// Asks the engine to draw the pane again. The engine sends the call on and catches its own failure (2.1.305); an engine
|
|
87
|
+
// that throws instead still runs the hooks beneath, so the session and the turn go on.
|
|
88
|
+
function redraw($) {
|
|
89
|
+
try {
|
|
90
|
+
$.ui.invalidate('ui.render');
|
|
91
|
+
} catch {
|
|
92
|
+
/* nothing to redraw now; the next event draws the pane */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
86
96
|
export const register = (on) => {
|
|
87
97
|
registerGracefulPause(on);
|
|
88
98
|
registerProgress(on);
|
|
@@ -97,7 +107,7 @@ export const register = (on) => {
|
|
|
97
107
|
on('session.end', async ($, e, next) => {
|
|
98
108
|
pauseReset();
|
|
99
109
|
progressReset();
|
|
100
|
-
|
|
110
|
+
redraw($); // the pane shows at once what /clear emptied
|
|
101
111
|
return next(e);
|
|
102
112
|
});
|
|
103
113
|
|
|
@@ -105,7 +115,7 @@ export const register = (on) => {
|
|
|
105
115
|
pauseTurnStarted(e);
|
|
106
116
|
progressTurnStarted(e);
|
|
107
117
|
await rereadPlan($);
|
|
108
|
-
|
|
118
|
+
redraw($);
|
|
109
119
|
return next(e);
|
|
110
120
|
});
|
|
111
121
|
|
|
@@ -114,7 +124,7 @@ export const register = (on) => {
|
|
|
114
124
|
if (late) await voidLateRequest($, late);
|
|
115
125
|
progressTurnCompleted(e);
|
|
116
126
|
await rereadPlan($);
|
|
117
|
-
|
|
127
|
+
redraw($);
|
|
118
128
|
return next(e);
|
|
119
129
|
});
|
|
120
130
|
};
|
package/hooks/update-checkin.mjs
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* - 미활성(countingActive) 또는 엔드포인트 없음이면 무동작(로그도 건드리지 않는다).
|
|
10
10
|
* - PII/IP/지속ID 절대 미포함. 페이로드는 {version, os, counts} 세 키뿐(os=플랫폼 계열,
|
|
11
11
|
* counts=스킬별 x 시간대별 호출수).
|
|
12
|
-
* -
|
|
12
|
+
* - 무활동 10초 timeout 과 요청마다 30초 교환 전체 마감으로 hang 하지 않는다.
|
|
13
|
+
* - 응답 본문은 256KiB 까지만 모은다. 넘으면 연결을 끊고 그 응답을 쓰지 않는다.
|
|
14
|
+
* - 응답에서 latest 를 얻지 못하면(전송 실패, 차단, 없는 값) 시도 시각(triedAt)만 캐시에 써 하루를 쉰다.
|
|
15
|
+
* - 어떤 오류도 삼키고 항상 exit 0. 어떤 경로도 reject 안 함.
|
|
13
16
|
* - 비-loopback 평문 http 는 전송하지 않는다(https-only). loopback http 만 로컬 테스트 mock 에 허용.
|
|
14
17
|
* - 함께 시작한 체크인은 같은 줄을 두 번 보내지 않는다(로그 이름 바꾸기는 하나만 성공한다).
|
|
15
18
|
* - 로그 자리가 FIFO 면 기다리지 않고, 로그나 last-flush 자리가 링크면 링크 너머 파일을 비우거나 덮지 않는다.
|
|
@@ -73,7 +76,8 @@ function isLoopback(hostname) {
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
// 엔드포인트로 JSON POST. 어떤 경로에서도 reject 하지 않고, 전송 시도가 끝나면 응답 본문(문자열)
|
|
76
|
-
// 또는 null 로 resolve 한다.
|
|
79
|
+
// 또는 null 로 resolve 한다. 무활동 timeout(NET_TIMEOUT_MS)과 교환 전체 마감(NET_DEADLINE_MS)으로 무응답
|
|
80
|
+
// 엔드포인트에도 hang 하지 않고, 본문은 MAX_BODY_BYTES 까지만 모은다.
|
|
77
81
|
function post(url, body) {
|
|
78
82
|
return new Promise((resolvePromise) => {
|
|
79
83
|
let done = false;
|
|
@@ -124,8 +128,9 @@ function extractLatest(responseBody) {
|
|
|
124
128
|
|
|
125
129
|
// 변경-스킬 매니페스트(공개 GitHub raw JSON: {"x.y.z": ["banker:skill", ...]})를 GET 해 객체로 반환한다.
|
|
126
130
|
// update-fetch 의 동일 함수와 self-contained 로 나란히 둔다(훅별 자기완결 관례·isLoopback 도 이미 각자 보유).
|
|
127
|
-
// post() 와 동일한 안전 가드: 비-https 는 loopback 만 허용·3xx no-op
|
|
128
|
-
// 실패에도 reject 하지 않고 null 로 resolve 한다(개인화는 best-effort).
|
|
131
|
+
// post() 와 동일한 안전 가드: 비-https 는 loopback 만 허용·3xx no-op·무활동 timeout 과 교환 전체 마감·
|
|
132
|
+
// 본문 256KiB 상한·malformed→null. 어떤 실패에도 reject 하지 않고 null 로 resolve 한다(개인화는 best-effort).
|
|
133
|
+
// 비배열 객체만 반환.
|
|
129
134
|
function fetchSkillChangesMap(url) {
|
|
130
135
|
return new Promise((resolvePromise) => {
|
|
131
136
|
let done = false;
|
|
@@ -228,17 +233,20 @@ function dropOldClaims(now = Date.now()) {
|
|
|
228
233
|
}
|
|
229
234
|
}
|
|
230
235
|
|
|
231
|
-
// 응답의 latest 로 공유 update-check 캐시를 갱신(알림 겸함). 없으면
|
|
236
|
+
// 응답의 latest 로 공유 update-check 캐시를 갱신(알림 겸함). 없으면 시도 시각(triedAt)만 쓴다.
|
|
232
237
|
// update-fetch 처럼 notified:null 재무장을 하지 않아도 등가다: dedupe 판정이 notified===latest
|
|
233
238
|
// (update-notify)라 전진 버전에서 구 notified 는 항상 신 latest 와 달라 재고지가 정상 발화하고,
|
|
234
239
|
// count-default-on 은 이 훅만·npm 배포는 update-fetch 만 써 같은 캐시를 겹쳐 쓰지 않는다.
|
|
235
240
|
async function refreshUpdateCache(responseBody) {
|
|
236
241
|
const latest = extractLatest(responseBody);
|
|
237
|
-
if (!latest)
|
|
242
|
+
if (!latest) { // 시도 시각만 남겨 update-notify 가 하루 동안 다시 띄우지 않게 한다(네트워크 없는 곳의 세션마다 시도).
|
|
243
|
+
try { writeUpdateCache({ triedAt: Date.now() }); } catch { /* best-effort */ }
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
238
246
|
// 개인화용: 변경-스킬 매니페스트를 best-effort GET(실패 시 changedSkills 생략, 기존 값 보존).
|
|
239
247
|
const changesUrl = process.env.BANKER_SKILL_CHANGES_URL || SKILL_CHANGES_URL; // 테스트 주입용 override.
|
|
240
248
|
const map = await fetchSkillChangesMap(changesUrl);
|
|
241
|
-
const patch = { latest, checkedAt: Date.now() };
|
|
249
|
+
const patch = { latest, checkedAt: Date.now(), triedAt: null }; // 성공: 남은 실패 시도 시각은 비움
|
|
242
250
|
if (map) patch.changedSkills = changedSkillsBetween(map, installedVersion(), latest);
|
|
243
251
|
try { writeUpdateCache(patch); } catch { /* best-effort */ }
|
|
244
252
|
}
|
package/hooks/update-fetch.mjs
CHANGED
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
* 자체는 캐시만 읽는 별도 훅이 담당한다(이 파일은 네트워크 조회만 - 화면 출력 없음).
|
|
6
6
|
*
|
|
7
7
|
* fail-closed 규약(각 항목은 update-fetch.test.mjs 가 검증한다):
|
|
8
|
-
* -
|
|
8
|
+
* - 마지막 성공(checkedAt)이나 마지막 실패한 시도(triedAt)에서 하루가 지나지 않았으면 무동작(updateCheckDue).
|
|
9
9
|
* - 비-https 는 loopback(localhost/127.0.0.1/::1) 에서만 http 허용, 그 외 비-https 는 거부.
|
|
10
10
|
* - 3xx 리다이렉트는 따라가지 않고 no-op.
|
|
11
|
-
* -
|
|
11
|
+
* - 무활동 10초 timeout 과 요청마다 30초 교환 전체 마감으로 hang 하지 않는다.
|
|
12
|
+
* - 응답 본문은 256KiB 까지만 모은다. 넘으면 연결을 끊고 그 응답을 쓰지 않는다.
|
|
13
|
+
* - 응답 JSON 에서 version 문자열만 방어 추출. 버전을 얻지 못하면(조회 실패, 거부, 없거나 비문자열) 시도 시각
|
|
14
|
+
* (triedAt)만 써서 하루를 쉰다. latest 와 checkedAt 은 쓰지 않는다.
|
|
12
15
|
* - 캐시는 writeUpdateCache 로 병합 기록: fetch 된 latest 가 기존 캐시와 다르면 notified 를 초기화
|
|
13
16
|
* (재무장)하고, 같으면 형제 필드(notified 등)를 patch 에서 생략해 그대로 보존한다.
|
|
14
17
|
* - 어떤 오류도 삼키고 항상 exit 0. GET 뿐이라 페이로드/식별자 전송이 없다.
|
|
@@ -19,7 +22,7 @@ import http from 'node:http';
|
|
|
19
22
|
import https from 'node:https';
|
|
20
23
|
|
|
21
24
|
import {
|
|
22
|
-
UPDATE_CHECK_PATH,
|
|
25
|
+
UPDATE_CHECK_PATH, NPM_LATEST_URL, writeUpdateCache, collectBody, netDeadlineMs, readRegular, updateCheckDue,
|
|
23
26
|
} from '../bin/lib/telemetry-config.mjs';
|
|
24
27
|
|
|
25
28
|
const NET_TIMEOUT_MS = 10_000;
|
|
@@ -45,7 +48,8 @@ function readCache() {
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
// url 에 GET 요청해 응답 JSON 의 version 문자열만 추출한다. 실패·거부·리다이렉트·malformed 는 전부
|
|
48
|
-
// null. reject 하지 않고 항상 resolve 한다(
|
|
51
|
+
// null. reject 하지 않고 항상 resolve 한다(무활동 timeout 과 교환 전체 마감, 본문 256KiB 상한). GET 뿐이라
|
|
52
|
+
// 페이로드·식별자 헤더가 없다.
|
|
49
53
|
function fetchLatestVersion(url) {
|
|
50
54
|
return new Promise((resolvePromise) => {
|
|
51
55
|
let done = false;
|
|
@@ -94,21 +98,23 @@ function fetchLatestVersion(url) {
|
|
|
94
98
|
|
|
95
99
|
async function main() {
|
|
96
100
|
const cache = readCache();
|
|
97
|
-
|
|
98
|
-
if (checkedAt !== null && (Date.now() - checkedAt) < UPDATE_THROTTLE_MS) return; // throttle 미경과 - no-op.
|
|
101
|
+
if (!updateCheckDue(cache)) return; // 성공이나 실패한 시도에서 하루가 지나지 않음 - no-op.
|
|
99
102
|
|
|
100
103
|
const url = process.env.BANKER_NPM_URL || NPM_LATEST_URL; // 테스트 주입용 override.
|
|
101
104
|
const latest = await fetchLatestVersion(url);
|
|
102
|
-
if (!latest)
|
|
105
|
+
if (!latest) { // 조회 실패·거부·malformed - 버전은 쓰지 않고 시도 시각만 남겨 하루를 쉰다.
|
|
106
|
+
writeUpdateCache({ triedAt: Date.now() });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
103
109
|
|
|
104
110
|
// 개인화 매니페스트는 여기서 조회하지 않는다: update-fetch 는 카운팅 opt-out(countingActive false) 경로의
|
|
105
111
|
// fetcher 이고, 개인화 표시도 countingActive() 로 게이트되므로 이 경로에선 표시될 일이 없다. 매니페스트
|
|
106
112
|
// 조회는 카운팅 활성 경로의 update-checkin 만 수행한다(중복·죽은 조회 제거).
|
|
107
113
|
const cachedLatest = typeof cache.latest === 'string' ? cache.latest : null;
|
|
108
114
|
if (cachedLatest !== latest) {
|
|
109
|
-
writeUpdateCache({ latest, checkedAt: Date.now(), notified: null }); // 신버전 - notified 재무장.
|
|
115
|
+
writeUpdateCache({ latest, checkedAt: Date.now(), notified: null, triedAt: null }); // 신버전 - notified 재무장. 남은 실패 시도 시각은 비움.
|
|
110
116
|
} else {
|
|
111
|
-
writeUpdateCache({ latest, checkedAt: Date.now() }); // 동일 버전 - notified 등 형제 필드는 병합
|
|
117
|
+
writeUpdateCache({ latest, checkedAt: Date.now(), triedAt: null }); // 동일 버전 - notified 등 형제 필드는 병합 보존, 실패 시도 시각은 비움.
|
|
112
118
|
}
|
|
113
119
|
}
|
|
114
120
|
|
package/hooks/update-notify.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { dirname, join } from 'node:path';
|
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
|
-
UPDATE_CHECK_PATH,
|
|
22
|
+
UPDATE_CHECK_PATH, updateCheckDue,
|
|
23
23
|
noUpdateCheck, countingActive, endpoint,
|
|
24
24
|
installedVersion, compareVersions, writeUpdateCache, readUsedSkills, readRegular,
|
|
25
25
|
} from '../bin/lib/telemetry-config.mjs';
|
|
@@ -89,13 +89,6 @@ function maybeNotify(cached) {
|
|
|
89
89
|
writeUpdateCache({ notified: cached.latest }); // 형제 필드(latest/checkedAt/changedSkills) 보존하며 notified 만 병합.
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
// checkedAt 기준 throttle 경과 여부. 캐시 없음·checkedAt 미기록/비수치 → 경과로 본다(첫 조회 유도).
|
|
93
|
-
function throttleElapsed(cached) {
|
|
94
|
-
const checkedAt = cached?.checkedAt;
|
|
95
|
-
if (typeof checkedAt !== 'number') return true;
|
|
96
|
-
return (Date.now() - checkedAt) >= UPDATE_THROTTLE_MS;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
92
|
// 갱신 fetcher 절대경로 결정: count-default-on 활성(countingActive) + 엔드포인트 설정 시
|
|
100
93
|
// update-checkin.mjs(체크인이 알림 조회를 겸함), 그 외에는 update-fetch.mjs(공개 npm GET 폴백).
|
|
101
94
|
// 경로는 이 파일 기준으로 resolve 한다. (형제 스토리가 생성 중 - 런타임에 존재하면 된다.)
|
|
@@ -121,7 +114,7 @@ async function main() {
|
|
|
121
114
|
|
|
122
115
|
const cached = readCache(); // 포그라운드는 캐시 읽기만(네트워크 없음).
|
|
123
116
|
maybeNotify(cached);
|
|
124
|
-
if (
|
|
117
|
+
if (updateCheckDue(cached)) spawnFetcher(); // 갱신은 백그라운드로 위임. 실패한 시도(triedAt)에서도 하루를 쉰다.
|
|
125
118
|
}
|
|
126
119
|
|
|
127
120
|
// 직접 실행(훅)일 때만 main() 을 돌린다. import(테스트)시엔 chooseFetcher 만 노출하고 main 은 미실행.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaydash9999/banker-plugins",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.5",
|
|
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"
|