@kaydash9999/banker-plugins 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +41 -0
  4. package/PRIVACY.md +1 -0
  5. package/README.md +39 -13
  6. package/bin/banker.js +118 -22
  7. package/bin/lib/codex-hooks.mjs +116 -18
  8. package/bin/lib/codex-names.mjs +4 -0
  9. package/bin/lib/codex-routes.mjs +93 -1
  10. package/bin/lib/command-skill.cjs +37 -6
  11. package/codex/command-skills/front-qa/SKILL.md +1 -1
  12. package/codex/command-skills/setup/SKILL.md +4 -4
  13. package/codex/hooks.json +5 -5
  14. package/codex/manifest.json +18 -15
  15. package/codex/skills/progress/scripts/progress.mjs +44 -14
  16. package/codex/skills/teamwork/SKILL.md +137 -0
  17. package/codex/skills/teamwork/scripts/teamwork.mjs +957 -0
  18. package/codex/transform-matrix.md +5 -3
  19. package/hooks/obsidize-hook.mjs +75 -32
  20. package/hooks/telemetry-count-codex.mjs +238 -57
  21. package/hooks/update-fetch.mjs +2 -1
  22. package/hooks/update-notify.mjs +82 -20
  23. package/package.json +3 -1
  24. package/skills/3d-intro-build/SKILL.md +101 -20
  25. package/skills/3d-intro-build/references/NOTICE.md +9 -2
  26. package/skills/3d-intro-build/references/assemble.mjs +113 -22
  27. package/skills/3d-intro-build/references/azure-adapter.mjs +110 -24
  28. package/skills/3d-intro-build/references/clip-post.mjs +443 -0
  29. package/skills/3d-intro-build/references/dive-mode.mjs +299 -0
  30. package/skills/3d-intro-build/references/intro-fixes.css +11 -0
  31. package/skills/3d-intro-build/references/prompts.md +51 -4
  32. package/skills/3d-intro-build/references/scrub-engine.js +1 -0
  33. package/skills/3d-intro-build/references/step-nav.css +29 -0
  34. package/skills/3d-intro-build/references/step-nav.js +277 -106
  35. package/skills/3d-intro-build/references/verify-intro.mjs +373 -32
  36. package/skills/3d-intro-build/references/verify.md +6 -0
  37. package/skills/3d-intro-build/references/video-pool.mjs +142 -12
  38. package/skills/3d-intro-setup/SKILL.md +20 -7
  39. package/skills/3d-intro-setup/references/azure-adapter.mjs +110 -24
  40. package/skills/3d-intro-setup/references/video-pool.mjs +142 -12
  41. package/skills/all-in-one/SKILL.md +125 -86
  42. package/skills/omc-patch/SKILL.md +1 -1
  43. package/skills/payload-mon/SKILL.md +1 -1
  44. package/skills/play-qa/SKILL.md +6 -4
  45. package/skills/progress-mon/README.md +162 -0
  46. package/skills/progress-mon/SKILL.md +165 -0
  47. package/skills/progress-mon/assets/fixture/README.md +1 -0
  48. package/skills/progress-mon/assets/fixture/arch.json +33 -0
  49. package/skills/progress-mon/assets/fixture/commits.json +7 -0
  50. package/skills/progress-mon/assets/fixture/config.json +30 -0
  51. package/skills/progress-mon/assets/fixture/events.jsonl +7 -0
  52. package/skills/progress-mon/assets/fixture/flow.json +44 -0
  53. package/skills/progress-mon/assets/fixture/items.json +14 -0
  54. package/skills/progress-mon/assets/fixture/req-spec.json +31 -0
  55. package/skills/progress-mon/assets/fixture/reqs.json +11 -0
  56. package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +58 -0
  57. package/skills/progress-mon/assets/fixture/variants/check-auto/arch.json +15 -0
  58. package/skills/progress-mon/assets/fixture/variants/check-overlap/arch.json +14 -0
  59. package/skills/progress-mon/assets/fixture/variants/history-manual/config.json +13 -0
  60. package/skills/progress-mon/assets/fixture/variants/history-manual/items.json +7 -0
  61. package/skills/progress-mon/assets/fixture/variants/workflow-wide/flow.json +35 -0
  62. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a01.jsonl +1 -0
  63. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a02.jsonl +1 -0
  64. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a03.jsonl +1 -0
  65. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a04.jsonl +1 -0
  66. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/journal.jsonl +7 -0
  67. package/skills/progress-mon/references/arch.md +280 -0
  68. package/skills/progress-mon/references/authoring.md +199 -0
  69. package/skills/progress-mon/references/config.md +115 -0
  70. package/skills/progress-mon/references/flow.md +75 -0
  71. package/skills/progress-mon/references/inventory.md +58 -0
  72. package/skills/progress-mon/references/items.md +74 -0
  73. package/skills/progress-mon/references/req-spec.md +74 -0
  74. package/skills/progress-mon/references/reqs.md +30 -0
  75. package/skills/progress-mon/scripts/index.js +31 -0
  76. package/skills/progress-mon/scripts/pm-check.mjs +526 -0
  77. package/skills/progress-mon/scripts/pm-config.mjs +341 -0
  78. package/skills/progress-mon/scripts/pm-data.mjs +394 -0
  79. package/skills/progress-mon/scripts/pm-html.mjs +77 -0
  80. package/skills/progress-mon/scripts/pm-memory.mjs +308 -0
  81. package/skills/progress-mon/scripts/pm-render.mjs +1644 -0
  82. package/skills/progress-mon/scripts/pm-server.mjs +735 -0
  83. package/skills/progress-mon/scripts/pm-sources.mjs +501 -0
  84. package/skills/progress-mon/scripts/progress-mon.mjs +167 -0
  85. package/skills/ralph-qa/SKILL.md +104 -15
  86. package/skills/ralph-qa/references/gate-evidence.mjs +457 -0
  87. package/skills/ralph-qa/references/payload-scan.mjs +19 -2
  88. package/skills/ralph-qa/references/run-folder.mjs +299 -0
  89. package/skills/ralph-qa/references/seat-payload.mjs +60 -13
  90. package/skills/ralph-qa/references/seat-run.mjs +159 -155
  91. package/skills/remains/SKILL.md +1 -0
  92. package/skills/remains/scripts/boxes.mjs +6 -2
  93. package/skills/setup-bypass-permissions/scripts/codex-config.mjs +19 -8
  94. package/skills/setup-mcp/SKILL.md +1 -1
  95. package/skills/setup-omc-hud/SKILL.md +2 -1
  96. package/skills/setup-omc-hud/assets/codex-config.mjs +19 -8
  97. package/skills/ultra-chain/SKILL.md +32 -14
  98. package/skills/ultra-chain/references/chain-ledger.mjs +111 -3
  99. package/skills/ultra-init/SKILL.md +30 -22
  100. package/skills/ultraqa/NOTICE.md +34 -0
  101. package/skills/ultraqa/SKILL.md +196 -0
  102. package/skills/ultraqa/references/gate-evidence.mjs +457 -0
  103. package/skills/ultraqa/references/run-folder.mjs +299 -0
  104. package/skills/ultraqa/references/tmux-drive.md +52 -0
  105. package/skills/ultraqa/references/uqa-run.mjs +339 -0
  106. package/skills/update-banker/SKILL.md +1 -1
@@ -6,11 +6,12 @@
6
6
  * 돌린다. Codex 는 신뢰한 훅을 샌드박스 밖에서 돌리므로, 작업 폴더 안의 파일을 가리키지 않게 하기 위해서다.
7
7
  * - 묶음은 각 이벤트의 맨 뒤에 더한다. Codex 는 신뢰를 <hooks.json>:<이벤트>:<묶음 번호>:<항목 번호> 로 기록해, 앞에
8
8
  * 끼우면 OMX 훅의 번호가 밀려 다시 신뢰해야 한다. 빼기는 우리 항목만, 낡은 명령은 같은 자리에서 고친다.
9
- * - Windows 의 Codex 는 훅 명령을 PowerShell 로 돌린다(Codex 0.160.1 실측): `& '<node>' '<스크립트>' 인자`.
9
+ * - Windows 의 Codex 는 훅 명령을 PowerShell 로 돌린다(Codex 0.160.1 실측): `& '<node>' -e '<검사>' '<스크립트>' 인자`.
10
+ * 검사(hookGuard)는 스크립트의 실제 경로가 임시 폴더나 세션 폴더 아래면 돌리지 않는다.
10
11
  * - hooks.json 이 링크이거나 JSON 객체가 아니거나 "없음" 말고 다른 이유로 확인할 수 없으면 바꾸지 않는다. 바꿀 때마다
11
12
  * 직전 내용을 hooks.json.banker.bak 에 둔다. smart-compact 의 codex-gate.mjs 와 같은 규칙이다.
12
13
  */
13
- import { spawnSync } from 'node:child_process';
14
+ import crypto from 'node:crypto';
14
15
  import fs from 'node:fs';
15
16
  import os from 'node:os';
16
17
  import path from 'node:path';
@@ -26,6 +27,7 @@ export const PAYLOAD = [
26
27
  '.claude-plugin/plugin.json',
27
28
  'bin/lib/telemetry-config.mjs',
28
29
  'bin/lib/codex-routes.mjs',
30
+ 'bin/lib/codex-names.mjs',
29
31
  'hooks/update-notify.mjs',
30
32
  'hooks/update-fetch.mjs',
31
33
  'hooks/update-checkin.mjs',
@@ -43,8 +45,9 @@ export function codexPaths(env = process.env) {
43
45
 
44
46
  const isObject = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
45
47
 
48
+ // 임시 파일은 난수 이름과 wx: 남이 미리 심은 같은 이름의 파일이나 링크를 따라가지 않는다.
46
49
  function writeAtomic(file, data) {
47
- const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
50
+ const tmp = `${file}.${crypto.randomBytes(6).toString('hex')}.tmp`;
48
51
  try {
49
52
  fs.writeFileSync(tmp, data, { flag: 'wx' });
50
53
  const mode = modeOf(file);
@@ -66,17 +69,43 @@ function modeOf(file) {
66
69
  }
67
70
 
68
71
  // 명령 글. POSIX 는 큰따옴표, 셸이 그 안에서 읽는 글자는 거부. Windows 는 PowerShell 호출 연산자와 작은따옴표.
69
- export function formatCommand(node, script, args, win = process.platform === 'win32') {
72
+ // guard 가 있으면 node -e <guard> <스크립트> 인자 꼴(guard 는 큰따옴표, $, 백틱, 백슬래시가 없는 고정 글).
73
+ export function formatCommand(node, script, args, win = process.platform === 'win32', guard = '') {
70
74
  const all = [node, script, ...args].join('');
71
75
  if (/[\r\n\0]/.test(all)) return null;
76
+ const pre = guard ? ['-e'] : [];
72
77
  if (win) {
73
78
  const q = (s) => `'${s.replace(/['‘-‛]/g, '$&$&')}'`;
74
- return ['&', q(node), q(script), ...args].join(' ');
79
+ return ['&', q(node), ...pre, ...(guard ? [q(guard)] : []), q(script), ...args].join(' ');
75
80
  }
76
81
  if (/["$`\\]/.test(all)) return null;
77
- return [`"${node}"`, `"${script}"`, ...args].join(' ');
82
+ return [`"${node}"`, ...pre, ...(guard ? [`"${guard}"`] : []), `"${script}"`, ...args].join(' ');
78
83
  }
79
84
 
85
+ // npm 길 훅 명령이 스크립트를 부르기 전에 하는 검사(Codex 플러그인 훅의 래퍼와 같은 규칙): 스크립트의 실제 경로가 임시
86
+ // 폴더나 세션 폴더(Codex 가 훅을 도는 폴더) 아래면 돌리지 않고 까닭을 알린다. 세션 폴더가 홈 폴더 자체면 세션 폴더
87
+ // 규칙은 쓰지 않는다. Codex 홈이 세션 폴더 깊은 곳(CODEX_HOME=<세션>/deep/.codex)이면 샌드박스가 그 사본을 고칠 수
88
+ // 있기 때문이다. 검사는 신뢰 해시에 묶인 명령 글에 있다. 세션 시작 훅은 systemMessage 로, 다른 훅은 stderr 로 알린다.
89
+ // 실제 경로는 realpathSync.native 로 푼다: Windows 의 JS 판은 8.3 짧은 이름의 임시 폴더(TEMP=C:\Users\LONGNA~1\...)를
90
+ // 긴 이름으로 풀지 않아 임시 폴더 규칙이 빗나간다(Windows 박스 실측).
91
+ export function hookGuard(notice) {
92
+ const say = notice
93
+ ? 'process.stdout.write(JSON.stringify({systemMessage:m}))'
94
+ : 'process.stderr.write(m+String.fromCharCode(10))';
95
+ return [
96
+ "const f=require('fs'),p=require('path'),o=require('os');",
97
+ 'const r=(x)=>{try{return f.realpathSync.native(x)}catch{try{return f.realpathSync(x)}catch{return x}}};',
98
+ 'const s=r(process.argv[1]),w=r(process.cwd()),h=r(o.homedir());',
99
+ "const u=(d)=>{const q=p.relative(d,s);return q===''||!(q.startsWith('..')||p.isAbsolute(q))};",
100
+ "if([o.tmpdir()].concat(process.platform==='win32'?[]:['/tmp']).map(r).some(u)||(w!==h&&u(w))){",
101
+ "const m='banker hooks skipped: the hook script is in a temp folder or the session folder ('+s+')';",
102
+ `${say};process.exit(0)}`,
103
+ "import(require('url').pathToFileURL(s).href)",
104
+ ].join('');
105
+ }
106
+
107
+ const hookCommand = (p, id, node, win = process.platform === 'win32') => formatCommand(node, scriptOf(p, id), HOOKS[id].args, win, hookGuard(HOOKS[id].event === 'SessionStart'));
108
+
80
109
  // 임시 폴더의 node 는 거부한다. Codex 안에서 돌 때는 작업 폴더 안의 node 도(샌드박스 안 세션이 고칠 수 있다).
81
110
  export function safeNode(node, { win = process.platform === 'win32', cwd = process.cwd(), codex = Boolean(process.env.CODEX_THREAD_ID), home = os.homedir() } = {}) {
82
111
  const P = win ? path.win32 : path.posix;
@@ -93,11 +122,18 @@ export function safeNode(node, { win = process.platform === 'win32', cwd = proce
93
122
  return !places.some((f) => under(f, os.tmpdir()) || (inWork && under(f, cwd)));
94
123
  }
95
124
 
125
+ // 실제 경로. 아직 없는 절대 경로는 있는 조상까지만 풀고 나머지를 붙인다(macOS 의 /var 는 /private/var 로 가는 링크라,
126
+ // 풀지 않으면 링크 아래의 없는 Codex 홈이 풀린 작업 폴더 밖으로 보인다). native 먼저: Windows 의 8.3 짧은 이름을 푼다.
96
127
  function real(file) {
97
128
  try {
98
- return fs.realpathSync(file);
129
+ return fs.realpathSync.native(file);
99
130
  } catch {
100
- return file;
131
+ try {
132
+ return fs.realpathSync(file);
133
+ } catch {
134
+ const up = path.dirname(file);
135
+ return path.isAbsolute(file) && up !== file ? path.join(real(up), path.basename(file)) : file;
136
+ }
101
137
  }
102
138
  }
103
139
 
@@ -146,6 +182,49 @@ function saveHooks(p, doc) {
146
182
  writeAtomic(p.hooks, `${JSON.stringify(doc, null, 2)}\n`);
147
183
  }
148
184
 
185
+ const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
186
+
187
+ // hooks.json 을 고치는 쪽(banker 와 Codex 전용 스킬 teamwork 의 엔진)이 함께 쓰는 잠금: hooks.json 옆의 폴더
188
+ // hooks.json.lock. 10초 넘은 잠금은 죽은 쓰기로 보고 치우고, 5초 안에 못 얻으면 바꾸지 않는다(까닭을 돌려줌).
189
+ // 치우지 못한 낡은 잠금(일반 파일, 비지 않은 폴더, 권한)도 기다림과 횟수에 들어, 끝없이 돌지 않고 그 까닭으로 끝난다.
190
+ export function withHooksLock(file, fn, onBusy) {
191
+ const lock = `${file}.lock`;
192
+ let stuck = '';
193
+ for (let i = 0; ; i++) {
194
+ try {
195
+ fs.mkdirSync(lock);
196
+ break;
197
+ } catch (e) {
198
+ // Codex 홈이 아직 없으면 다툴 파일도 없다. 그 밖에 잠금을 만들 수 없으면 바꾸지 않는다.
199
+ if (e.code === 'ENOENT') return fn();
200
+ if (e.code !== 'EEXIST') return onBusy(`잠금 ${lock} 을 확인하지 못함(${e.code})`);
201
+ if (i > 200) return onBusy(stuck ? `${file} 의 낡은 잠금 ${lock} 을 지우지 못함(${stuck}). 다른 banker 나 teamwork 가 돌고 있지 않으면 직접 지우십시오` : `${file} 의 잠금(${lock})을 얻지 못함`);
202
+ stuck = clearStale(lock);
203
+ if (stuck !== '') pause(25);
204
+ }
205
+ }
206
+ try {
207
+ return fn();
208
+ } finally {
209
+ fs.rmSync(lock, { recursive: true, force: true });
210
+ }
211
+ }
212
+
213
+ // 낡은 잠금 치우기: 치웠거나 그사이 풀렸으면 ''(바로 다시 잡음), 10초가 안 된 잠금이면 null(기다림), 10초 넘었는데
214
+ // 지우지 못했으면 그 오류 코드(기다림, 마지막에 까닭으로 알림).
215
+ function clearStale(lock) {
216
+ try {
217
+ if (Date.now() - fs.statSync(lock).mtimeMs <= 10000) return null;
218
+ fs.rmdirSync(lock);
219
+ return '';
220
+ } catch (e) {
221
+ // Windows 의 rmdir 은 파일인 잠금에 ENOENT 를 내므로, 정말 없어졌는지는 다시 보고 정한다.
222
+ let st;
223
+ try { st = fs.lstatSync(lock); } catch { return ''; }
224
+ return st.isDirectory() ? String((e && e.code) || e) : 'ENOTDIR';
225
+ }
226
+ }
227
+
149
228
  // 사본 폴더. 링크인 폴더는 거부하고, 파일은 내용이 다를 때만 새로 쓴다.
150
229
  export function installPayload(p, pkgRoot) {
151
230
  fs.mkdirSync(p.dir, { recursive: true });
@@ -172,33 +251,52 @@ function place(doc, id, command) {
172
251
  return true;
173
252
  }
174
253
 
175
- // Codex 홈이 지금 폴더나 그 저장소 안이면(예: 저장소의 .envrc 가 CODEX_HOME=./.codex) 훅 사본도 작업 폴더 안에 놓여,
176
- // 샌드박스 안의 세션이 샌드박스 밖에서 도는 훅을 고칠 수 있다. 홈 폴더와 파일 시스템 루트는 작업 폴더로 보지 않는다.
254
+ // Codex 홈이 지금 폴더 안이면(예: 폴더의 .envrc 가 CODEX_HOME=./.codex) 훅 사본도 작업 폴더 안에 놓여, 샌드박스 안의
255
+ // 세션이 샌드박스 밖에서 도는 훅을 고칠 수 있다. 홈 폴더와 파일 시스템 루트는 작업 폴더로 보지 않는다.
177
256
  export function homeInWorkspace(home, cwd = process.cwd()) {
178
- const top = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', windowsHide: true });
179
- const roots = [cwd, top.status === 0 ? top.stdout.trim() : ''].filter(Boolean).map(real);
180
- const skip = new Set([real(os.homedir()), path.parse(real(cwd)).root]);
257
+ const r = real(cwd);
258
+ if (r === real(os.homedir()) || r === path.parse(r).root) return false;
181
259
  const h = real(home);
182
- return roots.some((r) => !skip.has(r) && (h === r || h.startsWith(r.endsWith(path.sep) ? r : r + path.sep)));
260
+ return h === r || h.startsWith(r.endsWith(path.sep) ? r : r + path.sep);
183
261
  }
184
262
 
185
- // ids 를 켠다. { code, changed:[id], msg }. code 1 이면 아무것도 바꾸지 않았다.
263
+ // Codex 홈에서 위로 올라가며 .git(폴더나 파일)이 있는 첫 폴더, 없으면 ''. git 을 실행하지 않는다. 사용자 홈 폴더 자체의
264
+ // .git(dotfiles 저장소)은 보지 않는다.
265
+ export function repoAbove(home, userHome = os.homedir()) {
266
+ const skip = real(userHome);
267
+ for (let d = real(home); ; d = path.dirname(d)) {
268
+ if (d !== skip && fs.existsSync(path.join(d, '.git'))) return d;
269
+ if (path.dirname(d) === d) return '';
270
+ }
271
+ }
272
+
273
+ // ids 를 켠다. { code, changed:[id], msg, note }. code 1 이면 아무것도 바꾸지 않았다. note 는 알릴 것(Codex 홈 위의 저장소).
186
274
  export function enableHooks(p, ids, pkgRoot, node = process.execPath, cwd = process.cwd()) {
275
+ return withHooksLock(p.hooks, () => enableLocked(p, ids, pkgRoot, node, cwd), (msg) => ({ code: 1, changed: [], msg }));
276
+ }
277
+
278
+ function enableLocked(p, ids, pkgRoot, node, cwd) {
187
279
  if (!safeNode(node)) return { code: 1, changed: [], msg: `node 가 안전하지 않은 곳에 있음(${node})` };
188
280
  if (homeInWorkspace(p.home, cwd)) return { code: 1, changed: [], msg: `Codex 홈(${p.home})이 작업 폴더 안이라 훅을 설치하지 않음. 샌드박스가 고칠 수 있는 곳임` };
189
281
  const { doc, why } = loadHooks(p.hooks, ids.map((id) => HOOKS[id].event));
190
282
  if (why) return { code: 1, changed: [], msg: why };
191
- const cmds = ids.map((id) => [id, formatCommand(node, scriptOf(p, id), HOOKS[id].args)]);
283
+ const cmds = ids.map((id) => [id, hookCommand(p, id, node)]);
192
284
  if (cmds.some(([, c]) => !c)) return { code: 1, changed: [], msg: '경로에 셸이 읽는 글자가 있음' };
193
285
  const bad = installPayload(p, pkgRoot);
194
286
  if (bad) return { code: 1, changed: [], msg: bad };
195
287
  const changed = cmds.filter(([id, c]) => place(doc, id, c)).map(([id]) => id);
196
288
  if (changed.length) saveHooks(p, doc);
197
- return { code: 0, changed, msg: '' };
289
+ const repo = repoAbove(p.home);
290
+ const note = repo ? `Codex 홈(${p.home})이 git 저장소(${repo}) 안에 있음. Codex 홈을 품은 폴더에서 Codex 를 열면 banker 훅은 돌지 않고 세션 시작 때 까닭을 알림` : '';
291
+ return { code: 0, changed, msg: '', note };
198
292
  }
199
293
 
200
294
  // ids 를 끈다: 우리 항목만 빼고, 비는 묶음과 이벤트를 지운다. 뒤 묶음의 번호가 밀리면 shifted 에 이벤트를 적는다.
201
295
  export function disableHooks(p, ids) {
296
+ return withHooksLock(p.hooks, () => disableLocked(p, ids), (msg) => ({ code: 1, changed: [], shifted: [], msg }));
297
+ }
298
+
299
+ function disableLocked(p, ids) {
202
300
  const { doc, why } = loadHooks(p.hooks, ids.map((id) => HOOKS[id].event));
203
301
  if (why) return { code: 1, changed: [], shifted: [], msg: why };
204
302
  const changed = [];
@@ -231,7 +329,7 @@ export function hookStatus(p, ids, pkgRoot, node = process.execPath) {
231
329
  const groups = doc.hooks[HOOKS[id].event] || [];
232
330
  const at = findOurs(groups, id);
233
331
  if (!at) out[id] = 'off';
234
- else out[id] = groups[at[0]].hooks[at[1]].command === formatCommand(node, scriptOf(p, id), HOOKS[id].args) && copyCurrent(p, pkgRoot) ? 'on' : 'stale';
332
+ else out[id] = groups[at[0]].hooks[at[1]].command === hookCommand(p, id, node) && copyCurrent(p, pkgRoot) ? 'on' : 'stale';
235
333
  }
236
334
  return { status: out };
237
335
  }
@@ -0,0 +1,4 @@
1
+ // Written by scripts/sync-codex-plugin.js from codex/manifest.json (codexName). Do not edit.
2
+ // CODEX_NAMES: Claude name to Codex name; CLAUDE_NAMES: the other way.
3
+ export const CODEX_NAMES = Object.freeze({"omc-reference":"omx-reference","setup-omc":"setup-omx","setup-omc-hud":"setup-omx-hud"});
4
+ export const CLAUDE_NAMES = Object.freeze(Object.fromEntries(Object.entries(CODEX_NAMES).map(([a, b]) => [b, a])));
@@ -4,12 +4,13 @@
4
4
  * - 플러그인: `codex plugin add banker@<마켓플레이스>` 가 <CODEX_HOME>/plugins/cache/<마켓플레이스>/banker/<판>/ 에
5
5
  * 저장소를 두고 config.toml 에 [plugins."banker@<마켓플레이스>"] 를 적는다. 스킬은 $banker:<이름>.
6
6
  * 둘을 함께 두면 스킬과 훅이 두 벌이 된다. 지우지는 않고 알리기만 한다(update-notify, banker setup --codex, doctor).
7
- * 훅(update-notify, telemetry-count-codex)과 bin/banker.js 가 함께 쓰므로 npm 길의 훅 사본(codex-hooks.mjs 의
7
+ * 훅(update-notify, telemetry-count-codex, obsidize-hook 의 Codex 길)과 bin/banker.js 가 함께 쓰므로 npm 길의 훅 사본(codex-hooks.mjs 의
8
8
  * PAYLOAD)에도 들어간다.
9
9
  */
10
10
  import fs from 'node:fs';
11
11
  import os from 'node:os';
12
12
  import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
13
14
 
14
15
  export const PLUGIN = 'banker';
15
16
  export const DEFAULT_MARKETPLACE = 'banker-plugins';
@@ -54,6 +55,66 @@ export function codexHomeOf(transcript, env = process.env) {
54
55
  return env.CODEX_HOME || path.join(os.homedir(), '.codex');
55
56
  }
56
57
 
58
+ // Codex 홈의 실제 경로가 세션 폴더(cwd) 아래인데 그 맨 위의 .codex 가 아닌지. Codex 0.165.0 샌드박스는 세션 폴더 맨 위의
59
+ // .git, .codex, .agents 만 읽기 전용으로 두므로, 그 밖(CODEX_HOME=<세션>/deep/.codex, <세션>/codexhome)의 Codex 홈은
60
+ // 샌드박스 안의 세션이 고칠 수 있다(설정, 훅 사본, 플러그인). 세션 시작 알림과 banker doctor 가 쓴다.
61
+ export function homeInSession(home, cwd) {
62
+ if (!home || !cwd || !path.isAbsolute(cwd)) return false;
63
+ const rel = path.relative(realPath(cwd), realPath(home));
64
+ if (rel.startsWith('..') || path.isAbsolute(rel)) return false;
65
+ return rel !== '.codex';
66
+ }
67
+
68
+ // 실제 경로. 아직 없는 경로는 있는 조상까지만 풀고 나머지를 붙인다(macOS 의 /var 는 /private/var 로 가는 링크).
69
+ // native 를 먼저 쓴다: Windows 의 JS 판은 8.3 짧은 이름(C:\Users\LONGNA~1)을 긴 이름으로 풀지 않는다(Windows 박스 실측,
70
+ // Node 24.19.0). native 가 못 읽는 드라이브(일부 RAM 디스크)면 JS 판으로.
71
+ export function realPath(f) {
72
+ try {
73
+ return fs.realpathSync.native(f);
74
+ } catch {
75
+ try {
76
+ return fs.realpathSync(f);
77
+ } catch {
78
+ const up = path.dirname(f);
79
+ return up === f ? f : path.join(realPath(up), path.basename(f));
80
+ }
81
+ }
82
+ }
83
+
84
+ // config.toml 의 [sandbox_workspace_write] writable_roots(표 안의 키와 점 표기 둘 다). teamwork 엔진과 같은 읽기:
85
+ // 상대 경로는 Codex 홈 기준, ~ 는 홈 폴더, 실제 경로로. config.toml 이 없거나 읽지 못하면 [].
86
+ export function writableRoots(home) {
87
+ const t = readRegular(path.join(home, 'config.toml'));
88
+ const sec = /^\[sandbox_workspace_write\][^\n]*\n([\s\S]*?)(?=^\s*\[|(?![\s\S]))/m.exec(t);
89
+ const lists = [sec && /^\s*writable_roots\s*=\s*\[([\s\S]*?)\]/m.exec(sec[1]), /^\s*sandbox_workspace_write\.writable_roots\s*=\s*\[([\s\S]*?)\]/m.exec(t)];
90
+ const items = lists.filter(Boolean).flatMap((m) => [...m[1].matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'/g)].map((x) => x[1] ?? x[2]));
91
+ return items.map((p) => realPath(path.resolve(home, p.replace(/^~(?=[/\\]|$)/, os.homedir()))));
92
+ }
93
+
94
+ // 훅이 도는 곳: 훅 스크립트(모듈 URL), 그 위의 플러그인 폴더나 npm 길 사본 폴더(<폴더>/hooks/<스크립트>), 실행 중인 node.
95
+ export function hookPlaces(scriptUrl) {
96
+ const file = fileURLToPath(scriptUrl);
97
+ return [file, path.dirname(path.dirname(file)), process.execPath];
98
+ }
99
+
100
+ // 훅이 도는 곳 가운데 writable_roots 항목 아래인 것이 있으면 알림 글, 없으면 ''. Codex 0.165.0 샌드박스는 쓰기 허용
101
+ // 폴더마다 맨 위의 .git, .codex, .agents 를 읽기 전용으로 두므로 그 항목의 맨 위 .codex 아래(기본 ~/.codex)는 뺀다.
102
+ // 그런 곳의 훅 파일과 node 는 샌드박스 안의 세션이 고칠 수 있고, Codex 는 신뢰한 훅을 샌드박스 밖에서 돌린다.
103
+ // 이 검사는 고치기 전의 첫 세션에서 알리고 멈추는 것이다: 이미 고쳐진 파일은 이 검사도 고칠 수 있다.
104
+ export function writableRootsNote(home, places) {
105
+ const roots = writableRoots(home);
106
+ if (!roots.length) return '';
107
+ for (const place of places) {
108
+ const p = realPath(place);
109
+ const root = roots.find((d) => {
110
+ const rel = path.relative(d, p);
111
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel) && rel.split(path.sep)[0] !== '.codex');
112
+ });
113
+ if (root) return `banker hooks skipped: ${p} is under the sandbox writable root ${root} (config.toml [sandbox_workspace_write] writable_roots), so a Codex session could change what this hook runs outside the sandbox`;
114
+ }
115
+ return '';
116
+ }
117
+
57
118
  // npm 길로 설치된 banker 스킬이 있는지(<홈>/skills/banker-*/SKILL.md).
58
119
  export function npmRoute(home) {
59
120
  try {
@@ -110,3 +171,34 @@ export function dualInstallNote(home, cwd = '') {
110
171
  + '스킬이 $banker-<이름> 과 $banker:<이름> 으로 겹칩니다. 하나만 두십시오: '
111
172
  + `npm 쪽을 빼려면 banker uninstall --codex, 플러그인을 빼려면 codex plugin remove ${plugins[0]}`;
112
173
  }
174
+
175
+ // 플러그인 길의 스킬 목록(.codex-plugin/plugin.json skills 의 스킬별 경로)을 읽는다고 확인한 가장 낮은 Codex 판.
176
+ // 0.160.1(Windows), 0.165.0(Linux) 에서 확인했고 0.144.5 는 재지 못했다. 그보다 낮은 판은 banker 스킬이 거의 다 안 보일 수 있다.
177
+ export const MIN_PLUGIN_CODEX = '0.160.1';
178
+
179
+ const triple = (v) => (/^(\d+)\.(\d+)\.(\d+)/.exec(String(v || '')) || []).slice(1, 4).map(Number);
180
+
181
+ // 플러그인 길에서 Codex 판이 MIN_PLUGIN_CODEX 보다 낮으면 알림 한 줄, 아니면(판을 모를 때 포함) ''.
182
+ export function pluginVersionNote(version) {
183
+ const v = triple(version);
184
+ const min = triple(MIN_PLUGIN_CODEX);
185
+ if (v.length !== 3) return '';
186
+ const lower = v[0] !== min[0] ? v[0] < min[0] : v[1] !== min[1] ? v[1] < min[1] : v[2] < min[2];
187
+ if (!lower) return '';
188
+ return `banker: Codex ${v.join('.')} 은 ${MIN_PLUGIN_CODEX} 보다 낮아 banker 플러그인 스킬이 안 보일 수 있습니다(${MIN_PLUGIN_CODEX} 이상에서 확인). `
189
+ + 'Codex 를 올리거나 npm 길(banker setup --codex)을 쓰십시오';
190
+ }
191
+
192
+ // `codex --version` 의 판(x.y.z). 없거나 2초 안에 답하지 않으면 ''. Windows 는 codex.cmd 를 cmd 로 부른다. cmd 는
193
+ // 이름만 준 명령을 PATH 보다 지금 폴더에서 먼저 찾으므로(훅의 지금 폴더는 세션 폴더) 홈 폴더에서 부르고
194
+ // NoDefaultCurrentDirectoryInExePath 로 지금 폴더 찾기를 끈다.
195
+ export function codexCliVersion(spawn) {
196
+ const win = process.platform === 'win32';
197
+ try {
198
+ const opts = { encoding: 'utf8', timeout: 2000, shell: win, input: '', windowsHide: true, cwd: os.homedir(), env: { ...process.env, NoDefaultCurrentDirectoryInExePath: '1' } };
199
+ const r = spawn(win ? 'codex.cmd --version' : 'codex', win ? [] : ['--version'], opts);
200
+ return (/(\d+\.\d+\.\d+)/.exec(String(r.stdout || '')) || [])[1] || '';
201
+ } catch {
202
+ return '';
203
+ }
204
+ }
@@ -5,15 +5,46 @@
5
5
  * - npm (banker setup --codex): writes <CODEX_HOME>/skills/banker-<name>/SKILL.md, invoked as $banker-<name>.
6
6
  * - Codex plugin (codex plugin add banker@banker-plugins): scripts/sync-codex-plugin.js writes
7
7
  * codex/command-skills/<name>/SKILL.md in the repo, invoked as $banker:<name>.
8
- * The command's description becomes the skill's. Codex skills get no $ARGUMENTS substitution, so a note says where the
9
- * arguments come from.
8
+ * The command's description becomes the skill's, or the manifest's codexDescription when it has one. Skills Codex
9
+ * names apart (codex/manifest.json codexName: setup-omc is setup-omx there) take their Codex name in the body: whole
10
+ * names only, so omc-patch, payload-mon and .omc/ paths stay. A phrase that says what the renamed skill installs
11
+ * (CODEX_PHRASES) is written for Codex as a whole first: the name alone would send an OMX skill to install OMC. Codex
12
+ * skills get no $ARGUMENTS substitution, so a note says where the arguments come from.
10
13
  */
11
- function commandSkillText(text, name, invoke) {
14
+ // [command text, its Codex text]. scripts/sync-codex-plugin.test.mjs checks that each still occurs in commands/.
15
+ const CODEX_PHRASES = [
16
+ ['oh-my-claudecode(또는 `setup-omc` 스킬 / `npm install -g oh-my-claude-sisyphus@latest`)', 'oh-my-codex(OMX)(또는 `setup-omx` 스킬)'],
17
+ ['**setup-omc** — oh-my-claudecode(OMC) 설치/갱신(Codex는 OMX).', '**setup-omx** — oh-my-codex(OMX) 설치/갱신.'],
18
+ ['**setup-omc-hud** — omc_hud 상태표시줄(래퍼 v4, 스킬에 든 파일로 오프라인 설치).', '**setup-omx-hud** — Codex TUI 상태표시줄(`[tui] status_line`)을 OMX 프리셋으로. 같은 머신의 Claude Code omc_hud 래퍼(v4)를 고르면 스킬에 든 파일로 오프라인 설치.'],
19
+ ];
20
+
21
+ function codexNames(text, rename) {
22
+ const keys = Object.keys(rename || {}).sort((a, b) => b.length - a.length);
23
+ if (!keys.length) return text;
24
+ const re = new RegExp(`(?<![A-Za-z0-9_-])(${keys.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})(?![A-Za-z0-9_-])`, 'g');
25
+ return text.replace(re, (k) => rename[k]);
26
+ }
27
+
28
+ function commandSkillText(text, name, invoke, { description, rename } = {}) {
12
29
  const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
13
30
  const desc = fm && fm[1].match(/^description:\s*(.*)$/m);
14
- const body = fm ? text.slice(fm[0].length) : text;
31
+ const raw = fm ? text.slice(fm[0].length) : text;
32
+ const body = codexNames(rename ? CODEX_PHRASES.reduce((t, [from, to]) => t.split(from).join(to), raw) : raw, rename);
15
33
  const note = '> Codex: 이 명령은 스킬로 설치됐다. 본문의 `$ARGUMENTS`, `$1` 은 사용자가 `' + invoke + '` 뒤에 쓴 글이다.\n\n';
16
- return `---\nname: ${name}\ndescription: ${desc ? desc[1] : '""'}\n---\n\n${note}${body}`;
34
+ const line = description ? JSON.stringify(description) : (desc ? desc[1] : '""');
35
+ return `---\nname: ${name}\ndescription: ${line}\n---\n\n${note}${body}`;
36
+ }
37
+
38
+ // A SKILL.md with its frontmatter name set (added first when missing) and, when given, its description: only those
39
+ // two lines change, and the file's line ends stay. The description is written as a JSON string, which YAML reads as a
40
+ // double-quoted one. A file without frontmatter comes back as it is.
41
+ function codexFrontmatter(src, name, description) {
42
+ const fm = src.match(/^---(\r?\n)([\s\S]*?)(\r?\n)---/);
43
+ if (!fm) return src;
44
+ let block = fm[2];
45
+ block = /^name:.*$/m.test(block) ? block.replace(/^name:.*$/m, () => `name: ${name}`) : `name: ${name}${fm[1]}${block}`;
46
+ if (description) block = block.replace(/^description:.*$/m, () => `description: ${JSON.stringify(description)}`);
47
+ return `${src.slice(0, fm.index)}---${fm[1]}${block}${fm[3]}---${src.slice(fm.index + fm[0].length)}`;
17
48
  }
18
49
 
19
- module.exports = { commandSkillText };
50
+ module.exports = { commandSkillText, codexNames, codexFrontmatter, CODEX_PHRASES };
@@ -18,7 +18,7 @@ description: "스펙 파일(기본 `docs/note.md`)을 바탕으로 프론트엔
18
18
  ```bash
19
19
  omc ralph verify --help 2>&1 | grep -q 'Usage: omc ralph verify' && echo "omc ok" || echo "omc CLI 없음 또는 낡음"
20
20
  ```
21
- - 실패하면 `/banker:setup` → oh-my-claudecode(또는 `setup-omc` 스킬 / `npm install -g oh-my-claude-sisyphus@latest`)로 먼저 구성한 뒤 이어서 진행. 점검을 통과하기 전에는 절차 1을 시작하지 않는다.
21
+ - 실패하면 `/banker:setup` → oh-my-codex(OMX)(또는 `setup-omx` 스킬)로 먼저 구성한 뒤 이어서 진행. 점검을 통과하기 전에는 절차 1을 시작하지 않는다.
22
22
  - **런타임 매핑:** 아래 절차의 `/ralplan`·`/ralph`는 Claude Code에선 OMC 스킬, **Codex에선 oh-my-codex(OMX)의 동명 스킬**이다(Codex는 OMC 대신 OMX 전제). 각 런타임에서 사용 가능한 형태로 호출한다.
23
23
 
24
24
  ## 절차 (CLAUDE.md: 계획 없는 즉시실행 금지)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: setup
3
- description: "구성요소와 의존성을 여러 개 골라 설치하는 오케스트레이터. 런타임(Node, Python, Java), LSP, tmux, MCP, 샌드박스, OMC, Playwright 등. 권한 확인 끄기는 따로 묻고 기본으로 고르지 않으며, 골라도 실행하지 않고 직접 입력할 명령만 안내."
3
+ description: "구성요소와 의존성을 여러 개 골라 설치하는 오케스트레이터. 런타임(Node, Python, Java), LSP, tmux, MCP, 샌드박스, OMX, Playwright 등. 권한 확인 끄기는 따로 묻고 기본으로 고르지 않으며, 골라도 실행하지 않고 직접 입력할 명령만 안내."
4
4
  ---
5
5
 
6
6
  > Codex: 이 명령은 스킬로 설치됐다. 본문의 `$ARGUMENTS`, `$1` 은 사용자가 `$banker:setup` 뒤에 쓴 글이다.
@@ -29,7 +29,7 @@ banker 플러그인이 제공하는 설치 스킬들을 **multi-select**로 골
29
29
  - OS 패키지 관리자(dnf, apt, brew, winget)만 쓰는 단계는 자기 설정의 미러를 쓰므로 이 확인과 관계없이 각 스킬의 규칙대로 시도한다.
30
30
  - 패킷을 버리거나 답하지 않는 망에서 dnf 는 저장소마다 약 2분(SYN 을 버리는 망 실측 121초), apt 는 원천마다 약 4분(답하지 않는 원천 실측 247초) 기다린 뒤 실패로 끝난다. 시간 제한은 관리자 설정을 따르므로 이 스킬이 바꾸지 않는다.
31
31
  - `apt-get update` 는 받지 못해도 종료 0 이다. 출력의 `W: Failed to fetch` 가 있으면 그 단계를 실패로 보고한다.
32
- - 내려받기가 없는 항목(`setup-omc-hud`)과 이미 설치된 것의 확인은 그대로 진행한다.
32
+ - 내려받기가 없는 항목(`setup-omx-hud`)과 이미 설치된 것의 확인은 그대로 진행한다.
33
33
  - Windows PowerShell 에서는 `curl.exe --ssl-no-revoke -sSI -o NUL --connect-timeout 5 --max-time 10 https://github.com` 의 `$LASTEXITCODE` 로 본다. `curl.exe` 는 Windows 시스템 프록시를 따르지 않고 `HTTPS_PROXY` 만 따른다. `--ssl-no-revoke` 는 폐지 목록 서버에 닿지 못하는 망에서 Schannel 의 폐지 확인 실패를 오프라인으로 읽지 않게 한다. 닿는지만 보고 받은 내용은 쓰지 않으므로 이 확인에만 준다. `curl.exe` 가 없으면 `$LASTEXITCODE` 에 앞 명령의 값이 남으므로 `Get-Command curl.exe -ErrorAction SilentlyContinue` 로 먼저 보고, 없으면 확인하지 못한 것으로 적는다. `npm ping` 은 `Push-Location ~; npm ping --fetch-retries=0 --fetch-timeout=10000 --no-update-notifier; Pop-Location` 처럼 홈에서 부르고 돌아온다(돌아오지 않으면 뒤 단계가 프로젝트가 아니라 홈에서 돈다). PowerShell 에는 15초 감시를 두지 않는다(Windows 의 연결 시간 제한은 약 21초로 알려져 있다. 미실측).
34
34
  - Codex sandbox 가 네트워크를 막는 모드(기본 `workspace-write`, `read-only`)에서는 `CODEX_SANDBOX_NETWORK_DISABLED=1` 이 서고 확인이 모두 실패한다(curl 종료 7, npm `EPERM`). 블록의 첫 줄이 그렇다고 알린다. 그때는 '오프라인' 이 아니라 'sandbox 가 네트워크를 막음' 으로 보고하고, 승인을 받아 sandbox 밖에서 확인부터 다시 한다. Claude Code sandbox 안(`SANDBOX_RUNTIME=1`)에서는 허용 목록 밖 주소를 sandbox 프록시가 막는다. Windows PowerShell 에서는 `$env:CODEX_SANDBOX_NETWORK_DISABLED -eq '1'` 로 본다.
35
35
  - 확인이 실패하면 보고에 그 명령, 종료 코드, stderr 첫 줄을 붙인다(예: `npm error code ETIMEDOUT`, `curl: (28) ...`). 원인을 '오프라인' 하나로 짐작하지 않는다.
@@ -54,10 +54,10 @@ banker 플러그인이 제공하는 설치 스킬들을 **multi-select**로 골
54
54
  - **setup-sandbox** — OS별 샌드박스(bubblewrap+userns / AppContainer / Seatbelt) + rust + git safe.directory.
55
55
 
56
56
  **프레임워크·플러그인**
57
- - **setup-omc** — oh-my-claudecode(OMC) 설치/갱신(Codex는 OMX). `all-in-one`·`ultra-chain`·`ultra-init`·`/banker:front-qa` 의존성.
57
+ - **setup-omx** — oh-my-codex(OMX) 설치/갱신. `all-in-one`·`ultra-chain`·`ultra-init`·`/banker:front-qa` 의존성.
58
58
  - **setup-harness-factory** — revfactory/harness(팀 아키텍처 팩토리) 설치+구성+안내(Codex=meta-harness).
59
59
  - **setup-insane-search** — insane-search 플러그인(차단 사이트 우회, Claude/Codex 양쪽).
60
- - **setup-omc-hud** — omc_hud 상태표시줄(래퍼 v4, 스킬에 든 파일로 오프라인 설치).
60
+ - **setup-omx-hud** — Codex TUI 상태표시줄(`[tui] status_line`)을 OMX 프리셋으로. 같은 머신의 Claude Code omc_hud 래퍼(v4)를 고르면 스킬에 든 파일로 오프라인 설치.
61
61
  - **setup-stitch** — Stitch(디자인 생성) MCP 프록시 등록(RockyLinux8 proxy-script, API key 필요).
62
62
 
63
63
  **의존성 라이브러리**
package/codex/hooks.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
- "description": "banker hooks for the Codex plugin (codex plugin add banker@banker-plugins), written by scripts/sync-codex-plugin.js. Codex reads this file instead of hooks/hooks.json because .codex-plugin/plugin.json names it. update notice (SessionStart, opt-out BANKER_NO_UPDATE_CHECK), usage counting (Stop, opt-out BANKER_NO_TELEMETRY). The obsidizer hook is installed only by obsidizer --enable. Codex runs them only after you trust them in /hooks, and outside its sandbox: each command runs the node PATH names (a multicall shim such as Volta or mise, or a .cmd shim on Windows: the node binary it names) only when its real path is absolute and outside the temp folder and the session folder (a session in the home folder itself excepted), from the plugin folder, and otherwise says why it skipped. Each command starts with a hash of the hook files (banker-hooks-rev), so new hook code asks for trust again. command is for sh, commandWindows for PowerShell; PLUGIN_ROOT is the installed plugin folder.",
2
+ "description": "banker hooks for the Codex plugin (codex plugin add banker@banker-plugins), written by scripts/sync-codex-plugin.js. Codex reads this file instead of hooks/hooks.json because .codex-plugin/plugin.json names it. update notice (SessionStart, opt-out BANKER_NO_UPDATE_CHECK), usage counting (Stop, opt-out BANKER_NO_TELEMETRY). The obsidizer hook is installed only by obsidizer --enable. Codex runs them only after you trust them in /hooks, and outside its sandbox: each command runs the node PATH names (a multicall shim such as Volta or mise, or a .cmd shim on Windows: the node binary it names) only when its real path is absolute and outside the temp folder and the session folder (a session in the home folder itself excepted), from the plugin folder when that is outside them too, and otherwise says why it skipped. A hook whose script, plugin folder or node is under a config.toml [sandbox_workspace_write] writable_roots entry (not that entry's top .codex) does nothing but say why. PATH entries under a network mount outside the home folder are not read. Each command starts with a hash of the hook files (banker-hooks-rev), so new hook code asks for trust again. command is for sh, commandWindows for PowerShell; PLUGIN_ROOT is the installed plugin folder.",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  {
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": ": banker-hooks-rev 625aade934d2; x() { m=$(printf '%s' \"banker hooks skipped: $1\" | command -p tr -d '\\000-\\037\"\\\\'); printf '{\"systemMessage\":\"%s\"}\\n' \"$m\"; exit 0; }; [ -n \"$PLUGIN_ROOT\" ] || x \"PLUGIN_ROOT is not set\"; p=$(pwd -P); [ -n \"$p\" ] && [ \"$p\" != / ] || x \"the session folder is /\"; u=$(cd /tmp 2>/dev/null && pwd -P); t=$(cd \"${TMPDIR:-/tmp}\" 2>/dev/null && pwd -P); h=; case \"$HOME\" in /?*) h=$(cd \"$HOME\" 2>/dev/null && pwd -P);; esac; w() { for z in \"$u\" \"$t\"; do [ -n \"$z\" ] && case \"$1/\" in \"$z\"/*) return 0;; esac; done; [ \"$p\" = \"$h\" ] && return 1; case \"$1/\" in \"$p\"/*) return 0;; esac; return 1; }; n=$(command -v node) || for d in /opt/homebrew/bin /usr/local/bin; do [ -x \"$d/node\" ] && n=$d/node && break; done; [ -n \"$n\" ] || x \"node is not on PATH (a Codex started from the desktop may not have the shell's PATH)\"; case \"$n\" in /*) ;; *) x \"node on PATH is not an absolute path ($n)\";; esac; set -f; o=$IFS; IFS=:; q=; for v in $PATH; do case \"$v\" in /*) ;; *) continue;; esac; y=$(cd \"$v\" 2>/dev/null && pwd -P) || continue; w \"$y\" || q=$q${q:+:}$v; done; IFS=$o; set +f; PATH=${q:-/usr/bin:/bin}; export PATH; cd \"$PLUGIN_ROOT\" || x \"cannot enter the plugin folder\"; rp() { realpath \"$1\" 2>/dev/null || readlink -f \"$1\" 2>/dev/null || { f=$1; i=0; while [ -L \"$f\" ]; do i=$((i+1)); [ \"$i\" -le 40 ] || return 1; l=$(readlink \"$f\") || return 1; case \"$l\" in /*) f=$l;; *) f=${f%/*}/$l;; esac; done; [ -e \"$f\" ] || return 1; d=$(cd -P \"${f%/*}/\" 2>/dev/null && pwd -P) || return 1; printf '%s\\n' \"${d%/}/${f##*/}\"; }; }; r=$(rp \"$n\") || x \"cannot resolve the real path of $n\"; k() { case \"$1\" in /*) ;; *) x \"node path is not absolute ($1)\";; esac; for d in \"$u\" \"$t\"; do [ -n \"$d\" ] && case \"$1\" in \"$d\"/*) x \"node is in a temp folder ($1)\";; esac; done; [ \"$p\" = \"$h\" ] || case \"$1\" in \"$p\"/*) x \"node is in the session folder ($1)\";; esac; }; case \"${r##*/}\" in node) k \"$r\"; e=$r;; *) k \"$n\"; k \"$r\"; e=$(\"$n\" -p 'require(\"fs\").realpathSync(process.execPath)' 2>/dev/null) || x \"the node shim $n did not name its node\"; k \"$e\";; esac; \"$e\" \"$PLUGIN_ROOT/hooks/run.cjs\" \"$PLUGIN_ROOT/hooks/update-notify.mjs\" --codex --plugin",
10
- "commandWindows": "$null = 'banker-hooks-rev 625aade934d2'; function x($m) { Write-Output (ConvertTo-Json -Compress @{ systemMessage = \"banker hooks skipped: $m\" }); exit 0 }; if (-not $env:PLUGIN_ROOT) { x 'PLUGIN_ROOT is not set' }; function G($p) { try { (Get-Item -LiteralPath $p -Force -ErrorAction Stop).FullName } catch { try { [IO.Path]::GetFullPath($p) } catch { $p } } }; $sb = @{}; foreach ($l in @(& (Join-Path ([Environment]::SystemDirectory) 'subst.exe') 2>$null)) { if ($l -match '^([A-Za-z]:)\\\\: => (.+)$') { $sb[$matches[1].ToUpper()] = $matches[2] } }; function L($p) { if (-not $p) { return '' }; $p = G $p; for ($i = 0; $i -lt 32; $i++) { $d2 = if ($p.Length -ge 2) { $p.Substring(0, 2).ToUpper() } else { '' }; if ($sb.Contains($d2)) { $p = G ($sb[$d2].TrimEnd('\\') + $p.Substring(2)); continue }; $c = $p; $t = $null; while ($c) { $it = Get-Item -LiteralPath $c -Force -ErrorAction SilentlyContinue; if ($it -and $it.LinkType -in 'SymbolicLink', 'Junction' -and @($it.Target)[0]) { $t = [string]@($it.Target)[0]; break }; $c = Split-Path -LiteralPath $c }; if (-not $t) { break }; if (-not [IO.Path]::IsPathRooted($t)) { $t = Join-Path (Split-Path -LiteralPath $c) $t }; $p = G ($t.TrimEnd('\\') + $p.Substring($c.Length)) }; $p.TrimEnd('\\') }; $ds = @($env:TEMP, $env:TMP); if ($env:LOCALAPPDATA) { $ds += \"$env:LOCALAPPDATA\\Temp\" }; $w = L $PWD.Path; if ($w -ne (L $env:USERPROFILE)) { $ds += $w }; $ds = @($ds | Where-Object { $_ } | ForEach-Object { L $_ } | Where-Object { $_ }); function u($f) { $f = L $f; foreach ($d in $ds) { if ($f -eq $d -or $f.StartsWith($d + '\\', 'OrdinalIgnoreCase')) { return $true } }; $false }; function k($f) { if (u $f) { x \"node is in a temp folder or the session folder ($(L $f))\" }; L $f }; $nd = @(@($env:PATH -split ';') | ForEach-Object { [Environment]::ExpandEnvironmentVariables($_.Trim().Trim('\"')) } | Where-Object { $_ -match '^[A-Za-z]:\\\\' -and ([IO.DriveInfo]::new($_.Substring(0, 1))).DriveType -ne 'Network' }); $n = $null; foreach ($d in $nd) { foreach ($e in 'node.exe', 'node.bat', 'node.cmd') { $f = Join-Path $d $e; if (Test-Path -LiteralPath $f -PathType Leaf) { $n = $f; break } }; if ($n) { break } }; if (-not $n) { x 'node is not on PATH' }; $env:PATH = ($nd | Where-Object { -not (u $_) }) -join ';'; $r = k $n; if (-not (Set-Location -LiteralPath $env:PLUGIN_ROOT -PassThru -ErrorAction SilentlyContinue)) { x 'cannot enter the plugin folder' }; if ($r -notmatch '\\.exe$') { $e = & $r -p process.execPath 2>$null | Select-Object -Last 1; if (-not $e -or $e -notmatch '\\.exe$' -or -not (Test-Path -LiteralPath $e -PathType Leaf)) { x \"the node shim $r did not name its node\" }; $r = k $e }; & $r \"$env:PLUGIN_ROOT\\hooks\\run.cjs\" \"$env:PLUGIN_ROOT\\hooks\\update-notify.mjs\" --codex --plugin",
9
+ "command": ": banker-hooks-rev 7fad90560d65; x() { m=$(printf '%s' \"banker hooks skipped: $1\" | command -p tr -d '\\000-\\037\"\\\\'); printf '{\"systemMessage\":\"%s\"}\\n' \"$m\"; exit 0; }; [ -n \"$PLUGIN_ROOT\" ] || x \"PLUGIN_ROOT is not set\"; p=$(pwd -P); [ -n \"$p\" ] && [ \"$p\" != / ] || x \"the session folder is /\"; u=$(cd /tmp 2>/dev/null && pwd -P); t=$(cd \"${TMPDIR:-/tmp}\" 2>/dev/null && pwd -P); h=; case \"$HOME\" in /?*) h=$(cd \"$HOME\" 2>/dev/null && pwd -P);; esac; w() { for z in \"$u\" \"$t\"; do [ -n \"$z\" ] && case \"$1/\" in \"$z\"/*) return 0;; esac; done; [ \"$p\" = \"$h\" ] && return 1; case \"$1/\" in \"$p\"/*) return 0;; esac; return 1; }; pr=$(cd -P \"$PLUGIN_ROOT\" 2>/dev/null && pwd -P) || x \"cannot enter the plugin folder\"; w \"$pr\" && x \"the plugin folder is in a temp folder or the session folder ($pr)\"; A='function dec(s, o, i, c) { o = \"\"; for (i = 1; i <= length(s); i++) { c = substr(s, i, 1); if (c == \"\\\\\" && substr(s, i + 1, 3) ~ /^[0-7][0-7][0-7]$/) { o = o sprintf(\"%c\", substr(s, i + 1, 1) * 64 + substr(s, i + 2, 1) * 8 + substr(s, i + 3, 1)); i += 3 } else o = o c }; return o } mac { i = index($0, \" on \"); j = 0; for (k = length($0); k > i; k--) if (substr($0, k, 2) == \" (\") { j = k; break }; if (!i || !j) next; m = substr($0, i + 4, j - i - 4); t = substr($0, j + 2); sub(/[,)].*/, \"\", t) } !mac { m = dec($2); t = $3 } (t ~ /^(nfs|nfs4|cifs|smbfs|smb3|afpfs|webdav|davfs|autofs|ceph|glusterfs|lustre|9p)$/ || t ~ /^fuse\\./) && m !~ /:/ { printf \"%s:\", m }'; if [ -r '/proc/self/mounts' ]; then M=$(LC_ALL=C command -p awk \"$A\" '/proc/self/mounts' 2>/dev/null); else M=$('/sbin/mount' 2>/dev/null | LC_ALL=C command -p awk -v mac=1 \"$A\" 2>/dev/null); fi; nm() { for z in $M; do case \"$1/\" in \"$z\"/*) return 0;; esac; done; return 1; }; hm() { for z in \"$h\" \"$HOME\"; do case \"$z\" in /?*) case \"$1/\" in \"$z\"/*) return 0;; esac;; esac; done; return 1; }; set -f; o=$IFS; IFS=:; a=; q=; for v in $PATH; do case \"$v\" in /*) ;; *) a=$a${a:+:}$v; continue;; esac; nm \"$v\" && ! hm \"$v\" && continue; a=$a${a:+:}$v; y=$(cd \"$v\" 2>/dev/null && pwd -P) || continue; w \"$y\" || q=$q${q:+:}$v; done; n=; [ -n \"$a\" ] && n=$(PATH=$a; command -v node) || for d in /opt/homebrew/bin /usr/local/bin; do nm \"$d\" && continue; [ -x \"$d/node\" ] && n=$d/node && break; done; IFS=$o; set +f; [ -n \"$n\" ] || x \"node is not on PATH (a Codex started from the desktop may not have the shell's PATH)\"; case \"$n\" in /*) ;; *) x \"node on PATH is not an absolute path ($n)\";; esac; PATH=${q:-/usr/bin:/bin}; export PATH; cd \"$pr\" || x \"cannot enter the plugin folder\"; rp() { realpath \"$1\" 2>/dev/null || readlink -f \"$1\" 2>/dev/null || { f=$1; i=0; while [ -L \"$f\" ]; do i=$((i+1)); [ \"$i\" -le 40 ] || return 1; l=$(readlink \"$f\") || return 1; case \"$l\" in /*) f=$l;; *) f=${f%/*}/$l;; esac; done; [ -e \"$f\" ] || return 1; d=$(cd -P \"${f%/*}/\" 2>/dev/null && pwd -P) || return 1; printf '%s\\n' \"${d%/}/${f##*/}\"; }; }; r=$(rp \"$n\") || x \"cannot resolve the real path of $n\"; k() { case \"$1\" in /*) ;; *) x \"node path is not absolute ($1)\";; esac; for d in \"$u\" \"$t\"; do [ -n \"$d\" ] && case \"$1\" in \"$d\"/*) x \"node is in a temp folder ($1)\";; esac; done; [ \"$p\" = \"$h\" ] || case \"$1\" in \"$p\"/*) x \"node is in the session folder ($1)\";; esac; }; case \"${r##*/}\" in node) k \"$r\"; e=$r;; *) k \"$n\"; k \"$r\"; e=$(\"$n\" -p 'require(\"fs\").realpathSync(process.execPath)' 2>/dev/null) || x \"the node shim $n did not name its node\"; k \"$e\";; esac; \"$e\" \"$pr/hooks/run.cjs\" \"$pr/hooks/update-notify.mjs\" --codex --plugin",
10
+ "commandWindows": "$null = 'banker-hooks-rev 7fad90560d65'; function x($m) { Write-Output (ConvertTo-Json -Compress @{ systemMessage = \"banker hooks skipped: $m\" }); exit 0 }; if (-not $env:PLUGIN_ROOT) { x 'PLUGIN_ROOT is not set' }; function G($p) { try { (Get-Item -LiteralPath $p -Force -ErrorAction Stop).FullName } catch { try { [IO.Path]::GetFullPath($p) } catch { $p } } }; $sb = @{}; foreach ($l in @(& (Join-Path ([Environment]::SystemDirectory) 'subst.exe') 2>$null)) { if ($l -match '^([A-Za-z]:)\\\\: => (.+)$') { $sb[$matches[1].ToUpper()] = $matches[2] } }; function L($p) { if (-not $p) { return '' }; $p = G $p; for ($i = 0; $i -lt 32; $i++) { $d2 = if ($p.Length -ge 2) { $p.Substring(0, 2).ToUpper() } else { '' }; if ($sb.Contains($d2)) { $p = G ($sb[$d2].TrimEnd('\\') + $p.Substring(2)); continue }; $c = $p; $t = $null; while ($c) { $it = Get-Item -LiteralPath $c -Force -ErrorAction SilentlyContinue; if ($it -and $it.LinkType -in 'SymbolicLink', 'Junction' -and @($it.Target)[0]) { $t = [string]@($it.Target)[0]; break }; $c = Split-Path -LiteralPath $c }; if (-not $t) { break }; if (-not [IO.Path]::IsPathRooted($t)) { $t = Join-Path (Split-Path -LiteralPath $c) $t }; $p = G ($t.TrimEnd('\\') + $p.Substring($c.Length)) }; $p.TrimEnd('\\') }; $ds = @($env:TEMP, $env:TMP); if ($env:LOCALAPPDATA) { $ds += \"$env:LOCALAPPDATA\\Temp\" }; $w = L $PWD.Path; if ($w -ne (L $env:USERPROFILE)) { $ds += $w }; $ds = @($ds | Where-Object { $_ } | ForEach-Object { L $_ } | Where-Object { $_ }); function u($f) { $f = L $f; foreach ($d in $ds) { if ($f -eq $d -or $f.StartsWith($d + '\\', 'OrdinalIgnoreCase')) { return $true } }; $false }; function k($f) { if (u $f) { x \"node is in a temp folder or the session folder ($(L $f))\" }; L $f }; $pr = L $env:PLUGIN_ROOT; if (u $pr) { x \"the plugin folder is in a temp folder or the session folder ($pr)\" }; $nd = @(@($env:PATH -split ';') | ForEach-Object { [Environment]::ExpandEnvironmentVariables($_.Trim().Trim('\"')) } | Where-Object { $_ -match '^[A-Za-z]:\\\\' -and ([IO.DriveInfo]::new($_.Substring(0, 1))).DriveType -ne 'Network' }); $n = $null; foreach ($d in $nd) { foreach ($e in 'node.exe', 'node.bat', 'node.cmd') { $f = Join-Path $d $e; if (Test-Path -LiteralPath $f -PathType Leaf) { $n = $f; break } }; if ($n) { break } }; if (-not $n) { x 'node is not on PATH' }; $env:PATH = ($nd | Where-Object { -not (u $_) }) -join ';'; $r = k $n; if (-not (Set-Location -LiteralPath $pr -PassThru -ErrorAction SilentlyContinue)) { x 'cannot enter the plugin folder' }; if ($r -notmatch '\\.exe$') { $e = & $r -p process.execPath 2>$null | Select-Object -Last 1; if (-not $e -or $e -notmatch '\\.exe$' -or -not (Test-Path -LiteralPath $e -PathType Leaf)) { x \"the node shim $r did not name its node\" }; $r = k $e }; & $r \"$pr\\hooks\\run.cjs\" \"$pr\\hooks\\update-notify.mjs\" --codex --plugin",
11
11
  "timeout": 5
12
12
  }
13
13
  ]
@@ -18,8 +18,8 @@
18
18
  "hooks": [
19
19
  {
20
20
  "type": "command",
21
- "command": ": banker-hooks-rev 625aade934d2; x() { printf '%s\\n' \"banker hooks skipped: $1\" >&2; exit 0; }; [ -n \"$PLUGIN_ROOT\" ] || x \"PLUGIN_ROOT is not set\"; p=$(pwd -P); [ -n \"$p\" ] && [ \"$p\" != / ] || x \"the session folder is /\"; u=$(cd /tmp 2>/dev/null && pwd -P); t=$(cd \"${TMPDIR:-/tmp}\" 2>/dev/null && pwd -P); h=; case \"$HOME\" in /?*) h=$(cd \"$HOME\" 2>/dev/null && pwd -P);; esac; w() { for z in \"$u\" \"$t\"; do [ -n \"$z\" ] && case \"$1/\" in \"$z\"/*) return 0;; esac; done; [ \"$p\" = \"$h\" ] && return 1; case \"$1/\" in \"$p\"/*) return 0;; esac; return 1; }; n=$(command -v node) || for d in /opt/homebrew/bin /usr/local/bin; do [ -x \"$d/node\" ] && n=$d/node && break; done; [ -n \"$n\" ] || x \"node is not on PATH (a Codex started from the desktop may not have the shell's PATH)\"; case \"$n\" in /*) ;; *) x \"node on PATH is not an absolute path ($n)\";; esac; set -f; o=$IFS; IFS=:; q=; for v in $PATH; do case \"$v\" in /*) ;; *) continue;; esac; y=$(cd \"$v\" 2>/dev/null && pwd -P) || continue; w \"$y\" || q=$q${q:+:}$v; done; IFS=$o; set +f; PATH=${q:-/usr/bin:/bin}; export PATH; cd \"$PLUGIN_ROOT\" || x \"cannot enter the plugin folder\"; rp() { realpath \"$1\" 2>/dev/null || readlink -f \"$1\" 2>/dev/null || { f=$1; i=0; while [ -L \"$f\" ]; do i=$((i+1)); [ \"$i\" -le 40 ] || return 1; l=$(readlink \"$f\") || return 1; case \"$l\" in /*) f=$l;; *) f=${f%/*}/$l;; esac; done; [ -e \"$f\" ] || return 1; d=$(cd -P \"${f%/*}/\" 2>/dev/null && pwd -P) || return 1; printf '%s\\n' \"${d%/}/${f##*/}\"; }; }; r=$(rp \"$n\") || x \"cannot resolve the real path of $n\"; k() { case \"$1\" in /*) ;; *) x \"node path is not absolute ($1)\";; esac; for d in \"$u\" \"$t\"; do [ -n \"$d\" ] && case \"$1\" in \"$d\"/*) x \"node is in a temp folder ($1)\";; esac; done; [ \"$p\" = \"$h\" ] || case \"$1\" in \"$p\"/*) x \"node is in the session folder ($1)\";; esac; }; case \"${r##*/}\" in node) k \"$r\"; e=$r;; *) k \"$n\"; k \"$r\"; e=$(\"$n\" -p 'require(\"fs\").realpathSync(process.execPath)' 2>/dev/null) || x \"the node shim $n did not name its node\"; k \"$e\";; esac; \"$e\" \"$PLUGIN_ROOT/hooks/run.cjs\" \"$PLUGIN_ROOT/hooks/telemetry-count-codex.mjs\"",
22
- "commandWindows": "$null = 'banker-hooks-rev 625aade934d2'; function x($m) { [Console]::Error.WriteLine(\"banker hooks skipped: $m\"); exit 0 }; if (-not $env:PLUGIN_ROOT) { x 'PLUGIN_ROOT is not set' }; function G($p) { try { (Get-Item -LiteralPath $p -Force -ErrorAction Stop).FullName } catch { try { [IO.Path]::GetFullPath($p) } catch { $p } } }; $sb = @{}; foreach ($l in @(& (Join-Path ([Environment]::SystemDirectory) 'subst.exe') 2>$null)) { if ($l -match '^([A-Za-z]:)\\\\: => (.+)$') { $sb[$matches[1].ToUpper()] = $matches[2] } }; function L($p) { if (-not $p) { return '' }; $p = G $p; for ($i = 0; $i -lt 32; $i++) { $d2 = if ($p.Length -ge 2) { $p.Substring(0, 2).ToUpper() } else { '' }; if ($sb.Contains($d2)) { $p = G ($sb[$d2].TrimEnd('\\') + $p.Substring(2)); continue }; $c = $p; $t = $null; while ($c) { $it = Get-Item -LiteralPath $c -Force -ErrorAction SilentlyContinue; if ($it -and $it.LinkType -in 'SymbolicLink', 'Junction' -and @($it.Target)[0]) { $t = [string]@($it.Target)[0]; break }; $c = Split-Path -LiteralPath $c }; if (-not $t) { break }; if (-not [IO.Path]::IsPathRooted($t)) { $t = Join-Path (Split-Path -LiteralPath $c) $t }; $p = G ($t.TrimEnd('\\') + $p.Substring($c.Length)) }; $p.TrimEnd('\\') }; $ds = @($env:TEMP, $env:TMP); if ($env:LOCALAPPDATA) { $ds += \"$env:LOCALAPPDATA\\Temp\" }; $w = L $PWD.Path; if ($w -ne (L $env:USERPROFILE)) { $ds += $w }; $ds = @($ds | Where-Object { $_ } | ForEach-Object { L $_ } | Where-Object { $_ }); function u($f) { $f = L $f; foreach ($d in $ds) { if ($f -eq $d -or $f.StartsWith($d + '\\', 'OrdinalIgnoreCase')) { return $true } }; $false }; function k($f) { if (u $f) { x \"node is in a temp folder or the session folder ($(L $f))\" }; L $f }; $nd = @(@($env:PATH -split ';') | ForEach-Object { [Environment]::ExpandEnvironmentVariables($_.Trim().Trim('\"')) } | Where-Object { $_ -match '^[A-Za-z]:\\\\' -and ([IO.DriveInfo]::new($_.Substring(0, 1))).DriveType -ne 'Network' }); $n = $null; foreach ($d in $nd) { foreach ($e in 'node.exe', 'node.bat', 'node.cmd') { $f = Join-Path $d $e; if (Test-Path -LiteralPath $f -PathType Leaf) { $n = $f; break } }; if ($n) { break } }; if (-not $n) { x 'node is not on PATH' }; $env:PATH = ($nd | Where-Object { -not (u $_) }) -join ';'; $r = k $n; if (-not (Set-Location -LiteralPath $env:PLUGIN_ROOT -PassThru -ErrorAction SilentlyContinue)) { x 'cannot enter the plugin folder' }; if ($r -notmatch '\\.exe$') { $e = & $r -p process.execPath 2>$null | Select-Object -Last 1; if (-not $e -or $e -notmatch '\\.exe$' -or -not (Test-Path -LiteralPath $e -PathType Leaf)) { x \"the node shim $r did not name its node\" }; $r = k $e }; & $r \"$env:PLUGIN_ROOT\\hooks\\run.cjs\" \"$env:PLUGIN_ROOT\\hooks\\telemetry-count-codex.mjs\"",
21
+ "command": ": banker-hooks-rev 7fad90560d65; x() { printf '%s\\n' \"banker hooks skipped: $1\" >&2; exit 0; }; [ -n \"$PLUGIN_ROOT\" ] || x \"PLUGIN_ROOT is not set\"; p=$(pwd -P); [ -n \"$p\" ] && [ \"$p\" != / ] || x \"the session folder is /\"; u=$(cd /tmp 2>/dev/null && pwd -P); t=$(cd \"${TMPDIR:-/tmp}\" 2>/dev/null && pwd -P); h=; case \"$HOME\" in /?*) h=$(cd \"$HOME\" 2>/dev/null && pwd -P);; esac; w() { for z in \"$u\" \"$t\"; do [ -n \"$z\" ] && case \"$1/\" in \"$z\"/*) return 0;; esac; done; [ \"$p\" = \"$h\" ] && return 1; case \"$1/\" in \"$p\"/*) return 0;; esac; return 1; }; pr=$(cd -P \"$PLUGIN_ROOT\" 2>/dev/null && pwd -P) || x \"cannot enter the plugin folder\"; w \"$pr\" && x \"the plugin folder is in a temp folder or the session folder ($pr)\"; A='function dec(s, o, i, c) { o = \"\"; for (i = 1; i <= length(s); i++) { c = substr(s, i, 1); if (c == \"\\\\\" && substr(s, i + 1, 3) ~ /^[0-7][0-7][0-7]$/) { o = o sprintf(\"%c\", substr(s, i + 1, 1) * 64 + substr(s, i + 2, 1) * 8 + substr(s, i + 3, 1)); i += 3 } else o = o c }; return o } mac { i = index($0, \" on \"); j = 0; for (k = length($0); k > i; k--) if (substr($0, k, 2) == \" (\") { j = k; break }; if (!i || !j) next; m = substr($0, i + 4, j - i - 4); t = substr($0, j + 2); sub(/[,)].*/, \"\", t) } !mac { m = dec($2); t = $3 } (t ~ /^(nfs|nfs4|cifs|smbfs|smb3|afpfs|webdav|davfs|autofs|ceph|glusterfs|lustre|9p)$/ || t ~ /^fuse\\./) && m !~ /:/ { printf \"%s:\", m }'; if [ -r '/proc/self/mounts' ]; then M=$(LC_ALL=C command -p awk \"$A\" '/proc/self/mounts' 2>/dev/null); else M=$('/sbin/mount' 2>/dev/null | LC_ALL=C command -p awk -v mac=1 \"$A\" 2>/dev/null); fi; nm() { for z in $M; do case \"$1/\" in \"$z\"/*) return 0;; esac; done; return 1; }; hm() { for z in \"$h\" \"$HOME\"; do case \"$z\" in /?*) case \"$1/\" in \"$z\"/*) return 0;; esac;; esac; done; return 1; }; set -f; o=$IFS; IFS=:; a=; q=; for v in $PATH; do case \"$v\" in /*) ;; *) a=$a${a:+:}$v; continue;; esac; nm \"$v\" && ! hm \"$v\" && continue; a=$a${a:+:}$v; y=$(cd \"$v\" 2>/dev/null && pwd -P) || continue; w \"$y\" || q=$q${q:+:}$v; done; n=; [ -n \"$a\" ] && n=$(PATH=$a; command -v node) || for d in /opt/homebrew/bin /usr/local/bin; do nm \"$d\" && continue; [ -x \"$d/node\" ] && n=$d/node && break; done; IFS=$o; set +f; [ -n \"$n\" ] || x \"node is not on PATH (a Codex started from the desktop may not have the shell's PATH)\"; case \"$n\" in /*) ;; *) x \"node on PATH is not an absolute path ($n)\";; esac; PATH=${q:-/usr/bin:/bin}; export PATH; cd \"$pr\" || x \"cannot enter the plugin folder\"; rp() { realpath \"$1\" 2>/dev/null || readlink -f \"$1\" 2>/dev/null || { f=$1; i=0; while [ -L \"$f\" ]; do i=$((i+1)); [ \"$i\" -le 40 ] || return 1; l=$(readlink \"$f\") || return 1; case \"$l\" in /*) f=$l;; *) f=${f%/*}/$l;; esac; done; [ -e \"$f\" ] || return 1; d=$(cd -P \"${f%/*}/\" 2>/dev/null && pwd -P) || return 1; printf '%s\\n' \"${d%/}/${f##*/}\"; }; }; r=$(rp \"$n\") || x \"cannot resolve the real path of $n\"; k() { case \"$1\" in /*) ;; *) x \"node path is not absolute ($1)\";; esac; for d in \"$u\" \"$t\"; do [ -n \"$d\" ] && case \"$1\" in \"$d\"/*) x \"node is in a temp folder ($1)\";; esac; done; [ \"$p\" = \"$h\" ] || case \"$1\" in \"$p\"/*) x \"node is in the session folder ($1)\";; esac; }; case \"${r##*/}\" in node) k \"$r\"; e=$r;; *) k \"$n\"; k \"$r\"; e=$(\"$n\" -p 'require(\"fs\").realpathSync(process.execPath)' 2>/dev/null) || x \"the node shim $n did not name its node\"; k \"$e\";; esac; \"$e\" \"$pr/hooks/run.cjs\" \"$pr/hooks/telemetry-count-codex.mjs\"",
22
+ "commandWindows": "$null = 'banker-hooks-rev 7fad90560d65'; function x($m) { [Console]::Error.WriteLine(\"banker hooks skipped: $m\"); exit 0 }; if (-not $env:PLUGIN_ROOT) { x 'PLUGIN_ROOT is not set' }; function G($p) { try { (Get-Item -LiteralPath $p -Force -ErrorAction Stop).FullName } catch { try { [IO.Path]::GetFullPath($p) } catch { $p } } }; $sb = @{}; foreach ($l in @(& (Join-Path ([Environment]::SystemDirectory) 'subst.exe') 2>$null)) { if ($l -match '^([A-Za-z]:)\\\\: => (.+)$') { $sb[$matches[1].ToUpper()] = $matches[2] } }; function L($p) { if (-not $p) { return '' }; $p = G $p; for ($i = 0; $i -lt 32; $i++) { $d2 = if ($p.Length -ge 2) { $p.Substring(0, 2).ToUpper() } else { '' }; if ($sb.Contains($d2)) { $p = G ($sb[$d2].TrimEnd('\\') + $p.Substring(2)); continue }; $c = $p; $t = $null; while ($c) { $it = Get-Item -LiteralPath $c -Force -ErrorAction SilentlyContinue; if ($it -and $it.LinkType -in 'SymbolicLink', 'Junction' -and @($it.Target)[0]) { $t = [string]@($it.Target)[0]; break }; $c = Split-Path -LiteralPath $c }; if (-not $t) { break }; if (-not [IO.Path]::IsPathRooted($t)) { $t = Join-Path (Split-Path -LiteralPath $c) $t }; $p = G ($t.TrimEnd('\\') + $p.Substring($c.Length)) }; $p.TrimEnd('\\') }; $ds = @($env:TEMP, $env:TMP); if ($env:LOCALAPPDATA) { $ds += \"$env:LOCALAPPDATA\\Temp\" }; $w = L $PWD.Path; if ($w -ne (L $env:USERPROFILE)) { $ds += $w }; $ds = @($ds | Where-Object { $_ } | ForEach-Object { L $_ } | Where-Object { $_ }); function u($f) { $f = L $f; foreach ($d in $ds) { if ($f -eq $d -or $f.StartsWith($d + '\\', 'OrdinalIgnoreCase')) { return $true } }; $false }; function k($f) { if (u $f) { x \"node is in a temp folder or the session folder ($(L $f))\" }; L $f }; $pr = L $env:PLUGIN_ROOT; if (u $pr) { x \"the plugin folder is in a temp folder or the session folder ($pr)\" }; $nd = @(@($env:PATH -split ';') | ForEach-Object { [Environment]::ExpandEnvironmentVariables($_.Trim().Trim('\"')) } | Where-Object { $_ -match '^[A-Za-z]:\\\\' -and ([IO.DriveInfo]::new($_.Substring(0, 1))).DriveType -ne 'Network' }); $n = $null; foreach ($d in $nd) { foreach ($e in 'node.exe', 'node.bat', 'node.cmd') { $f = Join-Path $d $e; if (Test-Path -LiteralPath $f -PathType Leaf) { $n = $f; break } }; if ($n) { break } }; if (-not $n) { x 'node is not on PATH' }; $env:PATH = ($nd | Where-Object { -not (u $_) }) -join ';'; $r = k $n; if (-not (Set-Location -LiteralPath $pr -PassThru -ErrorAction SilentlyContinue)) { x 'cannot enter the plugin folder' }; if ($r -notmatch '\\.exe$') { $e = & $r -p process.execPath 2>$null | Select-Object -Last 1; if (-not $e -or $e -notmatch '\\.exe$' -or -not (Test-Path -LiteralPath $e -PathType Leaf)) { x \"the node shim $r did not name its node\" }; $r = k $e }; & $r \"$pr\\hooks\\run.cjs\" \"$pr\\hooks\\telemetry-count-codex.mjs\"",
23
23
  "timeout": 5
24
24
  }
25
25
  ]