@kaydash9999/banker-plugins 0.15.2 → 0.16.0

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.
Files changed (41) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +48 -0
  4. package/README.md +31 -9
  5. package/codex/manifest.json +6 -3
  6. package/codex/transform-matrix.md +2 -0
  7. package/commands/progress.md +10 -0
  8. package/commands/setup.md +1 -1
  9. package/hooks/graceful-pause.mjs +31 -71
  10. package/hooks/hooks.json +11 -1
  11. package/hooks/progress-fallback.mjs +10 -0
  12. package/hooks/progress.mjs +427 -0
  13. package/hooks/register.mjs +82 -5
  14. package/package.json +1 -1
  15. package/skills/3d-intro-build/SKILL.md +235 -122
  16. package/skills/3d-intro-build/references/NOTICE.md +2 -1
  17. package/skills/3d-intro-build/references/assemble.mjs +164 -108
  18. package/skills/3d-intro-build/references/azure-adapter.mjs +115 -29
  19. package/skills/3d-intro-build/references/curate.mjs +573 -267
  20. package/skills/3d-intro-build/references/intro-fixes.css +61 -0
  21. package/skills/3d-intro-build/references/panel-glass.css +55 -0
  22. package/skills/3d-intro-build/references/preview-lib.mjs +256 -0
  23. package/skills/3d-intro-build/references/prompts.md +32 -9
  24. package/skills/3d-intro-build/references/serve.mjs +111 -103
  25. package/skills/3d-intro-build/references/step-nav.css +59 -0
  26. package/skills/3d-intro-build/references/step-nav.js +503 -0
  27. package/skills/3d-intro-build/references/verify-intro.mjs +474 -0
  28. package/skills/3d-intro-build/references/verify.md +45 -0
  29. package/skills/3d-intro-build/references/video-pool.mjs +780 -0
  30. package/skills/3d-intro-setup/SKILL.md +49 -12
  31. package/skills/3d-intro-setup/references/azure-adapter.mjs +115 -29
  32. package/skills/3d-intro-setup/references/video-pool.mjs +780 -0
  33. package/skills/motion-graphic-make/SKILL.md +73 -28
  34. package/skills/motion-graphic-make/references/curate.mjs +607 -0
  35. package/skills/motion-graphic-make/references/preview-lib.mjs +256 -0
  36. package/skills/motion-graphic-make/references/serve.mjs +131 -0
  37. package/skills/omc-patch/SKILL.md +143 -0
  38. package/skills/omc-patch/scripts/omc-patch.mjs +577 -0
  39. package/skills/remains/SKILL.md +125 -0
  40. package/skills/remains/scripts/boxes.mjs +448 -0
  41. package/skills/trouble-shooting/SKILL.md +50 -0
@@ -0,0 +1,427 @@
1
+ /**
2
+ * banker's /progress, a Claude Mods command: a pane listing how the session's work stands.
3
+ *
4
+ * Steps are the task list Claude keeps (TaskCreate and TaskUpdate, or TodoWrite) until the prompt
5
+ * after the one in which its every item was completed. Without a list, each prompt of the session
6
+ * is a step and the running one is in progress; with nothing at all yet, the pane still shows one
7
+ * step for the work at hand. Under each step are the tool calls made while it was the step in
8
+ * progress (a subagent's or a background agent's included), one level deep. A press on a step or
9
+ * a call shows a short description under the list; a press on a step's arrow folds or unfolds its
10
+ * calls.
11
+ *
12
+ * `/progress` and `/progress show` toggle the pane, `on` and `off` set it. The command is
13
+ * registered `immediate`, so it answers while a turn runs. An engine without mods never loads
14
+ * this file: commands/progress.md and hooks/progress-fallback.mjs answer there instead.
15
+ */
16
+
17
+ const COMMAND = 'progress';
18
+ const PANE = 'banker-progress';
19
+ const TITLE = '진행 상황';
20
+ const USAGE = '사용법: /progress [show | on | off]. show(기본)는 패널을 켜고 끄며, on 과 off 는 그대로 켜거나 끕니다.';
21
+ const HINT = '단계나 하위 항목을 누르면 설명이 여기에 나옵니다.';
22
+ const MAX_STEPS = 50;
23
+ const MAX_KEPT_CALLS = 200;
24
+ const MAX_SHOWN_CALLS = 30;
25
+ // The id of the step made for work already under way when the module loaded mid-turn.
26
+ const AT_HAND = 'now';
27
+ // Bookkeeping calls: they become steps, not calls under a step.
28
+ const TASK_TOOLS = new Set(['TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList', 'TodoWrite']);
29
+ const STATUS = {
30
+ running: '진행 중', in_progress: '진행 중', pending: '대기', done: '완료', completed: '완료',
31
+ stopped: '중단됨', refused: '거절됨', failed: '오류로 끝남',
32
+ };
33
+ // How a prompt's turn ended (turn.complete's reason), as its step's state.
34
+ const ENDED = { answer: 'done', aborted: 'stopped', refusal: 'refused', error: 'failed' };
35
+ // Why a hook failed (the kind of the engine's HookFailure), for a failure that brings no message.
36
+ const FAILURES = { throw: '오류', timeout: '시간 초과', 're-entry': '중첩 호출' };
37
+ // The argument a call's row names it by.
38
+ const LABEL_ARG = {
39
+ Bash: 'command', Read: 'file_path', Write: 'file_path', Edit: 'file_path', NotebookEdit: 'notebook_path',
40
+ Grep: 'pattern', Glob: 'pattern', WebFetch: 'url', WebSearch: 'query', Agent: 'description', Task: 'description',
41
+ Skill: 'skill',
42
+ };
43
+ const PATH_ARGS = new Set(['file_path', 'notebook_path']);
44
+
45
+ // What a step keeps of its calls: the newest ones, how many it had and how many of them failed.
46
+ const tally = () => ({ calls: [], count: 0, failed: 0 });
47
+ // The calls made while the task list has no task in progress.
48
+ const looseStep = () => ({ key: 'loose', title: '작업 목록 밖의 작업', ...tally() });
49
+
50
+ // `rev` counts the task changes (TaskCreate, TaskUpdate) taken in, to tell a stale TaskList or TaskGet.
51
+ export const freshState = () => ({ turns: [], tasks: new Map(), todos: null, loose: looseStep(), open: new Set(), selected: null, seq: 0, rev: 0 });
52
+
53
+ // One line of plain text, cut to `n` characters. Control characters go too: a pasted escape
54
+ // sequence is not text to show.
55
+ const clip = (text, n) => {
56
+ const s = String(text ?? '').replace(/\p{Cc}+/gu, ' ').replace(/\s+/g, ' ').trim();
57
+ const chars = [...s];
58
+ return chars.length > n ? `${chars.slice(0, n - 1).join('')}…` : s;
59
+ };
60
+ const tail = (path) => String(path).split(/[\\/]/).filter(Boolean).slice(-2).join('/');
61
+ const seconds = (ms) => {
62
+ const all = Math.max(0, Math.round(ms / 1000));
63
+ return all < 60 ? `${all}초` : `${Math.floor(all / 60)}분 ${all % 60}초`;
64
+ };
65
+
66
+ export function callLabel(e) {
67
+ const arg = LABEL_ARG[e.tool];
68
+ const value = arg && typeof e[arg] === 'string' ? e[arg] : '';
69
+ if (!value) return e.tool;
70
+ return `${e.tool} ${clip(PATH_ARGS.has(arg) ? tail(value) : value, 48)}`;
71
+ }
72
+
73
+ // ── what the module learns from the session ──────────────────────────────────
74
+
75
+ // A prompt as a step. A prompt whose end never came is done where the next one starts.
76
+ function addTurn(s, e, now) {
77
+ for (const t of s.turns) if (t.status === 'running') Object.assign(t, { status: 'done', endedAt: now });
78
+ s.turns.push({ key: `t-${e.turnId}`, id: e.turnId, title: clip(e.text, 60) || '(이어서 진행)', text: String(e.text ?? ''),
79
+ startedAt: now, endedAt: null, status: 'running', ...tally() });
80
+ if (s.turns.length > MAX_STEPS) s.turns.shift();
81
+ }
82
+
83
+ // The task list's items, which are the steps while there are any: TodoWrite's, else the Tasks list's.
84
+ function listItems(s) {
85
+ return s.todos?.length ? s.todos : [...s.tasks.values()];
86
+ }
87
+
88
+ // A list whose every item is completed is over, as in Claude Code (TodoWrite drops it at once, the
89
+ // Tasks list soon after). It stays on the pane until the next prompt, which is a step again. An
90
+ // emptied list takes the calls made outside it along.
91
+ function dropFinished(s) {
92
+ const items = listItems(s);
93
+ if (items.some((t) => t.status !== 'completed')) return;
94
+ s.todos = null;
95
+ s.tasks = new Map();
96
+ s.loose = looseStep();
97
+ }
98
+
99
+ export function startTurn(s, e, now) {
100
+ dropFinished(s);
101
+ addTurn(s, e, now);
102
+ }
103
+
104
+ // A main turn's end; a subagent's (it carries an agentId) ends no step. A turn the module never
105
+ // saw start (it loaded mid-turn) ends the step for the work at hand.
106
+ export function endTurn(s, e, now) {
107
+ if (e.agentId) return;
108
+ const t = s.turns.find((x) => x.id === e.turnId) ?? s.turns.find((x) => x.id === AT_HAND && x.status === 'running');
109
+ if (!t) return;
110
+ t.status = ENDED[e.reason] ?? 'failed';
111
+ t.endedAt = now;
112
+ }
113
+
114
+ // The steps a call belongs to: the task in progress (or, while the list has none, the calls outside
115
+ // the list) and the running prompt, which keeps its calls for when the list is gone. A call before
116
+ // any prompt (the module loaded mid-turn) starts the step for the work at hand.
117
+ function owners(s, now) {
118
+ const items = listItems(s);
119
+ const task = [...items].reverse().find((t) => t.status === 'in_progress');
120
+ const first = task ?? (items.length ? s.loose : null);
121
+ if (!s.turns.length) addTurn(s, { turnId: AT_HAND, text: '지금 하는 작업' }, now);
122
+ return first ? [first, s.turns.at(-1)] : [s.turns.at(-1)];
123
+ }
124
+
125
+ export function startCall(s, e, now) {
126
+ if (TASK_TOOLS.has(e.tool)) return null;
127
+ const call = { key: `c-${++s.seq}`, tool: e.tool, label: callLabel(e), detail: clip(e[LABEL_ARG[e.tool]] ?? '', 200),
128
+ isAgent: Boolean(e.agentId), startedAt: now, endedAt: null, ok: null, steps: owners(s, now) };
129
+ for (const step of call.steps) {
130
+ step.calls.push(call);
131
+ step.count += 1;
132
+ if (step.calls.length > MAX_KEPT_CALLS) step.calls.shift();
133
+ }
134
+ return call;
135
+ }
136
+
137
+ export function endCall(call, r, now) {
138
+ if (!call) return;
139
+ call.endedAt = now;
140
+ call.ok = !(r?.deny || r?.isError);
141
+ if (!call.ok) for (const step of call.steps) step.failed += 1;
142
+ }
143
+
144
+ // When an item started and ended, as far as this module saw. A reopened item runs on from its start;
145
+ // one back to waiting has no time until it starts again.
146
+ function stamp(t, status, now) {
147
+ if (status === 'pending') t.startedAt = undefined;
148
+ if (status === 'in_progress') t.startedAt ??= now;
149
+ t.endedAt = status === 'completed' && Number.isFinite(t.startedAt) ? (t.endedAt ?? now) : null;
150
+ t.status = status;
151
+ }
152
+
153
+ // A list longer than the pane holds loses its oldest finished item first, else its oldest.
154
+ const overflow = (items) => items.find((t) => t.status === 'completed') ?? items[0];
155
+
156
+ // One item of a TodoWrite list: while its text stays it is the same step (the first unused one of
157
+ // that text), with its key, calls and times.
158
+ function todoItem(s, before, d, now) {
159
+ const t = before.get(d.content)?.shift() ?? { key: `d-${++s.seq}`, description: d.content, ...tally() };
160
+ t.title = clip(d.content, 60);
161
+ t.activeForm = d.activeForm;
162
+ stamp(t, d.status, now);
163
+ return t;
164
+ }
165
+
166
+ // TodoWrite sends the whole list each time. A subagent keeps a list of its own, not the session's.
167
+ function writeTodos(s, e, now) {
168
+ if (e.agentId || !Array.isArray(e.todos)) return;
169
+ const before = new Map();
170
+ for (const t of s.todos ?? []) before.set(t.description, [...(before.get(t.description) ?? []), t]);
171
+ s.todos = e.todos.map((d) => todoItem(s, before, d, now));
172
+ while (s.todos.length > MAX_STEPS) s.todos.splice(s.todos.indexOf(overflow(s.todos)), 1);
173
+ }
174
+
175
+ // Adds a task or brings it up to date; a task keeps its key, calls and times. One known only by
176
+ // its id is named by it until a TaskGet or a TaskList says more.
177
+ function putTask(s, t, now) {
178
+ const held = s.tasks.get(t.id) ?? { key: `k-${t.id}`, id: t.id, title: `작업 #${t.id}`, description: '', ...tally() };
179
+ const title = clip(t.subject, 60);
180
+ if (title) held.title = title;
181
+ for (const k of ['description', 'activeForm']) if (t[k] !== undefined) held[k] = t[k];
182
+ if (t.status !== undefined) stamp(held, t.status, now);
183
+ s.tasks.set(t.id, held);
184
+ while (s.tasks.size > MAX_STEPS) s.tasks.delete(overflow([...s.tasks.values()]).id);
185
+ }
186
+
187
+ function createTask(s, e, r, now) {
188
+ const id = r?.result?.task?.id;
189
+ if (id === undefined) return;
190
+ putTask(s, { id: String(id), subject: e.subject, description: e.description, activeForm: e.activeForm, status: 'pending' }, now);
191
+ }
192
+
193
+ // A task this module never saw (a list from before it loaded, or one another session shares) joins
194
+ // the list while it is not done.
195
+ function updateTask(s, e, r, now) {
196
+ const id = String(e.taskId);
197
+ const status = r?.result?.statusChange?.to ?? e.status;
198
+ if (status === 'deleted') {
199
+ s.tasks.delete(id);
200
+ return;
201
+ }
202
+ if (!s.tasks.has(id) && status !== 'pending' && status !== 'in_progress') return;
203
+ putTask(s, { id, subject: e.subject, description: e.description, activeForm: e.activeForm, status }, now);
204
+ }
205
+
206
+ // TaskList names the whole list, in its order: a task it leaves out is gone. A list whose every task
207
+ // is completed is over, and only brings up to date the tasks still on the pane.
208
+ function listTasks(s, _e, r, now, since) {
209
+ const listed = r?.result?.tasks;
210
+ if (since !== s.rev || !Array.isArray(listed)) return;
211
+ const over = listed.every((t) => t.status === 'completed');
212
+ const before = s.tasks;
213
+ s.tasks = new Map();
214
+ for (const t of listed) {
215
+ const id = String(t.id);
216
+ if (before.has(id)) s.tasks.set(id, before.get(id));
217
+ if (before.has(id) || !over) putTask(s, { id, subject: t.subject, status: t.status }, now);
218
+ }
219
+ }
220
+
221
+ // TaskGet reads one task: one it cannot find is gone, and a completed one off the pane stays off.
222
+ function getTask(s, e, r, now, since) {
223
+ if (since !== s.rev) return;
224
+ const t = r?.result?.task;
225
+ if (t === null) s.tasks.delete(String(e.taskId));
226
+ if (!t || (t.status === 'completed' && !s.tasks.has(String(t.id)))) return;
227
+ putTask(s, { id: String(t.id), subject: t.subject, description: t.description, status: t.status }, now);
228
+ }
229
+
230
+ const TASK_NOTES = {
231
+ TaskCreate: createTask,
232
+ TaskUpdate: updateTask,
233
+ TaskList: listTasks,
234
+ TaskGet: getTask,
235
+ TodoWrite: (s, e, _r, now) => writeTodos(s, e, now),
236
+ };
237
+
238
+ // A refused or failed call changes nothing: the task list stays as Claude Code holds it. A TaskUpdate
239
+ // that changed nothing (an id it cannot find, a TaskCompleted hook that blocks) answers
240
+ // `success: false` without isError.
241
+ // TaskList and TaskGet run beside other calls: one that read the list before a change landed (`since`,
242
+ // the count when it started, is behind) is stale and changes nothing.
243
+ const refused = (r) => Boolean(r?.deny || r?.isError || r?.result?.success === false);
244
+ const CHANGES = new Set(['TaskCreate', 'TaskUpdate']);
245
+
246
+ export function noteTaskTool(s, e, r, now, since) {
247
+ if (refused(r)) return;
248
+ TASK_NOTES[e.tool]?.(s, e, r, now, since ?? s.rev);
249
+ if (CHANGES.has(e.tool)) s.rev += 1;
250
+ }
251
+
252
+ // ── what the pane shows ──────────────────────────────────────────────────────
253
+
254
+ // Nothing seen yet: one step that says so, with no state word beside it.
255
+ const IDLE = { key: 'idle', title: '진행 중인 작업 없음', status: null, ...tally() };
256
+
257
+ // The calls outside the list are in progress while calls still go there: no task is in progress
258
+ // and the prompt runs.
259
+ function looseStatus(s, items) {
260
+ const going = !items.some((t) => t.status === 'in_progress') && s.turns.at(-1)?.status === 'running';
261
+ return going ? 'running' : 'done';
262
+ }
263
+
264
+ export function steps(s) {
265
+ const items = listItems(s);
266
+ if (!items.length) return s.turns.length ? s.turns : [IDLE];
267
+ return s.loose.calls.length ? [...items, { ...s.loose, status: looseStatus(s, items) }] : items;
268
+ }
269
+
270
+ function callCount(step) {
271
+ return `도구 호출 ${step.count}회${step.failed ? `(실패 ${step.failed}회)` : ''}`;
272
+ }
273
+
274
+ function stepState(step, now) {
275
+ const doing = step.status === 'in_progress' && step.activeForm ? `(${clip(step.activeForm, 40)})` : '';
276
+ const time = Number.isFinite(step.startedAt) ? `, ${seconds((step.endedAt ?? now) - step.startedAt)}` : '';
277
+ return `${STATUS[step.status] ?? step.status}${doing}${time}`;
278
+ }
279
+
280
+ function stepWords(step, now) {
281
+ if (step.key === 'idle') return '아직 시작한 작업이 없습니다. 요청을 보내면 여기에 단계가 쌓입니다.';
282
+ const words = step.text !== undefined ? `요청: ${clip(step.text, 160) || '(이어서 진행)'}` : clip(step.description || step.title, 160);
283
+ const what = words.replace(/[.!?。]+$/, '');
284
+ return `${what}. ${stepState(step, now)}, ${callCount(step)}.`;
285
+ }
286
+
287
+ function callWords(call, now) {
288
+ const state = call.ok === null ? '실행 중' : call.ok ? '성공' : '실패';
289
+ const who = call.isAgent ? '하위 에이전트의 ' : '';
290
+ return `${who}${call.tool}${call.detail ? `: ${call.detail}` : ''}. ${state}, ${seconds((call.endedAt ?? now) - call.startedAt)}.`;
291
+ }
292
+
293
+ // What the person pressed last, while it is still on the pane: a step, or a call under one.
294
+ function pressed(s) {
295
+ const all = steps(s);
296
+ return all.find((x) => x.key === s.selected) ?? all.flatMap((x) => x.calls).find((c) => c.key === s.selected);
297
+ }
298
+
299
+ // The description under the list: the pressed step's or call's, or how to get one.
300
+ export function describe(s, now) {
301
+ const item = pressed(s);
302
+ if (item) return item.tool ? callWords(item, now) : stepWords(item, now);
303
+ const all = steps(s);
304
+ return all.length === 1 && all[0].key === 'idle' ? stepWords(IDLE, now) : HINT;
305
+ }
306
+
307
+ function callRows(ui, step, act) {
308
+ const { Box, Text, Button } = ui;
309
+ const shown = step.calls.slice(-MAX_SHOWN_CALLS);
310
+ const rows = shown.map((c) => Box({ key: `row-${c.key}`, flexDirection: 'row', columnGap: 1, paddingLeft: 4, children: [
311
+ Button({ key: c.key, label: c.label, plain: true, dimColor: c.ok !== false, onPress: () => act.select(c.key) }),
312
+ Text({ children: [c.ok === null ? '실행 중' : c.ok ? '' : '실패'], dimColor: true }),
313
+ ] }));
314
+ const hidden = step.calls.length - shown.length;
315
+ return hidden ? [Box({ paddingLeft: 4, children: [Text({ children: [`앞의 ${hidden}개는 줄였습니다`], dimColor: true })] }), ...rows] : rows;
316
+ }
317
+
318
+ function stepRow(ui, s, step, act) {
319
+ const { Box, Text, Button } = ui;
320
+ const isOpen = s.open.has(step.key);
321
+ const arrow = step.calls.length
322
+ ? Button({ key: `arrow-${step.key}`, label: isOpen ? '▾' : '▸', plain: true, onPress: () => act.fold(step.key) })
323
+ : Text({ children: [' '] });
324
+ const running = step.status === 'running' || step.status === 'in_progress';
325
+ return Box({ key: `row-${step.key}`, flexDirection: 'row', columnGap: 1, children: [
326
+ arrow,
327
+ Button({ key: step.key, label: step.title, plain: true, onPress: () => act.select(step.key) }),
328
+ Text({ children: [STATUS[step.status] ?? step.status ?? ''], dimColor: !running, bold: running }),
329
+ ] });
330
+ }
331
+
332
+ // The pane's tree. `ui` is the surface's element table; `act` holds what a press does.
333
+ export function draw(ui, s, now, act) {
334
+ const { Box, Text } = ui;
335
+ const rows = [];
336
+ for (const step of steps(s)) {
337
+ rows.push(stepRow(ui, s, step, act));
338
+ if (s.open.has(step.key)) rows.push(...callRows(ui, step, act));
339
+ }
340
+ rows.push(Box({ key: 'about', marginTop: 1, children: [Text({ children: [describe(s, now)], dimColor: !pressed(s), wrap: 'wrap' })] }));
341
+ return Box({ flexDirection: 'column', children: rows });
342
+ }
343
+
344
+ // ── the command and the hooks ────────────────────────────────────────────────
345
+
346
+ // Opens the pane, or raises it from behind another plugin's pane, where it is open but not shown:
347
+ // only an open with `focus` raises a pane. A pane opened for a command is placed at any width.
348
+ async function showPane($, pane) {
349
+ if (!pane) await $.ui.open({ id: PANE, title: TITLE });
350
+ else if (!pane.isShown) await $.ui.open({ id: PANE, title: TITLE, focus: true });
351
+ }
352
+
353
+ // `show` (the default) toggles what the person sees; `on` and `off` set the pane whatever it was.
354
+ export async function runProgress($, args) {
355
+ const want = String(args ?? '').trim().toLowerCase() || 'show';
356
+ if (!['show', 'on', 'off'].includes(want)) return { text: USAGE };
357
+ const pane = (await $.ui.panes()).find((p) => p.id === PANE);
358
+ if (want === 'off' || (want === 'show' && pane?.isShown)) {
359
+ if (pane) await $.ui.close({ id: PANE });
360
+ } else await showPane($, pane);
361
+ return {};
362
+ }
363
+
364
+ // The command's answer when its hook fails (it threw, or ran past its time). Without one the hook
365
+ // would be absent, and `/banker:progress` would go on to commands/progress.md, which says mods are
366
+ // missing.
367
+ function commandFailed(_$, _e, next) {
368
+ const why = next?.error?.message || FAILURES[next?.error?.kind] || '알 수 없는 오류';
369
+ return { text: `진행 상황 패널을 바꾸지 못했습니다(${clip(why, 120)}). 다시 입력해 주세요.` };
370
+ }
371
+
372
+ // The command, which register.mjs registers at session.start with the module's others.
373
+ export const SPEC = { name: COMMAND, description: '진행 상황 패널을 켜고 끔(목록, 설명, 하위 목록 펼치기)', argumentHint: '[show | on | off]', immediate: true };
374
+
375
+ // A call that throws is recorded as failed, so its row does not stay running. A call goes to the
376
+ // list it started under: one that ends after /clear leaves the new conversation's list alone.
377
+ async function watchCall($, e, next) {
378
+ const s = ref.s;
379
+ const since = s.rev;
380
+ const call = startCall(s, e, Date.now());
381
+ let r = { isError: true };
382
+ try {
383
+ r = await next(e);
384
+ return r;
385
+ } finally {
386
+ endCall(call, r, Date.now());
387
+ if (TASK_TOOLS.has(e.tool)) noteTaskTool(s, e, r, Date.now(), since);
388
+ $.ui.invalidate('ui.render');
389
+ }
390
+ }
391
+
392
+ // The session's progress, one per loaded module.
393
+ const ref = { s: freshState() };
394
+
395
+ // The command and the events only this feature hooks. register.mjs hooks the events every banker
396
+ // feature shares (the engine takes one hook per event from the module) and calls the functions
397
+ // below for them. The fallback's name is hooked too: where mods run, `/banker:progress` toggles
398
+ // the pane instead of expanding commands/progress.md into a model turn.
399
+ export function registerProgress(on) {
400
+ ref.s = freshState();
401
+ on('command.run', { command: 'progress' }, async ($, e) => runProgress($, e.args)).catch(commandFailed);
402
+ on('command.run', { command: 'banker:progress' }, async ($, e) => runProgress($, e.args)).catch(commandFailed);
403
+ // Every tool call passes here: a fault in this hook leaves the call as if the hook were absent.
404
+ on('tool.call', async ($, e, next) => watchCall($, e, next)).catch(() => undefined);
405
+ on('ui.render', { component: 'Pane' }, async ($, e, next) => {
406
+ if (e.requestId !== PANE) return next(e);
407
+ const redraw = () => $.ui.invalidate('ui.render');
408
+ const act = {
409
+ select: (key) => { ref.s.selected = ref.s.selected === key ? null : key; redraw(); },
410
+ fold: (key) => { if (!ref.s.open.delete(key)) ref.s.open.add(key); redraw(); },
411
+ };
412
+ return draw($.ui.resolve(e), ref.s, Date.now(), act);
413
+ });
414
+ }
415
+
416
+ // A new session, or /clear (session.end, and no session.start after it), starts an empty list.
417
+ export function progressReset() {
418
+ ref.s = freshState();
419
+ }
420
+
421
+ export function progressTurnStarted(e) {
422
+ startTurn(ref.s, e, Date.now());
423
+ }
424
+
425
+ export function progressTurnCompleted(e) {
426
+ endTurn(ref.s, e, Date.now());
427
+ }
@@ -1,12 +1,89 @@
1
1
  /**
2
- * banker's function-hooks module: the one file hooks/hooks.json names under
3
- * `modules`. Claude Code loads it beside the classic hooks in the same file;
4
- * builds without function hooks, and Codex, leave it alone.
2
+ * banker's mods module (Claude Mods, the function hooks of early access): the one file
3
+ * hooks/hooks.json names under `modules`. Claude Code loads it beside the classic hooks in the
4
+ * same file; builds without mods, and Codex, leave it alone.
5
5
  *
6
- * Each feature registers its own hooks; this file only gathers them.
6
+ * The engine reads the module's source before it runs it: each `on("<event>", hook)` names its
7
+ * event as a string literal, an event without a matcher is hooked once in the whole module (its
8
+ * imported files included), and `$` goes only to functions declared in the file that holds it.
9
+ * So each feature (graceful-pause.mjs, progress.mjs) hooks the events only it needs, and this
10
+ * file hooks the four they share: it calls the features' plain functions for them and makes the
11
+ * engine calls itself, the module's command registrations among them. The engine shows a
12
+ * `$.ui.log` line under the plugin's name, so the lines here do not repeat it.
7
13
  */
8
- import { registerGracefulPause } from './graceful-pause.mjs';
14
+ import {
15
+ COMMAND as PAUSE, MIN_ENGINE, SPEC as PAUSE_SPEC, atLeast, pauseReset, pauseTurnCompleted, pauseTurnStarted,
16
+ registerGracefulPause,
17
+ } from './graceful-pause.mjs';
18
+ import {
19
+ SPEC as PROGRESS_SPEC, progressReset, progressTurnCompleted, progressTurnStarted, registerProgress,
20
+ } from './progress.mjs';
21
+
22
+ async function engineVersion($) {
23
+ try {
24
+ const v = await $.session.version();
25
+ return v?.base || v?.version || null;
26
+ } catch {
27
+ return null; /* an engine without the call is older than /graceful-pause needs */
28
+ }
29
+ }
30
+
31
+ async function registerOne($, spec) {
32
+ try {
33
+ await $.command.register(spec);
34
+ } catch (err) {
35
+ $.ui.log(`/${spec.name} 를 등록하지 못했습니다 (${String(err?.message ?? err)})`);
36
+ }
37
+ }
38
+
39
+ // /graceful-pause from 2.1.289 on (an older engine gets one line saying why), /progress wherever
40
+ // mods run. A name the engine refuses is logged and the other still registers.
41
+ async function registerCommands($) {
42
+ const version = await engineVersion($);
43
+ if (atLeast(version, MIN_ENGINE)) await registerOne($, PAUSE_SPEC);
44
+ else $.ui.log(`/${PAUSE} 는 Claude Code ${MIN_ENGINE.join('.')} 이상에서만 켭니다 (이 엔진: ${version || '버전 미상'})`);
45
+ await registerOne($, PROGRESS_SPEC);
46
+ }
47
+
48
+ // A /graceful-pause request that came after its turn's last model request: a note voids it.
49
+ async function voidLateRequest($, late) {
50
+ try {
51
+ await $.session.append(late.row);
52
+ } catch {
53
+ /* nothing more to do: the log line below still tells the person */
54
+ }
55
+ $.ui.log(late.log);
56
+ }
9
57
 
10
58
  export const register = (on) => {
11
59
  registerGracefulPause(on);
60
+ registerProgress(on);
61
+
62
+ on('session.start', async ($, e, next) => {
63
+ progressReset();
64
+ await registerCommands($);
65
+ return next(e);
66
+ });
67
+
68
+ // `/clear` ends the conversation and raises no session.start for the next one.
69
+ on('session.end', async ($, e, next) => {
70
+ pauseReset();
71
+ progressReset();
72
+ return next(e);
73
+ });
74
+
75
+ on('turn.start', async ($, e, next) => {
76
+ pauseTurnStarted(e);
77
+ progressTurnStarted(e);
78
+ $.ui.invalidate('ui.render');
79
+ return next(e);
80
+ });
81
+
82
+ on('turn.complete', async ($, e, next) => {
83
+ const late = pauseTurnCompleted(e);
84
+ if (late) await voidLateRequest($, late);
85
+ progressTurnCompleted(e);
86
+ $.ui.invalidate('ui.render');
87
+ return next(e);
88
+ });
12
89
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaydash9999/banker-plugins",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
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"