@kaydash9999/banker-plugins 0.16.3 → 0.17.1
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 +57 -0
- package/README.md +14 -11
- package/bin/banker.js +2 -2
- package/codex/manifest.json +2 -1
- package/codex/transform-matrix.md +2 -1
- package/commands/setup.md +2 -2
- package/hooks/progress-plan.mjs +408 -20
- package/hooks/progress.mjs +202 -50
- package/hooks/register.mjs +7 -1
- package/package.json +1 -1
- package/skills/3d-intro-build/references/assemble.mjs +10 -2
- package/skills/3d-intro-build/references/curate.mjs +10 -2
- package/skills/3d-intro-build/references/preview-lib.mjs +2 -0
- package/skills/3d-intro-build/references/serve.mjs +10 -2
- package/skills/3d-intro-build/references/verify-intro.mjs +10 -2
- package/skills/motion-graphic-make/references/curate.mjs +10 -2
- package/skills/motion-graphic-make/references/preview-lib.mjs +2 -0
- package/skills/motion-graphic-make/references/serve.mjs +10 -2
- package/skills/obsidizer/obsidize.mjs +10 -2
- package/skills/omc-patch/SKILL.md +105 -22
- package/skills/omc-patch/scripts/omc-patch.mjs +609 -255
- package/skills/omc-patch/scripts/probe-pm.mjs +192 -0
- package/skills/ultra-chain/SKILL.md +475 -0
- package/skills/ultra-chain/references/capture.mjs +232 -0
- package/skills/ultra-chain/references/chain-ledger.mjs +491 -0
- package/skills/ultra-chain/references/chain-report.mjs +257 -0
- package/skills/ultra-chain/references/preview-lib.mjs +258 -0
- package/skills/ultra-chain/references/serve.mjs +139 -0
- package/skills/update-banker/SKILL.md +2 -1
- package/skills/vertical-pptx/verify_a4p.mjs +10 -2
package/hooks/progress.mjs
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* calls.
|
|
11
11
|
*
|
|
12
12
|
* Claude 5 models get no task list tool in Claude Code 2.1.296 (only older models, a background
|
|
13
|
-
* job, or CLAUDE_CODE_ENABLE_TODO_TOOLS do)
|
|
14
|
-
*
|
|
13
|
+
* job, or CLAUDE_CODE_ENABLE_TODO_TOOLS do). Opening the pane offers the model one of its own
|
|
14
|
+
* (`mcp__banker__todo`, TodoWrite's shape) where it has none, and the list it sends is the steps.
|
|
15
|
+
* Above them the pane shows the plan file the session touched last (progress-plan.mjs reads it):
|
|
16
|
+
* the whole plan as a tree that folds like a toggle list, its sections folded at first.
|
|
15
17
|
* A prompt's harness tags leave its title, and a background task's notice (or a teammate's message)
|
|
16
18
|
* goes on with the last prompt's step instead of becoming one: the step runs again while it does,
|
|
17
19
|
* and keeps its own end and time.
|
|
@@ -31,13 +33,53 @@ const HINT = '단계나 하위 항목을 누르면 설명이 여기에 나옵니
|
|
|
31
33
|
const MAX_STEPS = 50;
|
|
32
34
|
const MAX_KEPT_CALLS = 200;
|
|
33
35
|
const MAX_SHOWN_CALLS = 30;
|
|
34
|
-
|
|
36
|
+
// Rows of the plan's tree shown at most, and the characters they hold: the engine draws 100,000
|
|
37
|
+
// characters a drawing and blanks the rest, so a long plan must leave room for the steps and the
|
|
38
|
+
// description below it. One line past them says how many more rows there are.
|
|
39
|
+
const MAX_TREE_ROWS = 300;
|
|
40
|
+
const MAX_TREE_TEXT = 40000;
|
|
35
41
|
// The calls whose file, when it is a plan file, becomes the plan the pane shows.
|
|
36
42
|
const PLAN_TOUCH = new Set(['Write', 'Edit', 'MultiEdit', 'Read', 'NotebookEdit']);
|
|
37
43
|
// The id of the step made for work already under way when the module loaded mid-turn.
|
|
38
44
|
const AT_HAND = 'now';
|
|
45
|
+
// The pane's own to-do tool. The model calls it as `mcp__<plugin>__todo`; it sends the whole list
|
|
46
|
+
// each time, as TodoWrite does. Claude Code's own list tools, where the model has one, take its place.
|
|
47
|
+
const TODO = 'todo';
|
|
48
|
+
export const TODO_TOOL = `mcp__banker__${TODO}`;
|
|
49
|
+
const LIST_TOOLS = new Set(['TodoWrite', 'TaskCreate']);
|
|
50
|
+
const TODO_STATUS = new Set(['pending', 'in_progress', 'completed']);
|
|
51
|
+
const TODO_SPEC = {
|
|
52
|
+
name: TODO,
|
|
53
|
+
description: '진행 상황 패널(/progress)이 켜져 있고, 사람이 이 패널로 지금 하는 일과 남은 일을 본다. '
|
|
54
|
+
+ '다음 경우에는 일을 시작하기 전에 이 도구로 할 일 목록 전체를 보낸다: 3단계 이상인 작업, 파일 여러 개를 '
|
|
55
|
+
+ '만들거나 고치는 작업, 사용자가 여러 가지를 한꺼번에 시킨 경우. 항목을 시작할 때 in_progress 로, 끝나면 '
|
|
56
|
+
+ '바로 completed 로 바꿔 목록 전체를 다시 보낸다. 상태가 바뀔 때만 보내고 같은 목록을 다시 보내지 않는다. '
|
|
57
|
+
+ 'in_progress 는 하나만 둔다. 한 단계로 끝나는 일, 질문에 답하기, 대화에는 쓰지 않는다. '
|
|
58
|
+
+ '패널이 꺼져 있다고 답하면 다시 부르지 않는다.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
todos: {
|
|
63
|
+
type: 'array',
|
|
64
|
+
description: '할 일 목록 전체, 하는 순서대로',
|
|
65
|
+
items: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
content: { type: 'string', description: '할 일 한 줄' },
|
|
69
|
+
status: { type: 'string', enum: [...TODO_STATUS] },
|
|
70
|
+
activeForm: { type: 'string', description: '진행 중일 때 보일 문구(선택)' },
|
|
71
|
+
},
|
|
72
|
+
required: ['content', 'status'],
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ['todos'],
|
|
78
|
+
additionalProperties: false,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
39
81
|
// Bookkeeping calls: they become steps, not calls under a step.
|
|
40
|
-
const TASK_TOOLS = new Set(['TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList', 'TodoWrite']);
|
|
82
|
+
const TASK_TOOLS = new Set(['TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList', 'TodoWrite', TODO_TOOL]);
|
|
41
83
|
const STATUS = {
|
|
42
84
|
running: '진행 중', in_progress: '진행 중', pending: '대기', done: '완료', completed: '완료',
|
|
43
85
|
stopped: '중단됨', refused: '거절됨', failed: '오류로 끝남',
|
|
@@ -67,11 +109,11 @@ const tally = () => ({ calls: [], count: 0, failed: 0 });
|
|
|
67
109
|
const looseStep = () => ({ key: 'loose', title: '작업 목록 밖의 작업', ...tally() });
|
|
68
110
|
|
|
69
111
|
// `rev` counts the task changes (TaskCreate, TaskUpdate) taken in, to tell a stale TaskList or TaskGet.
|
|
70
|
-
// `plan` is the plan last read from `planPath`, `planShut` whether
|
|
71
|
-
// the
|
|
72
|
-
// a newer one overtook.
|
|
112
|
+
// `plan` is the plan last read from `planPath`, `planShut` whether the whole plan is folded and
|
|
113
|
+
// `planOpen` the ids of its nodes unfolded. `touch` counts the calls that touched a plan file and
|
|
114
|
+
// `taken` is the touch the plan came from, to tell a read that a newer one overtook.
|
|
73
115
|
export const freshState = () => ({ turns: [], tasks: new Map(), todos: null, loose: looseStep(), open: new Set(), selected: null, seq: 0, rev: 0,
|
|
74
|
-
plan: null, planPath: null, planShut: false, touch: 0, taken: 0 });
|
|
116
|
+
plan: null, planPath: null, planShut: false, planOpen: new Set(), touch: 0, taken: 0 });
|
|
75
117
|
|
|
76
118
|
const tail = (path) => String(path).split(/[\\/]/).filter(Boolean).slice(-2).join('/');
|
|
77
119
|
const seconds = (ms) => {
|
|
@@ -298,13 +340,41 @@ export function noteTaskTool(s, e, r, now, since) {
|
|
|
298
340
|
if (CHANGES.has(e.tool)) s.rev += 1;
|
|
299
341
|
}
|
|
300
342
|
|
|
343
|
+
// The list a to-do call sent, each item's words cleaned, or what is wrong with it.
|
|
344
|
+
export function readTodos(todos) {
|
|
345
|
+
if (!Array.isArray(todos)) return 'todos 는 할 일의 배열이어야 합니다.';
|
|
346
|
+
if (todos.length > MAX_STEPS) return `할 일은 ${MAX_STEPS}개까지 보낼 수 있습니다.`;
|
|
347
|
+
const out = [];
|
|
348
|
+
for (const t of todos) {
|
|
349
|
+
const content = typeof t?.content === 'string' ? clip(t.content, 200) : '';
|
|
350
|
+
if (!content || !TODO_STATUS.has(t.status)) return '항목마다 content(할 일)와 status(pending, in_progress, completed 중 하나)가 있어야 합니다.';
|
|
351
|
+
out.push({ content, status: t.status, activeForm: typeof t.activeForm === 'string' ? clip(t.activeForm, 80) : undefined });
|
|
352
|
+
}
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// What the to-do tool answers, in words that say what happened. The list goes to the pane as
|
|
357
|
+
// TodoWrite's would, kept while the pane is closed too, so it shows when the pane opens again; the
|
|
358
|
+
// model is told it need not send more meanwhile. A subagent's list is its own, as with TodoWrite, and
|
|
359
|
+
// leaves the session's as it is. A list that is not one is refused, so the model reads why.
|
|
360
|
+
export function answerTodo(s, e, now, paneOpen = true) {
|
|
361
|
+
const todos = readTodos(e.todos);
|
|
362
|
+
if (typeof todos === 'string') return { deny: todos };
|
|
363
|
+
if (e.agentId) return { result: '하위 에이전트의 할 일 목록은 진행 상황 패널에 보이지 않습니다. 이 도구를 부르지 않아도 됩니다.' };
|
|
364
|
+
writeTodos(s, { todos }, now);
|
|
365
|
+
if (!paneOpen) return { result: '진행 상황 패널이 꺼져 있습니다. 목록은 받아 두었고, 패널이 다시 켜질 때까지 이 도구를 부르지 않아도 됩니다.' };
|
|
366
|
+
const going = todos.filter((t) => t.status === 'in_progress').length;
|
|
367
|
+
return { result: `진행 상황 패널에 할 일 ${todos.length}개를 보였습니다(진행 중 ${going}개).` };
|
|
368
|
+
}
|
|
369
|
+
|
|
301
370
|
// The plan file a call touched, with the touch's count, when it is one and the call went through:
|
|
302
371
|
// a write by anyone, a read by the session itself (an agent reviewing a plan does not make it the plan).
|
|
372
|
+
// `own` is a write by the session itself.
|
|
303
373
|
export function planTouched(s, e, call) {
|
|
304
374
|
const path = e.file_path ?? e.notebook_path;
|
|
305
375
|
if (!call?.ok || !PLAN_TOUCH.has(e.tool) || !isPlanPath(path) || (e.tool === 'Read' && e.agentId)) return null;
|
|
306
376
|
s.touch += 1;
|
|
307
|
-
return { path, touch: s.touch };
|
|
377
|
+
return { path, touch: s.touch, own: !e.agentId && e.tool !== 'Read' };
|
|
308
378
|
}
|
|
309
379
|
|
|
310
380
|
function parsed(path, text) {
|
|
@@ -323,23 +393,34 @@ function current(s, path, { touch, since }) {
|
|
|
323
393
|
return path === s.planPath && s.taken <= since;
|
|
324
394
|
}
|
|
325
395
|
|
|
326
|
-
const isPlanKey = (key) => key === 'plan' || /^p-\d+$/.test(key ?? '');
|
|
327
|
-
|
|
328
396
|
// Whether what was read stays off the pane: no plan, or a file with no steps in place of a plan that
|
|
329
|
-
// has some (
|
|
330
|
-
|
|
397
|
+
// has some, unless the session wrote it itself (an agent's review written beside a plan, or a note the
|
|
398
|
+
// session only read, does not push the plan away; a plan the session writes is the plan, steps or not).
|
|
399
|
+
const keepsOff = (s, path, plan, read) => !plan || (path !== s.planPath && !plan.total && Boolean(s.plan?.total) && !read.own);
|
|
400
|
+
|
|
401
|
+
// The ids in a tree, to keep the unfolded ones that are still there when a plan is read again.
|
|
402
|
+
function treeIds(nodes, out = new Set()) {
|
|
403
|
+
for (const n of nodes) {
|
|
404
|
+
out.add(n.id);
|
|
405
|
+
treeIds(n.children, out);
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
331
409
|
|
|
332
410
|
// What a read of a plan file found (`read` is `{ touch }` for a touch's read, `{ since }` for a read
|
|
333
|
-
// again). A file that cannot be read or is no plan leaves the plan as it was.
|
|
411
|
+
// again). A file that cannot be read or is no plan leaves the plan as it was. Another plan starts
|
|
412
|
+
// folded; the same plan read again keeps the nodes unfolded that it still holds.
|
|
334
413
|
export function takePlan(s, path, text, read = {}) {
|
|
335
414
|
if (!current(s, path, read)) return;
|
|
336
415
|
const plan = parsed(path, text);
|
|
337
|
-
if (keepsOff(s, path, plan)) return;
|
|
416
|
+
if (keepsOff(s, path, plan, read)) return;
|
|
338
417
|
const moving = path !== s.planPath;
|
|
339
418
|
if (read.touch !== undefined) s.taken = read.touch;
|
|
340
|
-
if (moving &&
|
|
419
|
+
if (moving && s.selected === 'plan') s.selected = null;
|
|
420
|
+
const ids = treeIds(plan.tree);
|
|
421
|
+
s.planOpen = moving ? new Set() : new Set([...s.planOpen].filter((id) => ids.has(id)));
|
|
341
422
|
s.planPath = path;
|
|
342
|
-
s.plan = { ...plan, path
|
|
423
|
+
s.plan = { ...plan, path };
|
|
343
424
|
}
|
|
344
425
|
|
|
345
426
|
// ── what the pane shows ──────────────────────────────────────────────────────
|
|
@@ -388,30 +469,23 @@ function callWords(call, now) {
|
|
|
388
469
|
|
|
389
470
|
function planWords(plan) {
|
|
390
471
|
const where = `계획 파일: ${clip(plan.path.split(/[\\/]/).filter(Boolean).slice(-3).join('/'), 120)}`;
|
|
391
|
-
const cut = plan.cut ? ' 파일이 길어 앞부분만 읽었습니다.' : '';
|
|
392
|
-
if (!plan.total) return `${where}. 단계를 찾지 못했습니다.${cut}`;
|
|
393
472
|
const done = plan.hasStates ? `, 완료 ${plan.done}개` : '';
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const what = st.text.replace(/[.!?。]+$/, '');
|
|
399
|
-
return st.status ? `${what}. ${PLAN_STATUS[st.status]}.` : `${what}.`;
|
|
473
|
+
const count = plan.total ? ` 단계 ${plan.total}개${done}.` : '';
|
|
474
|
+
const cut = plan.cut ? ' 파일이 길어 앞부분만 읽었습니다.' : '';
|
|
475
|
+
const many = plan.treeCut ? ' 항목이 많아 앞부분만 보입니다.' : '';
|
|
476
|
+
return `${where}.${count}${cut}${many}`;
|
|
400
477
|
}
|
|
401
478
|
|
|
402
|
-
// What the person pressed last, while it is still on the pane: the plan or
|
|
403
|
-
//
|
|
479
|
+
// What the person pressed last, while it is still on the pane: the plan, a step, or a call under one.
|
|
480
|
+
// A plan's sections fold where they are pressed, and say nothing more below.
|
|
404
481
|
function pressed(s) {
|
|
405
482
|
if (s.plan && s.selected === 'plan') return { key: 'plan', isPlan: true };
|
|
406
|
-
const plan = s.plan?.steps.find((st) => st.key === s.selected);
|
|
407
|
-
if (plan) return plan;
|
|
408
483
|
const all = steps(s);
|
|
409
484
|
return all.find((x) => x.key === s.selected) ?? all.flatMap((x) => x.calls).find((c) => c.key === s.selected);
|
|
410
485
|
}
|
|
411
486
|
|
|
412
487
|
function itemWords(s, item, now) {
|
|
413
488
|
if (item.isPlan) return planWords(s.plan);
|
|
414
|
-
if (item.planStep) return planStepWords(item);
|
|
415
489
|
return item.tool !== undefined ? callWords(item, now) : stepWords(item, now);
|
|
416
490
|
}
|
|
417
491
|
|
|
@@ -460,39 +534,74 @@ function stepRow(ui, s, step, act) {
|
|
|
460
534
|
] });
|
|
461
535
|
}
|
|
462
536
|
|
|
463
|
-
// The plan's row: its title in bold (a Button draws its Text children in place of its label) and
|
|
464
|
-
// how far along it is.
|
|
537
|
+
// The plan's row: its title in bold (a Button draws its Text children in place of its label) and,
|
|
538
|
+
// where its steps say, how far along it is.
|
|
465
539
|
function planHead(ui, s, act) {
|
|
466
540
|
const { Box, Text, Button } = ui;
|
|
467
541
|
const { plan } = s;
|
|
468
|
-
const count = !plan.total ? '
|
|
469
|
-
const arrow = plan.
|
|
542
|
+
const count = !plan.total ? '' : plan.hasStates ? `${plan.done}/${plan.total} 완료` : `${plan.total}단계`;
|
|
543
|
+
const arrow = plan.tree.length
|
|
470
544
|
? Button({ key: 'arrow-plan', label: s.planShut ? '▸' : '▾', plain: true, onPress: () => act.foldPlan() })
|
|
471
545
|
: Text({ children: [' '] });
|
|
472
546
|
const label = room(`계획: ${plan.title}`, act, 2, count);
|
|
473
547
|
return Box({ key: 'row-plan', flexDirection: 'row', columnGap: 1, children: [
|
|
474
548
|
arrow,
|
|
475
549
|
Button({ key: 'plan', label, plain: true, onPress: () => act.select('plan'), children: [Text({ bold: true, children: [label] })] }),
|
|
476
|
-
Text({ children: [count], dimColor: true }),
|
|
550
|
+
...(count ? [Text({ children: [count], dimColor: true })] : []),
|
|
477
551
|
] });
|
|
478
552
|
}
|
|
479
553
|
|
|
480
|
-
//
|
|
481
|
-
|
|
554
|
+
// A node's state on the pane: its checkbox's word, or how many of the checkboxes under it are done.
|
|
555
|
+
const nodeState = (n) => n.count ?? PLAN_STATUS[n.status] ?? '';
|
|
556
|
+
const NODE_STYLE = { head: { bold: true }, story: { bold: true }, 'code-line': { dimColor: true } };
|
|
557
|
+
// A part of a row that keeps its width while the words beside it wrap.
|
|
558
|
+
const fixed = (ui, children) => ui.Box({ flexShrink: 0, children });
|
|
559
|
+
|
|
560
|
+
// A node's row: an arrow where it holds nodes, its number or dash, its words (wrapping on the pane's
|
|
561
|
+
// width; a press on them folds it too) and its state.
|
|
562
|
+
function nodeRow(ui, s, n, depth, act) {
|
|
482
563
|
const { Box, Text, Button } = ui;
|
|
564
|
+
const holds = n.children.length > 0;
|
|
565
|
+
const fold = () => act.foldNode(n.id);
|
|
566
|
+
const words = Text({ children: [n.text], wrap: 'wrap', ...(pick(NODE_STYLE, n.kind) ?? {}) });
|
|
567
|
+
const state = nodeState(n);
|
|
568
|
+
return Box({ key: `row-${n.id}`, flexDirection: 'row', columnGap: 1, paddingLeft: depth * 2, children: [
|
|
569
|
+
holds ? Button({ key: `arrow-${n.id}`, label: s.planOpen.has(n.id) ? '▾' : '▸', plain: true, onPress: fold }) : Text({ children: [' '] }),
|
|
570
|
+
...(n.marker ? [fixed(ui, [Text({ children: [n.marker] })])] : []),
|
|
571
|
+
holds ? Button({ key: n.id, label: n.text, plain: true, onPress: fold, children: [words] }) : words,
|
|
572
|
+
...(state ? [fixed(ui, stateWord(ui, state, n.status === 'in_progress'))] : []),
|
|
573
|
+
] });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// The plan's nodes as rows, what a folded node holds left out: rows while they stay within
|
|
577
|
+
// MAX_TREE_ROWS and MAX_TREE_TEXT characters, then how many more would show.
|
|
578
|
+
function treeRows(ui, s, act) {
|
|
579
|
+
const rows = [];
|
|
580
|
+
let more = 0;
|
|
581
|
+
let used = 0;
|
|
582
|
+
const visit = (nodes, depth) => {
|
|
583
|
+
for (const n of nodes) {
|
|
584
|
+
used += n.text.length + (n.marker?.length ?? 0);
|
|
585
|
+
if (!more && rows.length < MAX_TREE_ROWS && used <= MAX_TREE_TEXT) rows.push(nodeRow(ui, s, n, depth, act));
|
|
586
|
+
else more += 1;
|
|
587
|
+
if (n.children.length && s.planOpen.has(n.id)) visit(n.children, depth + 1);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
visit(s.plan.tree, 1);
|
|
591
|
+
return { rows, more };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// The plan's rows: its title, then its tree unless folded, then a line for the rows past the first 300.
|
|
595
|
+
function planRows(ui, s, act) {
|
|
596
|
+
const { Box, Text } = ui;
|
|
483
597
|
if (!s.plan) return [];
|
|
484
598
|
const rows = [planHead(ui, s, act)];
|
|
485
|
-
if (s.planShut)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
rows.push(Box({ key:
|
|
490
|
-
Button({ key: st.key, label: room(st.title, act, 2, state), plain: true, onPress: () => act.select(st.key) }),
|
|
491
|
-
...stateWord(ui, state, st.status === 'in_progress'),
|
|
492
|
-
] }));
|
|
599
|
+
if (!s.planShut) {
|
|
600
|
+
const tree = treeRows(ui, s, act);
|
|
601
|
+
rows.push(...tree.rows);
|
|
602
|
+
if (tree.more) rows.push(Box({ key: 'plan-more', paddingLeft: 2, children: [Text({ children: [`외 ${tree.more}줄`], dimColor: true })] }));
|
|
603
|
+
if (s.plan.cut || s.plan.treeCut) rows.push(Box({ key: 'plan-cut', paddingLeft: 2, children: [Text({ children: ['계획이 길어 앞부분만 보입니다'], dimColor: true })] }));
|
|
493
604
|
}
|
|
494
|
-
const rest = s.plan.total - shown.length;
|
|
495
|
-
if (rest > 0) rows.push(Box({ key: 'plan-more', paddingLeft: 2, children: [Text({ children: [`외 ${rest}개`], dimColor: true })] }));
|
|
496
605
|
return [...rows, Box({ key: 'plan-gap', marginBottom: 1 })];
|
|
497
606
|
}
|
|
498
607
|
|
|
@@ -518,14 +627,36 @@ async function showPane($, pane) {
|
|
|
518
627
|
else if (!pane.isShown) await $.ui.open({ id: PANE, title: TITLE, focus: true });
|
|
519
628
|
}
|
|
520
629
|
|
|
630
|
+
// Offers the model the to-do tool where it has no task list tool of Claude Code's and the tool is not
|
|
631
|
+
// there yet. An engine without a tool list offers nothing; a registration the engine refuses (`--bare`,
|
|
632
|
+
// a user MCP server of the same name) is said in one line, and the pane goes on with prompts as steps.
|
|
633
|
+
async function offerTodo($) {
|
|
634
|
+
let tools;
|
|
635
|
+
try {
|
|
636
|
+
tools = await $.tool.list();
|
|
637
|
+
} catch {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (tools.some((t) => LIST_TOOLS.has(t.name) || t.name === TODO_TOOL)) return;
|
|
641
|
+
try {
|
|
642
|
+
await $.tool.register(TODO_SPEC);
|
|
643
|
+
} catch (e) {
|
|
644
|
+
$.ui.log(`할 일 도구를 등록하지 못했습니다(${clip(e?.message ?? e, 120)}). 진행 상황 패널은 요청을 단계로 보입니다.`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
521
648
|
// `show` (the default) toggles what the person sees; `on` and `off` set the pane whatever it was.
|
|
649
|
+
// Opening it offers the to-do tool, from the next prompt on.
|
|
522
650
|
export async function runProgress($, args) {
|
|
523
651
|
const want = String(args ?? '').trim().toLowerCase() || 'show';
|
|
524
652
|
if (!['show', 'on', 'off'].includes(want)) return { text: USAGE };
|
|
525
653
|
const pane = (await $.ui.panes()).find((p) => p.id === PANE);
|
|
526
654
|
if (want === 'off' || (want === 'show' && pane?.isShown)) {
|
|
527
655
|
if (pane) await $.ui.close({ id: PANE });
|
|
528
|
-
|
|
656
|
+
return {};
|
|
657
|
+
}
|
|
658
|
+
await showPane($, pane);
|
|
659
|
+
await offerTodo($);
|
|
529
660
|
return {};
|
|
530
661
|
}
|
|
531
662
|
|
|
@@ -554,13 +685,19 @@ async function watchCall($, e, next) {
|
|
|
554
685
|
endCall(call, r, Date.now());
|
|
555
686
|
if (TASK_TOOLS.has(e.tool)) noteTaskTool(s, e, r, Date.now(), since);
|
|
556
687
|
const touched = planTouched(s, e, call);
|
|
557
|
-
if (touched) takePlan(s, touched.path, await readText($, touched.path), { touch: touched.touch });
|
|
688
|
+
if (touched) takePlan(s, touched.path, await readText($, touched.path), { touch: touched.touch, own: touched.own });
|
|
558
689
|
$.ui.invalidate('ui.render');
|
|
559
690
|
}
|
|
560
691
|
}
|
|
561
692
|
|
|
562
|
-
// A file's text, or null where it cannot be read (missing, over the engine's 4 MiB, refused)
|
|
693
|
+
// A file's text, or null where it cannot be read (missing, over the engine's 4 MiB, refused) or is no
|
|
694
|
+
// plain file: a plan path that leads to a pipe or a device would hold the read, and the hooks worker.
|
|
563
695
|
async function readText($, path) {
|
|
696
|
+
try {
|
|
697
|
+
if ((await $.fs.stat(path)).kind !== 'file') return null;
|
|
698
|
+
} catch {
|
|
699
|
+
/* an engine without stat, or nothing there: the read below tells */
|
|
700
|
+
}
|
|
564
701
|
try {
|
|
565
702
|
return await $.fs.read(path);
|
|
566
703
|
} catch {
|
|
@@ -581,6 +718,20 @@ export function registerProgress(on) {
|
|
|
581
718
|
on('command.run', { command: 'banker:progress' }, async ($, e) => runProgress($, e.args)).catch(commandFailed);
|
|
582
719
|
// Every tool call passes here: a fault in this hook leaves the call as if the hook were absent.
|
|
583
720
|
on('tool.call', async ($, e, next) => watchCall($, e, next)).catch(() => undefined);
|
|
721
|
+
// The to-do tool: this hook answers it, and its schema is listed up front rather than behind
|
|
722
|
+
// ToolSearch, so the model sees it without asking.
|
|
723
|
+
on('tool.call', { tool: TODO_TOOL }, async ($, e) => {
|
|
724
|
+
let open = true;
|
|
725
|
+
try {
|
|
726
|
+
open = (await $.ui.panes()).some((p) => p.id === PANE);
|
|
727
|
+
} catch {
|
|
728
|
+
/* the pane list failed: take the list as for an open pane rather than refuse it */
|
|
729
|
+
}
|
|
730
|
+
const answer = answerTodo(ref.s, e, Date.now(), open);
|
|
731
|
+
$.ui.invalidate('ui.render');
|
|
732
|
+
return answer;
|
|
733
|
+
}).catch(() => ({ deny: '진행 상황 패널이 할 일 목록을 받지 못했습니다.' }));
|
|
734
|
+
on('tool.describe', { tool: TODO_TOOL }, async (_$, e, next) => ({ ...(await next(e)), isDeferred: false })).catch(() => undefined);
|
|
584
735
|
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
|
|
585
736
|
if (e.requestId !== PANE) return next(e);
|
|
586
737
|
const redraw = () => $.ui.invalidate('ui.render');
|
|
@@ -590,6 +741,7 @@ export function registerProgress(on) {
|
|
|
590
741
|
select: (key) => { ref.s.selected = ref.s.selected === key ? null : key; redraw(); },
|
|
591
742
|
fold: (key) => { if (!ref.s.open.delete(key)) ref.s.open.add(key); redraw(); },
|
|
592
743
|
foldPlan: () => { ref.s.planShut = !ref.s.planShut; redraw(); },
|
|
744
|
+
foldNode: (id) => { if (!ref.s.planOpen.delete(id)) ref.s.planOpen.add(id); redraw(); },
|
|
593
745
|
};
|
|
594
746
|
return draw($.ui.resolve(e), ref.s, Date.now(), act);
|
|
595
747
|
});
|
package/hooks/register.mjs
CHANGED
|
@@ -47,10 +47,16 @@ async function registerCommands($) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// /progress's plan file, read again at a turn's start and end: a ralph PRD or a plan's checkboxes
|
|
50
|
-
// change without a call this module sees. A read that fails keeps the plan as it was
|
|
50
|
+
// change without a call this module sees. A read that fails keeps the plan as it was, and a path that
|
|
51
|
+
// now leads to no plain file (a pipe would hold the read) is not read.
|
|
51
52
|
async function rereadPlan($) {
|
|
52
53
|
const target = progressPlanTarget();
|
|
53
54
|
if (!target) return;
|
|
55
|
+
try {
|
|
56
|
+
if ((await $.fs.stat(target.path)).kind !== 'file') return;
|
|
57
|
+
} catch {
|
|
58
|
+
/* an engine without stat, or nothing there: the read below tells */
|
|
59
|
+
}
|
|
54
60
|
let text = null;
|
|
55
61
|
try {
|
|
56
62
|
text = await $.fs.read(target.path);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaydash9999/banker-plugins",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
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"
|
|
@@ -280,8 +280,16 @@ export function assemble({ projectDir, outDir, manifestPath } = {}) {
|
|
|
280
280
|
// ---- CLI --------------------------------------------------------------------
|
|
281
281
|
|
|
282
282
|
// Run only when invoked directly (not when imported by a test/other module).
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
// Node resolves links for import.meta.url but not for process.argv[1], so both go through realpath:
|
|
284
|
+
// compared as given, a run through a linked folder would exit 0 without doing anything.
|
|
285
|
+
const invokedDirectly = () => {
|
|
286
|
+
try {
|
|
287
|
+
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
288
|
+
} catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
if (invokedDirectly()) {
|
|
285
293
|
const argv = process.argv.slice(2);
|
|
286
294
|
let projectDir = null, outDir = null, manifestPath = null;
|
|
287
295
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -601,7 +601,15 @@ async function main() {
|
|
|
601
601
|
return serveMain(dir, o);
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
-
|
|
605
|
-
|
|
604
|
+
// Node resolves links for import.meta.url but not for process.argv[1], so both go through realpath:
|
|
605
|
+
// compared as given, a run through a linked folder would exit 0 without doing anything.
|
|
606
|
+
const invokedDirectly = () => {
|
|
607
|
+
try {
|
|
608
|
+
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
609
|
+
} catch {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
if (invokedDirectly()) {
|
|
606
614
|
main().catch((e) => { console.error(String(e?.message || e)); process.exit(1); });
|
|
607
615
|
}
|
|
@@ -38,6 +38,8 @@ export const MIME = {
|
|
|
38
38
|
'.mjs': 'text/javascript;charset=utf-8', '.json': 'application/json;charset=utf-8', '.png': 'image/png',
|
|
39
39
|
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.gif': 'image/gif', '.svg': 'image/svg+xml',
|
|
40
40
|
'.mp4': 'video/mp4', '.webm': 'video/webm', '.mov': 'video/quicktime', '.woff2': 'font/woff2',
|
|
41
|
+
// Notes and logs a report links to open as text in the browser instead of downloading.
|
|
42
|
+
'.md': 'text/plain;charset=utf-8', '.txt': 'text/plain;charset=utf-8', '.log': 'text/plain;charset=utf-8', '.jsonl': 'text/plain;charset=utf-8',
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
// `strict` is the policy above; any other text is a policy as written; nothing means no header.
|
|
@@ -125,7 +125,15 @@ async function main() {
|
|
|
125
125
|
process.on('SIGTERM', quit);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
// Node resolves links for import.meta.url but not for process.argv[1], so both go through realpath:
|
|
129
|
+
// compared as given, a run through a linked folder would exit 0 without doing anything.
|
|
130
|
+
const invokedDirectly = () => {
|
|
131
|
+
try {
|
|
132
|
+
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
if (invokedDirectly()) {
|
|
130
138
|
main().catch((e) => { console.error(String(e?.message || e)); process.exit(1); });
|
|
131
139
|
}
|
|
@@ -468,8 +468,16 @@ export function parseArgs(argv) {
|
|
|
468
468
|
return o;
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
-
|
|
472
|
-
|
|
471
|
+
// Node resolves links for import.meta.url but not for process.argv[1], so both go through realpath:
|
|
472
|
+
// compared as given, a run through a linked folder would exit 0 without doing anything.
|
|
473
|
+
const invokedDirectly = () => {
|
|
474
|
+
try {
|
|
475
|
+
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
476
|
+
} catch {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
if (invokedDirectly()) {
|
|
473
481
|
let o;
|
|
474
482
|
try { o = parseArgs(process.argv.slice(2)); } catch (e) { console.error(e.message); process.exit(2); }
|
|
475
483
|
if (!o.url) {
|
|
@@ -601,7 +601,15 @@ async function main() {
|
|
|
601
601
|
return serveMain(dir, o);
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
-
|
|
605
|
-
|
|
604
|
+
// Node resolves links for import.meta.url but not for process.argv[1], so both go through realpath:
|
|
605
|
+
// compared as given, a run through a linked folder would exit 0 without doing anything.
|
|
606
|
+
const invokedDirectly = () => {
|
|
607
|
+
try {
|
|
608
|
+
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
609
|
+
} catch {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
if (invokedDirectly()) {
|
|
606
614
|
main().catch((e) => { console.error(String(e?.message || e)); process.exit(1); });
|
|
607
615
|
}
|
|
@@ -38,6 +38,8 @@ export const MIME = {
|
|
|
38
38
|
'.mjs': 'text/javascript;charset=utf-8', '.json': 'application/json;charset=utf-8', '.png': 'image/png',
|
|
39
39
|
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.gif': 'image/gif', '.svg': 'image/svg+xml',
|
|
40
40
|
'.mp4': 'video/mp4', '.webm': 'video/webm', '.mov': 'video/quicktime', '.woff2': 'font/woff2',
|
|
41
|
+
// Notes and logs a report links to open as text in the browser instead of downloading.
|
|
42
|
+
'.md': 'text/plain;charset=utf-8', '.txt': 'text/plain;charset=utf-8', '.log': 'text/plain;charset=utf-8', '.jsonl': 'text/plain;charset=utf-8',
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
// `strict` is the policy above; any other text is a policy as written; nothing means no header.
|
|
@@ -125,7 +125,15 @@ async function main() {
|
|
|
125
125
|
process.on('SIGTERM', quit);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
// Node resolves links for import.meta.url but not for process.argv[1], so both go through realpath:
|
|
129
|
+
// compared as given, a run through a linked folder would exit 0 without doing anything.
|
|
130
|
+
const invokedDirectly = () => {
|
|
131
|
+
try {
|
|
132
|
+
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
if (invokedDirectly()) {
|
|
130
138
|
main().catch((e) => { console.error(String(e?.message || e)); process.exit(1); });
|
|
131
139
|
}
|
|
@@ -542,9 +542,17 @@ function main(argv) {
|
|
|
542
542
|
return 0;
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
+
// Node resolves links for import.meta.url but not for process.argv[1], so both go through realpath:
|
|
546
|
+
// compared as given, a run through a linked folder would exit 0 without doing anything.
|
|
545
547
|
// fileURLToPath, not URL.pathname: the latter yields "/C:/..." on Windows and never matches argv[1].
|
|
546
|
-
const invoked =
|
|
547
|
-
|
|
548
|
+
const invoked = () => {
|
|
549
|
+
try {
|
|
550
|
+
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
551
|
+
} catch {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
if (invoked()) {
|
|
548
556
|
try {
|
|
549
557
|
process.exit(main(process.argv.slice(2)));
|
|
550
558
|
} catch (err) {
|