@kaydash9999/banker-plugins 0.20.5 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +52 -0
- package/PRIVACY.md +25 -1
- package/README.md +63 -18
- package/bin/banker.js +173 -14
- package/bin/lib/codex-hooks.mjs +254 -0
- package/bin/lib/codex-routes.mjs +112 -0
- package/bin/lib/command-skill.cjs +19 -0
- package/bin/lib/telemetry-config.mjs +4 -1
- package/codex/command-skills/front-qa/SKILL.md +56 -0
- package/codex/command-skills/setup/SKILL.md +87 -0
- package/codex/hooks.json +29 -0
- package/codex/manifest.json +13 -10
- package/codex/skills/graceful-pause/SKILL.md +30 -0
- package/codex/skills/progress/SKILL.md +42 -0
- package/codex/skills/progress/scripts/progress-plan.mjs +722 -0
- package/codex/skills/progress/scripts/progress.mjs +295 -0
- package/codex/transform-matrix.md +18 -13
- package/commands/setup.md +2 -2
- package/hooks/obsidize-hook.mjs +116 -4
- package/hooks/progress-plan.mjs +1 -1
- package/hooks/telemetry-count-codex.mjs +309 -0
- package/hooks/update-notify.mjs +105 -14
- package/package.json +1 -1
- package/skills/3d-intro-build/references/verify-intro.mjs +15 -1
- package/skills/anti-drm/SKILL.md +1 -1
- package/skills/anti-drm/scripts/instruction-toggle.mjs +57 -4
- package/skills/append_wiki/SKILL.md +34 -8
- package/skills/arch-diagram/build_pptx_template.py +47 -28
- package/skills/audit-web-page/SKILL.md +1 -1
- package/skills/audit-web-page/harness.py +0 -1
- package/skills/compact-wiki/SKILL.md +37 -6
- package/skills/deep-research/SKILL.md +6 -0
- package/skills/lfm/SKILL.md +148 -0
- package/skills/lineage/SKILL.md +14 -4
- package/skills/lineage/lineage.py +1582 -516
- package/skills/motion-graphic-make/SKILL.md +1 -1
- package/skills/obsidizer/SKILL.md +44 -10
- package/skills/obsidizer/obsidize.mjs +42 -11
- package/skills/omc-patch/SKILL.md +6 -0
- package/skills/payload-mon/SKILL.md +3 -1
- package/skills/play-qa/SKILL.md +1 -1
- package/skills/ralph-qa/SKILL.md +1 -1
- package/skills/remains/SKILL.md +1 -1
- package/skills/rfp-author/SKILL.md +5 -3
- package/skills/rfp-author/references/vendor-rfp-structure.md +1 -1
- package/skills/rfp-author/scripts/md2docx.py +63 -34
- package/skills/setup-bypass-permissions/SKILL.md +28 -10
- package/skills/setup-bypass-permissions/scripts/codex-bypass.mjs +135 -0
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +210 -0
- package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.py +66 -15
- package/skills/setup-omc-hud/SKILL.md +33 -3
- package/skills/setup-omc-hud/assets/codex-config.mjs +210 -0
- package/skills/setup-omc-hud/assets/codex-status-line.mjs +90 -0
- package/skills/smart-compact/SKILL.md +59 -13
- package/skills/smart-compact/scripts/codex-gate.mjs +385 -0
- package/skills/theme-selection/SKILL.md +1 -1
- package/skills/tone-compact/SKILL.md +1 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +57 -4
- package/skills/ultra-chain/SKILL.md +10 -6
- package/skills/ultra-interview/SKILL.md +13 -2
- package/skills/ultra-ui-qa/SKILL.md +1 -1
- package/skills/update-banker/SKILL.md +31 -6
- package/skills/vertical-pptx/SKILL.md +1 -1
- package/skills/vertical-pptx/build_a4p.js +20 -2
- package/skills/vertical-pptx/build_a4p.py +560 -359
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Codex 의 config.toml 을 줄 단위로 읽고 고치는 공통 모듈. setup-bypass-permissions 가 정본이고 setup-omc-hud 의
|
|
3
|
+
* assets/ 에 같은 사본을 둔다(scripts/sync-adapter.js 가 지킴).
|
|
4
|
+
*
|
|
5
|
+
* TOML 전체를 다시 쓰지 않는다. 바꾸는 키의 줄만 고쳐 주석, 순서, 다른 표는 그대로 둔다.
|
|
6
|
+
* - 줄을 훑으며 문자열(한 줄, 여러 줄)과 괄호 깊이를 따라가, 여러 줄 문자열이나 배열 안의 줄을 표 머리나 키로 보지 않는다.
|
|
7
|
+
* - 키 이름은 따옴표를 벗겨 비교한다(`"approval_policy"` 도 approval_policy). 값 끝의 주석은 값으로 보지 않는다.
|
|
8
|
+
* - 최상위가 표를 점 키나 인라인 표로 정했는지(definedInline) 알려, 그런 파일에 같은 표를 더하지 않게 한다.
|
|
9
|
+
* - 파일이 링크이거나 일반 파일이 아니면 고치지 않는다. 쓰기 전 원본을 백업(이 계정만 읽음)하고, 새 파일에 쓴 뒤
|
|
10
|
+
* 바꿔 넣으며 파일 모드를 지킨다.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
// Codex 처럼 상대 경로 CODEX_HOME 은 지금 폴더 기준으로 푼다.
|
|
17
|
+
export function codexConfigFile(env = process.env) {
|
|
18
|
+
const home = env.CODEX_HOME ? path.resolve(env.CODEX_HOME) : path.join(os.homedir(), '.codex');
|
|
19
|
+
return path.join(home, 'config.toml');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class Refused extends Error {}
|
|
23
|
+
|
|
24
|
+
// { text, mode, exists }. 없으면 빈 글. 링크나 일반 파일이 아니면 Refused.
|
|
25
|
+
export function readToml(file) {
|
|
26
|
+
let st;
|
|
27
|
+
try {
|
|
28
|
+
st = fs.lstatSync(file);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (e.code === 'ENOENT') return { text: '', mode: null, exists: false };
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
if (!st.isFile()) throw new Refused(`${file} 이 일반 파일이 아님(링크이거나 다른 종류)`);
|
|
34
|
+
return { text: fs.readFileSync(file, 'utf8'), mode: st.mode & 0o7777, exists: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 원본을 backup 에 복사한 뒤(원본이 있을 때) 새 파일에 써서 바꿔 넣는다.
|
|
38
|
+
export function writeToml(file, text, { mode = null, backup = null } = {}) {
|
|
39
|
+
if (backup && fs.existsSync(file)) {
|
|
40
|
+
fs.copyFileSync(file, backup);
|
|
41
|
+
fs.chmodSync(backup, 0o600); // 원본에 비밀 값이 있을 수 있다
|
|
42
|
+
}
|
|
43
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
44
|
+
try {
|
|
45
|
+
fs.writeFileSync(tmp, text, { flag: 'wx', mode: mode ?? 0o600 });
|
|
46
|
+
if (mode !== null) fs.chmodSync(tmp, mode);
|
|
47
|
+
fs.renameSync(tmp, file);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
fs.rmSync(tmp, { force: true });
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lines = (text) => text.split(/\r?\n/);
|
|
55
|
+
const eol = (text) => (text.includes('\r\n') ? '\r\n' : '\n');
|
|
56
|
+
// 파일이 쓰는 줄바꿈(CRLF 가 하나라도 있으면 CRLF).
|
|
57
|
+
export const eolOf = eol;
|
|
58
|
+
const TRIPLE = ['"""', "'''"];
|
|
59
|
+
|
|
60
|
+
// 키나 표 이름을 낱말 배열로. 따옴표 안의 점은 나누지 않는다(`profiles."a.b"` 는 ['profiles', 'a.b']).
|
|
61
|
+
export const nameParts = (s) => (s.match(/"[^"]*"|'[^']*'|[^.\s]+/g) ?? []).map((p) => p.replace(/^"(.*)"$|^'(.*)'$/, '$1$2'));
|
|
62
|
+
// 비교용 이름. 낱말 사이를 \u0000 으로 이어, 점이 든 낱말과 점으로 나뉜 낱말을 구분한다.
|
|
63
|
+
const plainName = (s) => nameParts(s).join('\u0000');
|
|
64
|
+
|
|
65
|
+
// 한 줄을 문자열 밖에서 훑는다. { d: 괄호 깊이 변화, multi: 줄 끝에 열려 있는 여러 줄 문자열 구분자, cut: 주석 시작 위치 }.
|
|
66
|
+
function scanLine(s, multi = '') {
|
|
67
|
+
let d = 0;
|
|
68
|
+
for (let i = 0; i < s.length; i++) {
|
|
69
|
+
if (multi) {
|
|
70
|
+
if (s.startsWith(multi, i)) { i += 2; multi = ''; } else if (multi === TRIPLE[0] && s[i] === '\\') i++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const c = s[i];
|
|
74
|
+
if (TRIPLE.some((t) => s.startsWith(t, i))) { multi = s.slice(i, i + 3); i += 2; }
|
|
75
|
+
else if (c === '"') { for (i++; i < s.length && s[i] !== '"'; i++) if (s[i] === '\\') i++; }
|
|
76
|
+
else if (c === "'") { const k = s.indexOf("'", i + 1); i = k < 0 ? s.length : k; }
|
|
77
|
+
else if (c === '#') return { d, multi, cut: i };
|
|
78
|
+
else if (c === '[' || c === '{') d++;
|
|
79
|
+
else if (c === ']' || c === '}') d--;
|
|
80
|
+
}
|
|
81
|
+
return { d, multi, cut: s.length };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 표 머리. 이름 안의 따옴표 친 낱말에는 ] 가 들어갈 수 있다(`[projects."/a]b"]`).
|
|
85
|
+
// 이름 부분은 탐욕 한 번으로 읽는다(겹치는 공백 패턴은 긴 줄에서 역추적이 폭증한다). 앞뒤 공백은 nameParts 가 지운다.
|
|
86
|
+
const HEADER_RE = /^\s*\[\[?((?:"[^"]*"|'[^']*'|[^\]"'])+)\]\]?\s*(#.*)?$/;
|
|
87
|
+
const KEY_RE = /^\s*((?:"[^"]*"|'[^']*'|[A-Za-z0-9_-]+)(?:\s*\.\s*(?:"[^"]*"|'[^']*'|[A-Za-z0-9_-]+))*)\s*=(.*)$/;
|
|
88
|
+
|
|
89
|
+
// 줄마다 { kind: 'header'|'key'|'inside'|'other', name, end }. header 의 name 은 표 이름, key 의 name 은 키 이름이고
|
|
90
|
+
// end 는 값이 끝나는 줄 다음. 여러 줄 문자열이나 배열 안의 줄은 inside.
|
|
91
|
+
export function scan(text) {
|
|
92
|
+
const ls = lines(text);
|
|
93
|
+
const out = ls.map(() => ({ kind: 'other' }));
|
|
94
|
+
for (let i = 0; i < ls.length; i++) {
|
|
95
|
+
const header = HEADER_RE.exec(ls[i]);
|
|
96
|
+
if (header) { out[i] = { kind: 'header', name: plainName(header[1]) }; continue; }
|
|
97
|
+
const key = KEY_RE.exec(ls[i]);
|
|
98
|
+
if (!key) continue;
|
|
99
|
+
let { d, multi } = scanLine(key[2]);
|
|
100
|
+
let j = i + 1;
|
|
101
|
+
for (; (d > 0 || multi) && j < ls.length; j++) {
|
|
102
|
+
out[j] = { kind: 'inside' };
|
|
103
|
+
const r = scanLine(ls[j], multi);
|
|
104
|
+
d += r.d;
|
|
105
|
+
multi = r.multi;
|
|
106
|
+
}
|
|
107
|
+
out[i] = { kind: 'key', name: plainName(key[1]), end: j };
|
|
108
|
+
i = j - 1;
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 표의 줄 범위 [from, to). table '' 은 최상위. 표가 없으면 null.
|
|
114
|
+
function tableRange(info, table) {
|
|
115
|
+
table = table.includes('\u0000') || !table ? table : plainName(table);
|
|
116
|
+
let from = table ? -1 : 0;
|
|
117
|
+
for (let i = 0; i < info.length; i++) {
|
|
118
|
+
if (info[i].kind !== 'header') continue;
|
|
119
|
+
if (from >= 0) return [from, i];
|
|
120
|
+
if (info[i].name === table) from = i + 1;
|
|
121
|
+
}
|
|
122
|
+
return from >= 0 ? [from, info.length] : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function keysOf(text, table) {
|
|
126
|
+
const info = scan(text);
|
|
127
|
+
const range = tableRange(info, table);
|
|
128
|
+
if (!range) return [];
|
|
129
|
+
const keys = [];
|
|
130
|
+
for (let i = range[0]; i < range[1]; i++) if (info[i].kind === 'key') keys.push({ at: i, end: info[i].end, name: info[i].name });
|
|
131
|
+
return keys;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 키의 { at, end, raw }. raw 는 값 끝의 주석을 뺀 글. 없으면 null.
|
|
135
|
+
export function findKey(text, table, key) {
|
|
136
|
+
const want = nameParts(key).join('\u0000');
|
|
137
|
+
const hit = keysOf(text, plainName(table)).find((k) => k.name === want);
|
|
138
|
+
if (!hit) return null;
|
|
139
|
+
const ls = lines(text).slice(hit.at, hit.end);
|
|
140
|
+
const parts = [ls[0].slice(ls[0].indexOf('=') + 1), ...ls.slice(1)];
|
|
141
|
+
let multi = '';
|
|
142
|
+
for (const part of parts.slice(0, -1)) multi = scanLine(part, multi).multi;
|
|
143
|
+
const last = parts.length - 1;
|
|
144
|
+
parts[last] = parts[last].slice(0, scanLine(parts[last], multi).cut); // 마지막 줄의 주석을 뗀다
|
|
145
|
+
const raw = parts.join('\n');
|
|
146
|
+
return { at: hit.at, end: hit.end, raw: raw.trim() };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 최상위가 이 표를 점 키나 인라인 표로 정했는지(`tui.x = 1`, `tui = { ... }`). 그런 파일에 [표] 를 더하면 키가 겹쳐
|
|
150
|
+
// Codex 가 설정을 읽지 못한다.
|
|
151
|
+
export function definedInline(text, table) {
|
|
152
|
+
const t = plainName(table);
|
|
153
|
+
return keysOf(text, '').some((k) => k.name === t || k.name.startsWith(`${t}\u0000`));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 키를 rawValue 로 둔다. 있으면 그 줄을 바꾸고, 없으면 표 끝(최상위는 첫 표 머리 앞)에 넣는다. 표가 없으면 끝에 만든다.
|
|
157
|
+
// comment 가 있으면 키 바로 위에 그 주석 줄을 둔다.
|
|
158
|
+
export function setKey(text, table, key, rawValue, comment = '') {
|
|
159
|
+
const ls = lines(text);
|
|
160
|
+
const nl = eol(text);
|
|
161
|
+
const line = [...(comment ? [comment] : []), `${key} = ${rawValue}`];
|
|
162
|
+
const hit = findKey(text, table, key);
|
|
163
|
+
if (hit) {
|
|
164
|
+
const above = comment && ls[hit.at - 1] === comment ? 1 : 0;
|
|
165
|
+
ls.splice(hit.at - above, hit.end - hit.at + above, ...line);
|
|
166
|
+
return ls.join(nl);
|
|
167
|
+
}
|
|
168
|
+
const range = tableRange(scan(text), table);
|
|
169
|
+
if (!range) {
|
|
170
|
+
while (ls.length && ls[ls.length - 1] === '') ls.pop();
|
|
171
|
+
return [...ls, ...(ls.length ? [''] : []), `[${table}]`, ...line, ''].join(nl);
|
|
172
|
+
}
|
|
173
|
+
// 다음 표 머리에 붙은 주석(`# MCP servers` 처럼 머리 바로 위의 주석 줄)과 그 앞 빈 줄은 건너뛰고 넣는다.
|
|
174
|
+
let at = range[1];
|
|
175
|
+
if (range[1] < ls.length) while (at > range[0] && ls[at - 1].trim().startsWith('#')) at--;
|
|
176
|
+
while (at > range[0] && ls[at - 1].trim() === '') at--;
|
|
177
|
+
ls.splice(at, 0, ...line);
|
|
178
|
+
return ls.join(nl);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 키를 뺀다. comment 와 같은 주석 줄이 바로 위에 있으면 그것도 뺀다.
|
|
182
|
+
export function removeKey(text, table, key, comment = '') {
|
|
183
|
+
const hit = findKey(text, table, key);
|
|
184
|
+
if (!hit) return text;
|
|
185
|
+
const ls = lines(text);
|
|
186
|
+
const above = comment && ls[hit.at - 1] === comment ? 1 : 0;
|
|
187
|
+
ls.splice(hit.at - above, hit.end - hit.at + above);
|
|
188
|
+
return ls.join(eol(text));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 표에 키도 주석도 없으면 그 머리와 뒤의 빈 줄을 뺀다.
|
|
192
|
+
export function dropEmptyTable(text, table) {
|
|
193
|
+
const info = scan(text);
|
|
194
|
+
const range = tableRange(info, table);
|
|
195
|
+
if (!range) return text;
|
|
196
|
+
const ls = lines(text);
|
|
197
|
+
if (ls.slice(range[0], range[1]).some((l) => l.trim())) return text;
|
|
198
|
+
ls.splice(range[0] - 1, range[1] - range[0] + 1);
|
|
199
|
+
return ls.join(eol(text));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 이름이 prefix 로 시작하는 표들 중 key 를 정한 표 이름(예: profiles.* 의 approval_policy).
|
|
203
|
+
export function tablesSetting(text, prefix, key) {
|
|
204
|
+
const p = plainName(prefix);
|
|
205
|
+
const names = scan(text).filter((x) => x.kind === 'header' && x.name.startsWith(`${p}\u0000`)).map((x) => x.name);
|
|
206
|
+
return [...new Set(names)].filter((n) => findKey(text, n, key)).map((n) => n.split('\u0000').join('.'));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 쓰기 실패가 이 계정의 권한(샌드박스, 읽기 전용, 다른 소유자) 때문인지.
|
|
210
|
+
export const denied = (e) => ['EACCES', 'EPERM', 'EROFS'].includes(e?.code);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* setup-omc-hud 의 Codex 대상. Codex TUI 의 상태표시줄은 `$CODEX_HOME/config.toml` 의 `[tui] status_line` 이다.
|
|
4
|
+
* OMX 는 `omx setup` 에서 이 줄을 `# omx:managed-status-line` 표시와 함께 관리한다.
|
|
5
|
+
*
|
|
6
|
+
* - on: status_line 이 없을 때만 OMX 의 프리셋 항목으로 넣고 `# banker:status-line` 표시를 붙인다. OMX 나 사용자가
|
|
7
|
+
* 이미 둔 줄은 건드리지 않는다(누가 둔 것인지 알린다).
|
|
8
|
+
* - off: banker 표시가 붙은 줄만 뺀다.
|
|
9
|
+
* - status: 지금 줄과 그 주인.
|
|
10
|
+
* 쓰기 전 원본을 config.toml.banker-hud.bak 에 둔다. 공통 모듈 codex-config.mjs 는 setup-bypass-permissions 의 같은 사본.
|
|
11
|
+
*
|
|
12
|
+
* 사용: node codex-status-line.mjs <on [--preset focused|minimal]|off|status>
|
|
13
|
+
* 종료: 0 완료 또는 이미 그 상태, 1 거부(파일은 그대로), 2 예기치 못한 오류, 3 이 계정이 쓸 수 없음(샌드박스 등)
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
import { codexConfigFile, definedInline, denied, dropEmptyTable, findKey, readToml, Refused, removeKey, setKey, writeToml } from './codex-config.mjs';
|
|
20
|
+
|
|
21
|
+
export const MARK = '# banker:status-line';
|
|
22
|
+
const OMX_MARK = '# omx:managed-status-line';
|
|
23
|
+
// oh-my-codex 의 상태표시줄 프리셋(config/generator.js 의 STATUS_LINE_PRESETS)과 같은 항목.
|
|
24
|
+
export const PRESETS = {
|
|
25
|
+
focused: ['model-with-reasoning', 'git-branch', 'context-remaining', 'total-input-tokens', 'total-output-tokens', 'five-hour-limit', 'weekly-limit'],
|
|
26
|
+
minimal: ['model-with-reasoning', 'git-branch'],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// 지금 status_line 과 그 주인: 'banker', 'omx', 'user', 없으면 null.
|
|
30
|
+
export function owner(text) {
|
|
31
|
+
const hit = findKey(text, 'tui', 'status_line');
|
|
32
|
+
if (!hit) return { owner: null, raw: '' };
|
|
33
|
+
const above = text.split(/\r?\n/)[hit.at - 1];
|
|
34
|
+
return { owner: above === MARK ? 'banker' : above === OMX_MARK ? 'omx' : 'user', raw: hit.raw };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const presetValue = (fields) => `[${fields.map((f) => `"${f}"`).join(', ')}]`;
|
|
38
|
+
const sameValue = (a, b) => a.replace(/\s+/g, '').replace(/,\]$/, ']') === b.replace(/\s+/g, '');
|
|
39
|
+
|
|
40
|
+
export function turnOn({ file = codexConfigFile(), preset = 'focused' } = {}) {
|
|
41
|
+
if (!Object.prototype.hasOwnProperty.call(PRESETS, preset)) return { code: 1, msg: `프리셋은 ${Object.keys(PRESETS).join(', ')} 중 하나` };
|
|
42
|
+
const { text, mode, exists } = readToml(file);
|
|
43
|
+
// 최상위의 `tui.x = ` 나 `tui = { }` 가 있는 파일에 [tui] 를 더하면 키가 겹쳐 Codex 가 시작하지 못한다.
|
|
44
|
+
if (definedInline(text, 'tui')) return { code: 1, msg: '거부: 최상위에서 tui 를 점 키나 인라인 표로 정한 설정이라 [tui] 를 더하지 않음. status_line 을 직접 넣으십시오' };
|
|
45
|
+
const now = owner(text);
|
|
46
|
+
if (now.owner === 'omx') return { code: 0, msg: '이미 있음: OMX 가 관리하는 상태표시줄(바꾸려면 omx setup)' };
|
|
47
|
+
if (now.owner === 'user') return { code: 0, msg: '이미 있음: 사용자가 둔 상태표시줄이라 그대로 둠' };
|
|
48
|
+
if (!fs.existsSync(path.dirname(file))) return { code: 1, msg: `Codex 설정 폴더가 없음: ${path.dirname(file)}` };
|
|
49
|
+
const next = setKey(text, 'tui', 'status_line', presetValue(PRESETS[preset]), MARK);
|
|
50
|
+
if (next === text) return { code: 0, msg: '이미 켜져 있음' };
|
|
51
|
+
writeToml(file, next, { mode, backup: exists ? `${file}.banker-hud.bak` : null });
|
|
52
|
+
return { code: 0, msg: `켬: [tui] status_line(${preset}). Codex 를 다시 열면 보임` };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function turnOff({ file = codexConfigFile() } = {}) {
|
|
56
|
+
const { text, mode } = readToml(file);
|
|
57
|
+
const now = owner(text);
|
|
58
|
+
if (now.owner !== 'banker') return { code: 0, msg: now.owner ? `banker 가 둔 줄이 아님(${now.owner}): 그대로 둠` : '이미 꺼져 있음' };
|
|
59
|
+
// 표시는 남았지만 값을 사용자가 고쳤으면 사용자의 줄로 본다.
|
|
60
|
+
if (!Object.values(PRESETS).some((f) => sameValue(now.raw, presetValue(f)))) return { code: 0, msg: '사용자가 값을 고친 줄이라 그대로 둠(banker 표시만 남음)' };
|
|
61
|
+
writeToml(file, dropEmptyTable(removeKey(text, 'tui', 'status_line', MARK), 'tui'), { mode });
|
|
62
|
+
return { code: 0, msg: '끔: banker 가 둔 status_line 을 뺌' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function status({ file = codexConfigFile() } = {}) {
|
|
66
|
+
const now = owner(readToml(file).text);
|
|
67
|
+
return { code: 0, msg: now.owner ? `status_line(${now.owner}): ${now.raw.replace(/\s+/g, ' ')}` : 'status_line 없음' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function run(argv) {
|
|
71
|
+
const [cmd] = argv;
|
|
72
|
+
const at = argv.indexOf('--preset');
|
|
73
|
+
if (cmd === 'on') return turnOn({ preset: at >= 0 ? argv[at + 1] : 'focused' });
|
|
74
|
+
if (cmd === 'off') return turnOff();
|
|
75
|
+
return status();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function main(argv) {
|
|
79
|
+
let r;
|
|
80
|
+
try {
|
|
81
|
+
r = run(argv);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
r = e instanceof Refused ? { code: 1, msg: `거부: ${e.message}` } : denied(e) ? { code: 3, msg: `쓸 수 없음(${e.code}): ${e.path ?? ''}${e.code === 'EPERM' && process.platform === 'win32' ? '. 다른 프로그램(Codex, 편집기)이 파일을 잡고 있을 수 있음' : ''}` } : { code: 2, msg: `오류: ${e.message}` };
|
|
84
|
+
}
|
|
85
|
+
console.log(`${r.msg}\n설정 파일: ${codexConfigFile()}`);
|
|
86
|
+
return r.code;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const same = (a, b) => { try { return fs.realpathSync(a) === fs.realpathSync(b); } catch { return false; } };
|
|
90
|
+
if (process.argv[1] && same(fileURLToPath(import.meta.url), path.resolve(process.argv[1]))) process.exitCode = main(process.argv.slice(2));
|
|
@@ -10,11 +10,11 @@ description: "컨텍스트 사용률이 임계(기본 50%)를 넘으면 `append_
|
|
|
10
10
|
→ `/compact` → paste. `smart-compact` 로 무장, `smart-compact --cancel` 로 해제한다.
|
|
11
11
|
`ready-compact` 는 0.14.0 부터 마지막 메시지를 resume 프롬프트만으로 내므로, 예전처럼 `compact-copy` 를 거치지 않는다.
|
|
12
12
|
|
|
13
|
-
## ⚠️ 정직한 실현 한계 (Claude Code 실측)
|
|
13
|
+
## ⚠️ 정직한 실현 한계 (Claude Code, Codex 실측)
|
|
14
14
|
"컨텍스트 50%에서 완전 무인 자동"은 **부분만 가능**하다 — 근거:
|
|
15
|
-
- **감지**: 컨텍스트
|
|
16
|
-
|
|
17
|
-
**정확히 50% 감지 가능**.
|
|
15
|
+
- **감지**: Claude Code 는 컨텍스트 사용률을 **statusLine stdin** 의 `context_window.used_percentage` 로만 실시간
|
|
16
|
+
노출한다(hook은 이 값을 못 받음; auto-compact은 ~95%에서만, 50% 이벤트 없음). → statusLine으로
|
|
17
|
+
**정확히 50% 감지 가능**. Codex 는 훅이 받는 세션 기록에서 계산한다(아래 Codex 절).
|
|
18
18
|
- **자동 실행 가능(어시스턴트)**: `append-wiki` · `ready-compact` — 스킬/도구
|
|
19
19
|
작업이라 어시스턴트가 수행.
|
|
20
20
|
- **자동 불가(TUI 전용)**: `/copy` · `/compact` · **paste** 는 인터랙티브 TUI 동작이라 hook·스킬이
|
|
@@ -26,7 +26,7 @@ description: "컨텍스트 사용률이 임계(기본 50%)를 넘으면 `append_
|
|
|
26
26
|
때만·클립보드 보장 없이 취약). smart-compact은 **가장 신뢰 가능한 최대 자동화**를 택한다.
|
|
27
27
|
|
|
28
28
|
## 메커니즘 (무장 시 설치)
|
|
29
|
-
런타임 인식 — Claude Code
|
|
29
|
+
런타임 인식 — Claude Code 는 statusLine 래퍼, Codex 는 세션 기록을 읽는 훅으로 둘 다 임계를 정확히 잰다.
|
|
30
30
|
|
|
31
31
|
**Claude Code (OMC):**
|
|
32
32
|
1. **statusLine 래퍼(compose-safe)**: 기존 `statusLine.command`(예: OMC HUD)를 **보존**하고, 그것을
|
|
@@ -39,9 +39,54 @@ description: "컨텍스트 사용률이 임계(기본 50%)를 넘으면 `append_
|
|
|
39
39
|
3. **게이트 동작(어시스턴트)**: 주입을 받으면 자동파트를 실행한다. TUI 3단 안내는 `ready-compact` **전에** 한 줄로 하고, 마지막 메시지는 `ready-compact` 의 프롬프트로 끝낸다(안내를 뒤에 붙이면 `/copy` 가 안내문을 복사한다).
|
|
40
40
|
|
|
41
41
|
**Codex (OMX):**
|
|
42
|
-
- Codex
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
- Codex 는 훅 입력에 사용률 칸을 주지 않는다. 대신 모든 훅 입력의 `transcript_path`(세션 기록, rollout)에 요청마다
|
|
43
|
+
`token_count` 가 남는다. 마지막 `last_token_usage.total_tokens` 를 `model_context_window` 로 나눈 값이 사용률이다
|
|
44
|
+
(Codex 0.144.5 실측). Codex 화면의 `context-remaining` 과는 몇 %p 다를 수 있다.
|
|
45
|
+
- 이 스킬 폴더의 `scripts/codex-gate.mjs` 가 계산과 훅을 맡는다. statusLine 래퍼는 쓰지 않는다(Codex 상태표시줄은
|
|
46
|
+
OMX 가 `config.toml` 의 `[tui]` `status_line` 으로 관리한다). 이 SKILL.md 가 있는 디렉터리의 절대경로로 실행한다.
|
|
47
|
+
```bash
|
|
48
|
+
node "<이 스킬 디렉터리 절대경로>/scripts/codex-gate.mjs" on --threshold <N> # 무장(N 은 1~95 정수, 기본 50)
|
|
49
|
+
node "<이 스킬 디렉터리 절대경로>/scripts/codex-gate.mjs" off # 해제
|
|
50
|
+
node "<이 스킬 디렉터리 절대경로>/scripts/codex-gate.mjs" status
|
|
51
|
+
node "<이 스킬 디렉터리 절대경로>/scripts/codex-gate.mjs" usage <rollout 경로> # 수동 확인
|
|
52
|
+
```
|
|
53
|
+
- 파일은 모두 `$CODEX_HOME`(없으면 `~/.codex`)에 둔다.
|
|
54
|
+
|
|
55
|
+
| 파일 | 내용 |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `hooks-custom/banker-smart-compact-gate.mjs` | 훅 사본. 훅은 이 사본을 돌린다 |
|
|
58
|
+
| `hooks.json` | `UserPromptSubmit` 맨 뒤에 훅 묶음 하나 |
|
|
59
|
+
| `smart-compact.json` | 임계값 |
|
|
60
|
+
| `smart-compact-state.json` | 이미 알린 세션 |
|
|
61
|
+
| `hooks.json.smart-compact.bak` | `on`, `off` 가 `hooks.json` 을 바꿀 때마다 그 직전 내용 |
|
|
62
|
+
|
|
63
|
+
- 사본을 두는 까닭: Codex 는 신뢰한 훅을 샌드박스 밖에서 돌린다. 스킬 폴더가 작업 폴더 안에 있으면(프로젝트 범위 설치)
|
|
64
|
+
샌드박스 안의 세션이 그 스크립트를 고쳐 샌드박스 밖에서 돌릴 수 있다. 사본은 샌드박스가 쓸 수 없는 곳에 있다.
|
|
65
|
+
- 같은 까닭으로 `on` 은 임시 폴더의 node 를 거부하고, Codex 안에서 실행될 때는 작업 폴더 안의 node 나 스크립트도 거부한다.
|
|
66
|
+
Codex 를 홈 폴더에서 열었으면 거부된다(그 세션은 홈 전체를 쓸 수 있다). Codex 밖 터미널에서 실행한다.
|
|
67
|
+
PowerShell 명령은 작은따옴표(`'` 와 U+2018~U+201B)를 두 번 써서 인용한다.
|
|
68
|
+
- 맨 뒤에 더하는 까닭: Codex 는 훅 신뢰를 묶음 번호와 함께 기록한다. 앞에 끼우면 OMX 훅의 번호가 밀려 다시 신뢰해야 한다.
|
|
69
|
+
임계값을 명령이 아니라 파일에 두는 것도 같은 까닭이다(명령이 바뀌면 다시 신뢰해야 한다).
|
|
70
|
+
- Windows 의 Codex 는 훅 명령을 PowerShell 로 돌린다(Codex 0.160.1 실측). 그래서 명령은 `& '<node>' '<사본>' hook` 꼴이다.
|
|
71
|
+
- Codex 는 신뢰하지 않은 새 훅을 돌리지 않는다. `on` 뒤 Codex 를 다시 열면 "Hooks need review" 창이 뜬다.
|
|
72
|
+
거기서 Review hooks 나 Trust all 을 고르거나, 나중에 `/hooks` 에서 이 훅을 신뢰하라고 사용자에게 알린다.
|
|
73
|
+
"Continue without trusting" 을 고르면 무장되지 않은 것과 같다.
|
|
74
|
+
- 다시 `on` 하면 임계값만 바꾼다. node 경로가 바뀌었거나 banker 를 갱신해 훅 명령이나 사본이 낡았으면, 같은 자리에서 명령과
|
|
75
|
+
사본을 고친다. 그때는 이 훅만 다시 신뢰하면 된다. `status` 가 "이 판과 다름" 이면 `on` 을 다시 실행한다.
|
|
76
|
+
- `off` 는 이 훅 항목과 사본, 설정, 세션 상태만 지운다. 같은 묶음의 다른 훅은 남긴다. 이 묶음 뒤에 다른 묶음이 있었으면
|
|
77
|
+
그 훅들의 번호가 당겨져 다시 신뢰해야 할 수 있다고 알린다.
|
|
78
|
+
- 훅은 사용률이 임계 이상이면 세션마다 한 번 `additionalContext` 로 체인 실행을 지시한다. `/compact` 로 내려갔다가
|
|
79
|
+
다시 넘으면 또 알린다. 오류가 나도 프롬프트를 막지 않는다. 세션 상태 기록에는 잠금이 없어, 두 세션이 같은 순간에
|
|
80
|
+
프롬프트를 보내면 한 세션이 한 번 더 알림을 받을 수 있다.
|
|
81
|
+
- `on`, `off` 는 `~/.codex` 에 쓴다. Codex 의 `workspace-write` 샌드박스가 막으므로 그 명령을 샌드박스 밖 실행으로
|
|
82
|
+
승인을 요청해 돌린다. 승인을 요청할 수 없거나(승인 정책 `never`) 거부되면, 같은 명령을 Codex 입력창에서 `!` 를 붙여 사용자가 직접 실행하게 안내한다(예: `!node "<경로>" on --threshold <N>`). 사용자가 친 `!` 명령은 샌드박스 밖에서 돈다(Codex 0.144.5, `-a never` 에서 실측). `codex exec` 처럼 사람이 없는 실행이면 그 명령을 보고하고 멈춘다.
|
|
83
|
+
`--add-dir ~/.codex` 로 설정 폴더를 통째로 여는 방법은 쓰지 않는다. 그 세션이 훅과 신뢰 기록을
|
|
84
|
+
고칠 수 있게 되어 샌드박스가 무력해진다. `status`, `usage` 는 읽기만 한다.
|
|
85
|
+
- 훅 묶음이 사라졌으면(다른 도구나 손 편집) `status` 가 "훅이 없음" 으로 알린다. `on` 을 다시 실행한다.
|
|
86
|
+
- banker 나 OMX 를 지우기 전에 `off` 를 실행한다. banker 를 지워도 사본과 훅은 남아 계속 돈다. OMX 의 훅 제거는
|
|
87
|
+
뒤에 다른 묶음이 있으면 번호가 밀린다며 멈출 수 있다.
|
|
88
|
+
- 훅을 쓸 수 없으면(훅 기능 꺼짐, 신뢰 거부) **휴리스틱 폴백**: 어시스턴트가 스스로 컨텍스트 압박을 감지해 체인을
|
|
89
|
+
제안하고, 유저가 트리거. 이때는 50% 정밀 트리거를 보장하지 않는다고 알린다. 준비 스킬은 banker 것이 양 런타임 동일(아래).
|
|
45
90
|
|
|
46
91
|
## 게이트 체인
|
|
47
92
|
무장 후 임계 초과가 감지되면 순서대로:
|
|
@@ -53,11 +98,11 @@ description: "컨텍스트 사용률이 임계(기본 50%)를 넘으면 `append_
|
|
|
53
98
|
|
|
54
99
|
1~3은 어시스턴트가 자동 수행. 4는 유저가 실행한다. 안내는 2에서 끝내고 3 뒤에는 아무것도 붙이지 않는다.
|
|
55
100
|
(Claude=`/banker:append_wiki`·`/banker:ready-compact`,
|
|
56
|
-
Codex=`$banker-append_wiki`·`$banker-ready-compact`; `/copy`·compaction은 양쪽 TUI 내장.)
|
|
101
|
+
Codex=`$banker-append_wiki`·`$banker-ready-compact`, 플러그인 설치면 `$banker:append_wiki`·`$banker:ready-compact`; `/copy`·compaction은 양쪽 TUI 내장.)
|
|
57
102
|
|
|
58
103
|
## 활성화 (idempotent · compose-safe)
|
|
59
|
-
`smart-compact [--threshold N]` (기본 N=50)
|
|
60
|
-
1. `~/.claude/settings.json`
|
|
104
|
+
`smart-compact [--threshold N]` (기본 N=50). 아래는 Claude Code 절차이고, Codex 는 위 Codex 절의 `codex-gate.mjs on` 으로 대신한다:
|
|
105
|
+
1. `~/.claude/settings.json` 의 기존 `statusLine`·hook 설정을 **읽어 백업**
|
|
61
106
|
(`~/.claude/.smart-compact-prev.json`).
|
|
62
107
|
2. statusLine 래퍼 스크립트를 설치하고 `statusLine.command` 를 래퍼로 교체(래퍼가 이전 command를 위임 호출).
|
|
63
108
|
3. `UserPromptSubmit` hook 항목을 **추가**(기존 hook 보존, 중복 방지 — 이미 있으면 skip).
|
|
@@ -67,7 +112,7 @@ Codex=`$banker-append_wiki`·`$banker-ready-compact`; `/copy`·compaction은 양
|
|
|
67
112
|
않고 **감싸거나 추가만** 한다(uid는 `id -u` 로 계산, 하드코딩 금지).
|
|
68
113
|
|
|
69
114
|
## 해제
|
|
70
|
-
`smart-compact --cancel
|
|
115
|
+
`smart-compact --cancel` (Codex 는 `codex-gate.mjs off`):
|
|
71
116
|
1. 백업(`~/.claude/.smart-compact-prev.json`)에서 원래 `statusLine`·hook 설정 **복원**.
|
|
72
117
|
2. 래퍼 스크립트·신호 파일·백업 제거.
|
|
73
118
|
3. 무장 해제를 보고. (백업이 없으면 smart-compact가 추가한 항목만 안전 제거.)
|
|
@@ -77,6 +122,7 @@ Codex=`$banker-append_wiki`·`$banker-ready-compact`; `/copy`·compaction은 양
|
|
|
77
122
|
- **완전 자동 과대약속 금지** — `/copy`·`/compact`·paste는 TUI 전용, 유저 핸드오프임을 항상 명시.
|
|
78
123
|
- **uid 하드코딩 금지** — 신호/경로는 `id -u` 로.
|
|
79
124
|
- **hook은 명령을 못 부른다** — `additionalContext` 로 어시스턴트에게 지시할 뿐, 스스로 체인을 실행하지 못함.
|
|
80
|
-
- **Codex
|
|
125
|
+
- **Codex 훅은 신뢰해야 돈다** — 설치 뒤 "Hooks need review" 창이나 `/hooks` 에서 신뢰하라고 반드시 알린다.
|
|
126
|
+
- **Codex 훅 묶음은 맨 뒤에** — 앞에 끼우면 OMX 훅의 신뢰가 풀린다. `hooks.json` 을 손으로 고치지 말고 `codex-gate.mjs` 를 쓴다.
|
|
81
127
|
|
|
82
128
|
ARGUMENTS: [--threshold N] | --cancel (없으면 기본 임계 50%로 무장)
|