llm-wiki-kit 0.2.3 → 0.2.4
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 +4 -1
- package/docs/integrations/claude-code.md +3 -1
- package/docs/integrations/codex.md +1 -1
- package/docs/manual.md +10 -1
- package/docs/operations.md +4 -1
- package/docs/troubleshooting.md +29 -0
- package/package.json +1 -1
- package/src/claude-compat.js +83 -0
- package/src/cli.js +31 -4
- package/src/doctor.js +7 -1
- package/src/hook.js +9 -0
- package/src/install.js +55 -8
- package/src/legacy-omx-wiki.js +222 -0
- package/src/projects.js +8 -0
- package/src/update.js +128 -18
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ Use Claude Code or Codex normally.
|
|
|
73
73
|
The installed hooks:
|
|
74
74
|
|
|
75
75
|
- inject `wiki/memory.md`, `wiki/index.md`, and relevant wiki context at session start, instructions loaded, prompt submit, and post-compact time
|
|
76
|
+
- remove Codex-facing legacy `oh-my-codex:wiki`/`omx_wiki` surfaces at session start so `llm-wiki/` remains the active wiki implementation
|
|
76
77
|
- record small redacted raw event envelopes and per-turn state
|
|
77
78
|
- capture decision points, debugging findings, changed files, and verification notes
|
|
78
79
|
- allow tool calls to proceed without secret/PII-based hook blocking
|
|
@@ -102,7 +103,7 @@ Most users should not need these during daily Claude Code/Codex work. They exist
|
|
|
102
103
|
|
|
103
104
|
`llm-wiki update --check [--to <version-or-tag>]` is the online update check. It compares the installed package version with the npm registry target without changing files, and reports an available update only when the target version is newer than the installed version.
|
|
104
105
|
|
|
105
|
-
`llm-wiki update` upgrades the global npm package when npm has a newer target, reinstalls the hook entries, and reapplies safe managed template updates across known project roots. If the installed runtime already satisfies the registry target, it skips `npm install -g` and still runs post-update maintenance. Use `--current-only` when you intentionally want to update only the supplied workspace. Existing wiki content is not overwritten.
|
|
106
|
+
`llm-wiki update` upgrades the global npm package when npm has a newer target, reinstalls the hook entries, and reapplies safe managed template updates across known project roots. If the installed runtime already satisfies the registry target, it skips `npm install -g` and still runs post-update maintenance. Use `--current-only` when you intentionally want to update only the supplied workspace. Existing wiki content is not overwritten. The command prints step progress to stderr, including registry lookup, npm install, post-update, and project discovery. Use `--timeout-ms <ms>` to bound external commands and `--max-dirs <n>` to bound project discovery under large or slow roots such as WSL `/mnt/*` trees.
|
|
106
107
|
|
|
107
108
|
Installed npm runtimes also perform a cached update notice check from hooks while the user works. This does not install anything automatically. When a newer npm release is detected, the injected hook context gives the active agent a Korean update reminder and tells it to offer `llm-wiki update --workspace <project-or-search-root>`. The cache is scoped to the npm command used for lookup so test/fake npm checks do not leak into normal hook sessions. Set `LLM_WIKI_KIT_UPDATE_NOTICE=0` only when diagnosing or suppressing that reminder.
|
|
108
109
|
|
|
@@ -118,6 +119,8 @@ Installed npm runtimes also perform a cached update notice check from hooks whil
|
|
|
118
119
|
|
|
119
120
|
`llm-wiki projects --workspace /apps` lists project roots that already have `llm-wiki-kit` state or an older `llm-wiki/wiki/index.md`, and shows the update commands to run. `llm-wiki update --workspace /apps` updates the global runtime once, then reapplies managed templates across every known or discovered project root under `/apps`.
|
|
120
121
|
|
|
122
|
+
For `llm-wiki-kit` code releases, source tests are not enough. After code changes, publish the package, install the newly published version, and verify the installed CLI and hooks with `version`, `status`, `doctor`, `update`, `lint`, and hook smoke checks before calling the release complete.
|
|
123
|
+
|
|
121
124
|
After a plain `npm install -g llm-wiki-kit@latest`, existing hooks keep working when they already point at the global npm package path. The next `SessionStart`/`InstructionsLoaded` hook automatically reapplies safe managed template updates for the active project root. Clearly generated older `llm-wiki/AGENTS.md` and procedure files are refreshed even when old state is missing; user-edited files are not overwritten and are surfaced to the active agent as cleanup work. If hooks point at a source checkout or stale shim, run `llm-wiki post-update --workspace <project>` or `llm-wiki install --workspace <project>` once to reconnect them.
|
|
122
125
|
|
|
123
126
|
`llm-wiki install` no longer creates a user-local `~/.local/bin/llm-wiki` shim when an npm/nvm global `llm-wiki` command already resolves to the current runtime. If an older kit-managed local shim is shadowing that npm command, install backs it up and removes it.
|
|
@@ -16,7 +16,7 @@ The installed hook stores the Node executable and the absolute package binary pa
|
|
|
16
16
|
|
|
17
17
|
User-local fallback installs may instead point under `$HOME/.local/lib/node_modules`.
|
|
18
18
|
|
|
19
|
-
Handled events:
|
|
19
|
+
Handled events on compatible Claude Code versions:
|
|
20
20
|
|
|
21
21
|
- `SessionStart`
|
|
22
22
|
- `InstructionsLoaded`
|
|
@@ -30,6 +30,8 @@ Handled events:
|
|
|
30
30
|
- `Stop`
|
|
31
31
|
- `SessionEnd`
|
|
32
32
|
|
|
33
|
+
`llm-wiki install` detects `claude --version` and installs a conservative event set when the local Claude Code binary is older or cannot be detected. `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PreCompact`, `PostCompact`, `SubagentStop`, and `Stop` are treated as the stable set. `InstructionsLoaded`, `PostToolBatch`, and `SessionEnd` are installed only for newer compatible versions, or when `LLM_WIKI_KIT_CLAUDE_MODERN_HOOKS=1` is set. Existing kit-managed hooks for unsupported event keys are removed during install so Claude Code does not skip the entire settings file with an “Invalid key in record” error.
|
|
34
|
+
|
|
33
35
|
Claude Code reads `CLAUDE.md`. For project compatibility, the kit creates a `CLAUDE.md` stub containing:
|
|
34
36
|
|
|
35
37
|
```text
|
|
@@ -29,7 +29,7 @@ Handled events:
|
|
|
29
29
|
|
|
30
30
|
Expected behavior:
|
|
31
31
|
|
|
32
|
-
- `SessionStart` first attempts a safe managed-template refresh, recovers stale turn state into `outputs/maintenance/queue.md`, performs a cached npm update notice check for npm installs, then injects `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, recent log context, operating rules, a one-item maintenance summary when needed, any Korean update notice, and any maintenance note for outdated or customized managed rules.
|
|
32
|
+
- `SessionStart` first attempts a safe managed-template refresh, removes Codex-facing legacy `oh-my-codex:wiki`/`omx_wiki` surfaces when they reappear, recovers stale turn state into `outputs/maintenance/queue.md`, performs a cached npm update notice check for npm installs, then injects `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, recent log context, operating rules, a one-item maintenance summary when needed, any Korean update notice, and any maintenance note for outdated or customized managed rules.
|
|
33
33
|
- `UserPromptSubmit` recovers stale turn state, searches project wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, performs the same cached update notice check, and injects the smallest useful context set. Update notice cache is scoped by npm command, and maintenance reminders are shown only when the prompt is wiki/maintenance related or matches a queue topic.
|
|
34
34
|
- `PreToolUse` records redacted tool summaries without blocking tool calls.
|
|
35
35
|
- `PostToolUse` records redacted tool summaries in a turn buffer.
|
package/docs/manual.md
CHANGED
|
@@ -69,6 +69,7 @@ llm-wiki/
|
|
|
69
69
|
- 사용자가 명시적으로 문서화/기록을 요청했는데 durable wiki 반영이 없으면 `outputs/maintenance/queue.md`에 정리 후보를 남긴다.
|
|
70
70
|
- 이전 session이 깨끗하게 종료되지 않았을 때 stale turn state를 maintenance queue로 복구할 수 있다.
|
|
71
71
|
- 새 runtime이 오래된 project rules/templates를 발견하면 안전하게 갱신 가능한 managed file만 refresh한다.
|
|
72
|
+
- `SessionStart`/`InstructionsLoaded`에서 Codex-facing legacy `oh-my-codex:wiki`/`omx_wiki` 설정과 스킬 표면이 되살아났는지 확인하고 제거한다. wiki 기능은 `llm-wiki/`가 단일 활성 구현이다.
|
|
72
73
|
|
|
73
74
|
매번 저장 여부를 사용자가 고민해야 한다면 잘못 쓰고 있는 것이다. 답변보다 wiki maintenance가 먼저 오면 안 된다. 현재 사용자 요청을 먼저 처리하고, 오래 쓸 지식만 필요한 만큼 정리한다.
|
|
74
75
|
|
|
@@ -141,11 +142,14 @@ llm-wiki update --workspace /path/to/search-root
|
|
|
141
142
|
llm-wiki update --workspace /path/to/search-root --to 0.2.1
|
|
142
143
|
llm-wiki update --workspace /path/to/project --current-only
|
|
143
144
|
llm-wiki update --workspace /path/to/project --dry-run
|
|
145
|
+
llm-wiki update --workspace /path/to/search-root --timeout-ms 120000 --max-dirs 5000
|
|
144
146
|
```
|
|
145
147
|
|
|
146
148
|
`--check`는 network를 사용하지만 파일을 변경하지 않는다. installed version보다 registry target이 최신일 때만 update available을 보고한다.
|
|
147
149
|
|
|
148
|
-
기본 `update --workspace <search-root>`는 runtime update를 한 번 수행한 뒤, known/discovered project roots의 managed templates를 재적용한다. 기존 wiki contents는 덮어쓰지 않는다.
|
|
150
|
+
기본 `update --workspace <search-root>`는 runtime update를 한 번 수행한 뒤, known/discovered project roots의 managed templates를 재적용한다. 기존 wiki contents는 덮어쓰지 않는다. `update`는 registry lookup, npm install, post-update, project discovery 진행 상태를 stderr에 출력한다. JSON 모드에서도 stdout은 JSON만 유지하고 progress는 stderr로 분리한다.
|
|
151
|
+
|
|
152
|
+
`--timeout-ms`는 `npm view`, `npm install -g`, internal `post-update` 같은 외부 command timeout을 지정한다. timeout 후 SIGTERM과 짧은 grace period 뒤 SIGKILL을 사용해 child process가 terminal을 계속 붙잡는 hard hang을 방지한다. `--max-dirs`는 search root 아래 project discovery가 검사할 최대 directory 수를 제한한다.
|
|
149
153
|
|
|
150
154
|
`update`는 source checkout에서 self-update하지 않는다. source checkout 개발 중에는 npm package나 local tarball로 global install한 뒤 update behavior를 테스트한다.
|
|
151
155
|
|
|
@@ -416,12 +420,17 @@ publish 후 검증:
|
|
|
416
420
|
|
|
417
421
|
```bash
|
|
418
422
|
npm view llm-wiki-kit version --registry=https://registry.npmjs.org/
|
|
423
|
+
npm install -g llm-wiki-kit@<published-version> --registry=https://registry.npmjs.org/ --prefer-online
|
|
419
424
|
llm-wiki version
|
|
420
425
|
llm-wiki manual
|
|
421
426
|
llm-wiki status --workspace /apps
|
|
422
427
|
llm-wiki doctor --workspace /apps
|
|
428
|
+
llm-wiki update --workspace /apps --dry-run
|
|
429
|
+
llm-wiki update --workspace /apps --current-only
|
|
423
430
|
```
|
|
424
431
|
|
|
432
|
+
`llm-wiki-kit` 코드 변경 릴리스는 publish와 새 published version 설치 후 검증까지 끝나야 완료로 본다. root-owned global npm prefix에서는 package install에 `sudo npm install -g`가 필요할 수 있고, user home hook 갱신은 일반 user로 `llm-wiki install` 또는 `post-update`를 실행한다.
|
|
433
|
+
|
|
425
434
|
## Troubleshooting Shortcuts
|
|
426
435
|
|
|
427
436
|
hook이 안 돈다면:
|
package/docs/operations.md
CHANGED
|
@@ -81,6 +81,7 @@ llm-wiki update --check --workspace /path/to/project [--to <version-or-tag>]
|
|
|
81
81
|
llm-wiki update --workspace /path/to/search-root
|
|
82
82
|
llm-wiki update --current-only --workspace /path/to/project
|
|
83
83
|
llm-wiki update --dry-run --workspace /path/to/project
|
|
84
|
+
llm-wiki update --workspace /path/to/search-root --timeout-ms 120000 --max-dirs 5000
|
|
84
85
|
llm-wiki post-update --workspace /path/to/project
|
|
85
86
|
llm-wiki post-update --all --workspace /path/to/search-root
|
|
86
87
|
llm-wiki context "search phrase" --workspace /path/to/project
|
|
@@ -113,8 +114,10 @@ Installed npm runtimes also run a cached hook-side update notice check while the
|
|
|
113
114
|
- reinstalls hook entries without duplicating them
|
|
114
115
|
- patches only managed project files across known or discovered project roots
|
|
115
116
|
- backs up changed files under `~/.local/share/llm-wiki-kit/backups/`
|
|
117
|
+
- prints update progress to stderr and keeps JSON output parseable on stdout
|
|
118
|
+
- force-kills unresponsive child commands after timeout so slow npm/WSL environments do not wait forever
|
|
116
119
|
|
|
117
|
-
By default, `update --workspace <search-root>` performs the npm runtime update once, then reapplies managed templates to every known or discovered project root under the search root. Use `update --current-only --workspace <project>` to limit template reapplication to one project.
|
|
120
|
+
By default, `update --workspace <search-root>` performs the npm runtime update once, then reapplies managed templates to every known or discovered project root under the search root. Use `update --current-only --workspace <project>` to limit template reapplication to one project. Use `--max-dirs` when a search root is a very large or slow tree.
|
|
118
121
|
|
|
119
122
|
`post-update --workspace <project>` skips npm installation and reapplies the current runtime's hook entries plus safe managed template updates. `post-update --all --workspace <search-root>` does the same template reapplication across discovered project roots.
|
|
120
123
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -36,6 +36,35 @@ llm-wiki update --check --workspace /path/to/project
|
|
|
36
36
|
|
|
37
37
|
`status` does not contact the network. `update --check` contacts npm but does not change files.
|
|
38
38
|
|
|
39
|
+
## llm-wiki update Appears To Hang
|
|
40
|
+
|
|
41
|
+
`update` runs `npm view`, optional `npm install -g`, project discovery, and `post-update`. It prints progress to stderr while keeping JSON output on stdout parseable. In slow WSL or Windows-mounted trees, start with a bounded dry run:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
llm-wiki update --dry-run --workspace /path/to/project --current-only --timeout-ms 30000
|
|
45
|
+
llm-wiki update --dry-run --workspace /path/to/search-root --max-dirs 1000 --timeout-ms 30000
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If the current project succeeds but the search root is slow, use `--current-only` for that project or raise `--max-dirs` intentionally. If npm itself is slow, compare with:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm view llm-wiki-kit version --registry=https://registry.npmjs.org/
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
When a child command exceeds `--timeout-ms`, `update` terminates it and then force-kills it after a short grace period.
|
|
55
|
+
|
|
56
|
+
## Claude Code Settings Error: Invalid Hook Key
|
|
57
|
+
|
|
58
|
+
Some Claude Code versions reject newer hook event keys such as `PostToolBatch`. Run:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
claude --version
|
|
62
|
+
llm-wiki install --workspace /path/to/project --profile standard
|
|
63
|
+
llm-wiki doctor --workspace /path/to/project
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The installer detects the local Claude version, installs only compatible hook events, and removes kit-managed hooks for unsupported event keys. User-defined hooks are preserved when possible.
|
|
67
|
+
|
|
39
68
|
## npm Says The Package Is Not Found
|
|
40
69
|
|
|
41
70
|
If `llm-wiki update --check` returns npm 404, the package is not published to the registry being used. Until publish, test npm installation with a local tarball:
|
package/package.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export const CLAUDE_STABLE_EVENTS = [
|
|
4
|
+
'SessionStart',
|
|
5
|
+
'UserPromptSubmit',
|
|
6
|
+
'PreToolUse',
|
|
7
|
+
'PostToolUse',
|
|
8
|
+
'PreCompact',
|
|
9
|
+
'PostCompact',
|
|
10
|
+
'SubagentStop',
|
|
11
|
+
'Stop',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const CLAUDE_MODERN_EVENTS = [
|
|
15
|
+
'InstructionsLoaded',
|
|
16
|
+
'PostToolBatch',
|
|
17
|
+
'SessionEnd',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export const CLAUDE_MODERN_MIN_VERSION = '2.1.138';
|
|
21
|
+
|
|
22
|
+
function parseVersion(value) {
|
|
23
|
+
return String(value || '')
|
|
24
|
+
.replace(/^v/, '')
|
|
25
|
+
.split('.', 3)
|
|
26
|
+
.map((part) => {
|
|
27
|
+
const parsed = Number.parseInt(part, 10);
|
|
28
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function compareClaudeVersions(a, b) {
|
|
33
|
+
const left = parseVersion(a);
|
|
34
|
+
const right = parseVersion(b);
|
|
35
|
+
for (let i = 0; i < 3; i += 1) {
|
|
36
|
+
const diff = (left[i] || 0) - (right[i] || 0);
|
|
37
|
+
if (diff !== 0) return diff > 0 ? 1 : -1;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseClaudeVersionOutput(output) {
|
|
43
|
+
return String(output || '').match(/\b(\d+\.\d+\.\d+)\b/)?.[1] || '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function detectClaudeVersion(options = {}) {
|
|
47
|
+
if (process.env.LLM_WIKI_KIT_CLAUDE_VERSION) {
|
|
48
|
+
const version = process.env.LLM_WIKI_KIT_CLAUDE_VERSION;
|
|
49
|
+
return {
|
|
50
|
+
available: true,
|
|
51
|
+
version,
|
|
52
|
+
raw: version,
|
|
53
|
+
modern: compareClaudeVersions(version, CLAUDE_MODERN_MIN_VERSION) >= 0,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const command = options.command || process.env.LLM_WIKI_KIT_CLAUDE || 'claude';
|
|
58
|
+
const result = spawnSync(command, ['--version'], {
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
timeout: options.timeout || 5000,
|
|
61
|
+
});
|
|
62
|
+
const raw = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
|
63
|
+
const version = parseClaudeVersionOutput(raw);
|
|
64
|
+
return {
|
|
65
|
+
available: result.status === 0 && !result.error,
|
|
66
|
+
version,
|
|
67
|
+
raw,
|
|
68
|
+
modern: Boolean(version) && compareClaudeVersions(version, CLAUDE_MODERN_MIN_VERSION) >= 0,
|
|
69
|
+
error: result.error?.message || null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function supportedClaudeEvents(detection = detectClaudeVersion()) {
|
|
74
|
+
if (process.env.LLM_WIKI_KIT_CLAUDE_MODERN_HOOKS === '1') {
|
|
75
|
+
return [...CLAUDE_STABLE_EVENTS, ...CLAUDE_MODERN_EVENTS];
|
|
76
|
+
}
|
|
77
|
+
if (process.env.LLM_WIKI_KIT_CLAUDE_MODERN_HOOKS === '0') {
|
|
78
|
+
return [...CLAUDE_STABLE_EVENTS];
|
|
79
|
+
}
|
|
80
|
+
return detection.modern
|
|
81
|
+
? [...CLAUDE_STABLE_EVENTS, ...CLAUDE_MODERN_EVENTS]
|
|
82
|
+
: [...CLAUDE_STABLE_EVENTS];
|
|
83
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -32,6 +32,22 @@ function parseOptions(args) {
|
|
|
32
32
|
} else if (arg === '--to') {
|
|
33
33
|
options.to = optionValue(arg, i);
|
|
34
34
|
i += 1;
|
|
35
|
+
} else if (arg === '--timeout-ms') {
|
|
36
|
+
const value = optionValue(arg, i);
|
|
37
|
+
const timeout = Number(value);
|
|
38
|
+
if (!Number.isInteger(timeout) || timeout < 1000) {
|
|
39
|
+
throw new Error('--timeout-ms must be an integer >= 1000');
|
|
40
|
+
}
|
|
41
|
+
options.timeout = timeout;
|
|
42
|
+
i += 1;
|
|
43
|
+
} else if (arg === '--max-dirs') {
|
|
44
|
+
const value = optionValue(arg, i);
|
|
45
|
+
const maxDirs = Number(value);
|
|
46
|
+
if (!Number.isInteger(maxDirs) || maxDirs < 1) {
|
|
47
|
+
throw new Error('--max-dirs must be a positive integer');
|
|
48
|
+
}
|
|
49
|
+
options.maxDirs = maxDirs;
|
|
50
|
+
i += 1;
|
|
35
51
|
} else if (arg === '--no-codex') {
|
|
36
52
|
options.codex = false;
|
|
37
53
|
} else if (arg === '--no-claude') {
|
|
@@ -88,7 +104,7 @@ export async function runCli(args) {
|
|
|
88
104
|
|
|
89
105
|
Usage:
|
|
90
106
|
llm-wiki install --workspace /apps [--profile standard]
|
|
91
|
-
llm-wiki update --workspace <project-or-search-root> [--check|--dry-run|--current-only|--to <version-or-tag>]
|
|
107
|
+
llm-wiki update --workspace <project-or-search-root> [--check|--dry-run|--current-only|--to <version-or-tag>] [--timeout-ms 120000] [--max-dirs 5000]
|
|
92
108
|
llm-wiki post-update --workspace <project> [--all]
|
|
93
109
|
llm-wiki doctor --workspace <project>
|
|
94
110
|
llm-wiki projects --workspace /apps
|
|
@@ -156,12 +172,21 @@ Usage:
|
|
|
156
172
|
}
|
|
157
173
|
|
|
158
174
|
if (command === 'update') {
|
|
159
|
-
printJsonOrText(await update(
|
|
175
|
+
printJsonOrText(await update({
|
|
176
|
+
...options,
|
|
177
|
+
onProgress: (message) => console.error(`[llm-wiki update] ${message}`),
|
|
178
|
+
}), options, formatUpdate);
|
|
160
179
|
return;
|
|
161
180
|
}
|
|
162
181
|
|
|
163
182
|
if (command === 'post-update') {
|
|
164
|
-
|
|
183
|
+
const postOptions = process.env.LLM_WIKI_KIT_PROGRESS === '1'
|
|
184
|
+
? {
|
|
185
|
+
...options,
|
|
186
|
+
onProgress: (message) => console.error(`[llm-wiki post-update] ${message}`),
|
|
187
|
+
}
|
|
188
|
+
: options;
|
|
189
|
+
printJsonOrText(await postUpdate(postOptions), options, formatPostUpdate);
|
|
165
190
|
return;
|
|
166
191
|
}
|
|
167
192
|
|
|
@@ -226,7 +251,7 @@ Usage:
|
|
|
226
251
|
|
|
227
252
|
async function listProjects(options) {
|
|
228
253
|
const workspace = resolve(options.workspace || process.cwd());
|
|
229
|
-
const roots = await knownProjectRoots({ workspace });
|
|
254
|
+
const roots = await knownProjectRoots({ workspace, maxDirs: options.maxDirs });
|
|
230
255
|
const projects = [];
|
|
231
256
|
for (const root of roots) {
|
|
232
257
|
let state = null;
|
|
@@ -264,6 +289,8 @@ function formatStatus(value) {
|
|
|
264
289
|
`- hooks current: ${value.hooksCurrent ? 'yes' : 'no'}`,
|
|
265
290
|
`- codex hook: ${value.codexInstalled ? 'current' : 'missing/outdated'}`,
|
|
266
291
|
`- claude hook: ${value.claudeInstalled ? 'current' : 'missing/outdated'}`,
|
|
292
|
+
`- claude version: ${value.claudeVersion || 'unknown'} (${value.claudeModernHooks ? 'modern hooks' : 'compatible hooks'})`,
|
|
293
|
+
`- claude unsupported kit hooks: ${(value.claudeUnsupportedKitEvents || []).join(', ') || 'none'}`,
|
|
267
294
|
`- project applied runtime: ${project.lastRuntimeVersionApplied || 'unknown'}`,
|
|
268
295
|
`- project templates current: ${project.managedFilesCurrent ? 'yes' : 'no'}`,
|
|
269
296
|
`- project auto-updateable rules: ${autoUpdateCount}`,
|
package/src/doctor.js
CHANGED
|
@@ -21,6 +21,12 @@ export async function runDoctor(options = {}) {
|
|
|
21
21
|
add('command-path', 'llm-wiki command resolves to current runtime', stat.commandMatchesRuntime, stat.commandPath ? `command=${stat.commandPath}; runtime=${stat.binPath}` : 'command not found on PATH');
|
|
22
22
|
add('codex-hook', 'Codex hook installed', stat.codexInstalled, stat.codexHooksPath);
|
|
23
23
|
add('claude-hook', 'Claude hook installed', stat.claudeInstalled, stat.claudeSettingsPath);
|
|
24
|
+
add(
|
|
25
|
+
'claude-settings-compatible',
|
|
26
|
+
'Claude hook settings compatible',
|
|
27
|
+
(stat.claudeUnsupportedKitEvents || []).length === 0,
|
|
28
|
+
`version=${stat.claudeVersion || 'unknown'}; unsupported=${(stat.claudeUnsupportedKitEvents || []).join(', ') || 'none'}`
|
|
29
|
+
);
|
|
24
30
|
add('project-templates', 'project templates current', stat.project.managedFilesCurrent, stat.project.statePath);
|
|
25
31
|
add('codex-command', 'codex command available', spawnSync('codex', ['--version'], { encoding: 'utf8' }).status === 0, 'codex --version');
|
|
26
32
|
add('claude-command', 'claude command available', spawnSync('claude', ['--version'], { encoding: 'utf8' }).status === 0, 'claude --version');
|
|
@@ -111,7 +117,7 @@ function doctorRemediation(result) {
|
|
|
111
117
|
}
|
|
112
118
|
suggestions.push(`reconnect command and hooks: llm-wiki install --workspace ${workspace} --profile standard`);
|
|
113
119
|
}
|
|
114
|
-
if (failed(result, 'codex-hook') || failed(result, 'claude-hook')) {
|
|
120
|
+
if (failed(result, 'codex-hook') || failed(result, 'claude-hook') || failed(result, 'claude-settings-compatible')) {
|
|
115
121
|
suggestions.push(`install hooks: llm-wiki install --workspace ${workspace} --profile standard, then restart Codex/Claude Code sessions`);
|
|
116
122
|
}
|
|
117
123
|
if (failed(result, 'project-templates')) {
|
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
|
@@ -2,7 +2,8 @@ import { realpathSync } from 'fs';
|
|
|
2
2
|
import { chmod, lstat, readlink, unlink } from 'fs/promises';
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
4
|
import { join, resolve } from 'path';
|
|
5
|
-
import {
|
|
5
|
+
import { CODEX_EVENTS, KIT_NAME } from './constants.js';
|
|
6
|
+
import { detectClaudeVersion, supportedClaudeEvents } from './claude-compat.js';
|
|
6
7
|
import { backupFile, exists, homeDir, readJson, safeSymlink, writeJson } from './fs-utils.js';
|
|
7
8
|
import { maintenanceSummary } from './maintenance.js';
|
|
8
9
|
import { inspectProjectState } from './project-state.js';
|
|
@@ -50,6 +51,11 @@ function isKitPath(path) {
|
|
|
50
51
|
return String(path || '').replace(/\\/g, '/').includes('/llm-wiki-kit/');
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
function isKitHookEntry(entry) {
|
|
55
|
+
const serialized = JSON.stringify(entry || {});
|
|
56
|
+
return serialized.includes(KIT_NAME) || serialized.includes(binPath) || serialized.includes('/llm-wiki-kit/');
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
async function inspectLocalBin(localBinPath) {
|
|
54
60
|
try {
|
|
55
61
|
const stat = await lstat(localBinPath);
|
|
@@ -145,18 +151,43 @@ function removeKitHooks(hooks) {
|
|
|
145
151
|
let changed = false;
|
|
146
152
|
for (const [eventName, entries] of Object.entries(hooks || {})) {
|
|
147
153
|
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
|
-
});
|
|
154
|
+
const next = entries.filter((entry) => !isKitHookEntry(entry));
|
|
152
155
|
if (next.length !== entries.length) {
|
|
153
|
-
hooks[eventName] = next;
|
|
156
|
+
if (next.length > 0) hooks[eventName] = next;
|
|
157
|
+
else delete hooks[eventName];
|
|
154
158
|
changed = true;
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
return changed;
|
|
158
162
|
}
|
|
159
163
|
|
|
164
|
+
function removeUnsupportedKitClaudeHooks(hooks, supportedEvents) {
|
|
165
|
+
const supported = new Set(supportedEvents);
|
|
166
|
+
const removed = [];
|
|
167
|
+
for (const [eventName, entries] of Object.entries(hooks || {})) {
|
|
168
|
+
if (supported.has(eventName) || !Array.isArray(entries)) continue;
|
|
169
|
+
const next = entries.filter((entry) => !isKitHookEntry(entry));
|
|
170
|
+
if (next.length !== entries.length) {
|
|
171
|
+
removed.push(eventName);
|
|
172
|
+
if (next.length > 0) hooks[eventName] = next;
|
|
173
|
+
else delete hooks[eventName];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return [...new Set(removed)];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function unsupportedKitClaudeEvents(hooks, supportedEvents) {
|
|
180
|
+
const supported = new Set(supportedEvents);
|
|
181
|
+
return Object.entries(hooks || {})
|
|
182
|
+
.filter(([eventName, entries]) => (
|
|
183
|
+
!supported.has(eventName) &&
|
|
184
|
+
Array.isArray(entries) &&
|
|
185
|
+
entries.some((entry) => isKitHookEntry(entry))
|
|
186
|
+
))
|
|
187
|
+
.map(([eventName]) => eventName)
|
|
188
|
+
.sort();
|
|
189
|
+
}
|
|
190
|
+
|
|
160
191
|
export async function install(options = {}) {
|
|
161
192
|
const workspace = resolve(options.workspace || process.cwd());
|
|
162
193
|
await chmod(binPath, 0o755).catch(() => {});
|
|
@@ -196,12 +227,19 @@ export async function install(options = {}) {
|
|
|
196
227
|
if (options.claude !== false) {
|
|
197
228
|
const claude = (await readJson(claudeSettingsPath, null)) || {};
|
|
198
229
|
claude.hooks = claude.hooks || {};
|
|
230
|
+
const claudeDetection = detectClaudeVersion();
|
|
231
|
+
const claudeEvents = supportedClaudeEvents(claudeDetection);
|
|
199
232
|
let claudeChanged = false;
|
|
200
233
|
if (options.replaceHooks && removeKitHooks(claude.hooks)) {
|
|
201
234
|
claudeChanged = true;
|
|
202
235
|
changed.push('claude:replaced');
|
|
203
236
|
}
|
|
204
|
-
|
|
237
|
+
const removedUnsupported = removeUnsupportedKitClaudeHooks(claude.hooks, claudeEvents);
|
|
238
|
+
if (removedUnsupported.length > 0) {
|
|
239
|
+
claudeChanged = true;
|
|
240
|
+
changed.push(...removedUnsupported.map((eventName) => `claude:removed-unsupported:${eventName}`));
|
|
241
|
+
}
|
|
242
|
+
for (const eventName of claudeEvents) {
|
|
205
243
|
const matcher = eventName === 'SessionStart' ? 'startup|resume|clear' : undefined;
|
|
206
244
|
if (addHook(claude.hooks, eventName, hookCommand('claude', eventName), { matcher })) {
|
|
207
245
|
claudeChanged = true;
|
|
@@ -260,8 +298,12 @@ export async function status(options = {}) {
|
|
|
260
298
|
const claudeSettingsPath = join(homeDir(), '.claude', 'settings.json');
|
|
261
299
|
const codex = await readJson(codexHooksPath, {});
|
|
262
300
|
const claude = await readJson(claudeSettingsPath, {});
|
|
301
|
+
const claudeDetection = detectClaudeVersion();
|
|
302
|
+
const claudeEvents = supportedClaudeEvents(claudeDetection);
|
|
303
|
+
const claudeMissingEvents = claudeEvents.filter((eventName) => !JSON.stringify(claude.hooks?.[eventName] || []).includes(binPath));
|
|
304
|
+
const claudeUnsupportedKitEvents = unsupportedKitClaudeEvents(claude.hooks || {}, claudeEvents);
|
|
263
305
|
const codexInstalled = JSON.stringify(codex.hooks || {}).includes(binPath);
|
|
264
|
-
const claudeInstalled =
|
|
306
|
+
const claudeInstalled = claudeMissingEvents.length === 0 && claudeUnsupportedKitEvents.length === 0;
|
|
265
307
|
const commandPaths = llmWikiCommandPaths();
|
|
266
308
|
const commandPath = commandPaths[0] || null;
|
|
267
309
|
const resolvedCommandPath = realpathOrOriginal(commandPath);
|
|
@@ -280,6 +322,11 @@ export async function status(options = {}) {
|
|
|
280
322
|
localBin,
|
|
281
323
|
codexInstalled,
|
|
282
324
|
claudeInstalled,
|
|
325
|
+
claudeVersion: claudeDetection.version || 'unknown',
|
|
326
|
+
claudeModernHooks: claudeDetection.modern,
|
|
327
|
+
claudeSupportedEvents: claudeEvents,
|
|
328
|
+
claudeMissingEvents,
|
|
329
|
+
claudeUnsupportedKitEvents,
|
|
283
330
|
hooksCurrent: codexInstalled && claudeInstalled,
|
|
284
331
|
codexHooksPath,
|
|
285
332
|
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
|
+
}
|
package/src/projects.js
CHANGED
|
@@ -58,12 +58,17 @@ async function looksLikeProjectRoot(root) {
|
|
|
58
58
|
export async function discoverProjectRoots(searchRoot, options = {}) {
|
|
59
59
|
const root = resolve(searchRoot || process.cwd());
|
|
60
60
|
const maxDirs = options.maxDirs || 5000;
|
|
61
|
+
const progressEvery = options.progressEvery || 1000;
|
|
62
|
+
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
|
|
61
63
|
const roots = new Set();
|
|
62
64
|
let seen = 0;
|
|
63
65
|
|
|
64
66
|
async function walk(dir) {
|
|
65
67
|
if (seen >= maxDirs) return;
|
|
66
68
|
seen += 1;
|
|
69
|
+
if (onProgress && seen % progressEvery === 0) {
|
|
70
|
+
onProgress(`scanned ${seen}/${maxDirs} directories while discovering projects`);
|
|
71
|
+
}
|
|
67
72
|
|
|
68
73
|
if (await looksLikeProjectRoot(dir)) {
|
|
69
74
|
roots.add(dir);
|
|
@@ -91,6 +96,9 @@ export async function discoverProjectRoots(searchRoot, options = {}) {
|
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
await walk(root);
|
|
99
|
+
if (onProgress) {
|
|
100
|
+
onProgress(`project discovery scanned ${seen} director${seen === 1 ? 'y' : 'ies'}; found ${roots.size}`);
|
|
101
|
+
}
|
|
94
102
|
return [...roots].sort();
|
|
95
103
|
}
|
|
96
104
|
|
package/src/update.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
2
|
import { join, resolve } from 'path';
|
|
3
3
|
import { exists } from './fs-utils.js';
|
|
4
4
|
import { appendWikiLog } from './project.js';
|
|
@@ -7,18 +7,101 @@ import { applyProjectTemplateUpdate, inspectProjectState } from './project-state
|
|
|
7
7
|
import { knownProjectRoots, recordProject } from './projects.js';
|
|
8
8
|
import { binPath, detectInstallSource, packageName, runtimeVersion } from './version.js';
|
|
9
9
|
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
function commandLine(command, args) {
|
|
11
|
+
return [command, ...args].join(' ');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function elapsedMs(startedAt) {
|
|
15
|
+
return Date.now() - startedAt;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function progress(options, message) {
|
|
19
|
+
if (typeof options.onProgress === 'function') options.onProgress(message);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function runCommand(command, args, options = {}) {
|
|
23
|
+
const timeout = options.timeout || 120000;
|
|
24
|
+
const killGraceMs = options.killGraceMs || 2000;
|
|
25
|
+
const label = options.label || commandLine(command, args);
|
|
26
|
+
const startedAt = Date.now();
|
|
27
|
+
const detached = process.platform !== 'win32';
|
|
28
|
+
let stdout = '';
|
|
29
|
+
let stderr = '';
|
|
30
|
+
let settled = false;
|
|
31
|
+
let timedOut = false;
|
|
32
|
+
let timeoutId = null;
|
|
33
|
+
let killId = null;
|
|
34
|
+
|
|
35
|
+
progress(options, `starting ${label}`);
|
|
36
|
+
|
|
37
|
+
return new Promise((resolveResult) => {
|
|
38
|
+
let child;
|
|
39
|
+
const finish = (result) => {
|
|
40
|
+
if (settled) return;
|
|
41
|
+
settled = true;
|
|
42
|
+
clearTimeout(timeoutId);
|
|
43
|
+
clearTimeout(killId);
|
|
44
|
+
progress(options, `${label} finished in ${elapsedMs(startedAt)}ms`);
|
|
45
|
+
resolveResult({
|
|
46
|
+
...result,
|
|
47
|
+
stdout,
|
|
48
|
+
stderr,
|
|
49
|
+
elapsedMs: elapsedMs(startedAt),
|
|
50
|
+
timedOut,
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const killChild = (signal) => {
|
|
54
|
+
if (!child?.pid) return;
|
|
55
|
+
try {
|
|
56
|
+
if (detached) {
|
|
57
|
+
process.kill(-child.pid, signal);
|
|
58
|
+
} else {
|
|
59
|
+
child.kill(signal);
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// The process may have already exited.
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
child = spawn(command, args, {
|
|
68
|
+
detached,
|
|
69
|
+
env: options.env || process.env,
|
|
70
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
finish({ status: null, signal: null, error });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
child.stdout.setEncoding('utf8');
|
|
78
|
+
child.stderr.setEncoding('utf8');
|
|
79
|
+
child.stdout.on('data', (chunk) => {
|
|
80
|
+
stdout += chunk;
|
|
81
|
+
});
|
|
82
|
+
child.stderr.on('data', (chunk) => {
|
|
83
|
+
stderr += chunk;
|
|
84
|
+
});
|
|
85
|
+
child.on('error', (error) => {
|
|
86
|
+
finish({ status: null, signal: null, error });
|
|
87
|
+
});
|
|
88
|
+
child.on('close', (status, signal) => {
|
|
89
|
+
const error = timedOut
|
|
90
|
+
? Object.assign(new Error(`${label} timed out after ${timeout}ms`), { code: 'ETIMEDOUT' })
|
|
91
|
+
: null;
|
|
92
|
+
finish({ status, signal, error });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
timeoutId = setTimeout(() => {
|
|
96
|
+
timedOut = true;
|
|
97
|
+
progress(options, `${label} exceeded ${timeout}ms; terminating`);
|
|
98
|
+
killChild('SIGTERM');
|
|
99
|
+
killId = setTimeout(() => {
|
|
100
|
+
progress(options, `${label} did not exit after SIGTERM; killing`);
|
|
101
|
+
killChild('SIGKILL');
|
|
102
|
+
}, killGraceMs);
|
|
103
|
+
}, timeout);
|
|
15
104
|
});
|
|
16
|
-
return {
|
|
17
|
-
status: result.status,
|
|
18
|
-
stdout: result.stdout || '',
|
|
19
|
-
stderr: result.stderr || '',
|
|
20
|
-
error: result.error || null,
|
|
21
|
-
};
|
|
22
105
|
}
|
|
23
106
|
|
|
24
107
|
function npmCommand() {
|
|
@@ -31,7 +114,11 @@ function binCommand() {
|
|
|
31
114
|
|
|
32
115
|
function assertCommandOk(result, label) {
|
|
33
116
|
if (result.status === 0 && !result.error) return;
|
|
34
|
-
const detail = result.stderr.trim() ||
|
|
117
|
+
const detail = result.stderr.trim() ||
|
|
118
|
+
result.stdout.trim() ||
|
|
119
|
+
result.error?.message ||
|
|
120
|
+
(result.signal ? `signal ${result.signal}` : '') ||
|
|
121
|
+
'unknown error';
|
|
35
122
|
throw new Error(`${label} failed: ${detail}`);
|
|
36
123
|
}
|
|
37
124
|
|
|
@@ -45,7 +132,13 @@ function shouldUpdateAllProjects(options = {}) {
|
|
|
45
132
|
|
|
46
133
|
async function projectRootsForUpdate(workspace, options = {}) {
|
|
47
134
|
if (!shouldUpdateAllProjects(options)) return [workspace];
|
|
48
|
-
|
|
135
|
+
progress(options, `discovering project roots under ${workspace}`);
|
|
136
|
+
const roots = await knownProjectRoots({
|
|
137
|
+
workspace,
|
|
138
|
+
maxDirs: options.maxDirs,
|
|
139
|
+
onProgress: options.onProgress,
|
|
140
|
+
});
|
|
141
|
+
progress(options, `discovered ${roots.length} project root(s)`);
|
|
49
142
|
return roots.length > 0 ? roots : [workspace];
|
|
50
143
|
}
|
|
51
144
|
|
|
@@ -97,7 +190,9 @@ export function compareVersions(a, b) {
|
|
|
97
190
|
export async function checkForUpdate(options = {}) {
|
|
98
191
|
const target = options.to || 'latest';
|
|
99
192
|
const installedVersion = runtimeVersion();
|
|
100
|
-
const result = runCommand(npmCommand(), ['view', `${packageName()}@${target}`, 'version'], {
|
|
193
|
+
const result = await runCommand(npmCommand(), ['view', `${packageName()}@${target}`, 'version'], {
|
|
194
|
+
...options,
|
|
195
|
+
label: 'npm view',
|
|
101
196
|
timeout: options.timeout || 30000,
|
|
102
197
|
});
|
|
103
198
|
assertCommandOk(result, 'npm view');
|
|
@@ -120,9 +215,16 @@ export async function postUpdate(options = {}) {
|
|
|
120
215
|
});
|
|
121
216
|
|
|
122
217
|
if (options.all) {
|
|
123
|
-
|
|
218
|
+
progress(options, `discovering project roots under ${workspace}`);
|
|
219
|
+
const workspaces = await knownProjectRoots({
|
|
220
|
+
workspace,
|
|
221
|
+
maxDirs: options.maxDirs,
|
|
222
|
+
onProgress: options.onProgress,
|
|
223
|
+
});
|
|
224
|
+
progress(options, `post-update will inspect ${workspaces.length} project root(s)`);
|
|
124
225
|
const projects = [];
|
|
125
226
|
for (const projectRoot of workspaces) {
|
|
227
|
+
progress(options, `applying managed templates to ${projectRoot}`);
|
|
126
228
|
const projectResult = await applyTemplatesToProject(projectRoot, options);
|
|
127
229
|
if (!options.noProject && await hasProjectWiki(projectRoot)) {
|
|
128
230
|
await appendWikiLog(projectRoot, `llm-wiki-kit post-update applied runtime ${runtimeVersion()}; changed templates: ${projectResult.changed.length}`);
|
|
@@ -194,7 +296,9 @@ export async function update(options = {}) {
|
|
|
194
296
|
const target = options.to || 'latest';
|
|
195
297
|
const shouldRunNpmInstall = check.updateAvailable;
|
|
196
298
|
const installResult = shouldRunNpmInstall
|
|
197
|
-
? runCommand(npmCommand(), ['install', '-g', `${packageName()}@${target}`], {
|
|
299
|
+
? await runCommand(npmCommand(), ['install', '-g', `${packageName()}@${target}`], {
|
|
300
|
+
...options,
|
|
301
|
+
label: 'npm install -g',
|
|
198
302
|
timeout: options.timeout || 120000,
|
|
199
303
|
})
|
|
200
304
|
: {
|
|
@@ -210,7 +314,13 @@ export async function update(options = {}) {
|
|
|
210
314
|
if (options.noProject) postArgs.push('--no-project');
|
|
211
315
|
if (options.codex === false) postArgs.push('--no-codex');
|
|
212
316
|
if (options.claude === false) postArgs.push('--no-claude');
|
|
213
|
-
const postResult = runCommand(process.execPath, postArgs, {
|
|
317
|
+
const postResult = await runCommand(process.execPath, postArgs, {
|
|
318
|
+
...options,
|
|
319
|
+
env: {
|
|
320
|
+
...process.env,
|
|
321
|
+
LLM_WIKI_KIT_PROGRESS: process.env.LLM_WIKI_KIT_PROGRESS || '1',
|
|
322
|
+
},
|
|
323
|
+
label: 'post-update',
|
|
214
324
|
timeout: options.timeout || 120000,
|
|
215
325
|
});
|
|
216
326
|
assertCommandOk(postResult, 'post-update');
|