@kaydash9999/banker-plugins 0.17.0 → 0.18.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 +44 -0
- package/README.md +10 -3
- package/codex/manifest.json +2 -1
- package/codex/transform-matrix.md +4 -2
- package/commands/front-qa.md +3 -3
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +1 -1
- package/skills/3d-intro-build/references/assemble.mjs +3 -3
- package/skills/3d-intro-setup/SKILL.md +3 -3
- package/skills/anti-drm/SKILL.md +230 -0
- package/skills/anti-drm/scripts/anti-drm.mjs +552 -0
- package/skills/anti-drm/scripts/instruction-toggle.mjs +527 -0
- package/skills/lineage/SKILL.md +3 -3
- package/skills/lineage/lineage.py +16 -7
- package/skills/omc-patch/SKILL.md +12 -4
- package/skills/omc-patch/scripts/omc-patch.mjs +477 -225
- package/skills/omc-reference/SKILL.md +1 -1
- package/skills/ralph-qa/SKILL.md +19 -19
- package/skills/tone-compact/SKILL.md +1 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +527 -0
- package/skills/tone-compact/scripts/tone-compact.mjs +46 -394
- package/skills/ultra-chain/SKILL.md +7 -6
- package/skills/update-banker/SKILL.md +2 -1
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* instruction-toggle: puts one set of agent instructions where Claude Code and
|
|
3
|
+
* Codex load them at every session start, and takes it out again.
|
|
4
|
+
*
|
|
5
|
+
* banker's tone-compact and anti-drm share this module. Each skill folder holds
|
|
6
|
+
* a byte-identical copy, since Codex installs every skill folder on its own and
|
|
7
|
+
* one skill cannot import from another. tone-compact's copy is the source:
|
|
8
|
+
* `node scripts/sync-adapter.js` mirrors it, and the smoke test and anti-drm's
|
|
9
|
+
* tests compare the two.
|
|
10
|
+
*
|
|
11
|
+
* Claude Code loads every rules file under <config>/rules/ (<config> is
|
|
12
|
+
* $CLAUDE_CONFIG_DIR or ~/.claude) at session start and again after compaction.
|
|
13
|
+
* turnOn writes banker-<slug>.md there and turnOff deletes it. A file at that
|
|
14
|
+
* name that does not start with banker's mark belongs to someone else and is
|
|
15
|
+
* left alone.
|
|
16
|
+
*
|
|
17
|
+
* Codex reads $CODEX_HOME/AGENTS.override.md when it holds any non-whitespace
|
|
18
|
+
* text and $CODEX_HOME/AGENTS.md otherwise (CODEX_HOME defaults to ~/.codex).
|
|
19
|
+
* turnOn adds one marked block at the end of AGENTS.md, or of the override when
|
|
20
|
+
* the override has text of its own. No banker block counts as the override's
|
|
21
|
+
* own text: banker's blocks alone there would hide AGENTS.md. When other
|
|
22
|
+
* banker blocks alone fill the override, Codex reads only them; turnOn and the
|
|
23
|
+
* status line say so, and those skills' turnOn moves them out. turnOff takes
|
|
24
|
+
* the block out and gives back the bytes from before.
|
|
25
|
+
*
|
|
26
|
+
* The block sits inside OMX's USER:OMX:POLICY markers, the only part of a
|
|
27
|
+
* generated AGENTS.md that `omx setup --merge-agents` carries over; a plain
|
|
28
|
+
* `omx setup` with team mode on only refreshes the model table. `omx setup
|
|
29
|
+
* --force`, confirming its overwrite prompt, or a plain `omx setup` with team
|
|
30
|
+
* mode off replaces the file block and all; turnOn then has to run again. Text
|
|
31
|
+
* other tools keep in these files is left alone, and the bytes from before each
|
|
32
|
+
* change are kept in <file>.<slug>.bak. Markers count only as whole lines that
|
|
33
|
+
* open and close in turn; a marker quoted inside a sentence, or an end before a
|
|
34
|
+
* start, makes turnOn and turnOff refuse rather than guess where the block ends.
|
|
35
|
+
* So does a file that is not UTF-8 (CP949, UTF-16): written back as text it
|
|
36
|
+
* would lose its bytes.
|
|
37
|
+
*
|
|
38
|
+
* Results are { ok, message }; ok false means refused with nothing changed.
|
|
39
|
+
* Anything else that goes wrong throws.
|
|
40
|
+
*/
|
|
41
|
+
import {
|
|
42
|
+
accessSync,
|
|
43
|
+
constants,
|
|
44
|
+
lstatSync,
|
|
45
|
+
mkdirSync,
|
|
46
|
+
readFileSync,
|
|
47
|
+
renameSync,
|
|
48
|
+
rmSync,
|
|
49
|
+
statSync,
|
|
50
|
+
writeFileSync,
|
|
51
|
+
} from "node:fs";
|
|
52
|
+
import { dirname, join } from "node:path";
|
|
53
|
+
|
|
54
|
+
/** The banker skills that keep a block in Codex's instructions files. */
|
|
55
|
+
export const BANKER_SLUGS = ["tone-compact", "anti-drm"];
|
|
56
|
+
export const RUNTIMES = ["claude", "codex"];
|
|
57
|
+
// OMX keeps only these regions when `omx setup` rewrites a generated AGENTS.md.
|
|
58
|
+
export const KEEP_OPEN = "<!-- USER:OMX:POLICY:START -->";
|
|
59
|
+
export const KEEP_CLOSE = "<!-- USER:OMX:POLICY:END -->";
|
|
60
|
+
|
|
61
|
+
// The byte order mark an editor may put in front of a file.
|
|
62
|
+
const BOM = String.fromCharCode(0xfeff);
|
|
63
|
+
const noBom = (text) => (text.startsWith(BOM) ? text.slice(BOM.length) : text);
|
|
64
|
+
const lf = (text) => text.replace(/\r\n/g, "\n");
|
|
65
|
+
const eolOf = (text) => (text.includes("\r\n") ? "\r\n" : "\n");
|
|
66
|
+
const isDir = (path) => {
|
|
67
|
+
try {
|
|
68
|
+
return statSync(path).isDirectory();
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const isLink = (path) => {
|
|
74
|
+
try {
|
|
75
|
+
return lstatSync(path).isSymbolicLink();
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
82
|
+
|
|
83
|
+
// A file's bytes and text, null when there is no such file. text is null when
|
|
84
|
+
// the bytes are not UTF-8, which callers refuse: decoding them would replace
|
|
85
|
+
// them with U+FFFD for good. Any other read failure (permissions, a folder at
|
|
86
|
+
// that name) is not "missing" and propagates.
|
|
87
|
+
function readFile(path) {
|
|
88
|
+
let bytes;
|
|
89
|
+
try {
|
|
90
|
+
bytes = readFileSync(path);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR") return null;
|
|
93
|
+
throw e;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
return { bytes, text: UTF8.decode(bytes) };
|
|
97
|
+
} catch {
|
|
98
|
+
return { bytes, text: null };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const done = (message) => ({ ok: true, message });
|
|
103
|
+
const refuse = (message) => ({ ok: false, message });
|
|
104
|
+
|
|
105
|
+
/** A skill's marks: the first line of its rules file, and the lines around its Codex block. */
|
|
106
|
+
export function markersOf(slug) {
|
|
107
|
+
return { ownerMark: `<!-- banker ${slug}`, open: `<!-- banker:${slug}:start -->`, close: `<!-- banker:${slug}:end -->` };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* What one skill's toggle needs: its slug and marks, the first line of its
|
|
112
|
+
* rules file, and two texts it words its own way: the status of rules that
|
|
113
|
+
* differ from the ones given (staleText), and what turnOn says when it replaced
|
|
114
|
+
* them (updatedText).
|
|
115
|
+
*/
|
|
116
|
+
export function toggleSpec({ slug, staleText, updatedText }) {
|
|
117
|
+
const marks = markersOf(slug);
|
|
118
|
+
const header = `${marks.ownerMark}: banker 가 관리하는 파일입니다. 끄려면 ${slug} 에 off 를 실행하세요. -->`;
|
|
119
|
+
return { slug, ...marks, header, staleText, updatedText };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Replaces file through a fresh temp file ("wx") and a rename: a link planted
|
|
124
|
+
* at either name is replaced rather than written through, and a write that
|
|
125
|
+
* fails leaves the old file as it was.
|
|
126
|
+
*/
|
|
127
|
+
export function writeFresh(file, data, mode = 0o644) {
|
|
128
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
129
|
+
rmSync(tmp, { force: true });
|
|
130
|
+
try {
|
|
131
|
+
writeFileSync(tmp, data, { flag: "wx", mode });
|
|
132
|
+
renameSync(tmp, file);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
rmSync(tmp, { force: true });
|
|
135
|
+
throw e;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Whether an error is about permissions or a read-only file system. */
|
|
140
|
+
export const denied = (e) => e.code === "EACCES" || e.code === "EPERM" || e.code === "EROFS";
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A path or an error text as a message shows it. Control, format, line and
|
|
144
|
+
* paragraph separator, private-use, unassigned and variation selector
|
|
145
|
+
* characters become "?": a path from the environment cannot start a line of
|
|
146
|
+
* its own, reorder the text around it, or hide text after a visible letter.
|
|
147
|
+
*/
|
|
148
|
+
export const where = (value) =>
|
|
149
|
+
String(value).replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Co}\p{Cn}\p{Variation_Selector}]/gu, "?");
|
|
150
|
+
|
|
151
|
+
// ---- Claude Code: one rules file that is banker's alone ----
|
|
152
|
+
|
|
153
|
+
const claudeFile = (t, o) => join(o.claudeDir, "rules", `banker-${t.slug}.md`);
|
|
154
|
+
const claudeText = (t, rules) => `${t.header}\n${rules}\n`;
|
|
155
|
+
|
|
156
|
+
// "off", "foreign" (not banker's file), or, when rules are given, "on" or
|
|
157
|
+
// "stale" (other rules); without rules a banker file is "ours".
|
|
158
|
+
function claudeState(t, file, rules) {
|
|
159
|
+
if (file === null) return "off";
|
|
160
|
+
if (file.text === null) return "foreign";
|
|
161
|
+
const body = lf(noBom(file.text));
|
|
162
|
+
if (!body.startsWith(t.ownerMark)) return "foreign";
|
|
163
|
+
if (rules === undefined) return "ours";
|
|
164
|
+
return body === claudeText(t, rules) ? "on" : "stale";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function claudeOn(t, o, getRules) {
|
|
168
|
+
const file = claudeFile(t, o);
|
|
169
|
+
const shown = where(file);
|
|
170
|
+
if (!isDir(o.claudeDir)) return refuse(`Claude Code 설정 폴더가 없습니다: ${where(o.claudeDir)}`);
|
|
171
|
+
const rules = getRules();
|
|
172
|
+
const state = claudeState(t, readFile(file), rules);
|
|
173
|
+
if (state === "foreign") return refuse(`같은 이름의 다른 파일이 있어 그대로 두었습니다: ${shown}`);
|
|
174
|
+
if (state === "on") return done(`Claude Code: 이미 켜져 있습니다 (변경 없음). ${shown}`);
|
|
175
|
+
try {
|
|
176
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o755 });
|
|
177
|
+
writeFresh(file, claudeText(t, rules));
|
|
178
|
+
} catch (e) {
|
|
179
|
+
if (denied(e)) return refuse(`쓸 권한이 없어 켜지 못했습니다 (${e.code}): ${shown}`);
|
|
180
|
+
throw e;
|
|
181
|
+
}
|
|
182
|
+
return done(
|
|
183
|
+
state === "stale"
|
|
184
|
+
? `Claude Code: ${t.updatedText}. ${shown}`
|
|
185
|
+
: `Claude Code: 켰습니다. 다음 세션부터 자동 적용됩니다. ${shown}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function claudeOff(t, o) {
|
|
190
|
+
const file = claudeFile(t, o);
|
|
191
|
+
const state = claudeState(t, readFile(file));
|
|
192
|
+
if (state === "off") return done("Claude Code: 이미 꺼져 있습니다 (변경 없음).");
|
|
193
|
+
if (state === "foreign") return refuse(`같은 이름의 다른 파일이 있어 그대로 두었습니다: ${where(file)}`);
|
|
194
|
+
try {
|
|
195
|
+
rmSync(file);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
if (denied(e)) return refuse(`지울 권한이 없어 끄지 못했습니다 (${e.code}): ${where(file)}`);
|
|
198
|
+
throw e;
|
|
199
|
+
}
|
|
200
|
+
return done(`Claude Code: 껐습니다. 다음 세션부터 적용되지 않습니다. ${where(file)}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---- Codex: one block in the global instructions file Codex reads ----
|
|
204
|
+
|
|
205
|
+
const lineBare = (line) => line.replace(/\r?\n$/, "");
|
|
206
|
+
// A line without its line break, and the first one without the byte order mark
|
|
207
|
+
// an editor may put in front of it.
|
|
208
|
+
const bareAt = (lines, i) => (i === 0 ? noBom(lineBare(lines[i])) : lineBare(lines[i]));
|
|
209
|
+
|
|
210
|
+
// One block's place in the text: [from, to) runs from its first line (the keep
|
|
211
|
+
// marker when one wraps it directly) through the end of its last line; cut is
|
|
212
|
+
// from minus the line break `on` puts before the block, and blankBefore says
|
|
213
|
+
// whether that break makes a blank line of its own.
|
|
214
|
+
function spanOf(lines, offsets, first, last) {
|
|
215
|
+
const wrapped =
|
|
216
|
+
first > 0 &&
|
|
217
|
+
last + 1 < lines.length &&
|
|
218
|
+
bareAt(lines, first - 1) === KEEP_OPEN &&
|
|
219
|
+
bareAt(lines, last + 1) === KEEP_CLOSE;
|
|
220
|
+
const top = wrapped ? first - 1 : first;
|
|
221
|
+
const bottom = wrapped ? last + 1 : last;
|
|
222
|
+
const before = top > 0 ? lines[top - 1] : "";
|
|
223
|
+
const joint = before.endsWith("\r\n") ? 2 : before.endsWith("\n") ? 1 : 0;
|
|
224
|
+
const blankBefore = before !== "" && lineBare(before) === "";
|
|
225
|
+
return { cut: offsets[top] - joint, from: offsets[top], to: offsets[bottom] + lines[bottom].length, blankBefore };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Where each line starts in the text.
|
|
229
|
+
function lineOffsets(lines) {
|
|
230
|
+
const offsets = [];
|
|
231
|
+
let at = 0;
|
|
232
|
+
for (const line of lines) {
|
|
233
|
+
offsets.push(at);
|
|
234
|
+
at += line.length;
|
|
235
|
+
}
|
|
236
|
+
return offsets;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Any management marker, a banker block's or OMX's, anywhere on a line.
|
|
240
|
+
const ANY_MARKER = /<!-- (?:banker:[a-z0-9-]+:(?:start|end)|USER:OMX:POLICY:(?:START|END)) -->/;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Whether text quotes any management marker. Rules must not: a block holding
|
|
244
|
+
* one would be refused as soon as it is written, and could never come out.
|
|
245
|
+
*/
|
|
246
|
+
export const hasMarker = (text) => ANY_MARKER.test(text);
|
|
247
|
+
// Whether a marker sits between a block's first and last line: then its end was
|
|
248
|
+
// moved, by hand or by an editor, and cutting to it would take other text along.
|
|
249
|
+
const markerInside = (lines, first, last) => lines.slice(first + 1, last).some((line) => ANY_MARKER.test(line));
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Where the blocks between whole lines `open` and `close` sit in text, or null
|
|
253
|
+
* when those markers are not whole lines that open and close in turn, or when
|
|
254
|
+
* another management marker sits inside a block. turnOn and turnOff refuse
|
|
255
|
+
* null, so a marker quoted inside a sentence, or an end put back by hand around
|
|
256
|
+
* other text, can never make them cut the user's own text.
|
|
257
|
+
*/
|
|
258
|
+
export function blockSpansOf(text, open, close) {
|
|
259
|
+
const lines = text.split(/(?<=\n)/);
|
|
260
|
+
const offsets = lineOffsets(lines);
|
|
261
|
+
const spans = [];
|
|
262
|
+
let first = -1;
|
|
263
|
+
for (let i = 0; i < lines.length; i++) {
|
|
264
|
+
const bare = bareAt(lines, i);
|
|
265
|
+
if (bare === open && first === -1) first = i;
|
|
266
|
+
else if (bare === close && first !== -1) {
|
|
267
|
+
if (markerInside(lines, first, i)) return null;
|
|
268
|
+
spans.push(spanOf(lines, offsets, first, i));
|
|
269
|
+
first = -1;
|
|
270
|
+
} else if (lines[i].includes(open) || lines[i].includes(close)) return null;
|
|
271
|
+
}
|
|
272
|
+
return first === -1 ? spans : null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// A block, banker's or any USER:OMX:POLICY one, right after a line break.
|
|
276
|
+
const BLOCK_NEXT = /^\r?\n(?:<!-- USER:OMX:POLICY:START -->|<!-- banker:)/;
|
|
277
|
+
|
|
278
|
+
// The text without its blocks: exactly what it was before `on` added them. The
|
|
279
|
+
// line break `on` put in front goes with the block when the block ends the file
|
|
280
|
+
// or has a blank line of its own in front; with other text after it, the break
|
|
281
|
+
// stays, so the lines on either side never run together. When another block
|
|
282
|
+
// follows, its own leading break keeps the lines apart, so this one goes too:
|
|
283
|
+
// two blocks taken out in the order they went in then leave a text that had no
|
|
284
|
+
// final line break as it was.
|
|
285
|
+
function strip(text, spans) {
|
|
286
|
+
let out = "";
|
|
287
|
+
let at = 0;
|
|
288
|
+
for (const s of spans) {
|
|
289
|
+
const keepBreak = s.to < text.length && !s.blankBefore && !BLOCK_NEXT.test(text.slice(s.to));
|
|
290
|
+
out += text.slice(at, keepBreak ? s.from : s.cut);
|
|
291
|
+
at = s.to;
|
|
292
|
+
}
|
|
293
|
+
return out + text.slice(at);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Whether text holds anything but whitespace, the way Codex judges a file empty:
|
|
297
|
+
// Rust's trim, which strips the Unicode White_Space set and, unlike
|
|
298
|
+
// JavaScript's trim, keeps a byte order mark.
|
|
299
|
+
const hasContent = (text) => /\P{White_Space}/u.test(text);
|
|
300
|
+
|
|
301
|
+
// The text without any banker skill's blocks. A skill whose markers do not pair
|
|
302
|
+
// up keeps its text, which then counts as content: nothing is cut on a guess.
|
|
303
|
+
function withoutBankerBlocks(text) {
|
|
304
|
+
return BANKER_SLUGS.reduce((out, slug) => {
|
|
305
|
+
const { open, close } = markersOf(slug);
|
|
306
|
+
const spans = blockSpansOf(out, open, close);
|
|
307
|
+
return spans === null ? out : strip(out, spans);
|
|
308
|
+
}, text);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// The banker skills other than t with a block in text.
|
|
312
|
+
const otherBlocks = (t, text) =>
|
|
313
|
+
BANKER_SLUGS.filter((slug) => {
|
|
314
|
+
if (slug === t.slug) return false;
|
|
315
|
+
const { open, close } = markersOf(slug);
|
|
316
|
+
const spans = blockSpansOf(text, open, close);
|
|
317
|
+
return spans !== null && spans.length > 0;
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const binary = (f) => f.bytes !== null && f.text === null;
|
|
321
|
+
const unpaired = (f) => f.spans === null;
|
|
322
|
+
const hasBlock = (f) => f.spans !== null && f.spans.length > 0;
|
|
323
|
+
const stripped = (f) => (f.text === null ? "" : strip(f.text, f.spans));
|
|
324
|
+
|
|
325
|
+
// Both global instruction files with their bytes and text (null when absent;
|
|
326
|
+
// text null when not UTF-8) and blocks (null when the markers do not pair up).
|
|
327
|
+
// The block's place (target) is the override when the override has text of its
|
|
328
|
+
// own, AGENTS.md otherwise. shadowers names the other banker skills whose
|
|
329
|
+
// blocks alone fill the override: Codex then reads the override, not AGENTS.md.
|
|
330
|
+
function codexFiles(t, o) {
|
|
331
|
+
const read = (name) => {
|
|
332
|
+
const path = join(o.codexDir, name);
|
|
333
|
+
const file = readFile(path);
|
|
334
|
+
if (file === null) return { path, bytes: null, text: null, spans: [] };
|
|
335
|
+
return { path, ...file, spans: file.text === null ? null : blockSpansOf(file.text, t.open, t.close) };
|
|
336
|
+
};
|
|
337
|
+
const override = read("AGENTS.override.md");
|
|
338
|
+
const main = read("AGENTS.md");
|
|
339
|
+
const ownContent = binary(override) || (override.text !== null && hasContent(withoutBankerBlocks(override.text)));
|
|
340
|
+
const target = ownContent ? override : main;
|
|
341
|
+
const shadowers = ownContent || override.text === null ? [] : otherBlocks(t, override.text);
|
|
342
|
+
return { target, other: target === override ? main : override, all: [override, main], shadowers };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const blockFor = (t, rules, eol) => [KEEP_OPEN, t.open, ...rules.split("\n"), t.close, KEEP_CLOSE, ""].join(eol);
|
|
346
|
+
// A block's text compared without line-break style, a leading byte order mark
|
|
347
|
+
// or the final line break, which editors add and drop.
|
|
348
|
+
const normBlock = (text) => noBom(lf(text)).replace(/\n$/, "");
|
|
349
|
+
// The block exactly as `on` writes it, and only once.
|
|
350
|
+
const blockCurrent = (t, f, rules) =>
|
|
351
|
+
f.spans.length === 1 && normBlock(f.text.slice(f.spans[0].from, f.spans[0].to)) === normBlock(blockFor(t, rules, "\n"));
|
|
352
|
+
|
|
353
|
+
// The text with the block at the end, one line break between them; an empty
|
|
354
|
+
// file gets the block alone. strip() undoes exactly this.
|
|
355
|
+
function withBlock(t, text, rules) {
|
|
356
|
+
const eol = eolOf(text);
|
|
357
|
+
return text === "" ? blockFor(t, rules, eol) : text + eol + blockFor(t, rules, eol);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// "off", "on", "stale", "misplaced" (only in AGENTS.md while Codex reads the
|
|
361
|
+
// override, so not applied), "hidden" (in the override with no text of its own,
|
|
362
|
+
// applied but hiding AGENTS.md), "unpaired" or "notutf8". Without rules any
|
|
363
|
+
// block in its place counts as "on".
|
|
364
|
+
function codexState(t, files, rules) {
|
|
365
|
+
if (files.all.some(binary)) return "notutf8";
|
|
366
|
+
if (files.all.some(unpaired)) return "unpaired";
|
|
367
|
+
const inTarget = hasBlock(files.target);
|
|
368
|
+
const inOther = hasBlock(files.other);
|
|
369
|
+
if (!inTarget) return inOther ? (files.other === files.all[0] ? "hidden" : "misplaced") : "off";
|
|
370
|
+
if (rules === undefined) return "on";
|
|
371
|
+
return blockCurrent(t, files.target, rules) && !inOther ? "on" : "stale";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Why these files cannot be written, or "" when they can. The folder counts
|
|
375
|
+
// too: the .bak is written through a temp file beside the file.
|
|
376
|
+
function writeBlocked(changes) {
|
|
377
|
+
for (const { file } of changes) {
|
|
378
|
+
for (const path of file.bytes === null ? [dirname(file.path)] : [file.path, dirname(file.path)]) {
|
|
379
|
+
try {
|
|
380
|
+
accessSync(path, constants.W_OK);
|
|
381
|
+
} catch (e) {
|
|
382
|
+
return `쓸 권한이 없어 그대로 두었습니다 (${where(e.code || e.message)}): ${where(path)}`;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return "";
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Writes each file in place, so a symlinked AGENTS.md stays a link and its
|
|
390
|
+
// target changes, after keeping the old bytes in a .bak only this account can
|
|
391
|
+
// read (the file may be a link into a private folder). A new file gets 0644
|
|
392
|
+
// whatever the umask allows more. A file left empty is removed, unless it is a
|
|
393
|
+
// link: Codex skips an empty file anyway.
|
|
394
|
+
function apply(t, changes) {
|
|
395
|
+
for (const { file, after } of changes) {
|
|
396
|
+
if (file.bytes !== null) writeFresh(`${file.path}.${t.slug}.bak`, file.bytes, 0o600);
|
|
397
|
+
if (after === "" && !isLink(file.path)) rmSync(file.path, { force: true });
|
|
398
|
+
else writeFileSync(file.path, after, { mode: 0o644 });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const OMX_NOTE =
|
|
403
|
+
"참고: omx setup 이 AGENTS.md 를 새로 만들면(--force, 덮어쓰기 확인, team 모드를 끈 상태의 omx setup) 이 블록이 사라집니다. 그 뒤에는 on 을 다시 실행하세요. --merge-agents 와 기본 omx setup 은 블록을 남깁니다.";
|
|
404
|
+
|
|
405
|
+
// The note for a block other banker blocks keep from being read.
|
|
406
|
+
const shadowNote = (files) =>
|
|
407
|
+
files.shadowers.length === 0
|
|
408
|
+
? ""
|
|
409
|
+
: `\n주의: AGENTS.override.md 에 다른 banker 블록(${files.shadowers.join(", ")})만 있어 Codex 가 AGENTS.md 를 읽지 않습니다. 그 스킬에서 on 을 실행해 블록을 옮기세요.`;
|
|
410
|
+
|
|
411
|
+
// The refusal for files on and off must not touch, or null.
|
|
412
|
+
function codexRefusal(t, files, state) {
|
|
413
|
+
if (state === "notutf8") return refuse(`UTF-8 이 아닌 파일이라 그대로 두었습니다: ${where(files.all.find(binary).path)}`);
|
|
414
|
+
if (state === "unpaired") return refuse(`${t.slug} 표시의 짝이 맞지 않아 그대로 두었습니다: ${where(files.all.find(unpaired).path)}`);
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function codexOn(t, o, getRules) {
|
|
419
|
+
if (!isDir(o.codexDir)) return refuse(`Codex 설정 폴더가 없습니다: ${where(o.codexDir)}`);
|
|
420
|
+
const rules = getRules();
|
|
421
|
+
const files = codexFiles(t, o);
|
|
422
|
+
const state = codexState(t, files, rules);
|
|
423
|
+
const refusal = codexRefusal(t, files, state);
|
|
424
|
+
if (refusal) return refusal;
|
|
425
|
+
const tail = `${where(files.target.path)}${shadowNote(files)}`;
|
|
426
|
+
if (state === "on") return done(`Codex: 이미 켜져 있습니다 (변경 없음). ${tail}`);
|
|
427
|
+
const changes = [{ file: files.target, after: withBlock(t, stripped(files.target), rules) }];
|
|
428
|
+
if (hasBlock(files.other)) changes.push({ file: files.other, after: stripped(files.other) });
|
|
429
|
+
const why = writeBlocked(changes);
|
|
430
|
+
if (why) return refuse(why);
|
|
431
|
+
apply(t, changes);
|
|
432
|
+
const moved = "블록을 맞는 파일로 옮겼습니다";
|
|
433
|
+
const what = { stale: t.updatedText, misplaced: moved, hidden: moved }[state];
|
|
434
|
+
const note = files.target === files.all[1] ? `\n${OMX_NOTE}` : "";
|
|
435
|
+
return done(`Codex: ${what ?? "켰습니다. 다음 세션부터 자동 적용됩니다"}. ${tail}${note}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function codexOff(t, o) {
|
|
439
|
+
if (!isDir(o.codexDir)) return done(`Codex: 설정 폴더가 없어 끌 것이 없습니다 (변경 없음). ${where(o.codexDir)}`);
|
|
440
|
+
const files = codexFiles(t, o);
|
|
441
|
+
const refusal = codexRefusal(t, files, codexState(t, files));
|
|
442
|
+
if (refusal) return refusal;
|
|
443
|
+
const changes = files.all.filter(hasBlock).map((f) => ({ file: f, after: stripped(f) }));
|
|
444
|
+
if (changes.length === 0) return done("Codex: 이미 꺼져 있습니다 (변경 없음).");
|
|
445
|
+
const why = writeBlocked(changes);
|
|
446
|
+
if (why) return refuse(why);
|
|
447
|
+
apply(t, changes);
|
|
448
|
+
return done(`Codex: 껐습니다. 다음 세션부터 적용되지 않습니다. ${changes.map((c) => where(c.file.path)).join(", ")}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ---- status ----
|
|
452
|
+
|
|
453
|
+
const STATE_TEXT = {
|
|
454
|
+
on: "켜짐",
|
|
455
|
+
ours: "켜짐 (규칙 버전 확인 불가)",
|
|
456
|
+
off: "꺼짐",
|
|
457
|
+
foreign: "꺼짐, 같은 이름의 다른 파일이 있음 (손대지 않음)",
|
|
458
|
+
misplaced: "꺼짐, 단 블록이 Codex 가 읽지 않는 AGENTS.md 에 있음 (on 으로 옮김)",
|
|
459
|
+
hidden: "켜짐, 단 AGENTS.override.md 에 블록만 있어 AGENTS.md 가 가려짐 (on 으로 옮김)",
|
|
460
|
+
notutf8: "UTF-8 이 아닌 지침 파일이 있어 손대지 않음 (on, off 모두 거부)",
|
|
461
|
+
unpaired: "표시의 짝이 맞지 않음 (on, off 모두 거부. 남은 표시 줄 정리 필요)",
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// A state as the status line says it.
|
|
465
|
+
function stateText(t, { state, who }) {
|
|
466
|
+
if (state === "stale") return t.staleText;
|
|
467
|
+
if (state === "shadowed") {
|
|
468
|
+
return `꺼짐, 단 AGENTS.override.md 에 다른 banker 블록(${who.join(", ")})만 있어 Codex 가 AGENTS.md 를 읽지 않음 (그 스킬에서 on 으로 옮김)`;
|
|
469
|
+
}
|
|
470
|
+
return STATE_TEXT[state] ?? state;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// One status line. Read failures are reported, not thrown: status is where an
|
|
474
|
+
// exit 2 sends the user to look.
|
|
475
|
+
function describe(t, name, look) {
|
|
476
|
+
try {
|
|
477
|
+
const found = look();
|
|
478
|
+
return `- ${name}: ${stateText(t, found)} (${where(found.path)})`;
|
|
479
|
+
} catch (e) {
|
|
480
|
+
return `- ${name}: 읽을 수 없음 (${where(e.code || e.message)})`;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function codexLook(t, o, rules) {
|
|
485
|
+
if (!isDir(o.codexDir)) return { state: "설정 폴더 없음", path: o.codexDir };
|
|
486
|
+
const files = codexFiles(t, o);
|
|
487
|
+
const state = codexState(t, files, rules);
|
|
488
|
+
const shadowed = files.shadowers.length > 0 && (state === "on" || state === "stale");
|
|
489
|
+
return { state: shadowed ? "shadowed" : state, path: files.target.path, who: files.shadowers };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** One status line per runtime: "- Claude Code: ..." and "- Codex: ...". rules may be undefined. */
|
|
493
|
+
export function stateLines(t, o, rules) {
|
|
494
|
+
return [
|
|
495
|
+
describe(t, "Claude Code", () => {
|
|
496
|
+
if (!isDir(o.claudeDir)) return { state: "설정 폴더 없음", path: o.claudeDir };
|
|
497
|
+
return { state: claudeState(t, readFile(claudeFile(t, o)), rules), path: claudeFile(t, o) };
|
|
498
|
+
}),
|
|
499
|
+
describe(t, "Codex", () => codexLook(t, o, rules)),
|
|
500
|
+
];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Whether a runtime holds the skill's rules in some form (current, older, in
|
|
505
|
+
* the wrong file or with broken markers), so that changed rules should reach
|
|
506
|
+
* it. Throws when a file cannot be read.
|
|
507
|
+
*/
|
|
508
|
+
export function present(t, runtime, o) {
|
|
509
|
+
if (runtime === "claude") return isDir(o.claudeDir) && claudeState(t, readFile(claudeFile(t, o))) === "ours";
|
|
510
|
+
return isDir(o.codexDir) && !["off", "notutf8"].includes(codexState(t, codexFiles(t, o)));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const knownRuntime = (runtime) => {
|
|
514
|
+
if (!RUNTIMES.includes(runtime)) throw new Error(`알 수 없는 런타임입니다: ${runtime}`);
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
/** Puts the rules where one runtime loads them. getRules runs after the config folder check. */
|
|
518
|
+
export function turnOn(t, runtime, o, getRules) {
|
|
519
|
+
knownRuntime(runtime);
|
|
520
|
+
return runtime === "claude" ? claudeOn(t, o, getRules) : codexOn(t, o, getRules);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Takes the rules out for one runtime; it never needs them. */
|
|
524
|
+
export function turnOff(t, runtime, o) {
|
|
525
|
+
knownRuntime(runtime);
|
|
526
|
+
return runtime === "claude" ? claudeOff(t, o) : codexOff(t, o);
|
|
527
|
+
}
|
package/skills/lineage/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: lineage
|
|
3
3
|
description: "현재 세션 대화를 카카오톡 스타일의 단일 HTML 로 내보냄. Claude 답변은 한 줄 요약 아래에 원문을 접어 두고, 펼치면 마크다운으로 렌더해 보여 줌. 기본은 세션 모델이 요약을 쓰고 노이즈 턴을 걸러 내며, `--rulebase` 는 모델 없이 규칙만 씀. 여러 세션을 합칠 수 있음. 'lineage'/'대화 export'/'카톡 스타일 html' 시 사용."
|
|
4
4
|
invocation: /lineage
|
|
5
|
-
version: 3.0.
|
|
5
|
+
version: 3.0.3
|
|
6
6
|
schema_version: 1
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -212,7 +212,7 @@ Schema-tolerant 파서: 미지 record type → stderr WARN + 다음 line. `v1`/`
|
|
|
212
212
|
|
|
213
213
|
기본 흐름에서는 검토자 요약이 우선이고, 검토자가 `null` 로 둔 턴만 아래 규칙 요약을 쓴다. `--rulebase` 는 규칙 요약만 쓴다.
|
|
214
214
|
|
|
215
|
-
- 캐시 hit(`~/.cache/lineage/<schema>/<sid>/<uuid>-<digest>.txt`) → 그대로. digest 는 본문+요약기버전
|
|
215
|
+
- 캐시 hit(`~/.cache/lineage/<schema>/<sid>/<uuid>-<digest>.md`, 3.0.2 까지 쓴 `.txt` 도 읽음) → 그대로. digest 는 본문+요약기버전
|
|
216
216
|
해시라 **요약기를 고치면 자동 무효화**된다.
|
|
217
217
|
- miss → head+tail 추출(도입이 순수 의도문이면 결론절을 끝에서 읽어 승격) + 0600 캐시 쓰기.
|
|
218
218
|
- 요약 저장은 항상 **redact 적용 후**(평문 secret 이 디스크에 남지 않음).
|
|
@@ -344,7 +344,7 @@ pip install 'detect-secrets>=1.5' # (선택) 강한 redaction
|
|
|
344
344
|
|
|
345
345
|
~/.cache/lineage/
|
|
346
346
|
├── stdin-id.key (stdin 턴 id 의 HMAC 키, 0600. --purge-cache 로 지우면 stdin id 가 바뀐다)
|
|
347
|
-
└── <schema_version>/<session_id>/<turn_uuid>-<digest>.
|
|
347
|
+
└── <schema_version>/<session_id>/<turn_uuid>-<digest>.md (규칙 요약, 0700/0600)
|
|
348
348
|
<turn_uuid>-<digest>-llm.json (검토자 결정)
|
|
349
349
|
|
|
350
350
|
work/.lineage-review.json, .lineage-review.part-K.json (검토 중에만, 0600)
|
|
@@ -189,7 +189,7 @@ def redact(text: str, extra: "str | None" = None, mode: str = "full"):
|
|
|
189
189
|
from detect_secrets import SecretsCollection
|
|
190
190
|
from detect_secrets.settings import default_settings
|
|
191
191
|
import tempfile
|
|
192
|
-
with tempfile.NamedTemporaryFile("w", suffix=".
|
|
192
|
+
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False,
|
|
193
193
|
encoding="utf-8") as tf:
|
|
194
194
|
tf.write(out)
|
|
195
195
|
tmp = tf.name
|
|
@@ -734,6 +734,17 @@ def _page_clean_turn(t, extra, mode):
|
|
|
734
734
|
return dict(t, text=clean(t["text"]), parts=[clean(p) for p in t.get("parts") or []])
|
|
735
735
|
|
|
736
736
|
|
|
737
|
+
def _read_cached(p):
|
|
738
|
+
"""The summary cached at p, else at the same name with .txt, which 3.0.2 and earlier wrote.
|
|
739
|
+
None when neither can be read: missing, or bytes that are not UTF-8 (a DRM encrypted them)."""
|
|
740
|
+
for cached in (p, p.with_name(p.stem + ".txt")):
|
|
741
|
+
try:
|
|
742
|
+
return cached.read_text(encoding="utf-8").strip()
|
|
743
|
+
except (OSError, UnicodeDecodeError):
|
|
744
|
+
continue
|
|
745
|
+
return None
|
|
746
|
+
|
|
747
|
+
|
|
737
748
|
def read_or_summarize(turn: dict, session_id: str, rebuild: bool = False,
|
|
738
749
|
redact_extra=None, redact_mode: str = "full"):
|
|
739
750
|
"""Return (summary, cache_hit). Cache key includes the content hash AND the
|
|
@@ -745,15 +756,13 @@ def read_or_summarize(turn: dict, session_id: str, rebuild: bool = False,
|
|
|
745
756
|
).hexdigest()[:8]
|
|
746
757
|
try: # ids come from the transcript: keep them in the cache
|
|
747
758
|
p = (cache_dir(_safe_name(turn.get("session") or session_id))
|
|
748
|
-
/ f"{_safe_name(turn['uuid'])}-{digest}.
|
|
759
|
+
/ f"{_safe_name(turn['uuid'])}-{digest}.md")
|
|
749
760
|
except OSError as e: # no writable cache: summarize anyway
|
|
750
761
|
_warn_once("_no_cache", f"cache unavailable ({e}); summaries are not cached")
|
|
751
762
|
p = None
|
|
752
|
-
if p is
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
except (OSError, UnicodeDecodeError):
|
|
756
|
-
pass
|
|
763
|
+
cached = None if p is None or rebuild else _read_cached(p)
|
|
764
|
+
if cached is not None:
|
|
765
|
+
return cached, True
|
|
757
766
|
redacted = summarize_turn(_page_clean_turn(turn, redact_extra, redact_mode))
|
|
758
767
|
if p is not None:
|
|
759
768
|
try:
|
|
@@ -22,6 +22,7 @@ OMC(oh-my-claudecode)가 새 버전으로 자동 업데이트되면 로컬 훅
|
|
|
22
22
|
- 그러면 다른 블록의 import 가 ReferenceError 로 실패하고, 훅의 `catch` 가 그 오류를 삼킨다. 예: `project-memory-session` 은 종료 코드 0 으로 끝나지만 project-memory 를 만들지 않는다.
|
|
23
23
|
- banker 0.16.x 의 도구는 새로 패치할 때 맨 앞에 넣었지만, 옛 킷이 남긴 이 결함은 진단하지 못하고 `결과: 모두 적용된 상태입니다.` 를 냈다.
|
|
24
24
|
- 지금 도구의 `--check` 는 이 결함을 `헬퍼 자리 결함` 으로 알린다. 이미 감싼 파일이라 다시 적용만으로는 고쳐지지 않는다. 활성 폴더를 되돌린 뒤 다시 적용한다.
|
|
25
|
+
- 헬퍼 선언을 찾지 못하거나 선언이 첫 사용보다 뒤에 있는 파일도 같은 결함으로 센다. 뒤에 있으면 첫 사용이 먼저 돌아 ReferenceError 가 난다.
|
|
25
26
|
|
|
26
27
|
## 인자
|
|
27
28
|
|
|
@@ -35,8 +36,10 @@ OMC(oh-my-claudecode)가 새 버전으로 자동 업데이트되면 로컬 훅
|
|
|
35
36
|
|
|
36
37
|
- 이 작업은 사용자 설정 폴더(`~/.claude`)의 플러그인 캐시를 고치는 설정 변경이다. 서브에이전트에 맡기지 않고 메인 세션에서 직접 실행하고 직접 검증한다.
|
|
37
38
|
- 적용은 `--no-update` 로만 실행한다. 인자 없는 apply 는 쓰지 않는다. 활성 OMC 를 도구의 `TARGET_VERSION`(5.7.0)으로 바꾸고, `revert` 로도 버전이 돌아오지 않는다.
|
|
38
|
-
- 도구가
|
|
39
|
-
-
|
|
39
|
+
- 진단도 늘 `--no-update` 와 함께 한다. 그것 없이 진단하면 도구가 `인자 없이 다시 실행하십시오.` 라고 안내하는데, 인자 없는 apply 를 뜻하므로 따르지 않는다.
|
|
40
|
+
- 인자 없는 `--revert` 는 설치된 모든 버전을 되돌리고 고정까지 푼다. 헬퍼 자리 결함에는 도구가 안내하는 대로 활성 폴더만 `--revert --root` 로 되돌린다(3단계 1).
|
|
41
|
+
- `--root` 는 폴더의 이름표가 OMC 가 아니면 `OMC 플러그인 폴더가 아님` 으로 멈춘다. `.claude-plugin/plugin.json` 의 name 이 있으면 그것으로, 없으면 `package.json` 의 name 으로 정한다. 다른 폴더를 잘못 준 실수를 막는 확인이고, 이름표가 없는 폴더는 통과한다.
|
|
42
|
+
- 쓸 파일이 링크로 플러그인 폴더 밖을 가리키거나 백업 자리(`.omcbak`)가 링크면 도구는 쓰지 않고 실패로 센다. 문구는 적용 때 `쓰지 않음`, 되돌리기 때 `되돌리지 않음`, hooks.json 은 `hooks.json 을 건드리지 않음` 이다.
|
|
40
43
|
- 패치 규칙은 OMC 훅 코드의 모양에 맞춰져 있다. 처음 보는 OMC 버전이면 반드시 `check` 를 먼저 실행한다.
|
|
41
44
|
- `실패 N건` 이 1건 이상이면 멈추고 출력을 그대로 보고한다. 문법 오류가 난 파일은 도구가 원본으로 자동 롤백한다.
|
|
42
45
|
- 판정은 출력 문구로 한다. 종료 코드 0 만으로 완료라고 보고하지 않는다.
|
|
@@ -102,7 +105,8 @@ node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --check --no
|
|
|
102
105
|
```
|
|
103
106
|
|
|
104
107
|
- `되돌림 <훅 이름>` 줄과 `되돌리기 완료.` 를 확인한다. 끝의 `재시작해야 반영됩니다` 는 이 중간 단계에서는 무시한다. 2 에서 바로 다시 패치한다.
|
|
105
|
-
-
|
|
108
|
+
- `되돌리기를 끝내지 못했습니다.` 가 나오면 멈추고 위의 `[!]` 줄을 그대로 보고한다. 종료 코드는 1 이다. 끝 줄의 `실패 N건` 은 쓰기를 거부한 파일, `되돌리지 못한 것 N건` 은 백업이 없거나 수동 백업 후보를 쓸 수 없어 남은 파일이다. 끝 줄은 링크로 같은 폴더를 가리키는 판이 여럿이어도 파일마다 한 번 센다.
|
|
109
|
+
- `[!]` 줄에 `패치는 있는데 백업이 없어 되돌리지 못했습니다 — <이름>` 이 있으면 그 파일에 `.omcbak` 백업이 없는 것이다. 이 도구로는 되돌릴 원본이 없으니 멈추고 보고한다. 원본은 같은 버전의 OMC 저장소에서 다시 받아야 한다. 파이프 대신 파일을 거친다(Windows PowerShell 5.1 의 파이프는 바이너리를 텍스트로 바꿔 파일을 깨뜨린다): `git -C "<마켓플레이스 폴더>" archive -o "<임시 tar>" v<버전> scripts/<이름>.mjs` 뒤 `tar -xf "<임시 tar>" -C "<활성 폴더>"`. 이 명령은 사용자 승인 뒤에만 실행한다.
|
|
106
110
|
|
|
107
111
|
2. 적용한다.
|
|
108
112
|
|
|
@@ -204,6 +208,7 @@ node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --revert
|
|
|
204
208
|
|
|
205
209
|
- `.omcbak` 백업으로 설치된 모든 버전의 훅 스크립트와 `hooks.json` 을 되돌리고 마켓플레이스 `origin` 을 원래 URL 로 복구한다.
|
|
206
210
|
- 되돌린 뒤 Claude Code 를 다시 시작해야 반영된다.
|
|
211
|
+
- `되돌리기를 끝내지 못했습니다.` 가 나오면 위의 `[!]` 줄을 그대로 보고한다. 종료 코드는 1 이다. 백업이 없어 남은 파일과 hooks.json 도 이 끝 줄로 끝난다.
|
|
207
212
|
- `이 도구가 만들지 않은 수정이 남아 있습니다` 가 나오면 백업이 없는 손 수정이다. 출력에 적힌 복구 명령을 그대로 보고하고, 사용자 승인 없이 실행하지 않는다.
|
|
208
213
|
|
|
209
214
|
## 함정
|
|
@@ -213,6 +218,9 @@ node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --revert
|
|
|
213
218
|
- 헬퍼 자리 결함이 남은 채 `revert` 없이 적용을 다시 돌려도 바뀌는 것이 없다. 이미 감싼 파일은 건너뛰기 때문이다.
|
|
214
219
|
- `--root` 와 `--revert --root` 는 그 폴더만 다룬다. 고정 해제까지 하려면 인자 없는 `--revert` 를 쓴다.
|
|
215
220
|
- 고정을 풀지 않고 OMC 를 올리려면 먼저 `revert` 한다. 그렇지 않으면 마켓플레이스 fetch 가 계속 실패한다.
|
|
216
|
-
- 고정 기록(`PINNED`)은 `~/.claude/omc-local-patches/PINNED` 에 생긴다. 기록일 뿐이라 지워도 고정은 풀리지 않고, 이미 고정된 상태에서는 다시 만들어지지 않는다. `revert` 는 origin
|
|
221
|
+
- 고정 기록(`PINNED`)은 `~/.claude/omc-local-patches/PINNED` 에 생긴다. 기록일 뿐이라 지워도 고정은 풀리지 않고, 이미 고정된 상태에서는 다시 만들어지지 않는다. `revert` 는 origin 값에서만 원래 URL 을 되찾고, `PINNED` 의 값으로 origin 을 쓰지 않는다. 다른 기기의 기록이면 알리기만 한다.
|
|
222
|
+
- `--check` 와 `--revert` 를 함께 주면 도구는 아무것도 하지 않고 종료 코드 1 로 멈춘다. 진단만 하려면 `--check` 만 준다.
|
|
223
|
+
- 도구는 git 을 부를 때 저장소를 고르는 변수(`GIT_DIR`, `GIT_WORK_TREE` 등 `git rev-parse --local-env-vars` 목록)를 지운다. git 훅 안에서 불러도 마켓플레이스 저장소만 다룬다.
|
|
224
|
+
- 마켓플레이스의 `.git` 이 깨졌으면(중단된 clone 등) git 이 위쪽 저장소로 올라가지 못하게 막는다. 그때 `[4/4]` 는 `origin remote 없음` 으로 끝나고 고정하지 않는다.
|
|
217
225
|
- `--scan` 은 읽기만 한다. 플러그인 캐시도 `installed_plugins.json` 도 읽지 않고 준 파일만 본다.
|
|
218
226
|
- 도구의 주석과 출력 문구는 한글이다.
|