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 +5 -2
- package/docs/integrations/claude-code.md +4 -2
- package/docs/integrations/codex.md +2 -2
- package/docs/manual.md +11 -2
- package/docs/operations.md +5 -2
- package/docs/troubleshooting.md +29 -0
- package/package.json +1 -1
- package/src/capture-policy.js +4 -3
- package/src/claude-compat.js +83 -0
- package/src/cli.js +33 -6
- package/src/doctor.js +7 -1
- package/src/hook.js +10 -1
- package/src/install.js +55 -8
- package/src/legacy-omx-wiki.js +222 -0
- package/src/projects.js +8 -0
- package/src/templates.js +3 -3
- package/src/update-notice.js +13 -10
- 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,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
|
|
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.
|
|
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.
|
|
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이 안 돈다면:
|
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
|
|
@@ -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
|
|
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
|
|
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
package/src/capture-policy.js
CHANGED
|
@@ -148,15 +148,16 @@ export function formatDurableCaptureGuidance(query) {
|
|
|
148
148
|
if (!text) return '';
|
|
149
149
|
if (explicitDurableRequested(text)) {
|
|
150
150
|
return [
|
|
151
|
-
'LLM Wiki
|
|
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
|
|
159
|
-
'- 현재 답변을 먼저 한다.
|
|
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(
|
|
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}`,
|
|
@@ -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 ? [`-
|
|
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 ? [`-
|
|
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
|
-
? '이
|
|
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 {
|
|
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/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/\`에 보존하고, 재사용 가능한
|
|
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
|
-
- 오래 쓸
|
|
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. 반복해서 쓸
|
|
187
|
+
8. 반복해서 쓸 사실/지식은 중요도와 동의 흐름에 따라 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 합친다.
|
|
188
188
|
`,
|
|
189
189
|
'lint.md': `# Lint Procedure
|
|
190
190
|
|
package/src/update-notice.js
CHANGED
|
@@ -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
|
|
113
|
-
`-
|
|
114
|
-
`-
|
|
115
|
-
|
|
116
|
-
`-
|
|
117
|
-
'-
|
|
118
|
-
'-
|
|
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 {
|
|
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');
|