@m16khb/llm-wiki 0.1.1 → 0.1.2

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
@@ -236,6 +236,10 @@ UserPromptSubmit -> llm-wiki hook -> prompt keyword + project/tech terms로 wik
236
236
  Stop -> llm-wiki hook -> assistant가 명시한 llm-wiki-capture block만 30-sessions/에 저장
237
237
  ```
238
238
 
239
+ `SessionStart`/`UserPromptSubmit` hook은 `suppressOutput: true`로 transcript 출력을 숨긴다. 실제 context는
240
+ `hookSpecificOutput.additionalContext`로 주입되지만, Codex/Claude transcript에 긴 `hook context:` 문자열이 빽빽하게
241
+ 보이지 않게 하기 위한 UX 안전장치다.
242
+
239
243
  `go.mod`가 있으면 Go/Golang을 감지하고, MCP SDK·SQLite·Gin·GORM·Redis 같은 dependency 신호, `package.json`/`bin` 기반 Node/npm/npx wrapper 신호, `swaggo` dependency나 `swagger.yaml`/`openapi.yaml` 같은 API spec 파일을 tech context로 별도 검색한다. 검색 결과는 현재 project와 맞는 문서를 우선하고, 다른 repository 전용 reference는 hook tech context에서 제외한다.
240
244
 
241
245
  공용 wrapper는 `scripts/hooks/llm-wiki-hook.sh`다. `LLM_WIKI_BIN`이 있으면 그 binary를 쓰고, 없으면 `llm-wiki` PATH, 마지막으로 이 repo를 `npx --package <repo>`로 실행한다.
@@ -293,7 +297,7 @@ tool_timeout_sec = 60
293
297
 
294
298
  `llm-wiki mcp`는 daemon을 거치지 않고 stdio MCP server가 직접 vault/service를 연다. stdio-only 환경의 비상/호환 경로이며, Claude Code와 Codex가 같은 daemon을 공유해야 하는 기본 운영에는 `mcp-autostart`를 사용한다.
295
299
 
296
- npx wrapper는 기존 `bin/llm-wiki`가 있으면 그대로 실행하고, 없으면 Go toolchain으로 CLI를 cache에 빌드한 뒤 실행한다. MCP stdio 안전을 위해 wrapper 자체는 stdout에 진단을 쓰지 않는다. `--prefix /tmp`는 이 repo처럼 현재 프로젝트의 `package.json` 이름이 `@m16khb/llm-wiki`와 같을 때 npx가 local package로 오인하는 것을 피하기 위한 안전장치다.
300
+ npx wrapper는 기존 `bin/llm-wiki`가 있고 Go build input보다 최신이면 그대로 실행한다. bundled binary가 stale이거나 없으면 Go toolchain으로 CLI를 cache에 빌드한 뒤 실행한다. `LLM_WIKI_NPM_REBUILD=1`은 bundled/cache binary를 모두 무시하고 재빌드한다. MCP stdio 안전을 위해 wrapper 자체는 stdout에 진단을 쓰지 않는다. `--prefix /tmp`는 이 repo처럼 현재 프로젝트의 `package.json` 이름이 `@m16khb/llm-wiki`와 같을 때 npx가 local package로 오인하는 것을 피하기 위한 안전장치다.
297
301
 
298
302
  ## 개발 문서
299
303
 
@@ -0,0 +1,108 @@
1
+ package main
2
+
3
+ import (
4
+ "encoding/json"
5
+ "io"
6
+ "os"
7
+ "path/filepath"
8
+ "testing"
9
+ )
10
+
11
+ func TestContextHookSuppressesTranscriptOutput(t *testing.T) {
12
+ vaultRoot := t.TempDir()
13
+ writeTestFile(t, vaultRoot, "20-wiki/concepts/llm-wiki-hooks.md", `---
14
+ title: LLM Wiki Hooks
15
+ type: concept
16
+ status: active
17
+ created: 2026-05-23
18
+ updated: 2026-05-23
19
+ tags: [llm-wiki, hooks]
20
+ domain: agent-memory
21
+ ---
22
+
23
+ # LLM Wiki Hooks
24
+
25
+ Hook context for llm-wiki.
26
+ `)
27
+ projectRoot := filepath.Join(t.TempDir(), "llm-wiki")
28
+ writeTestFile(t, projectRoot, "go.mod", "module github.com/m16khb/llm-wiki\n")
29
+
30
+ stdout := captureRunHook(t, []string{"--vault", vaultRoot, "--limit", "5"}, `{
31
+ "hook_event_name": "UserPromptSubmit",
32
+ "cwd": "`+projectRoot+`",
33
+ "prompt": "hook output"
34
+ }`)
35
+
36
+ var payload struct {
37
+ SuppressOutput bool `json:"suppressOutput"`
38
+ HookSpecificOutput struct {
39
+ HookEventName string `json:"hookEventName"`
40
+ AdditionalContext string `json:"additionalContext"`
41
+ } `json:"hookSpecificOutput"`
42
+ }
43
+ if err := json.Unmarshal([]byte(stdout), &payload); err != nil {
44
+ t.Fatalf("hook output is not valid JSON: %v\n%s", err, stdout)
45
+ }
46
+ if !payload.SuppressOutput {
47
+ t.Fatalf("expected context hook to suppress transcript output, got:\n%s", stdout)
48
+ }
49
+ if payload.HookSpecificOutput.HookEventName != "UserPromptSubmit" {
50
+ t.Fatalf("unexpected event name: %q", payload.HookSpecificOutput.HookEventName)
51
+ }
52
+ if payload.HookSpecificOutput.AdditionalContext == "" {
53
+ t.Fatalf("expected additionalContext to remain available for model injection")
54
+ }
55
+ }
56
+
57
+ func captureRunHook(t *testing.T, args []string, stdin string) string {
58
+ t.Helper()
59
+
60
+ oldStdin := os.Stdin
61
+ oldStdout := os.Stdout
62
+ defer func() {
63
+ os.Stdin = oldStdin
64
+ os.Stdout = oldStdout
65
+ }()
66
+
67
+ inRead, inWrite, err := os.Pipe()
68
+ if err != nil {
69
+ t.Fatalf("pipe stdin: %v", err)
70
+ }
71
+ if _, err := inWrite.WriteString(stdin); err != nil {
72
+ t.Fatalf("write stdin: %v", err)
73
+ }
74
+ if err := inWrite.Close(); err != nil {
75
+ t.Fatalf("close stdin writer: %v", err)
76
+ }
77
+ defer inRead.Close()
78
+
79
+ outRead, outWrite, err := os.Pipe()
80
+ if err != nil {
81
+ t.Fatalf("pipe stdout: %v", err)
82
+ }
83
+ defer outRead.Close()
84
+
85
+ os.Stdin = inRead
86
+ os.Stdout = outWrite
87
+ runHook(args)
88
+ if err := outWrite.Close(); err != nil {
89
+ t.Fatalf("close stdout writer: %v", err)
90
+ }
91
+
92
+ data, err := io.ReadAll(outRead)
93
+ if err != nil {
94
+ t.Fatalf("read stdout: %v", err)
95
+ }
96
+ return string(data)
97
+ }
98
+
99
+ func writeTestFile(t *testing.T, root, rel, content string) {
100
+ t.Helper()
101
+ abs := filepath.Join(root, filepath.FromSlash(rel))
102
+ if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil {
103
+ t.Fatalf("mkdir %s: %v", rel, err)
104
+ }
105
+ if err := os.WriteFile(abs, []byte(content), 0o644); err != nil {
106
+ t.Fatalf("write %s: %v", rel, err)
107
+ }
108
+ }
@@ -581,7 +581,13 @@ func runContextHook(vaultRoot, projectPath, query string, limit int, event strin
581
581
  printHookJSON(map[string]any{})
582
582
  return
583
583
  }
584
- printHookJSON(map[string]any{"hookSpecificOutput": map[string]any{"hookEventName": event, "additionalContext": sessionctx.FormatAdditionalContext(ctx)}})
584
+ printHookJSON(map[string]any{
585
+ "suppressOutput": true,
586
+ "hookSpecificOutput": map[string]any{
587
+ "hookEventName": event,
588
+ "additionalContext": sessionctx.FormatAdditionalContext(ctx),
589
+ },
590
+ })
585
591
  }
586
592
 
587
593
  func runStopHook(vaultRoot, dbPath string, in hookInput) {
package/go.mod CHANGED
@@ -1,6 +1,6 @@
1
1
  module github.com/m16khb/llm-wiki
2
2
 
3
- go 1.25.5
3
+ go 1.26.3
4
4
 
5
5
  require github.com/modelcontextprotocol/go-sdk v1.6.0
6
6
 
package/npm/lib/runner.js CHANGED
@@ -9,6 +9,9 @@ const SIGNAL_EXIT_CODES = {
9
9
  SIGTERM: 143,
10
10
  };
11
11
 
12
+ const FINGERPRINT_INPUT_ENTRIES = ['go.mod', 'go.sum', 'package.json', 'cmd', 'internal'];
13
+ const BINARY_INPUT_ENTRIES = ['go.mod', 'go.sum', 'cmd', 'internal'];
14
+
12
15
  function executableName(platform = process.platform) {
13
16
  return platform === 'win32' ? 'llm-wiki.exe' : 'llm-wiki';
14
17
  }
@@ -25,7 +28,9 @@ function isExecutable(file) {
25
28
 
26
29
  function findBundledBinary(packageRoot, platform = process.platform) {
27
30
  const candidate = path.join(packageRoot, 'bin', executableName(platform));
28
- return isExecutable(candidate) ? candidate : '';
31
+ if (!isExecutable(candidate)) return '';
32
+ if (isBundledBinaryStale(packageRoot, candidate)) return '';
33
+ return candidate;
29
34
  }
30
35
 
31
36
  function readPackageVersion(packageRoot) {
@@ -38,7 +43,11 @@ function readPackageVersion(packageRoot) {
38
43
  }
39
44
  }
40
45
 
41
- function hashFile(hash, root, file) {
46
+ function isBuildInput(file) {
47
+ return file.endsWith('.go') || ['go.mod', 'go.sum', 'package.json'].includes(path.basename(file));
48
+ }
49
+
50
+ function walkBuildInputs(root, file, visitor) {
42
51
  let stat;
43
52
  try {
44
53
  stat = fs.statSync(file);
@@ -49,14 +58,23 @@ function hashFile(hash, root, file) {
49
58
  if (stat.isDirectory()) {
50
59
  const entries = fs.readdirSync(file).sort();
51
60
  for (const entry of entries) {
52
- hashFile(hash, root, path.join(file, entry));
61
+ walkBuildInputs(root, path.join(file, entry), visitor);
53
62
  }
54
63
  return;
55
64
  }
56
- const isBuildInput = file.endsWith('.go') || ['go.mod', 'go.sum', 'package.json'].includes(path.basename(file));
57
- if (!stat.isFile() || !isBuildInput) {
65
+ if (!stat.isFile() || !isBuildInput(file)) {
58
66
  return;
59
67
  }
68
+ visitor(file, stat);
69
+ }
70
+
71
+ function visitBuildInputs(packageRoot, entries, visitor) {
72
+ for (const entry of entries) {
73
+ walkBuildInputs(packageRoot, path.join(packageRoot, entry), visitor);
74
+ }
75
+ }
76
+
77
+ function hashFile(hash, root, file, stat) {
60
78
  hash.update(path.relative(root, file));
61
79
  hash.update('\0');
62
80
  hash.update(String(stat.size));
@@ -67,12 +85,30 @@ function hashFile(hash, root, file) {
67
85
 
68
86
  function sourceFingerprint(packageRoot) {
69
87
  const hash = crypto.createHash('sha256');
70
- for (const entry of ['go.mod', 'go.sum', 'package.json', 'cmd', 'internal']) {
71
- hashFile(hash, packageRoot, path.join(packageRoot, entry));
72
- }
88
+ visitBuildInputs(packageRoot, FINGERPRINT_INPUT_ENTRIES, (file, stat) => hashFile(hash, packageRoot, file, stat));
73
89
  return hash.digest('hex').slice(0, 16);
74
90
  }
75
91
 
92
+ function latestBuildInputMTime(packageRoot) {
93
+ let latest = 0;
94
+ visitBuildInputs(packageRoot, BINARY_INPUT_ENTRIES, (_file, stat) => {
95
+ if (stat.mtimeMs > latest) latest = stat.mtimeMs;
96
+ });
97
+ return latest;
98
+ }
99
+
100
+ function isBundledBinaryStale(packageRoot, binary) {
101
+ let binaryStat;
102
+ try {
103
+ binaryStat = fs.statSync(binary);
104
+ } catch (err) {
105
+ if (err && err.code === 'ENOENT') return true;
106
+ throw err;
107
+ }
108
+ const latestInput = latestBuildInputMTime(packageRoot);
109
+ return latestInput > binaryStat.mtimeMs;
110
+ }
111
+
76
112
  function defaultCacheRoot(env = process.env) {
77
113
  if (env.LLM_WIKI_NPM_CACHE_DIR) return env.LLM_WIKI_NPM_CACHE_DIR;
78
114
  if (env.XDG_CACHE_HOME) return path.join(env.XDG_CACHE_HOME, 'llm-wiki', 'npm-wrapper');
@@ -110,8 +146,10 @@ function buildBinary(packageRoot, output, env = process.env) {
110
146
  function ensureBinary(options = {}) {
111
147
  const env = options.env || process.env;
112
148
  const packageRoot = options.packageRoot || env.LLM_WIKI_NPM_PACKAGE_ROOT || path.resolve(__dirname, '..', '..');
113
- const bundled = findBundledBinary(packageRoot);
114
- if (bundled) return bundled;
149
+ if (env.LLM_WIKI_NPM_REBUILD !== '1') {
150
+ const bundled = findBundledBinary(packageRoot);
151
+ if (bundled) return bundled;
152
+ }
115
153
 
116
154
  const cached = cacheBinaryPath(packageRoot, env);
117
155
  if (isExecutable(cached) && env.LLM_WIKI_NPM_REBUILD !== '1') return cached;
@@ -162,6 +200,8 @@ module.exports = {
162
200
  ensureBinary,
163
201
  executableName,
164
202
  findBundledBinary,
203
+ isBundledBinaryStale,
204
+ latestBuildInputMTime,
165
205
  run,
166
206
  sourceFingerprint,
167
207
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m16khb/llm-wiki",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "npx wrapper for the llm-wiki Go MCP server and CLI",
5
5
  "bin": {
6
6
  "llm-wiki": "npm/bin/llm-wiki.js"