llm-wiki-kit 0.2.3 → 0.2.5
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/README.md +21 -2
- package/docs/integrations/claude-code.md +3 -1
- package/docs/integrations/codex.md +1 -1
- package/docs/manual.md +41 -2
- package/docs/operations.md +55 -3
- package/docs/troubleshooting.md +49 -1
- package/package.json +1 -1
- package/src/claude-compat.js +83 -0
- package/src/cli.js +34 -6
- package/src/doctor.js +30 -8
- package/src/fs-utils.js +11 -0
- package/src/hook.js +9 -0
- package/src/install.js +122 -56
- package/src/legacy-omx-wiki.js +222 -0
- package/src/platform.js +178 -0
- package/src/projects.js +8 -0
- package/src/update.js +128 -18
package/src/fs-utils.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from 'fs/promises';
|
|
15
15
|
import { basename, dirname, join, parse, resolve } from 'path';
|
|
16
16
|
import { PROJECT_MARKERS } from './constants.js';
|
|
17
|
+
import { cacheHomeRelative, dataHomeRelative, isWindows } from './platform.js';
|
|
17
18
|
import { normalizeForStorage } from './redaction.js';
|
|
18
19
|
|
|
19
20
|
export function homeDir() {
|
|
@@ -21,13 +22,23 @@ export function homeDir() {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export function dataHome() {
|
|
25
|
+
if (isWindows() && process.env.LOCALAPPDATA) return process.env.LOCALAPPDATA;
|
|
24
26
|
return process.env.XDG_DATA_HOME || join(homeDir(), '.local', 'share');
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export function cacheHome() {
|
|
30
|
+
if (isWindows() && process.env.LOCALAPPDATA) return join(process.env.LOCALAPPDATA, 'llm-wiki-kit', 'cache');
|
|
28
31
|
return process.env.XDG_CACHE_HOME || join(homeDir(), '.cache');
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
export function defaultDataHome(options = {}) {
|
|
35
|
+
return join(homeDir(), dataHomeRelative(options));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function defaultCacheHome(options = {}) {
|
|
39
|
+
return join(homeDir(), cacheHomeRelative(options));
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
export function kitDataDir() {
|
|
32
43
|
return join(dataHome(), 'llm-wiki-kit');
|
|
33
44
|
}
|
package/src/hook.js
CHANGED
|
@@ -7,6 +7,7 @@ import { recordProject } from './projects.js';
|
|
|
7
7
|
import { summarizeForStorage } from './redaction.js';
|
|
8
8
|
import { buildEntryFromState, clearTurnState, rememberQuestion, rememberTool } from './state.js';
|
|
9
9
|
import { updateNoticeContext } from './update-notice.js';
|
|
10
|
+
import { removeLegacyOmxWikiSurfaces } from './legacy-omx-wiki.js';
|
|
10
11
|
import { relative } from 'path';
|
|
11
12
|
|
|
12
13
|
async function readStdinJson() {
|
|
@@ -104,6 +105,13 @@ async function autoUpdateManagedProject(projectRoot, eventName) {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
async function removeLegacyWikiSurfaces(projectRoot, eventName) {
|
|
109
|
+
if (eventName !== 'SessionStart' && eventName !== 'InstructionsLoaded') return;
|
|
110
|
+
const result = await removeLegacyOmxWikiSurfaces();
|
|
111
|
+
if (!result.changed) return;
|
|
112
|
+
await appendWikiLog(projectRoot, `removed legacy oh-my-codex wiki surfaces: ${result.removed.length}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
107
115
|
export async function handleHook(provider, explicitEvent) {
|
|
108
116
|
const payload = await readStdinJson();
|
|
109
117
|
payload.__provider = provider;
|
|
@@ -113,6 +121,7 @@ export async function handleHook(provider, explicitEvent) {
|
|
|
113
121
|
await bootstrapProject(projectRoot);
|
|
114
122
|
await recordProject(projectRoot, 'hook').catch(() => {});
|
|
115
123
|
await autoUpdateManagedProject(projectRoot, eventName).catch(() => {});
|
|
124
|
+
await removeLegacyWikiSurfaces(projectRoot, eventName).catch(() => {});
|
|
116
125
|
await appendSessionEnvelope(projectRoot, eventName, payload).catch(() => {});
|
|
117
126
|
if (eventName === 'SessionStart' || eventName === 'InstructionsLoaded' || eventName === 'UserPromptSubmit') {
|
|
118
127
|
await recoverStaleTurnStates(projectRoot).catch(() => []);
|
package/src/install.js
CHANGED
|
@@ -1,53 +1,39 @@
|
|
|
1
|
-
import { realpathSync } from 'fs';
|
|
2
1
|
import { chmod, lstat, readlink, unlink } from 'fs/promises';
|
|
3
|
-
import { spawnSync } from 'child_process';
|
|
4
2
|
import { join, resolve } from 'path';
|
|
5
|
-
import {
|
|
3
|
+
import { CODEX_EVENTS, KIT_NAME } from './constants.js';
|
|
4
|
+
import { detectClaudeVersion, supportedClaudeEvents } from './claude-compat.js';
|
|
6
5
|
import { backupFile, exists, homeDir, readJson, safeSymlink, writeJson } from './fs-utils.js';
|
|
7
6
|
import { maintenanceSummary } from './maintenance.js';
|
|
7
|
+
import {
|
|
8
|
+
commandForNodeScript,
|
|
9
|
+
commandMatchesRuntime,
|
|
10
|
+
commandPaths as findCommandPaths,
|
|
11
|
+
inspectCommandPath,
|
|
12
|
+
isWindows,
|
|
13
|
+
realpathOrOriginal,
|
|
14
|
+
samePath,
|
|
15
|
+
sameResolvedPath,
|
|
16
|
+
} from './platform.js';
|
|
8
17
|
import { inspectProjectState } from './project-state.js';
|
|
9
18
|
import { bootstrapProject } from './project.js';
|
|
10
19
|
import { recordProject } from './projects.js';
|
|
11
20
|
import { binPath, detectInstallSource, packageRoot, runtimeVersion } from './version.js';
|
|
12
21
|
|
|
13
|
-
function
|
|
14
|
-
return
|
|
22
|
+
export function hookCommand(provider, eventName, options = {}) {
|
|
23
|
+
return commandForNodeScript(binPath, ['hook', provider, eventName], options);
|
|
15
24
|
}
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function llmWikiCommandPaths() {
|
|
22
|
-
const result = spawnSync('sh', ['-lc', 'which -a llm-wiki 2>/dev/null || true'], {
|
|
23
|
-
encoding: 'utf8',
|
|
24
|
-
});
|
|
25
|
-
if (result.error) return [];
|
|
26
|
-
return [...new Set(result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean))];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function realpathOrOriginal(path) {
|
|
30
|
-
if (!path) return null;
|
|
31
|
-
try {
|
|
32
|
-
return realpathSync(path);
|
|
33
|
-
} catch {
|
|
34
|
-
return path;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function sameResolvedPath(left, right) {
|
|
39
|
-
const resolvedLeft = realpathOrOriginal(left);
|
|
40
|
-
const resolvedRight = realpathOrOriginal(right);
|
|
41
|
-
return Boolean(resolvedLeft && resolvedRight && resolvedLeft === resolvedRight);
|
|
26
|
+
function isKitPath(path) {
|
|
27
|
+
return String(path || '').replace(/\\/g, '/').includes('/llm-wiki-kit/');
|
|
42
28
|
}
|
|
43
29
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
return
|
|
30
|
+
function isKitHookEntry(entry) {
|
|
31
|
+
const serialized = normalizeHookPathText(JSON.stringify(entry || {}));
|
|
32
|
+
return serialized.includes(KIT_NAME) || serialized.includes(normalizeHookPathText(binPath)) || serialized.includes('/llm-wiki-kit/');
|
|
47
33
|
}
|
|
48
34
|
|
|
49
|
-
function
|
|
50
|
-
return String(
|
|
35
|
+
function normalizeHookPathText(value) {
|
|
36
|
+
return String(value || '').replace(/\\\\/g, '/').replace(/\\/g, '/');
|
|
51
37
|
}
|
|
52
38
|
|
|
53
39
|
async function inspectLocalBin(localBinPath) {
|
|
@@ -79,7 +65,7 @@ async function inspectLocalBin(localBinPath) {
|
|
|
79
65
|
}
|
|
80
66
|
|
|
81
67
|
async function reconcileLocalBin(localBinPath) {
|
|
82
|
-
const commandPathsBefore =
|
|
68
|
+
const commandPathsBefore = await findCommandPaths('llm-wiki');
|
|
83
69
|
const localBefore = await inspectLocalBin(localBinPath);
|
|
84
70
|
const alternateRuntimeCommand = commandPathsBefore.some((path) => (
|
|
85
71
|
!samePath(path, localBinPath) && sameResolvedPath(path, binPath)
|
|
@@ -108,7 +94,7 @@ async function reconcileLocalBin(localBinPath) {
|
|
|
108
94
|
await safeSymlink(binPath, localBinPath);
|
|
109
95
|
}
|
|
110
96
|
|
|
111
|
-
const commandPathsAfter =
|
|
97
|
+
const commandPathsAfter = await findCommandPaths('llm-wiki');
|
|
112
98
|
const localAfter = await inspectLocalBin(localBinPath);
|
|
113
99
|
return {
|
|
114
100
|
...localAfter,
|
|
@@ -120,6 +106,23 @@ async function reconcileLocalBin(localBinPath) {
|
|
|
120
106
|
};
|
|
121
107
|
}
|
|
122
108
|
|
|
109
|
+
async function reconcileWindowsCommand() {
|
|
110
|
+
const commandPathsBefore = await findCommandPaths('llm-wiki', { platform: 'win32' });
|
|
111
|
+
const commandPath = commandPathsBefore[0] || null;
|
|
112
|
+
const command = await inspectCommandPath(commandPath, binPath, {
|
|
113
|
+
platform: 'win32',
|
|
114
|
+
installSource: detectInstallSource(),
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
...command,
|
|
118
|
+
action: command.matchesRuntime ? 'skipped-windows-npm-shim-available' : 'skipped-windows-no-local-shim',
|
|
119
|
+
alternateRuntimeCommand: command.matchesRuntime,
|
|
120
|
+
commandPathsBefore,
|
|
121
|
+
commandPathsAfter: commandPathsBefore,
|
|
122
|
+
commandPath,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
123
126
|
function addHook(hooks, eventName, command, options = {}) {
|
|
124
127
|
hooks[eventName] = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
|
|
125
128
|
const already = hooks[eventName].some((entry) => (
|
|
@@ -136,6 +139,7 @@ function addHook(hooks, eventName, command, options = {}) {
|
|
|
136
139
|
},
|
|
137
140
|
],
|
|
138
141
|
};
|
|
142
|
+
if (options.commandWindows) entry.hooks[0].commandWindows = options.commandWindows;
|
|
139
143
|
if (options.matcher) entry.matcher = options.matcher;
|
|
140
144
|
hooks[eventName].push(entry);
|
|
141
145
|
return true;
|
|
@@ -145,24 +149,60 @@ function removeKitHooks(hooks) {
|
|
|
145
149
|
let changed = false;
|
|
146
150
|
for (const [eventName, entries] of Object.entries(hooks || {})) {
|
|
147
151
|
if (!Array.isArray(entries)) continue;
|
|
148
|
-
const next = entries.filter((entry) =>
|
|
149
|
-
const serialized = JSON.stringify(entry);
|
|
150
|
-
return !serialized.includes(KIT_NAME) && !serialized.includes(binPath);
|
|
151
|
-
});
|
|
152
|
+
const next = entries.filter((entry) => !isKitHookEntry(entry));
|
|
152
153
|
if (next.length !== entries.length) {
|
|
153
|
-
hooks[eventName] = next;
|
|
154
|
+
if (next.length > 0) hooks[eventName] = next;
|
|
155
|
+
else delete hooks[eventName];
|
|
154
156
|
changed = true;
|
|
155
157
|
}
|
|
156
158
|
}
|
|
157
159
|
return changed;
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
function removeUnsupportedKitClaudeHooks(hooks, supportedEvents) {
|
|
163
|
+
const supported = new Set(supportedEvents);
|
|
164
|
+
const removed = [];
|
|
165
|
+
for (const [eventName, entries] of Object.entries(hooks || {})) {
|
|
166
|
+
if (supported.has(eventName) || !Array.isArray(entries)) continue;
|
|
167
|
+
const next = entries.filter((entry) => !isKitHookEntry(entry));
|
|
168
|
+
if (next.length !== entries.length) {
|
|
169
|
+
removed.push(eventName);
|
|
170
|
+
if (next.length > 0) hooks[eventName] = next;
|
|
171
|
+
else delete hooks[eventName];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return [...new Set(removed)];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function unsupportedKitClaudeEvents(hooks, supportedEvents) {
|
|
178
|
+
const supported = new Set(supportedEvents);
|
|
179
|
+
return Object.entries(hooks || {})
|
|
180
|
+
.filter(([eventName, entries]) => (
|
|
181
|
+
!supported.has(eventName) &&
|
|
182
|
+
Array.isArray(entries) &&
|
|
183
|
+
entries.some((entry) => isKitHookEntry(entry))
|
|
184
|
+
))
|
|
185
|
+
.map(([eventName]) => eventName)
|
|
186
|
+
.sort();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function hookTextIncludes(text, value) {
|
|
190
|
+
return normalizeHookPathText(text).includes(normalizeHookPathText(value));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function hookEventIncludesBin(entries) {
|
|
194
|
+
return hookTextIncludes(JSON.stringify(entries || {}), binPath);
|
|
195
|
+
}
|
|
196
|
+
|
|
160
197
|
export async function install(options = {}) {
|
|
161
198
|
const workspace = resolve(options.workspace || process.cwd());
|
|
162
199
|
await chmod(binPath, 0o755).catch(() => {});
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
const
|
|
200
|
+
const platform = options.platform || process.platform;
|
|
201
|
+
const localBin = isWindows({ platform }) ? null : join(homeDir(), '.local', 'bin');
|
|
202
|
+
const localBinPath = localBin ? join(localBin, 'llm-wiki') : null;
|
|
203
|
+
const localBinResult = isWindows({ platform })
|
|
204
|
+
? await reconcileWindowsCommand()
|
|
205
|
+
: await reconcileLocalBin(localBinPath);
|
|
166
206
|
if (!options.noProject) {
|
|
167
207
|
await bootstrapProject(workspace, { profile: options.profile || 'standard', recordState: true });
|
|
168
208
|
await recordProject(workspace, 'install');
|
|
@@ -182,7 +222,9 @@ export async function install(options = {}) {
|
|
|
182
222
|
}
|
|
183
223
|
for (const eventName of CODEX_EVENTS) {
|
|
184
224
|
const matcher = eventName === 'SessionStart' ? 'startup|resume|clear' : undefined;
|
|
185
|
-
|
|
225
|
+
const command = hookCommand('codex', eventName, { platform });
|
|
226
|
+
const commandWindows = isWindows({ platform }) ? hookCommand('codex', eventName, { platform: 'win32' }) : undefined;
|
|
227
|
+
if (addHook(codex.hooks, eventName, command, { matcher, commandWindows })) {
|
|
186
228
|
codexChanged = true;
|
|
187
229
|
changed.push(`codex:${eventName}`);
|
|
188
230
|
}
|
|
@@ -196,14 +238,21 @@ export async function install(options = {}) {
|
|
|
196
238
|
if (options.claude !== false) {
|
|
197
239
|
const claude = (await readJson(claudeSettingsPath, null)) || {};
|
|
198
240
|
claude.hooks = claude.hooks || {};
|
|
241
|
+
const claudeDetection = detectClaudeVersion();
|
|
242
|
+
const claudeEvents = supportedClaudeEvents(claudeDetection);
|
|
199
243
|
let claudeChanged = false;
|
|
200
244
|
if (options.replaceHooks && removeKitHooks(claude.hooks)) {
|
|
201
245
|
claudeChanged = true;
|
|
202
246
|
changed.push('claude:replaced');
|
|
203
247
|
}
|
|
204
|
-
|
|
248
|
+
const removedUnsupported = removeUnsupportedKitClaudeHooks(claude.hooks, claudeEvents);
|
|
249
|
+
if (removedUnsupported.length > 0) {
|
|
250
|
+
claudeChanged = true;
|
|
251
|
+
changed.push(...removedUnsupported.map((eventName) => `claude:removed-unsupported:${eventName}`));
|
|
252
|
+
}
|
|
253
|
+
for (const eventName of claudeEvents) {
|
|
205
254
|
const matcher = eventName === 'SessionStart' ? 'startup|resume|clear' : undefined;
|
|
206
|
-
if (addHook(claude.hooks, eventName, hookCommand('claude', eventName), { matcher })) {
|
|
255
|
+
if (addHook(claude.hooks, eventName, hookCommand('claude', eventName, { platform }), { matcher })) {
|
|
207
256
|
claudeChanged = true;
|
|
208
257
|
changed.push(`claude:${eventName}`);
|
|
209
258
|
}
|
|
@@ -256,30 +305,47 @@ export async function uninstall(options = {}) {
|
|
|
256
305
|
|
|
257
306
|
export async function status(options = {}) {
|
|
258
307
|
const workspace = resolve(options.workspace || process.cwd());
|
|
308
|
+
const platform = options.platform || process.platform;
|
|
259
309
|
const codexHooksPath = join(homeDir(), '.codex', 'hooks.json');
|
|
260
310
|
const claudeSettingsPath = join(homeDir(), '.claude', 'settings.json');
|
|
261
311
|
const codex = await readJson(codexHooksPath, {});
|
|
262
312
|
const claude = await readJson(claudeSettingsPath, {});
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
const
|
|
313
|
+
const claudeDetection = detectClaudeVersion();
|
|
314
|
+
const claudeEvents = supportedClaudeEvents(claudeDetection);
|
|
315
|
+
const claudeMissingEvents = claudeEvents.filter((eventName) => !hookEventIncludesBin(claude.hooks?.[eventName] || []));
|
|
316
|
+
const claudeUnsupportedKitEvents = unsupportedKitClaudeEvents(claude.hooks || {}, claudeEvents);
|
|
317
|
+
const codexInstalled = hookEventIncludesBin(codex.hooks || {});
|
|
318
|
+
const claudeInstalled = claudeMissingEvents.length === 0 && claudeUnsupportedKitEvents.length === 0;
|
|
319
|
+
const discoveredCommandPaths = await findCommandPaths('llm-wiki', { platform });
|
|
320
|
+
const commandPath = discoveredCommandPaths[0] || null;
|
|
267
321
|
const resolvedCommandPath = realpathOrOriginal(commandPath);
|
|
268
322
|
const resolvedBinPath = realpathOrOriginal(binPath);
|
|
269
|
-
const localBinPath = join(homeDir(), '.local', 'bin', 'llm-wiki');
|
|
270
|
-
const localBin =
|
|
323
|
+
const localBinPath = isWindows({ platform }) ? null : join(homeDir(), '.local', 'bin', 'llm-wiki');
|
|
324
|
+
const localBin = localBinPath
|
|
325
|
+
? await inspectLocalBin(localBinPath)
|
|
326
|
+
: await inspectCommandPath(commandPath, binPath, { platform, installSource: detectInstallSource() });
|
|
327
|
+
const commandMatches = await commandMatchesRuntime(commandPath, binPath, {
|
|
328
|
+
platform,
|
|
329
|
+
installSource: detectInstallSource(),
|
|
330
|
+
});
|
|
271
331
|
return {
|
|
272
332
|
workspace,
|
|
333
|
+
platform,
|
|
273
334
|
runtimeVersion: runtimeVersion(),
|
|
274
335
|
packageRoot,
|
|
275
336
|
installSource: detectInstallSource(),
|
|
276
337
|
binPath,
|
|
277
338
|
commandPath,
|
|
278
|
-
commandPaths,
|
|
279
|
-
commandMatchesRuntime: Boolean(resolvedCommandPath && resolvedBinPath && resolvedCommandPath === resolvedBinPath),
|
|
339
|
+
commandPaths: discoveredCommandPaths,
|
|
340
|
+
commandMatchesRuntime: Boolean(commandMatches || (resolvedCommandPath && resolvedBinPath && resolvedCommandPath === resolvedBinPath)),
|
|
280
341
|
localBin,
|
|
281
342
|
codexInstalled,
|
|
282
343
|
claudeInstalled,
|
|
344
|
+
claudeVersion: claudeDetection.version || 'unknown',
|
|
345
|
+
claudeModernHooks: claudeDetection.modern,
|
|
346
|
+
claudeSupportedEvents: claudeEvents,
|
|
347
|
+
claudeMissingEvents,
|
|
348
|
+
claudeUnsupportedKitEvents,
|
|
283
349
|
hooksCurrent: codexInstalled && claudeInstalled,
|
|
284
350
|
codexHooksPath,
|
|
285
351
|
claudeSettingsPath,
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { mkdtemp, rm, readdir, writeFile } from 'fs/promises';
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { backupFile, exists, homeDir, readJson, readText, writeJson, writeText } from './fs-utils.js';
|
|
6
|
+
|
|
7
|
+
const CODEX_CONFIG = '.codex/config.toml';
|
|
8
|
+
const TOML_TABLE = /^\s*\[([^\]]+)\]\s*$/;
|
|
9
|
+
|
|
10
|
+
function normalizeTomlHeader(header) {
|
|
11
|
+
return String(header || '').replace(/\s+/g, '').replace(/'/g, '"');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isLegacyOmxWikiTable(header) {
|
|
15
|
+
const normalized = normalizeTomlHeader(header);
|
|
16
|
+
if (normalized === 'mcp_servers.omx_wiki') return true;
|
|
17
|
+
return normalized.includes('oh-my-codex') &&
|
|
18
|
+
normalized.includes('.mcp_servers.omx_wiki');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function removeLegacyOmxWikiTomlTables(text) {
|
|
22
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
23
|
+
const output = [];
|
|
24
|
+
let changed = false;
|
|
25
|
+
let skipping = false;
|
|
26
|
+
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
const table = line.match(TOML_TABLE);
|
|
29
|
+
if (table) {
|
|
30
|
+
skipping = isLegacyOmxWikiTable(table[1]);
|
|
31
|
+
if (skipping) {
|
|
32
|
+
changed = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!skipping) output.push(line);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!changed) return { changed: false, text: String(text || '') };
|
|
40
|
+
return {
|
|
41
|
+
changed: true,
|
|
42
|
+
text: `${output.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\s+$/g, '')}\n`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sourceRootsFromConfig(text) {
|
|
47
|
+
const roots = new Set();
|
|
48
|
+
const sourceRe = /^\s*source\s*=\s*["']([^"']*oh-my-codex[^"']*)["']\s*$/gm;
|
|
49
|
+
let match;
|
|
50
|
+
while ((match = sourceRe.exec(String(text || '')))) {
|
|
51
|
+
roots.add(match[1]);
|
|
52
|
+
}
|
|
53
|
+
return roots;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function configuredPackageRoots(home, configText) {
|
|
57
|
+
const roots = new Set([
|
|
58
|
+
'/usr/lib/node_modules/oh-my-codex',
|
|
59
|
+
'/usr/local/lib/node_modules/oh-my-codex',
|
|
60
|
+
join(home, '.local', 'lib', 'node_modules', 'oh-my-codex'),
|
|
61
|
+
]);
|
|
62
|
+
for (const root of sourceRootsFromConfig(configText)) roots.add(root);
|
|
63
|
+
for (const root of String(process.env.LLM_WIKI_KIT_OMX_WIKI_ROOTS || '').split(':').filter(Boolean)) {
|
|
64
|
+
roots.add(root);
|
|
65
|
+
}
|
|
66
|
+
return roots;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function safeLegacyWikiSkillDir(path) {
|
|
70
|
+
const normalized = resolve(path).replace(/\\/g, '/');
|
|
71
|
+
return /(?:^|\/)oh-my-codex(?:\/|$)/.test(normalized) &&
|
|
72
|
+
/\/skills\/wiki$/.test(normalized);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function safeLegacyWikiMcpPath(path) {
|
|
76
|
+
const normalized = resolve(path).replace(/\\/g, '/');
|
|
77
|
+
return /(?:^|\/)oh-my-codex(?:\/|$)/.test(normalized) &&
|
|
78
|
+
/\/\.mcp\.json$/.test(normalized);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function collectCachedSkillDirs(root, maxDepth = 8) {
|
|
82
|
+
const found = [];
|
|
83
|
+
async function walk(dir, depth) {
|
|
84
|
+
if (depth < 0) return;
|
|
85
|
+
let entries = [];
|
|
86
|
+
try {
|
|
87
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
88
|
+
} catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (!entry.isDirectory()) continue;
|
|
93
|
+
const full = join(dir, entry.name);
|
|
94
|
+
const normalized = full.replace(/\\/g, '/');
|
|
95
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
96
|
+
if (entry.name === 'wiki' && normalized.includes('/oh-my-codex/') && normalized.includes('/skills/wiki')) {
|
|
97
|
+
found.push(full);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
await walk(full, depth - 1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
await walk(root, maxDepth);
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function legacyWikiSkillDirs(home, configText) {
|
|
108
|
+
const dirs = new Set();
|
|
109
|
+
for (const root of configuredPackageRoots(home, configText)) {
|
|
110
|
+
dirs.add(join(root, 'skills', 'wiki'));
|
|
111
|
+
dirs.add(join(root, 'plugins', 'oh-my-codex', 'skills', 'wiki'));
|
|
112
|
+
}
|
|
113
|
+
for (const root of [
|
|
114
|
+
join(home, '.codex', 'plugins', 'cache'),
|
|
115
|
+
join(home, '.codex', '.tmp'),
|
|
116
|
+
]) {
|
|
117
|
+
for (const dir of await collectCachedSkillDirs(root)) dirs.add(dir);
|
|
118
|
+
}
|
|
119
|
+
return [...dirs].filter(safeLegacyWikiSkillDir);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function removeSkillDir(dir) {
|
|
123
|
+
if (!(await exists(dir))) return false;
|
|
124
|
+
try {
|
|
125
|
+
await rm(dir, { recursive: true, force: true });
|
|
126
|
+
return true;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (!['EACCES', 'EPERM'].includes(error?.code)) throw error;
|
|
129
|
+
}
|
|
130
|
+
if (process.env.LLM_WIKI_KIT_OMX_WIKI_SUDO === '0') return false;
|
|
131
|
+
const result = spawnSync('sudo', ['-n', 'rm', '-rf', dir], {
|
|
132
|
+
encoding: 'utf8',
|
|
133
|
+
timeout: 5000,
|
|
134
|
+
});
|
|
135
|
+
return result.status === 0 && !(await exists(dir));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function removePluginMcpEntry(path) {
|
|
139
|
+
const json = await readJson(path, null);
|
|
140
|
+
if (!json?.mcpServers?.omx_wiki) return false;
|
|
141
|
+
delete json.mcpServers.omx_wiki;
|
|
142
|
+
try {
|
|
143
|
+
await writeJson(path, json);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (!['EACCES', 'EPERM'].includes(error?.code) || !safeLegacyWikiMcpPath(path)) throw error;
|
|
146
|
+
if (process.env.LLM_WIKI_KIT_OMX_WIKI_SUDO === '0') return false;
|
|
147
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'llm-wiki-kit-mcp-'));
|
|
148
|
+
const tempPath = join(tempDir, '.mcp.json');
|
|
149
|
+
await writeFile(tempPath, `${JSON.stringify(json, null, 2)}\n`, 'utf8');
|
|
150
|
+
const result = spawnSync('sudo', ['-n', 'cp', tempPath, path], {
|
|
151
|
+
encoding: 'utf8',
|
|
152
|
+
timeout: 5000,
|
|
153
|
+
});
|
|
154
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
155
|
+
if (result.status !== 0) return false;
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function removeWritablePluginMcpEntries(home, configText) {
|
|
161
|
+
const changed = [];
|
|
162
|
+
const candidates = new Set();
|
|
163
|
+
for (const root of configuredPackageRoots(home, configText)) {
|
|
164
|
+
candidates.add(join(root, 'plugins', 'oh-my-codex', '.mcp.json'));
|
|
165
|
+
}
|
|
166
|
+
for (const root of [
|
|
167
|
+
join(home, '.codex', 'plugins', 'cache'),
|
|
168
|
+
join(home, '.codex', '.tmp'),
|
|
169
|
+
]) {
|
|
170
|
+
const dirs = await collectCachedSkillDirs(root);
|
|
171
|
+
for (const dir of dirs) {
|
|
172
|
+
candidates.add(resolve(dir, '..', '..', '.mcp.json'));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const candidate of candidates) {
|
|
176
|
+
try {
|
|
177
|
+
if (await removePluginMcpEntry(candidate)) changed.push(candidate);
|
|
178
|
+
} catch {
|
|
179
|
+
// Root-owned package manifests may be read-only to the hook user. The
|
|
180
|
+
// active Codex config cleanup is the enforcement point in that case.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return changed;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function removeLegacyOmxWikiSurfaces(options = {}) {
|
|
187
|
+
if (process.env.LLM_WIKI_KIT_OMX_WIKI_GUARD === '0') {
|
|
188
|
+
return { changed: false, removed: [], configChanged: false };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const home = options.home || homeDir();
|
|
192
|
+
const configPath = options.configPath || join(home, CODEX_CONFIG);
|
|
193
|
+
const originalConfig = await readText(configPath, null);
|
|
194
|
+
const removed = [];
|
|
195
|
+
let configText = originalConfig || '';
|
|
196
|
+
let configChanged = false;
|
|
197
|
+
|
|
198
|
+
if (originalConfig !== null) {
|
|
199
|
+
const cleaned = removeLegacyOmxWikiTomlTables(originalConfig);
|
|
200
|
+
if (cleaned.changed) {
|
|
201
|
+
await backupFile(configPath, 'codex-config-before-omx-wiki-removal.toml');
|
|
202
|
+
await writeText(configPath, cleaned.text);
|
|
203
|
+
configText = cleaned.text;
|
|
204
|
+
configChanged = true;
|
|
205
|
+
removed.push('codex-config:omx_wiki');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const dir of await legacyWikiSkillDirs(home, configText)) {
|
|
210
|
+
if (await removeSkillDir(dir)) removed.push(`skill:${dir}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const path of await removeWritablePluginMcpEntries(home, configText)) {
|
|
214
|
+
removed.push(`mcp:${path}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
changed: removed.length > 0,
|
|
219
|
+
removed,
|
|
220
|
+
configChanged,
|
|
221
|
+
};
|
|
222
|
+
}
|