llm-wiki-kit 0.2.2 → 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 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,9 +103,9 @@ 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
- 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 tells the active agent to mention the available update and offer `llm-wiki update --workspace <project-or-search-root>`. Set `LLM_WIKI_KIT_UPDATE_NOTICE=0` only when diagnosing or suppressing that reminder.
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
 
109
110
  `llm-wiki post-update --workspace <project>` reapplies the current runtime's hook entries and safe managed template updates without running `npm install -g`. Use `post-update --all --workspace <search-root>` to reapply templates across discovered project roots.
110
111
 
@@ -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
@@ -40,7 +42,7 @@ when no project `CLAUDE.md` exists. Existing `CLAUDE.md` files are not overwritt
40
42
 
41
43
  The hook records redacted turn summaries but does not deny tool calls only because an input looks sensitive. Hook payloads are stored as small redacted event envelopes rather than full transcripts, and context output is redacted field by field before it is returned to Claude Code.
42
44
 
43
- At `SessionStart`/`InstructionsLoaded`, the hook 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 update notice, and any maintenance note for outdated or customized managed rules. At `UserPromptSubmit`, it recovers stale turn state, searches 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. Maintenance reminders are shown only when the prompt is wiki/maintenance related or matches a queue topic.
45
+ At `SessionStart`/`InstructionsLoaded`, the hook 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. At `UserPromptSubmit`, it recovers stale turn state, searches 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.
44
46
 
45
47
  `PostToolUse` and `PostToolBatch` record redacted tool summaries in the same turn buffer. `PreCompact` records a compaction note, and `PostCompact` records the note and returns fresh wiki context. In the default `answer-first` mode, `SubagentStop` does not create live Q&A, query, decision, or maintenance files. `Stop` and `SessionEnd` append live Q&A only for meaningful work turns and do not auto-create `wiki/queries/` or `wiki/decisions/`. If the user explicitly asked to record or document durable knowledge and no durable wiki update is detected, `Stop`/`SessionEnd` queue a pending maintenance item for agent review. `Stop` and `SessionEnd` then clear the per-session turn buffer; `SubagentStop` does not.
46
48
 
@@ -29,8 +29,8 @@ 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 update notice, and any maintenance note for outdated or customized managed rules.
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. Maintenance reminders are shown only when the prompt is wiki/maintenance related or matches a queue topic.
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
+ - `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.
36
36
  - `PreCompact` records a compaction note; `PostCompact` records the note and returns fresh wiki context.
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,15 +142,18 @@ 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
 
152
- global npm runtime으로 설치된 경우 hook은 사용 흐름 중 cached update notice check를 수행한다. 자동 설치는 하지 않는다. 새 npm release가 감지되면 hook context가 active agent에게 “업데이트 가능”을 짧게 알리고 `llm-wiki update --workspace <project-or-search-root>` 실행을 제안하게 한다. `LLM_WIKI_KIT_UPDATE_NOTICE=0`으로 이 reminder만 끌 수 있다.
156
+ global npm runtime으로 설치된 경우 hook은 사용 흐름 중 cached update notice check를 수행한다. 자동 설치는 하지 않는다. 새 npm release가 감지되면 hook context가 active agent에게 한국어로 “업데이트 가능”을 짧게 알리고 `llm-wiki update --workspace <project-or-search-root>` 실행을 제안하게 한다. cache는 lookup에 사용한 npm command별로 분리되어 test/fake npm 결과가 일반 hook session에 섞이지 않는다. `LLM_WIKI_KIT_UPDATE_NOTICE=0`으로 이 reminder만 끌 수 있다.
153
157
 
154
158
  ### `llm-wiki post-update`
155
159
 
@@ -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이 안 돈다면:
@@ -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
@@ -101,7 +102,7 @@ llm-wiki maintenance --workspace /path/to/project
101
102
 
102
103
  `update --check [--to <version-or-tag>]` is online and asks npm for the target version. It reports `update available` only when that registry target is newer than the installed version, so it does not suggest downgrades.
103
104
 
104
- Installed npm runtimes also run a cached hook-side update notice check while the user works. It never installs automatically. If npm has a newer release, `SessionStart`/`InstructionsLoaded`/`UserPromptSubmit` context tells the active agent to mention the available update and offer `llm-wiki update --workspace <project-or-search-root>`. Set `LLM_WIKI_KIT_UPDATE_NOTICE=0` to suppress this reminder while diagnosing.
105
+ Installed npm runtimes also run a cached hook-side update notice check while the user works. It never installs automatically. If npm has a newer release, `SessionStart`/`InstructionsLoaded`/`UserPromptSubmit` 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` to suppress this reminder while diagnosing.
105
106
 
106
107
  `projects --workspace <search-root>` lists discovered project roots that have `llm-wiki/.kit-state.json` or an older `llm-wiki/wiki/index.md`, reports whether their managed templates are current, and prints the update commands for the search root.
107
108
 
@@ -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
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-wiki-kit",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Hook-first living LLM Wiki runtime for Codex and Claude Code.",
5
5
  "type": "module",
6
6
  "files": [
@@ -148,15 +148,16 @@ export function formatDurableCaptureGuidance(query) {
148
148
  if (!text) return '';
149
149
  if (explicitDurableRequested(text)) {
150
150
  return [
151
- 'LLM Wiki durable documentation request detected:',
151
+ 'LLM Wiki 문서화 요청 감지:',
152
152
  '- 현재 사용자 요청을 먼저 처리하되, 사용자가 기록/문서화를 요청했으므로 기존 durable wiki 문서를 찾아 갱신한다.',
153
153
  '- 새 문서는 관련 기존 문서가 없을 때만 만들고, credentials/private data는 저장하지 않는다.',
154
154
  ].join('\n');
155
155
  }
156
156
  if (hasDurableKeyword(text)) {
157
157
  return [
158
- 'LLM Wiki durable knowledge note:',
159
- '- 현재 답변을 먼저 한다. 다음 세션에도 쓸 결론이면 답변 끝에서 짧게 wiki 문서화 여부를 물어본다.',
158
+ 'LLM Wiki 저장 후보 알림:',
159
+ '- 현재 답변을 먼저 한다.',
160
+ '- 다음 세션에도 재사용할 만한 내용(결정, 정책, 디버깅 결과, 운영 절차, 검증된 상태 등)이 있으면 답변 끝에서 짧게 wiki 문서화 여부를 물어본다.',
160
161
  ].join('\n');
161
162
  }
162
163
  return '';
@@ -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(options), options, formatUpdate);
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
- printJsonOrText(await postUpdate(options), options, formatPostUpdate);
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}`,
@@ -279,7 +306,7 @@ function formatUpdate(value) {
279
306
  `- installed: ${value.installedVersion}`,
280
307
  `- latest: ${value.latestVersion}`,
281
308
  `- update available: ${value.updateAvailable ? 'yes' : 'no'}`,
282
- ...(value.updateAvailable ? [`- recommended action: ${commandForProject('update', value.workspace)}`] : []),
309
+ ...(value.updateAvailable ? [`- 권장 실행: ${commandForProject('update', value.workspace)}`] : []),
283
310
  `- project applied runtime: ${value.project?.lastRuntimeVersionApplied || 'unknown'}`,
284
311
  ].join('\n');
285
312
  }
@@ -296,7 +323,7 @@ function formatUpdate(value) {
296
323
  `- installed: ${value.installedVersion}`,
297
324
  `- latest: ${value.latestVersion}`,
298
325
  `- update available: ${value.updateAvailable ? 'yes' : 'no'}`,
299
- ...(value.updateAvailable ? [`- recommended action: ${commandForProject('update', value.workspace)}`] : []),
326
+ ...(value.updateAvailable ? [`- 권장 실행: ${commandForProject('update', value.workspace)}`] : []),
300
327
  `- scope: ${value.scope || 'current'}`,
301
328
  ...(projects.length > 0 ? [`- projects checked: ${projects.length}`] : []),
302
329
  `- project template changes: ${changed}`,
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() {
@@ -75,7 +76,7 @@ async function handleAnswerFirstStop(projectRoot, eventName, payload, entry) {
75
76
  const archiveEntry = {
76
77
  ...entry,
77
78
  followUp: classification.suggestDurable
78
- ? '이 결론은 다음 세션에도 있어 보입니다. 사용자가 승인하면 기존 durable wiki 문서에 합친다.'
79
+ ? '이 turn에 저장할 만한 내용이 있어 보입니다. 사용자가 승인하면 기존 durable wiki 문서에 합친다.'
79
80
  : entry.followUp,
80
81
  };
81
82
  const liveQaPath = await appendLiveQa(projectRoot, archiveEntry);
@@ -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 { CLAUDE_EVENTS, CODEX_EVENTS, KIT_NAME } from './constants.js';
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
- for (const eventName of CLAUDE_EVENTS) {
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 = JSON.stringify(claude.hooks || {}).includes(binPath);
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/templates.js CHANGED
@@ -17,7 +17,7 @@ This repository uses llm-wiki-kit as a hook-first living Markdown wiki for Codex
17
17
  - hook은 redacted raw envelope와 필요한 live Q&A를 안전하게 남긴다. \`wiki/queries\`/\`wiki/decisions\` 자동 승격은 기본값이 아니며, durable 지식은 중요도와 동의 흐름에 따라 agent가 기존 정식 wiki 문서에 합친다.
18
18
  - hook은 종료/시작 경계에서 정말 필요한 정리 후보를 \`llm-wiki/outputs/maintenance/queue.md\`에 남길 수 있다. pending 항목은 현재 응답을 지연시키지 않는 범위에서 agent가 병합하거나 done/skipped로 표시한다.
19
19
  - 새 문서를 만들기 전에 기존 wiki 문서를 먼저 찾아 갱신한다. 반복해서 쓸 사실은 \`outputs/questions/\`에만 두지 말고 적절한 wiki 문서에 합친다.
20
- - 일회성 작업 기록은 필요할 때 \`llm-wiki/outputs/questions/\`에 보존하고, 재사용 가능한 결론은 승인된 경우 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 반영한다.
20
+ - 일회성 작업 기록은 필요할 때 \`llm-wiki/outputs/questions/\`에 보존하고, 재사용 가능한 사실/지식은 승인된 경우 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 반영한다.
21
21
  - 검증 명령, 근거 파일, 불확실한 점을 함께 남긴다. 추론은 추론이라고 표시하고, 모순은 지우지 말고 Open Questions 또는 Contradictions에 남긴다.
22
22
  - 인증값, token, password, private key, \`.env\` 원문은 저장하지 않는다. 필요한 경우 redacted summary만 남긴다.
23
23
 
@@ -107,7 +107,7 @@ Generated by llm-wiki-kit.
107
107
 
108
108
  - 넓은 질문은 memory와 이 index에서 시작한다.
109
109
  - 새 문서를 만들기 전에 관련 기존 문서 3-7개를 먼저 확인한다.
110
- - 오래 쓸 결론은 기존 정식 문서에 합치고, 일회성 기록은 outputs/questions에 둔다.
110
+ - 오래 쓸 사실/지식은 기존 정식 문서에 합치고, 일회성 기록은 outputs/questions에 둔다.
111
111
  - 관련 페이지가 생기면 \`[[page-or-topic]]\` 링크를 추가한다.
112
112
 
113
113
  <!-- llm-wiki-kit:index-start -->
@@ -184,7 +184,7 @@ export function procedure(name) {
184
184
  5. 검증된 사실과 추론을 분리한다.
185
185
  6. 수동 확인이 필요할 때만 \`llm-wiki context "<query>"\`를 쓴다. 과거 episodic query까지 봐야 할 때는 \`--include-episodic\`을 붙인다.
186
186
  7. 일회성 답변은 durable wiki로 승격하지 않는다. 필요한 작업 turn만 \`outputs/questions/\`에 남긴다.
187
- 8. 반복해서 쓸 결론은 중요도와 동의 흐름에 따라 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 합친다.
187
+ 8. 반복해서 쓸 사실/지식은 중요도와 동의 흐름에 따라 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 합친다.
188
188
  `,
189
189
  'lint.md': `# Lint Procedure
190
190
 
@@ -48,10 +48,11 @@ function freshEnough(record, now = Date.now()) {
48
48
  return Number.isFinite(checkedAt) && now - checkedAt < intervalMs();
49
49
  }
50
50
 
51
- function cacheMatches(record, target, installedVersion) {
51
+ function cacheMatches(record, target, installedVersion, npm) {
52
52
  return record?.schemaVersion === NOTICE_SCHEMA_VERSION &&
53
53
  record.target === target &&
54
- record.installedVersion === installedVersion;
54
+ record.installedVersion === installedVersion &&
55
+ record.npmCommand === npm;
55
56
  }
56
57
 
57
58
  function trimDetail(value) {
@@ -82,9 +83,10 @@ export async function checkUpdateNotice(options = {}) {
82
83
  if (!noticeEnabled()) return null;
83
84
  const target = options.to || process.env.LLM_WIKI_KIT_UPDATE_NOTICE_TARGET || 'latest';
84
85
  const installedVersion = runtimeVersion();
86
+ const npm = npmCommand();
85
87
  const cachePath = noticeCachePath();
86
88
  const cached = await readJson(cachePath, null);
87
- if (!options.force && cacheMatches(cached, target, installedVersion) && freshEnough(cached)) {
89
+ if (!options.force && cacheMatches(cached, target, installedVersion, npm) && freshEnough(cached)) {
88
90
  return cached;
89
91
  }
90
92
 
@@ -95,6 +97,7 @@ export async function checkUpdateNotice(options = {}) {
95
97
  checkedAt,
96
98
  target,
97
99
  installedVersion,
100
+ npmCommand: npm,
98
101
  ...registry,
99
102
  };
100
103
  await writeJson(cachePath, record).catch(() => {});
@@ -109,12 +112,12 @@ export async function updateNoticeContext(projectRoot, eventName, options = {})
109
112
  const workspace = resolve(projectRoot || process.cwd());
110
113
  const updateCommand = commandForProject('update', workspace);
111
114
  return [
112
- 'LLM Wiki update notice:',
113
- `- Installed llm-wiki-kit: ${notice.installedVersion}`,
114
- `- Latest npm release: ${notice.latestVersion}`,
115
- `- A newer llm-wiki-kit release was detected automatically while the user was using the hooks.`,
116
- `- Briefly tell the user an update is available and offer to run: \`${updateCommand}\``,
117
- '- If the user wants every known project root updated, use the broader search root in the same command, for example `--workspace /apps`.',
118
- '- Do not install automatically unless the user asks for the update.',
115
+ 'LLM Wiki 업데이트 알림:',
116
+ `- 설치된 llm-wiki-kit: ${notice.installedVersion}`,
117
+ `- npm 최신 릴리스: ${notice.latestVersion}`,
118
+ '- hook 사용 중 더 새로운 llm-wiki-kit 릴리스가 자동 감지되었습니다.',
119
+ `- 사용자에게 업데이트 가능 사실을 짧게 알리고 다음 명령 실행을 제안하세요: \`${updateCommand}\``,
120
+ '- 알려진 모든 프로젝트 root를 업데이트하려면 같은 명령에서 넓은 search root 사용합니다. 예: `--workspace /apps`.',
121
+ '- 사용자가 요청하기 전에는 자동 설치하지 마세요.',
119
122
  ].join('\n');
120
123
  }
package/src/update.js CHANGED
@@ -1,4 +1,4 @@
1
- import { spawnSync } from 'child_process';
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 runCommand(command, args, options = {}) {
11
- const result = spawnSync(command, args, {
12
- encoding: 'utf8',
13
- env: options.env || process.env,
14
- timeout: options.timeout || 120000,
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() || result.stdout.trim() || result.error?.message || 'unknown error';
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
- const roots = await knownProjectRoots({ workspace });
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
- const workspaces = await knownProjectRoots({ workspace });
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');