@kaydash9999/banker-plugins 0.20.2 → 0.21.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +203 -1
- package/PRIVACY.md +34 -4
- package/README.md +46 -15
- package/bin/banker.js +228 -51
- package/bin/lib/codex-hooks.mjs +249 -0
- package/bin/lib/telemetry-config.mjs +33 -16
- package/codex/manifest.json +14 -11
- package/codex/skills/graceful-pause/SKILL.md +30 -0
- package/codex/skills/progress/SKILL.md +42 -0
- package/codex/skills/progress/scripts/progress-plan.mjs +722 -0
- package/codex/skills/progress/scripts/progress.mjs +295 -0
- package/codex/transform-matrix.md +19 -14
- package/commands/setup.md +23 -0
- package/hooks/obsidize-hook.mjs +116 -4
- package/hooks/progress-plan.mjs +1 -1
- package/hooks/progress.mjs +143 -38
- package/hooks/register.mjs +13 -3
- package/hooks/telemetry-count-codex.mjs +222 -0
- package/hooks/update-checkin.mjs +15 -7
- package/hooks/update-fetch.mjs +15 -9
- package/hooks/update-notify.mjs +30 -17
- 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/append_wiki/SKILL.md +34 -8
- package/skills/audit-mock/SKILL.md +1 -1
- package/skills/audit-security/SKILL.md +64 -2
- package/skills/compact-wiki/SKILL.md +37 -6
- package/skills/deep-research/SKILL.md +11 -0
- package/skills/docs-setup/SKILL.md +37 -7
- package/skills/lfm/SKILL.md +148 -0
- package/skills/lineage/SKILL.md +25 -15
- package/skills/lineage/lineage.py +564 -301
- 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 +30 -11
- package/skills/obsidizer/obsidize.mjs +50 -12
- package/skills/omc-patch/SKILL.md +86 -23
- package/skills/omc-patch/scripts/omc-patch.mjs +460 -116
- package/skills/payload-mon/SKILL.md +6 -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/rfp-author/SKILL.md +5 -3
- package/skills/rfp-author/references/vendor-rfp-structure.md +1 -1
- package/skills/setup-bypass-permissions/SKILL.md +27 -9
- package/skills/setup-bypass-permissions/scripts/codex-bypass.mjs +135 -0
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +210 -0
- 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 +42 -9
- package/skills/setup-omc-hud/assets/apply-hud.mjs +242 -27
- package/skills/setup-omc-hud/assets/codex-config.mjs +210 -0
- package/skills/setup-omc-hud/assets/codex-status-line.mjs +90 -0
- 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/smart-compact/SKILL.md +58 -12
- package/skills/smart-compact/scripts/codex-gate.mjs +384 -0
- package/skills/tone-compact/SKILL.md +2 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +105 -20
- package/skills/ultra-chain/SKILL.md +12 -7
- 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 +14 -2
- 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
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
|
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* banker 텔레메트리 count 훅 (Codex Stop) - Codex 세션이 banker 스킬을 쓴 턴이 끝날 때 로컬 usage-log 에
|
|
4
|
+
* `banker:<name>\t<hour>` 한 줄을 남긴다. Claude Code 의 telemetry-count(.mjs, -skill.mjs)에 해당한다.
|
|
5
|
+
*
|
|
6
|
+
* Codex 0.144.5 실측으로 스킬이 쓰이는 길은 둘이다.
|
|
7
|
+
* - 모델이 고른 스킬: 도구 호출로 SKILL.md 를 읽는다(Bash `cat <CODEX_HOME>/skills/banker-<name>/SKILL.md`).
|
|
8
|
+
* - 사용자가 $banker-<name> 으로 부른 스킬: Codex 가 도구 호출 없이 `<skill>\n<name>..</name>\n<path>..</path>` 로
|
|
9
|
+
* 시작하는 사용자 역할 메시지로 본문을 넣는다. 이때는 머리의 <path> 줄만 보고 본문은 보지 않는다.
|
|
10
|
+
* 도구 호출마다 도는 PreToolUse 대신 턴마다 한 번 도는 Stop 에서, 훅 입력의 transcript_path(rollout) 중
|
|
11
|
+
* 이번 턴(task_started 의 turn_id 뒤)의 두 기록에 `skills/banker-<name>/SKILL.md` 가 있으면 센다. 나눠 읽어도 한 번이고, Stop 이 한 턴에 두 번 와도
|
|
12
|
+
* 최근 턴 키(configDir 의 codex-count-turns)로 한 번만 센다. 기록은 끝에서부터 32 MB 까지만 읽는다.
|
|
13
|
+
*
|
|
14
|
+
* fail-closed 규약은 Claude 쪽과 같다(telemetry-count-codex.test.mjs 가 검증):
|
|
15
|
+
* - countingActive() 가 아니면 로컬 쓰기조차 하지 않는다.
|
|
16
|
+
* - 이름은 SKILL_NAME_RE 전체 일치이고 이 Codex 에 설치된 banker 스킬(<CODEX_HOME>/skills/banker-<name>/SKILL.md)만.
|
|
17
|
+
* 모델이 `echo skills/banker-<아무 글>/SKILL.md` 를 돌려 체크인의 이름 칸으로 글을 내보내지 못하게 한다. 턴 키 파일은 일반 파일만 읽고, 링크 자리에 쓰지 않는다.
|
|
18
|
+
* - 어떤 오류도 삼키고 항상 exit 0. 도구 호출을 절대 막지 않는다. 표준 출력은 비운다.
|
|
19
|
+
*/
|
|
20
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { basename, dirname, isAbsolute, join } from 'node:path';
|
|
23
|
+
|
|
24
|
+
import { appendUsageLog, CODEX_SHAPE_MISS_PATH, configDir, countingActive, readRegular, recordUsedSkill, SKILL_NAME_RE, writeByRename } from '../bin/lib/telemetry-config.mjs';
|
|
25
|
+
|
|
26
|
+
const SKILL_PATH_RE = /skills[\\/]+banker-([a-z0-9_-]{1,64})[\\/]+SKILL\.md/gi;
|
|
27
|
+
const KEEP_TURNS = 100;
|
|
28
|
+
export const TURNS_FILE = () => join(configDir(), 'codex-count-turns');
|
|
29
|
+
|
|
30
|
+
function readStdin() {
|
|
31
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
32
|
+
let data = '';
|
|
33
|
+
process.stdin.setEncoding('utf8');
|
|
34
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
35
|
+
process.stdin.on('end', () => resolvePromise(data));
|
|
36
|
+
process.stdin.on('error', rejectPromise);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const MAX_READ = 32 << 20;
|
|
41
|
+
const FIRST_READ = 1 << 20;
|
|
42
|
+
|
|
43
|
+
// 글에 나오는 banker 스킬 이름(banker:<name>), 중복 없이.
|
|
44
|
+
export function namesIn(text, names = new Set()) {
|
|
45
|
+
for (const m of text.matchAll(SKILL_PATH_RE)) {
|
|
46
|
+
const name = `banker:${m[1]}`;
|
|
47
|
+
if (SKILL_NAME_RE.test(name)) names.add(name);
|
|
48
|
+
}
|
|
49
|
+
return names;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// rollout 끝에서 max 바이트의 줄과, 파일 전체를 읽었는지. 일반 파일만: 기다리지 않게 O_NONBLOCK 으로 열고 연 것을 fstat 으로 본다. 앞에서 잘린 첫 줄은 버린다.
|
|
53
|
+
function tailLines(file, max) {
|
|
54
|
+
const fd = openSync(file, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
55
|
+
try {
|
|
56
|
+
if (!fstatSync(fd).isFile()) return { lines: [], whole: true };
|
|
57
|
+
const size = fstatSync(fd).size;
|
|
58
|
+
const start = Math.max(0, size - max);
|
|
59
|
+
const buf = Buffer.alloc(size - start);
|
|
60
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
61
|
+
const lines = buf.toString('utf8').split('\n');
|
|
62
|
+
if (start > 0) lines.shift();
|
|
63
|
+
return { lines, whole: start === 0 };
|
|
64
|
+
} finally {
|
|
65
|
+
closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const isCall = (rec) => rec?.type === 'response_item' && /_call$/.test(String(rec?.payload?.type ?? ''));
|
|
70
|
+
const SKILL_HEAD_RE = /^<skill>\n<name>[^\n<]{1,80}<\/name>\n<path>([^\n<]{1,4096})<\/path>\n/;
|
|
71
|
+
|
|
72
|
+
// $banker-<name> 으로 부른 스킬을 Codex 가 넣은 메시지의 <path>. 그 밖의 기록은 ''.
|
|
73
|
+
function injectedPath(rec) {
|
|
74
|
+
const p = rec?.payload;
|
|
75
|
+
if (rec?.type !== 'response_item' || p?.type !== 'message' || p.role !== 'user' || !Array.isArray(p.content)) return '';
|
|
76
|
+
const text = p.content[0]?.text;
|
|
77
|
+
return typeof text === 'string' ? (SKILL_HEAD_RE.exec(text)?.[1] ?? '') : '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 이번 턴(turn_id 의 task_started 뒤)의 도구 호출과 넣은 스킬 머리에서 읽은 banker 스킬 이름. 사용자 글과 모델 답은 세지 않는다.
|
|
81
|
+
// 턴 시작은 대개 기록 끝 가까이에 있으므로 끝 1 MB 부터 읽고, 못 찾으면 두 배씩 max 까지 넓힌다.
|
|
82
|
+
// 기록 전체를 읽고도 task_started 가 하나도 없으면 null: Codex 는 턴마다 task_started 를 쓰므로 기록 형식이 바뀐 것이다.
|
|
83
|
+
// 이 턴의 것만 없을 때(시작이 기록되지 않는 턴)는 형식 변경으로 보지 않는다.
|
|
84
|
+
export function turnSkills(file, turnId, max = MAX_READ) {
|
|
85
|
+
for (let size = Math.min(FIRST_READ, max); ; size = Math.min(size * 2, max)) {
|
|
86
|
+
const { lines, whole } = tailLines(file, size);
|
|
87
|
+
const start = turnStart(lines, turnId);
|
|
88
|
+
if (start >= 0) return skillsAfter(lines, start);
|
|
89
|
+
if (whole) return lines.some((l) => l.trim()) && !lines.some((l) => l.includes('"task_started"')) ? null : [];
|
|
90
|
+
if (size >= max) return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// rollout 첫 기록(session_meta)의 Codex 판. Codex 기록이 아니면 ''.
|
|
95
|
+
export function codexVersionOf(file) {
|
|
96
|
+
try {
|
|
97
|
+
const fd = openSync(file, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
98
|
+
try {
|
|
99
|
+
if (!fstatSync(fd).isFile()) return '';
|
|
100
|
+
const buf = Buffer.alloc(Math.min(fstatSync(fd).size, 8 << 20)); // session_meta 는 지침을 품어 클 수 있다
|
|
101
|
+
readSync(fd, buf, 0, buf.length, 0);
|
|
102
|
+
const rec = JSON.parse(buf.toString('utf8').split('\n')[0]);
|
|
103
|
+
return rec?.type === 'session_meta' ? String(rec.payload?.cli_version ?? '?').slice(0, 40) : '';
|
|
104
|
+
} finally {
|
|
105
|
+
closeSync(fd);
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 이 Codex 판의 기록을 읽지 못했음을 남긴다(같은 판은 한 번만). 다음 세션 시작 때 update-notify --codex 가 알린다.
|
|
113
|
+
function noteShapeMiss(file) {
|
|
114
|
+
const version = codexVersionOf(file);
|
|
115
|
+
if (!version) return;
|
|
116
|
+
let prev = {};
|
|
117
|
+
try { prev = JSON.parse(readRegular(CODEX_SHAPE_MISS_PATH)); } catch { /* 없음 */ }
|
|
118
|
+
if (prev?.version !== version) writeByRename(CODEX_SHAPE_MISS_PATH, JSON.stringify({ version, notified: false }));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function turnStart(lines, turnId) {
|
|
122
|
+
let start = lines.length - 1;
|
|
123
|
+
while (start >= 0 && !(lines[start].includes('"task_started"') && lines[start].includes(turnId))) start--;
|
|
124
|
+
return start;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function skillsAfter(lines, start) {
|
|
128
|
+
const names = new Set();
|
|
129
|
+
for (const line of lines.slice(start + 1)) {
|
|
130
|
+
if (!line.includes('SKILL.md')) continue;
|
|
131
|
+
let rec;
|
|
132
|
+
try { rec = JSON.parse(line); } catch { continue; }
|
|
133
|
+
if (isCall(rec)) namesIn(JSON.stringify(rec.payload), names);
|
|
134
|
+
else namesIn(injectedPath(rec), names);
|
|
135
|
+
}
|
|
136
|
+
return [...names];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const ID_RE = /^[A-Za-z0-9._-]{1,80}$/;
|
|
140
|
+
|
|
141
|
+
// 이 턴의 키. session_id 와 turn_id 가 모양이 맞을 때만(파일 한 줄 형식을 깨지 않게).
|
|
142
|
+
export function turnKey(payload) {
|
|
143
|
+
const s = payload?.session_id;
|
|
144
|
+
const t = payload?.turn_id;
|
|
145
|
+
return typeof s === 'string' && typeof t === 'string' && ID_RE.test(s) && ID_RE.test(t) ? `${s}:${t}` : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readTurns() {
|
|
149
|
+
try {
|
|
150
|
+
return readRegular(TURNS_FILE()).split('\n').filter(Boolean);
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 링크나 일반 파일이 아닌 자리에는 쓰지 않는다. 새 파일을 만들어 바꿔 넣는다.
|
|
157
|
+
function writeTurns(lines) {
|
|
158
|
+
const file = TURNS_FILE();
|
|
159
|
+
try {
|
|
160
|
+
if (!lstatSync(file).isFile()) return;
|
|
161
|
+
} catch (e) {
|
|
162
|
+
if (e?.code !== 'ENOENT') return;
|
|
163
|
+
}
|
|
164
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
165
|
+
try {
|
|
166
|
+
writeFileSync(tmp, `${lines.slice(-KEEP_TURNS).join('\n')}\n`, { flag: 'wx', mode: 0o600 });
|
|
167
|
+
renameSync(tmp, file);
|
|
168
|
+
} catch {
|
|
169
|
+
rmSync(tmp, { force: true });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 이 세션의 Codex 홈: rollout 이 <홈>/sessions/YYYY/MM/DD/ 에 있으면 그 홈(상대 경로 CODEX_HOME 도 Codex 가 푼 그대로),
|
|
174
|
+
// 아니면 CODEX_HOME, 그것도 없으면 ~/.codex.
|
|
175
|
+
export function codexHomeOf(transcript, env = process.env) {
|
|
176
|
+
const sessions = dirname(dirname(dirname(dirname(String(transcript)))));
|
|
177
|
+
if (isAbsolute(String(transcript)) && basename(sessions) === 'sessions') return dirname(sessions);
|
|
178
|
+
return env.CODEX_HOME || join(homedir(), '.codex');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 이 Codex 에 설치된 banker 스킬인지.
|
|
182
|
+
export const installed = (name, home) => existsSync(join(home, 'skills', `banker-${name.slice('banker:'.length)}`, 'SKILL.md'));
|
|
183
|
+
|
|
184
|
+
// 이 턴에 아직 세지 않은 이름만 센다.
|
|
185
|
+
export function countOnce(payload) {
|
|
186
|
+
const key = turnKey(payload);
|
|
187
|
+
const file = typeof payload?.transcript_path === 'string' ? payload.transcript_path : '';
|
|
188
|
+
const home = codexHomeOf(file);
|
|
189
|
+
const got = key && file ? turnSkills(file, payload.turn_id) : [];
|
|
190
|
+
if (got === null) {
|
|
191
|
+
noteShapeMiss(file);
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
const names = got.filter((n) => installed(n, home));
|
|
195
|
+
if (!names.length) return [];
|
|
196
|
+
const seen = readTurns();
|
|
197
|
+
const fresh = names.filter((n) => !seen.includes(`${key}\t${n}`));
|
|
198
|
+
if (!fresh.length) return [];
|
|
199
|
+
for (const n of fresh) {
|
|
200
|
+
appendUsageLog(n);
|
|
201
|
+
recordUsedSkill(n);
|
|
202
|
+
}
|
|
203
|
+
writeTurns([...seen, ...fresh.map((n) => `${key}\t${n}`)]);
|
|
204
|
+
return fresh;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function main() {
|
|
208
|
+
let payload;
|
|
209
|
+
try {
|
|
210
|
+
payload = JSON.parse(await readStdin());
|
|
211
|
+
} catch {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!countingActive()) return;
|
|
215
|
+
countOnce(payload);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (process.argv[1] && /telemetry-count-codex\.mjs$/.test(process.argv[1])) {
|
|
219
|
+
main()
|
|
220
|
+
.catch(() => {})
|
|
221
|
+
.finally(() => process.exit(0));
|
|
222
|
+
}
|
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
|
}
|