@kaydash9999/banker-plugins 0.22.0 → 0.23.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +68 -0
- package/PRIVACY.md +1 -0
- package/README.md +39 -13
- package/bin/banker.js +118 -22
- package/bin/lib/codex-hooks.mjs +116 -18
- package/bin/lib/codex-names.mjs +4 -0
- package/bin/lib/codex-routes.mjs +93 -1
- package/bin/lib/command-skill.cjs +37 -6
- package/codex/command-skills/front-qa/SKILL.md +1 -1
- package/codex/command-skills/setup/SKILL.md +4 -4
- package/codex/hooks.json +5 -5
- package/codex/manifest.json +18 -15
- package/codex/skills/progress/scripts/progress.mjs +44 -14
- package/codex/skills/teamwork/SKILL.md +143 -0
- package/codex/skills/teamwork/scripts/teamwork.mjs +1007 -0
- package/codex/transform-matrix.md +5 -3
- package/hooks/obsidize-hook.mjs +75 -32
- package/hooks/telemetry-count-codex.mjs +238 -57
- package/hooks/update-fetch.mjs +2 -1
- package/hooks/update-notify.mjs +82 -20
- package/package.json +3 -1
- package/skills/3d-intro-build/SKILL.md +101 -20
- package/skills/3d-intro-build/references/NOTICE.md +9 -2
- package/skills/3d-intro-build/references/assemble.mjs +113 -22
- package/skills/3d-intro-build/references/azure-adapter.mjs +110 -24
- package/skills/3d-intro-build/references/clip-post.mjs +443 -0
- package/skills/3d-intro-build/references/dive-mode.mjs +299 -0
- package/skills/3d-intro-build/references/intro-fixes.css +11 -0
- package/skills/3d-intro-build/references/prompts.md +51 -4
- package/skills/3d-intro-build/references/scrub-engine.js +1 -0
- package/skills/3d-intro-build/references/step-nav.css +29 -0
- package/skills/3d-intro-build/references/step-nav.js +284 -106
- package/skills/3d-intro-build/references/verify-intro.mjs +373 -32
- package/skills/3d-intro-build/references/verify.md +6 -0
- package/skills/3d-intro-build/references/video-pool.mjs +142 -12
- package/skills/3d-intro-setup/SKILL.md +20 -7
- package/skills/3d-intro-setup/references/azure-adapter.mjs +110 -24
- package/skills/3d-intro-setup/references/video-pool.mjs +142 -12
- package/skills/all-in-one/SKILL.md +125 -86
- package/skills/omc-patch/SKILL.md +1 -1
- package/skills/payload-mon/SKILL.md +1 -1
- package/skills/play-qa/SKILL.md +6 -4
- package/skills/progress-mon/README.md +164 -0
- package/skills/progress-mon/SKILL.md +165 -0
- package/skills/progress-mon/assets/fixture/README.md +1 -0
- package/skills/progress-mon/assets/fixture/arch.json +33 -0
- package/skills/progress-mon/assets/fixture/commits.json +7 -0
- package/skills/progress-mon/assets/fixture/config.json +30 -0
- package/skills/progress-mon/assets/fixture/events.jsonl +7 -0
- package/skills/progress-mon/assets/fixture/flow.json +44 -0
- package/skills/progress-mon/assets/fixture/items.json +14 -0
- package/skills/progress-mon/assets/fixture/req-spec.json +31 -0
- package/skills/progress-mon/assets/fixture/reqs.json +11 -0
- package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +58 -0
- package/skills/progress-mon/assets/fixture/variants/check-auto/arch.json +15 -0
- package/skills/progress-mon/assets/fixture/variants/check-overlap/arch.json +14 -0
- package/skills/progress-mon/assets/fixture/variants/history-manual/config.json +13 -0
- package/skills/progress-mon/assets/fixture/variants/history-manual/items.json +7 -0
- package/skills/progress-mon/assets/fixture/variants/workflow-wide/flow.json +35 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a01.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a02.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a03.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a04.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/journal.jsonl +7 -0
- package/skills/progress-mon/references/arch.md +280 -0
- package/skills/progress-mon/references/authoring.md +199 -0
- package/skills/progress-mon/references/config.md +115 -0
- package/skills/progress-mon/references/flow.md +75 -0
- package/skills/progress-mon/references/inventory.md +58 -0
- package/skills/progress-mon/references/items.md +74 -0
- package/skills/progress-mon/references/req-spec.md +74 -0
- package/skills/progress-mon/references/reqs.md +30 -0
- package/skills/progress-mon/scripts/index.js +31 -0
- package/skills/progress-mon/scripts/pm-check.mjs +549 -0
- package/skills/progress-mon/scripts/pm-config.mjs +343 -0
- package/skills/progress-mon/scripts/pm-data.mjs +394 -0
- package/skills/progress-mon/scripts/pm-html.mjs +77 -0
- package/skills/progress-mon/scripts/pm-memory.mjs +333 -0
- package/skills/progress-mon/scripts/pm-render.mjs +1644 -0
- package/skills/progress-mon/scripts/pm-server.mjs +781 -0
- package/skills/progress-mon/scripts/pm-sources.mjs +501 -0
- package/skills/progress-mon/scripts/progress-mon.mjs +167 -0
- package/skills/ralph-qa/SKILL.md +107 -17
- package/skills/ralph-qa/references/gate-evidence.mjs +457 -0
- package/skills/ralph-qa/references/payload-scan.mjs +19 -2
- package/skills/ralph-qa/references/run-folder.mjs +299 -0
- package/skills/ralph-qa/references/seat-payload.mjs +60 -13
- package/skills/ralph-qa/references/seat-run.mjs +200 -159
- package/skills/remains/SKILL.md +2 -0
- package/skills/remains/scripts/boxes.mjs +6 -2
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +19 -8
- package/skills/setup-mcp/SKILL.md +1 -1
- package/skills/setup-omc-hud/SKILL.md +2 -1
- package/skills/setup-omc-hud/assets/codex-config.mjs +19 -8
- package/skills/ultra-chain/SKILL.md +32 -14
- package/skills/ultra-chain/references/chain-ledger.mjs +111 -3
- package/skills/ultra-init/SKILL.md +30 -22
- package/skills/ultraqa/NOTICE.md +34 -0
- package/skills/ultraqa/SKILL.md +197 -0
- package/skills/ultraqa/references/gate-evidence.mjs +457 -0
- package/skills/ultraqa/references/run-folder.mjs +299 -0
- package/skills/ultraqa/references/tmux-drive.md +52 -0
- package/skills/ultraqa/references/uqa-run.mjs +339 -0
- package/skills/update-banker/SKILL.md +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Transform matrix: Claude Code surfaces → Codex CLI
|
|
2
2
|
|
|
3
3
|
Source of truth: this repo's `skills/<name>/` + `commands/<name>.md`. `codex/manifest.json` tags each surface.
|
|
4
|
-
The generator (`banker setup --codex`) applies the rules below for `target: both` and `codex-only` surfaces (`claude-only` surfaces are never written to Codex — currently there are none). A `codex-only` surface has its source in `codex/skills/<name>/` and is never part of the Claude plugin (currently `graceful-pause` and `
|
|
4
|
+
The generator (`banker setup --codex`) applies the rules below for `target: both` and `codex-only` surfaces (`claude-only` surfaces are never written to Codex — currently there are none). A `codex-only` surface has its source in `codex/skills/<name>/` and is never part of the Claude plugin (currently `graceful-pause`, `progress` and `teamwork`).
|
|
5
5
|
|
|
6
6
|
| Source (Claude) | Codex destination | Transform |
|
|
7
7
|
|---|---|---|
|
|
@@ -19,8 +19,8 @@ The generator (`banker setup --codex`) applies the rules below for `target: both
|
|
|
19
19
|
- `--scope user` (default) → `~/.codex/…`. `--scope project` → `./.codex/…`.
|
|
20
20
|
|
|
21
21
|
## Runtime-aware surfaces (both) — Codex uses OMX / Codex equivalents
|
|
22
|
-
|
|
23
|
-
- 오케스트레이터 `all-in-one`·`ultra-init`·커맨드 `front-qa` → OMX `ralplan`/`ralph
|
|
22
|
+
`claude-only` 표면은 `ultraqa` 하나다(Codex 는 OMX 의 `$ultraqa` 를 쓰므로 npm 설치와 Codex 플러그인 스킬 목록에서 빠진다). 그 밖의 Claude 플러그인 표면은 `both`, Codex 에만 있는 `codex-only` 는 `graceful-pause`, `progress`, `teamwork`. Codex 이름이나 설명이 다른 `both` 스킬은 manifest 의 `codexName`, `codexDescription` 으로 정한다(`setup-omc` 는 `setup-omx`, `omc-reference` 는 `omx-reference`, `setup-omc-hud` 는 `setup-omx-hud`, 설명만 다른 것은 `append_wiki`, `compact-wiki`, `setup-tmux`, 커맨드 `setup`). 플러그인 길은 `codex/renamed-skills/` 의 생성 사본, npm 길은 설치 때 바꿔 쓴다. OMC/Claude 에 결합됐던 표면은 본문이 런타임 인식으로 작성돼 Codex에서도 동작한다 — Codex에선 OMC 대신 oh-my-codex(OMX)의 동명 스킬을 쓴다:
|
|
23
|
+
- 오케스트레이터 `all-in-one`·`ultra-init`·커맨드 `front-qa` → OMX `ralplan`/`ralph`. 마지막 QA 단계는 두 런타임 모두 banker `ralph-qa` 하나다(`--qa` 목표를 `--gates` 로 넘김, OMX `ultraqa` 에 기대지 않음). `ultra-init` 은 0.14.0 부터 ultragoal 원장을 쓰지 않는다(OMC 5.6.1 checkpoint 가 `/goal` 스냅샷을 요구해 ralph 와 Stop 훅 루프가 겹친다). Claude 쪽은 `ralph` 가 부르는 `omc ralph verify` 때문에 전역 `omc` CLI 가 플러그인과 같은 5.x 여야 한다(사전 점검은 종료 코드가 아니라 출력 문구로).
|
|
24
24
|
- `setup-omc` → `omx setup`(본문에 이미 존재). `setup-omc-hud` → Codex 자신의 상태표시줄이면 `[tui] status_line` 을 OMX 프리셋으로 켜고 끔(`assets/codex-status-line.mjs`, 없을 때만 넣고 banker 가 넣은 줄만 뺌), 같은 머신의 Claude Code 상태표시줄이면 그대로 설치(대상을 말하지 않으면 먼저 묻는다). `setup-stitch` → `codex mcp add`. `setup-bypass-permissions` → 대상 `codex` 면 `config.toml` 최상위 `approval_policy = "never"`, `sandbox_mode = "danger-full-access"`(`scripts/codex-bypass.mjs`, 켜기 전 값 기록과 `off` 원복, 조직 정책 확인), 대상 `claude` 면 같은 머신의 Claude Code 설정.
|
|
25
25
|
- `compact-copy` → Codex 내장 `/copy`(경로는 Codex 규약). `omc-reference` → Codex는 OMX 카탈로그 기준. `setup` 커맨드 → Codex는 프롬프트 선택 UX.
|
|
26
26
|
`setup-insane-search` 도 `both`: Codex에선 `codex plugin add insane-research-codex@gptaku-codex`.
|
|
@@ -50,8 +50,10 @@ The generator (`banker setup --codex`) applies the rules below for `target: both
|
|
|
50
50
|
- `remains` — the same procedure in both runtimes; box runs go through `scripts/boxes.mjs` (Codex: `~/.codex/skills/banker-remains/scripts/boxes.mjs`), which needs `ssh`, `scp` and `git` on the machine and key-only logins to the boxes in `~/.config/banker/test-boxes.json`. It reads and runs tests only: no fix, no commit, no push.
|
|
51
51
|
- `ultra-chain` — the same nine stages in both runtimes. Claude=OMC 5 (`ralplan --deliberate`, `ralph --critic=critic`); Codex=OMX (`$ralplan --deliberate`, then `$ralph` with the skill's own critic loop through `spawn_agent`, because OMX `ralph` has no `--critic`). Both add a `ralph-qa` plan quorum after ralplan (two reviewers, no option to add more), run OS tests through the sibling `remains/scripts/boxes.mjs` (Codex: `banker-remains`), judge screenshots with a fresh agent (OMX 0.20.2's `visual-verdict` is a deprecated stub), and render the process report from the skill's own ledger (`lineage` reads Claude Code sessions only). Box runs need `ssh`, `scp` and `git`; screenshots need Playwright with Chromium; under Codex's `workspace-write` sandbox the ssh calls and the review seats' home writes may ask for approval.
|
|
52
52
|
- `theme-selection` — the same procedure in both runtimes; the engine is `scripts/themes.mjs` (Codex: `~/.codex/skills/banker-theme-selection/scripts/themes.mjs`), which reads `assets/palettes.json` beside it. Pure Node with no network; it writes only the file `--out` names (refused when that file itself is a symbolic link), and the skill asks before it overwrites one that exists.
|
|
53
|
+
- `progress-mon` — the same procedure in both runtimes; the engine is `scripts/progress-mon.mjs` (Codex npm install: `~/.codex/skills/banker-progress-mon/scripts/progress-mon.mjs`; plugin install: `~/.codex/plugins/cache/<marketplace>/banker/<version>/skills/progress-mon/scripts/progress-mon.mjs`), every command with `--runtime claude|codex`. The data folder is `.omc/progress-mon/` for Claude Code and `.omx/progress-mon/` for Codex. Pure Node with no network; the server binds 127.0.0.1 by default (`--host` takes a private IPv4 only). On Codex `start`, `stop`, `status` and `init` each ask for `sandbox_permissions: "require_escalated"` (`init` writes the git exclude list), and since closing the session may stop the server, a long-lived screen runs from the user's terminal (`serve`). `init --memory agents-md` writes its rule to `AGENTS.override.md` when that file has content of its own, as Codex then reads it instead of `AGENTS.md`. Not the same as `/progress` (the session's own task panel).
|
|
53
54
|
- `/graceful-pause` — in Claude Code a function-hooks command (`hooks/register.mjs`, 2.1.289+, not a manifest surface) registered `immediate`, so it runs mid-turn; it appends one hidden user row the running turn reads at its next model request. In Codex the `codex-only` skill `graceful-pause` (`$banker-graceful-pause`): a message sent with Enter while a turn runs is held as "Messages to be submitted after next tool call" and put into the same turn when the running tool call ends, so the session stops after the current step (Codex 0.144.5: verified with `sleep 40` in a real TUI).
|
|
54
55
|
- `/progress` — in Claude Code a mod panel (`hooks/progress.mjs`). In Codex the `codex-only` skill `progress` (`$banker-progress`, `codex/skills/progress/scripts/progress.mjs`): it reads this session's rollout, found by `CODEX_THREAD_ID` (the id at the end of the rollout file name), and prints the last plan file the session wrote or read (tool-call commands and `apply_patch` file heads; `.omx/plans/` counts like `.omc/plans/`) as a tree with the same engine (`progress-plan.mjs`, a byte-identical copy kept by `scripts/sync-adapter.js`), then the last `update_plan` steps (or the typed requests) with the recent tool calls under the step in progress. Not a live panel: `--pane` opens a tmux side pane that redraws every 2 s.
|
|
56
|
+
- `teamwork` — `codex-only` (`$banker-teamwork`, plugin `$banker:teamwork`, `codex/skills/teamwork/scripts/teamwork.mjs`): peer work between the Codex sessions of one Codex home. `on` copies the engine to `$CODEX_HOME/teamwork/bin/` beside an sh wrapper, adds a `SessionStart` and a `UserPromptSubmit` hook last under each event (taking the same `hooks.json.lock` as `bin/lib/codex-hooks.mjs`, `timeout: 5`, fixed in place; the command ends with `--rev <hash of engine and wrapper>`, so a newer engine asks for trust once more) and writes `$CODEX_HOME/rules/teamwork.rules`, a `prefix_rule` that lets only the wrapper's `peers`, `role`, `send`, `reply`, `status` and `version` run outside the sandbox (`on`, `off`, `uninstall` ask for approval; a same-named file without the `# teamwork` first line is left alone). The wrapper and the hooks run outside the sandbox, so `node`, `codex`, `ps` and `sh` are recorded as absolute paths at `on` and nothing is looked up on PATH afterwards; programs, and a Codex home, under a temp folder, the session folder or its git top, or a `writable_roots` entry are refused (exit 6). Requests and results travel by `codex queue --thread <UUID>` (Codex 0.149.0 or later). Windows ends `on` with exit 6 and the reason: on Codex 0.160.1 for Windows, `codex execpolicy check` matches the rule only for the wrapper called directly, not for the PowerShell call form (`& '<wrapper>' send ...`), so every call would ask for approval. `banker uninstall --codex` removes it only when no other way of banker into that Codex home is left.
|
|
55
57
|
- Update notice and usage counting — Claude Code uses the plugin's own hooks (`SessionStart`, `PostToolUse` Skill, `UserPromptExpansion`). Codex gets `SessionStart` (`update-notify.mjs --codex`, shown as a warning line in the TUI) and `Stop` (`telemetry-count-codex.mjs`, once per turn rather than per tool call). Codex shows a skill in two ways and the Stop hook counts both from this turn of the rollout: a model-picked skill is read by a tool call (`cat .../skills/banker-<name>/SKILL.md`), and a skill the user names with `$banker-<name>` is injected without a tool call as a user-role message starting `<skill>\n<name>..</name>\n<path>..</path>`, of which only the `<path>` head is read. Codex runs trusted hooks outside the sandbox, so the update query and check-in reach the network as in Claude Code.
|
|
56
58
|
- Rollout shape — `lineage`, `progress` and the Stop hook read Codex's session record, whose shape Codex does not promise (0.144 and 0.160 already differ: event `user_message`/`agent_message` vs `item_completed` items, `exec_command` calls vs `exec` code). A shape banker cannot read is never an empty result: lineage stops with exit 2 naming the Codex version when a rollout gives no user turn, progress prints a warning line, and the Stop hook, finding no `task_started` for the turn in the whole record, notes the version in `codex-shape-miss.json` so the next SessionStart (`update-notify.mjs --codex`) shows one warning. `banker doctor` checks the newest rollout (turn start, user text, tool calls) after a Codex update.
|
|
57
59
|
|
package/hooks/obsidize-hook.mjs
CHANGED
|
@@ -56,35 +56,46 @@ const CODEX = process.argv.includes('--codex');
|
|
|
56
56
|
// JSON names, regular single-name pages, a canonicalizer pinned to the checked omx_wiki/ of the session folder).
|
|
57
57
|
const OMX_WIKI_WRITE_RE = /^\s*(?:&\s*)?['"]?omx(?:\.cmd|\.ps1)?['"]?\s+wiki\s+(?:wiki_)?(?:ingest|add)\b/;
|
|
58
58
|
|
|
59
|
-
// A command's statements, split at `;`, `&&`, `||` and line ends outside quotes
|
|
60
|
-
//
|
|
61
|
-
// sh a
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
59
|
+
// A command's statements, split at `;`, `&&`, `||` and line ends outside quotes, by the rules of the shell Codex runs
|
|
60
|
+
// it in: PowerShell on Windows, sh elsewhere (shell defaults by process.platform).
|
|
61
|
+
// - sh: a backslash keeps the next character, outside quotes and inside double quotes. A backtick outside quotes opens
|
|
62
|
+
// a command substitution that the next backtick closes; its inside is part of the statement around it.
|
|
63
|
+
// - PowerShell: a backslash is an ordinary character (`cd C:\work\; omx wiki add` is two statements). A backtick keeps
|
|
64
|
+
// the next character, outside quotes and inside double quotes.
|
|
65
|
+
// Inside single quotes nothing is special. A quote or backtick left open (an apostrophe in a here-string or a comment)
|
|
66
|
+
// leaves the scan inside it to the end; then the plain split is taken too, so such a command is not missed.
|
|
67
|
+
const SHELL_RULES = {
|
|
68
|
+
sh: { escape: (c, quote) => c === '\\' && quote !== "'", opens: `"'\``, },
|
|
69
|
+
powershell: { escape: (c, quote) => c === '`' && quote !== "'", opens: `"'` },
|
|
70
|
+
};
|
|
71
|
+
const PAIRS = new Set(['&&', '||']);
|
|
72
|
+
const isBreak = (c, two) => c === ';' || c === '\n' || c === '\r' || PAIRS.has(two);
|
|
73
|
+
export function statements(command, shell = process.platform === 'win32' ? 'powershell' : 'sh') {
|
|
74
|
+
const rule = SHELL_RULES[shell] ?? SHELL_RULES.sh;
|
|
75
|
+
const st = { out: [], cur: '', quote: '' };
|
|
76
|
+
for (let i = 0; i < command.length; i++) i += scan(st, rule, command[i], command.slice(i, i + 2));
|
|
77
|
+
return st.quote ? [...st.out, st.cur, ...command.split(/;|&&|\|\||\r?\n/)] : [...st.out, st.cur];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// One character of statements(): it joins the statement or ends one. Returns how many more characters it took (0, 1).
|
|
81
|
+
function scan(st, rule, c, two) {
|
|
82
|
+
if (rule.escape(c, st.quote)) {
|
|
83
|
+
st.cur += two;
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
if (st.quote || rule.opens.includes(c)) {
|
|
87
|
+
if (!st.quote) st.quote = c;
|
|
88
|
+
else if (c === st.quote) st.quote = '';
|
|
89
|
+
st.cur += c;
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
if (!isBreak(c, two)) {
|
|
93
|
+
st.cur += c;
|
|
94
|
+
return 0;
|
|
84
95
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
96
|
+
st.out.push(st.cur);
|
|
97
|
+
st.cur = '';
|
|
98
|
+
return PAIRS.has(two) ? 1 : 0;
|
|
88
99
|
}
|
|
89
100
|
const PAGE_NAME_RE = /^[\w.-]+\.md$/;
|
|
90
101
|
|
|
@@ -270,9 +281,29 @@ function codexWikiWrite(payload) {
|
|
|
270
281
|
return typeof command === 'string' && statements(command).some((part) => OMX_WIKI_WRITE_RE.test(part));
|
|
271
282
|
}
|
|
272
283
|
|
|
284
|
+
// Codex: when where this hook runs (the script, its plugin or copy folder, node) is under a config.toml writable_roots
|
|
285
|
+
// entry, it does nothing and says why on stderr (the session start hook says it as a systemMessage). Unreadable: runs.
|
|
286
|
+
// Loaded only on Codex, so the Claude Code hook needs nothing beside it.
|
|
287
|
+
async function codexUnsafe(payload) {
|
|
288
|
+
let why = '';
|
|
289
|
+
try {
|
|
290
|
+
const r = await import('../bin/lib/codex-routes.mjs');
|
|
291
|
+
why = r.writableRootsNote(r.codexHomeOf(payload?.transcript_path), r.hookPlaces(import.meta.url));
|
|
292
|
+
} catch { /* as before */ }
|
|
293
|
+
if (why) process.stderr.write(`${why}\n`);
|
|
294
|
+
return Boolean(why);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Codex: only an `omx wiki` write goes on (an unreadable payload goes on to be logged), and only from outside the
|
|
298
|
+
// writable roots.
|
|
299
|
+
async function codexSkip(payload, error) {
|
|
300
|
+
if (!error && !codexWikiWrite(payload)) return true;
|
|
301
|
+
return codexUnsafe(payload);
|
|
302
|
+
}
|
|
303
|
+
|
|
273
304
|
async function main() {
|
|
274
305
|
const { payload, error } = await readPayload();
|
|
275
|
-
if (CODEX &&
|
|
306
|
+
if (CODEX && await codexSkip(payload, error)) return;
|
|
276
307
|
const wikiDir = findArmedWikiDir(writeRoot(payload));
|
|
277
308
|
if (!wikiDir) return; // Not armed: change nothing, log nothing.
|
|
278
309
|
|
|
@@ -286,6 +317,18 @@ async function main() {
|
|
|
286
317
|
}
|
|
287
318
|
}
|
|
288
319
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
320
|
+
// Run as the hook only (node, run.cjs, or the npm hook command's import); a test imports statements() alone. Both
|
|
321
|
+
// paths go through realpath: Node resolves links for import.meta.url but not for process.argv[1].
|
|
322
|
+
function invokedDirectly() {
|
|
323
|
+
try {
|
|
324
|
+
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
325
|
+
} catch {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (invokedDirectly()) {
|
|
331
|
+
main()
|
|
332
|
+
.catch(() => {}) // An un-canonicalized page is not-yet-formatted; the next bare run repairs it.
|
|
333
|
+
.finally(() => process.exit(0));
|
|
334
|
+
}
|
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
* - 사용자가 $banker-<name> 으로 부른 스킬: Codex 가 도구 호출 없이 `<skill>\n<name>..</name>\n<path>..</path>` 로
|
|
9
9
|
* 시작하는 사용자 역할 메시지로 본문을 넣는다. 이때는 머리의 <path> 줄만 보고 본문은 보지 않는다.
|
|
10
10
|
* Codex 플러그인으로 설치한 banker(codex plugin add, $banker:<name>)는 SKILL.md 가
|
|
11
|
-
* `<CODEX_HOME>/plugins/cache/<마켓플레이스>/banker/<판>/{skills,codex/skills,codex/command-skills}/<name>/`
|
|
12
|
-
* 두 길의 경로를 모두 센다.
|
|
11
|
+
* `<CODEX_HOME>/plugins/cache/<마켓플레이스>/banker/<판>/{skills,codex/skills,codex/command-skills,codex/renamed-skills}/<name>/`
|
|
12
|
+
* 에 있다. 두 길의 경로를 모두 센다. Codex 가 다른 이름으로 부르는 스킬(setup-omc 는 Codex 에서 setup-omx)은 Codex 이름으로
|
|
13
|
+
* 설치를 확인한 뒤 Claude 이름으로 센다(bin/lib/codex-names.mjs, codex/manifest.json 의 codexName 에서 생성). 개인화 알림의
|
|
14
|
+
* 바뀐 스킬 목록(skill-changes.json)도 Claude 이름이라 두 런타임의 셈이 한 이름으로 맞는다.
|
|
13
15
|
* 도구 호출마다 도는 PreToolUse 대신 턴마다 한 번 도는 Stop 에서, 훅 입력의 transcript_path(rollout) 중
|
|
14
16
|
* 이번 턴(task_started 의 turn_id 뒤)의 두 기록에 `skills/banker-<name>/SKILL.md` 가 있으면 센다. 나눠 읽어도 한 번이고, Stop 이 한 턴에 두 번 와도
|
|
15
|
-
*
|
|
17
|
+
* (턴, 이름)마다 configDir 의 codex-count-turns.d 에 표지 파일을 만든 훅만 센다(옛 판과는 codex-count-turns 를 함께 봄).
|
|
18
|
+
* 기록은 끝에서부터 32 MB 를 줄로 읽고, 턴 시작이 그 밖이면 256 MB 까지 바이트로 찾는다.
|
|
16
19
|
*
|
|
17
20
|
* fail-closed 규약은 Claude 쪽과 같다(telemetry-count-codex.test.mjs 가 검증):
|
|
18
21
|
* - countingActive() 가 아니면 로컬 쓰기조차 하지 않는다.
|
|
@@ -22,9 +25,11 @@
|
|
|
22
25
|
* - 어떤 오류도 삼키고 항상 exit 0. 도구 호출을 절대 막지 않는다. 표준 출력은 비운다.
|
|
23
26
|
*/
|
|
24
27
|
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, rmdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
25
29
|
import { join } from 'node:path';
|
|
26
30
|
|
|
27
|
-
import {
|
|
31
|
+
import { CLAUDE_NAMES } from '../bin/lib/codex-names.mjs';
|
|
32
|
+
import { codexHomeOf, hookPlaces, pluginRoutes, writableRootsNote } from '../bin/lib/codex-routes.mjs';
|
|
28
33
|
import { appendUsageLog, CODEX_SHAPE_MISS_PATH, configDir, countingActive, randomHex, readRegular, recordUsedSkill, SKILL_NAME_RE, writeByRename } from '../bin/lib/telemetry-config.mjs';
|
|
29
34
|
|
|
30
35
|
export { codexHomeOf };
|
|
@@ -32,9 +37,13 @@ export { codexHomeOf };
|
|
|
32
37
|
const SKILL_PATH_RE = /skills[\\/]+banker-([a-z0-9_-]{1,64})[\\/]+SKILL\.md/g;
|
|
33
38
|
// 플러그인 길: plugins/cache/<마켓플레이스>/banker/<판>/<스킬 폴더>/<name>/SKILL.md. 1 마켓플레이스, 2 판, 3 스킬 폴더, 4 이름.
|
|
34
39
|
// 대소문자를 가리지 않는 파일 시스템에서 `FRONT-QA` 같은 변형이 다른 이름으로 세어지지 않게 i 를 두지 않는다.
|
|
35
|
-
const PLUGIN_PATH_RE = /plugins[\\/]+cache[\\/]+([A-Za-z0-9._-]{1,80})[\\/]+banker[\\/]+([A-Za-z0-9._-]{1,80})[\\/]+(skills|codex[\\/]+skills|codex[\\/]+command-skills)[\\/]+([a-z0-9_-]{1,64})[\\/]+SKILL\.md/g;
|
|
40
|
+
const PLUGIN_PATH_RE = /plugins[\\/]+cache[\\/]+([A-Za-z0-9._-]{1,80})[\\/]+banker[\\/]+([A-Za-z0-9._-]{1,80})[\\/]+(skills|codex[\\/]+skills|codex[\\/]+command-skills|codex[\\/]+renamed-skills)[\\/]+([a-z0-9_-]{1,64})[\\/]+SKILL\.md/g;
|
|
36
41
|
const KEEP_TURNS = 100;
|
|
37
42
|
export const TURNS_FILE = () => join(configDir(), 'codex-count-turns');
|
|
43
|
+
// 센 (턴, 이름)마다 하나씩 만드는 표지 파일의 폴더, 기록 끝 256 MB 안에서도 턴 시작을 못 찾은 일을 적는 한 줄 파일.
|
|
44
|
+
export const MARKS_DIR = () => join(configDir(), 'codex-count-turns.d');
|
|
45
|
+
export const FAR_NOTE_FILE = () => join(configDir(), 'codex-count-note');
|
|
46
|
+
const requireBuiltin = createRequire(import.meta.url);
|
|
38
47
|
|
|
39
48
|
function readStdin() {
|
|
40
49
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
@@ -85,24 +94,98 @@ const isCall = (rec) => rec?.type === 'response_item' && /_call$/.test(String(re
|
|
|
85
94
|
const SKILL_HEAD_RE = /^<skill>\n<name>[^\n<]{1,80}<\/name>\n<path>([^\n<]{1,4096})<\/path>\n/;
|
|
86
95
|
|
|
87
96
|
// $banker-<name> 으로 부른 스킬을 Codex 가 넣은 메시지의 <path>. 그 밖의 기록은 ''.
|
|
97
|
+
const isUserMessage = (rec) => rec?.type === 'response_item' && rec.payload?.type === 'message' && rec.payload.role === 'user';
|
|
98
|
+
|
|
88
99
|
function injectedPath(rec) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const text = p.content[0]?.text;
|
|
100
|
+
if (!isUserMessage(rec) || !Array.isArray(rec.payload.content)) return '';
|
|
101
|
+
const text = rec.payload.content[0]?.text;
|
|
92
102
|
return typeof text === 'string' ? (SKILL_HEAD_RE.exec(text)?.[1] ?? '') : '';
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
// 이번 턴(turn_id 의 task_started 뒤)의 도구 호출과 넣은 스킬 머리에서 읽은 banker 스킬 이름. 사용자 글과 모델 답은 세지 않는다.
|
|
96
106
|
// 턴 시작은 대개 기록 끝 가까이에 있으므로 끝 1 MB 부터 읽고, 못 찾으면 두 배씩 max 까지 넓힌다.
|
|
107
|
+
// 그래도 없으면 farSkills 가 그 앞을 바이트로 찾는다(한 턴이 32 MB 를 넘게 쓴 경우).
|
|
97
108
|
// 기록 전체를 읽고도 task_started 가 하나도 없으면 null: Codex 는 턴마다 task_started 를 쓰므로 기록 형식이 바뀐 것이다.
|
|
98
109
|
// 이 턴의 것만 없을 때(시작이 기록되지 않는 턴)는 형식 변경으로 보지 않는다.
|
|
99
|
-
export function turnSkills(file, turnId, max = MAX_READ) {
|
|
110
|
+
export function turnSkills(file, turnId, max = MAX_READ, far = {}) {
|
|
100
111
|
for (let size = Math.min(FIRST_READ, max); ; size = Math.min(size * 2, max)) {
|
|
101
112
|
const { lines, whole } = tailLines(file, size);
|
|
102
113
|
const start = turnStart(lines, turnId);
|
|
103
114
|
if (start >= 0) return skillsAfter(lines, start);
|
|
104
115
|
if (whole) return lines.some((l) => l.trim()) && !lines.some((l) => l.includes('"task_started"')) ? null : [];
|
|
105
|
-
if (size >= max) return
|
|
116
|
+
if (size >= max) return farSkills(file, turnId, max, far);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 기록 끝 max 바이트 앞에서 이 턴의 시작을 찾는다. 끝에서 앞으로 chunk 씩 읽어 Buffer.lastIndexOf 로 턴 id 를 찾고
|
|
121
|
+
// (문자열로 바꾸지 않음, 조각 경계는 4 KB 겹쳐 읽음), 그 줄에 task_started 가 있으면 거기서부터 chunk 씩 앞으로 읽으며
|
|
122
|
+
// 이름만 모은다. 기록 끝에서 cap 바이트까지, ms 밀리초까지만(훅 시간 제한 5초). 못 찾으면 빈 목록에 far 표시를 단다.
|
|
123
|
+
const FAR_READ = 256 << 20;
|
|
124
|
+
const FAR_CHUNK = 32 << 20;
|
|
125
|
+
const FAR_MS = 2000;
|
|
126
|
+
const OVERLAP = 4 << 10;
|
|
127
|
+
const NL = 0x0a;
|
|
128
|
+
export function farSkills(file, turnId, max, { cap = FAR_READ, chunk = FAR_CHUNK, ms = FAR_MS } = {}) {
|
|
129
|
+
const fd = openSync(file, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
130
|
+
try {
|
|
131
|
+
const size = fstatSync(fd).size;
|
|
132
|
+
const deadline = Date.now() + ms;
|
|
133
|
+
const at = findStart(fd, { size, needle: Buffer.from(`"${turnId}"`), from: size - max, floor: Math.max(0, size - cap), chunk, deadline });
|
|
134
|
+
if (at < 0) return Object.assign([], { far: true });
|
|
135
|
+
return [...namesFrom(fd, at, size, chunk, deadline)];
|
|
136
|
+
} finally {
|
|
137
|
+
closeSync(fd);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readAt(fd, start, len) {
|
|
142
|
+
const buf = Buffer.alloc(len);
|
|
143
|
+
return buf.subarray(0, readSync(fd, buf, 0, len, start));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// task_started 와 이 턴 id 가 한 줄에 있는 마지막 자리(파일 위치). 없으면 -1.
|
|
147
|
+
function findStart(fd, { size, needle, from, floor, chunk, deadline }) {
|
|
148
|
+
const mark = Buffer.from('"task_started"');
|
|
149
|
+
for (let end = from; end > floor && Date.now() <= deadline; end -= chunk) {
|
|
150
|
+
const lo = Math.max(floor, end - chunk);
|
|
151
|
+
const buf = readAt(fd, lo, Math.min(end + OVERLAP, size) - lo);
|
|
152
|
+
for (let i = buf.lastIndexOf(needle); i >= 0; i = i > 0 ? buf.lastIndexOf(needle, i - 1) : -1) {
|
|
153
|
+
const ls = buf.lastIndexOf(NL, i) + 1;
|
|
154
|
+
const le = buf.indexOf(NL, i);
|
|
155
|
+
const at = buf.subarray(ls, le < 0 ? buf.length : le).indexOf(mark);
|
|
156
|
+
if (at >= 0) return lo + ls + at;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return -1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// at(턴 시작 줄) 뒤로 기록 끝까지 chunk 씩 읽으며 SKILL.md 가 든 줄만 문자열로 바꿔 이름을 모은다.
|
|
163
|
+
function namesFrom(fd, at, size, chunk, deadline) {
|
|
164
|
+
const names = new Set();
|
|
165
|
+
let carry = Buffer.alloc(0);
|
|
166
|
+
let skip = true; // 턴 시작 줄의 남은 부분
|
|
167
|
+
for (let pos = at; pos < size && Date.now() <= deadline; pos += chunk) {
|
|
168
|
+
let buf = Buffer.concat([carry, readAt(fd, pos, Math.min(chunk, size - pos))]);
|
|
169
|
+
carry = Buffer.alloc(0);
|
|
170
|
+
if (skip) {
|
|
171
|
+
const nl = buf.indexOf(NL);
|
|
172
|
+
if (nl < 0) continue;
|
|
173
|
+
[buf, skip] = [buf.subarray(nl + 1), false];
|
|
174
|
+
}
|
|
175
|
+
const cut = pos + chunk >= size ? buf.length : buf.lastIndexOf(NL) + 1;
|
|
176
|
+
linesWithSkill(buf.subarray(0, cut), names);
|
|
177
|
+
carry = buf.subarray(cut);
|
|
178
|
+
}
|
|
179
|
+
return names;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const SKILL_BYTES = Buffer.from('SKILL.md');
|
|
183
|
+
function linesWithSkill(buf, names) {
|
|
184
|
+
for (let hit = buf.indexOf(SKILL_BYTES); hit >= 0;) {
|
|
185
|
+
const le = buf.indexOf(NL, hit);
|
|
186
|
+
const end = le < 0 ? buf.length : le;
|
|
187
|
+
lineNames(buf.toString('utf8', buf.lastIndexOf(NL, hit) + 1, end), names);
|
|
188
|
+
hit = buf.indexOf(SKILL_BYTES, end);
|
|
106
189
|
}
|
|
107
190
|
}
|
|
108
191
|
|
|
@@ -141,16 +224,18 @@ function turnStart(lines, turnId) {
|
|
|
141
224
|
|
|
142
225
|
function skillsAfter(lines, start) {
|
|
143
226
|
const names = new Set();
|
|
144
|
-
for (const line of lines.slice(start + 1))
|
|
145
|
-
if (!line.includes('SKILL.md')) continue;
|
|
146
|
-
let rec;
|
|
147
|
-
try { rec = JSON.parse(line); } catch { continue; }
|
|
148
|
-
if (isCall(rec)) namesIn(JSON.stringify(rec.payload), names);
|
|
149
|
-
else namesIn(injectedPath(rec), names);
|
|
150
|
-
}
|
|
227
|
+
for (const line of lines.slice(start + 1)) lineNames(line, names);
|
|
151
228
|
return [...names];
|
|
152
229
|
}
|
|
153
230
|
|
|
231
|
+
function lineNames(line, names) {
|
|
232
|
+
if (!line.includes('SKILL.md')) return;
|
|
233
|
+
let rec;
|
|
234
|
+
try { rec = JSON.parse(line); } catch { return; }
|
|
235
|
+
if (isCall(rec)) namesIn(JSON.stringify(rec.payload), names);
|
|
236
|
+
else namesIn(injectedPath(rec), names);
|
|
237
|
+
}
|
|
238
|
+
|
|
154
239
|
const ID_RE = /^[A-Za-z0-9._-]{1,80}$/;
|
|
155
240
|
|
|
156
241
|
// 이 턴의 키. session_id 와 turn_id 가 모양이 맞을 때만(파일 한 줄 형식을 깨지 않게).
|
|
@@ -193,38 +278,115 @@ export function installed(entry, home, routes = pluginRoutes(home)) {
|
|
|
193
278
|
return existsSync(join(home, 'skills', `banker-${name.slice('banker:'.length)}`, 'SKILL.md'));
|
|
194
279
|
}
|
|
195
280
|
|
|
196
|
-
//
|
|
197
|
-
export function
|
|
281
|
+
// 'banker:<Codex 이름>' 을 'banker:<Claude 이름>' 으로. 이름이 같은 스킬은 그대로.
|
|
282
|
+
export function claudeName(entry) {
|
|
283
|
+
const name = entry.slice('banker:'.length);
|
|
284
|
+
return Object.hasOwn(CLAUDE_NAMES, name) ? `banker:${CLAUDE_NAMES[name]}` : entry;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 이 턴에 아직 세지 않은 이름만 센다. far 는 기록 읽기 한도(시험이 낮춤): max 줄로 읽는 끝 부분, 그 밖은 farSkills 의 것.
|
|
288
|
+
export function countOnce(payload, far = {}) {
|
|
198
289
|
const key = turnKey(payload);
|
|
199
290
|
const file = typeof payload?.transcript_path === 'string' ? payload.transcript_path : '';
|
|
200
291
|
const home = codexHomeOf(file);
|
|
201
|
-
const got = key
|
|
292
|
+
const got = readTurn(key, file, payload, far);
|
|
202
293
|
if (got === null) {
|
|
203
294
|
noteShapeMiss(file);
|
|
204
295
|
return [];
|
|
205
296
|
}
|
|
297
|
+
if (got.far) noteFar(key);
|
|
206
298
|
const routes = got.some((n) => n.includes('\t')) ? pluginRoutes(home) : []; // config.toml 은 한 번만 읽는다
|
|
207
|
-
const names = [...new Set(got.filter((n) => installed(n, home, routes)).map((n) => n.split('\t')[0]))];
|
|
299
|
+
const names = [...new Set(got.filter((n) => installed(n, home, routes)).map((n) => claudeName(n.split('\t')[0])))];
|
|
208
300
|
if (!names.length) return [];
|
|
209
|
-
return withTurnsLock(() =>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
301
|
+
return withTurnsLock(() => countFresh(key, names));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function readTurn(key, file, payload, far) {
|
|
305
|
+
return key && file ? turnSkills(file, payload.turn_id, far.max ?? MAX_READ, far) : [];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 옛 판(0.22.0 까지)은 codex-count-turns 만 보고, 이 판은 (턴, 이름)마다 표지 파일을 wx 로 만든 훅만 센다.
|
|
309
|
+
// 옛 파일에 이미 있는 것은 표지만 만들고 세지 않는다. 센 것은 옛 판이 다시 세지 않게 옛 파일에도 적는다.
|
|
310
|
+
function countFresh(key, names) {
|
|
311
|
+
const seen = readTurns();
|
|
312
|
+
const fresh = names.filter((n) => markTurn(key, n) && !seen.includes(`${key}\t${n}`));
|
|
313
|
+
for (const n of fresh) {
|
|
314
|
+
appendUsageLog(n);
|
|
315
|
+
recordUsedSkill(n);
|
|
316
|
+
}
|
|
317
|
+
if (fresh.length) writeTurns([...seen, ...fresh.map((n) => `${key}\t${n}`)]);
|
|
318
|
+
dropOldMarks();
|
|
319
|
+
return fresh;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// (턴, 이름)의 표지를 이 훅이 만들었는지. 만들기는 wx(이미 있으면 실패, 링크도 따라가지 않음)라 함께 도는 훅 가운데
|
|
323
|
+
// 하나만 성공한다. 표지 폴더가 링크이거나 남의 폴더이면, 또 Windows 의 wx 가 끊어진 링크를 따라가 만든 것이면 세지 않는다.
|
|
324
|
+
// Windows 의 wx 는 끊어진 링크를 따라가 그 대상을 만들므로, 그 자리에 무엇이든(끊어진 링크 포함) 있으면 열지 않는다.
|
|
325
|
+
function markTurn(key, name) {
|
|
326
|
+
const dir = MARKS_DIR();
|
|
327
|
+
const file = join(dir, markName(`${key}\t${name}`));
|
|
328
|
+
try {
|
|
329
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
330
|
+
const st = lstatSync(dir);
|
|
331
|
+
if (!st.isDirectory() || (typeof process.getuid === 'function' && st.uid !== process.getuid())) return false;
|
|
332
|
+
if (taken(file)) return false;
|
|
333
|
+
closeSync(openSync(file, 'wx', 0o600));
|
|
334
|
+
return lstatSync(file).isFile();
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function taken(file) {
|
|
341
|
+
try {
|
|
342
|
+
lstatSync(file);
|
|
343
|
+
return true;
|
|
344
|
+
} catch (e) {
|
|
345
|
+
return e.code !== 'ENOENT';
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 표지 이름: sha256 앞 32자. node:crypto 는 셀 것이 있을 때만 불러오고, crypto 없이 빌드한 Node 에서는 FNV-1a 넷.
|
|
350
|
+
function markName(text) {
|
|
351
|
+
try {
|
|
352
|
+
return requireBuiltin('node:crypto').createHash('sha256').update(text).digest('hex').slice(0, 32);
|
|
353
|
+
} catch {
|
|
354
|
+
let out = '';
|
|
355
|
+
for (let seed = 0; seed < 4; seed++) {
|
|
356
|
+
let h = (0x811c9dc5 ^ seed) >>> 0;
|
|
357
|
+
for (const c of Buffer.from(text)) h = Math.imul(h ^ c, 0x01000193) >>> 0;
|
|
358
|
+
out += h.toString(16).padStart(8, '0');
|
|
216
359
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
});
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
220
362
|
}
|
|
221
363
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
364
|
+
// 이틀 지난 표지를 지운다. Codex 는 끝난 턴에 Stop 을 다시 보내지 않는다.
|
|
365
|
+
const MARK_KEEP_MS = 2 * 24 * 60 * 60 * 1000;
|
|
366
|
+
function dropOldMarks() {
|
|
367
|
+
const dir = MARKS_DIR();
|
|
368
|
+
try {
|
|
369
|
+
for (const name of readdirSync(dir)) {
|
|
370
|
+
try {
|
|
371
|
+
if (Date.now() - lstatSync(join(dir, name)).mtimeMs > MARK_KEEP_MS) rmSync(join(dir, name), { force: true });
|
|
372
|
+
} catch { /* 다른 훅이 먼저 지움 */ }
|
|
373
|
+
}
|
|
374
|
+
} catch { /* 다음에 */ }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 턴 시작이 기록 끝 256 MB 밖이라 세지 않은 일을 한 줄로 남긴다(마지막 한 번만).
|
|
378
|
+
function noteFar(key) {
|
|
379
|
+
try {
|
|
380
|
+
writeByRename(FAR_NOTE_FILE(), `${new Date().toISOString()}\t${key}\t턴 시작이 기록 끝 256 MB 밖, 이 턴은 세지 않음\n`);
|
|
381
|
+
} catch { /* 기록만 못 함 */ }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// npm 길과 플러그인 길의 Stop 훅이 같은 턴에 함께 돌면 둘 다 턴 키를 읽고 셀 수 있다. 셀지는 표지 파일(markTurn)이
|
|
385
|
+
// 정하고, 잠금은 codex-count-turns 를 읽고 고쳐 쓰는 동안 잡는다(mkdir 은 이미 있으면 실패하고 링크를 따라가지 않음).
|
|
386
|
+
// 2초 안에 못 잡으면 잠금 없이 표지로 정한다. 잠금은 몇 ms 만 잡으므로 훅 시간 제한(5초)보다 조금 긴 6초가 지난
|
|
387
|
+
// 잠금은 죽은 훅의 것이다: 고유 이름으로 옮겨(rename 은 하나만 성공) 지운 뒤 다시 잡는다. 옮긴 것이 새것이면(그사이
|
|
388
|
+
// 다른 훅이 넘겨받아 잡은 것) 그 훅에 이 턴을 맡기고 세지 않고 물러난다.
|
|
389
|
+
// configDir 을 만들 수 없거나 잠금 폴더를 만들 수 없는 다른 오류면 잠금 없이 표지로 정한다.
|
|
228
390
|
const LOCK_WAIT_MS = 2000;
|
|
229
391
|
const LOCK_STALE_MS = 6000;
|
|
230
392
|
export function withTurnsLock(fn, { wait = LOCK_WAIT_MS } = {}) {
|
|
@@ -237,26 +399,13 @@ export function withTurnsLock(fn, { wait = LOCK_WAIT_MS } = {}) {
|
|
|
237
399
|
const end = Date.now() + wait;
|
|
238
400
|
const token = `${process.pid}-${randomHex()}`;
|
|
239
401
|
for (;;) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
if (Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
249
|
-
const old = `${lock}.stale-${process.pid}-${randomHex()}`;
|
|
250
|
-
renameSync(lock, old);
|
|
251
|
-
// 본 것과 옮긴 것 사이에 다른 훅이 낡은 잠금을 치우고 새로 잡았을 수 있다. 옮긴 것이 새것이면 그 훅이 이
|
|
252
|
-
// 턴을 세는 중이므로 세지 않고 물러난다. 돌려놓지 않는다(그사이 세 번째 훅이 잡은 잠금을 덮을 수 있다).
|
|
253
|
-
// 옮긴 폴더는 다음에 잠금을 잡은 훅이 치운다.
|
|
254
|
-
if (Date.now() - statSync(old).mtimeMs <= LOCK_STALE_MS) return [];
|
|
255
|
-
rmSync(old, { recursive: true, force: true });
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
} catch { /* 방금 풀렸거나 다른 훅이 먼저 치움: 다시 잡아 본다 */ }
|
|
259
|
-
if (Date.now() >= end) return [];
|
|
402
|
+
const got = takeLock(lock, token);
|
|
403
|
+
if (got === 'error') return fn();
|
|
404
|
+
if (got === 'held') break;
|
|
405
|
+
const old = takeOverStale(lock);
|
|
406
|
+
if (old === 'backoff') return [];
|
|
407
|
+
if (old === 'retry') continue;
|
|
408
|
+
if (Date.now() >= end) return fn();
|
|
260
409
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
261
410
|
}
|
|
262
411
|
try {
|
|
@@ -267,6 +416,34 @@ export function withTurnsLock(fn, { wait = LOCK_WAIT_MS } = {}) {
|
|
|
267
416
|
}
|
|
268
417
|
}
|
|
269
418
|
|
|
419
|
+
// 'held' 잡음, 'busy' 다른 훅이 잡고 있음, 'error' 잠금 폴더를 만들 수 없음.
|
|
420
|
+
function takeLock(lock, token) {
|
|
421
|
+
try {
|
|
422
|
+
mkdirSync(lock);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
return e?.code === 'EEXIST' ? 'busy' : 'error';
|
|
425
|
+
}
|
|
426
|
+
try { writeFileSync(join(lock, 'owner'), token, { flag: 'wx' }); } catch { /* 표시 없이도 잠금은 잡았다 */ }
|
|
427
|
+
return 'held';
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 낡은 잠금을 넘겨받는다. 'retry' 치웠으니 다시 잡기, 'backoff' 물러남, 'live' 살아 있는 잠금(또는 방금 풀림).
|
|
431
|
+
function takeOverStale(lock) {
|
|
432
|
+
try {
|
|
433
|
+
if (Date.now() - statSync(lock).mtimeMs <= LOCK_STALE_MS) return 'live';
|
|
434
|
+
const old = `${lock}.stale-${process.pid}-${randomHex()}`;
|
|
435
|
+
renameSync(lock, old);
|
|
436
|
+
// 본 것과 옮긴 것 사이에 다른 훅이 낡은 잠금을 치우고 새로 잡았을 수 있다. 옮긴 것이 새것이면 그 훅이 이
|
|
437
|
+
// 턴을 세는 중이므로 세지 않고 물러난다. 돌려놓지 않는다(그사이 세 번째 훅이 잡은 잠금을 덮을 수 있다).
|
|
438
|
+
// 옮긴 폴더는 다음에 잠금을 잡은 훅이 치운다.
|
|
439
|
+
if (Date.now() - statSync(old).mtimeMs <= LOCK_STALE_MS) return 'backoff';
|
|
440
|
+
rmSync(old, { recursive: true, force: true });
|
|
441
|
+
return 'retry';
|
|
442
|
+
} catch {
|
|
443
|
+
return 'live'; // 방금 풀렸거나 다른 훅이 먼저 치움: 다시 잡아 본다
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
270
447
|
// 잠금 폴더 안의 주인 표시(owner)가 이 훅의 것일 때만 놓는다. 넘겨받던 훅이 이 잠금을 옮기고 다른 훅이 그 이름을
|
|
271
448
|
// 새로 잡았으면, 그 훅의 잠금은 지우지 않는다.
|
|
272
449
|
function releaseLock(lock, token) {
|
|
@@ -299,6 +476,10 @@ async function main() {
|
|
|
299
476
|
return;
|
|
300
477
|
}
|
|
301
478
|
if (!countingActive()) return;
|
|
479
|
+
// 이 훅이 도는 곳이 config.toml 의 writable_roots 아래면 세지 않고 까닭만 stderr 로(세션 시작 훅은 systemMessage 로 알림).
|
|
480
|
+
let unsafe = '';
|
|
481
|
+
try { unsafe = writableRootsNote(codexHomeOf(payload?.transcript_path), hookPlaces(import.meta.url)); } catch { /* 읽지 못하면 전처럼 */ }
|
|
482
|
+
if (unsafe) { process.stderr.write(`${unsafe}\n`); return; }
|
|
302
483
|
countOnce(payload);
|
|
303
484
|
}
|
|
304
485
|
|
package/hooks/update-fetch.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import http from 'node:http';
|
|
|
22
22
|
import https from 'node:https';
|
|
23
23
|
|
|
24
24
|
import {
|
|
25
|
-
UPDATE_CHECK_PATH, NPM_LATEST_URL, writeUpdateCache, collectBody, netDeadlineMs, readRegular, updateCheckDue,
|
|
25
|
+
UPDATE_CHECK_PATH, NPM_LATEST_URL, writeUpdateCache, collectBody, netDeadlineMs, readRegular, updateCheckDue, noUpdateCheck,
|
|
26
26
|
} from '../bin/lib/telemetry-config.mjs';
|
|
27
27
|
|
|
28
28
|
const NET_TIMEOUT_MS = 10_000;
|
|
@@ -97,6 +97,7 @@ function fetchLatestVersion(url) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
async function main() {
|
|
100
|
+
if (noUpdateCheck()) return; // 업데이트 확인을 껐으면 직접 실행돼도 조회하지 않는다(update-notify 는 이때 이 파일을 띄우지 않음).
|
|
100
101
|
const cache = readCache();
|
|
101
102
|
if (!updateCheckDue(cache)) return; // 성공이나 실패한 시도에서 하루가 지나지 않음 - no-op.
|
|
102
103
|
|