llm-wiki-kit 0.2.11 → 0.2.13

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.
@@ -127,7 +127,9 @@ Installed npm runtimes also run a cached hook-side update notice check while the
127
127
 
128
128
  - runs `npm install -g llm-wiki-kit@<target>` only when the registry target is newer than the installed runtime
129
129
  - skips npm installation when the installed runtime already satisfies the target, then still runs post-update maintenance
130
- - restarts into the updated binary for `post-update`
130
+ - checks `npm root -g`/`npm prefix -g` before install and refuses to claim success when npm would update a different global prefix than the active runtime
131
+ - verifies that the active runtime and `post-update` child runtime reached the registry target after install
132
+ - prints before/after runtime, registry target, npm global root, active runtime root, and PATH command diagnostics
131
133
  - reinstalls hook entries without duplicating them
132
134
  - patches only managed project files across known or discovered project roots
133
135
  - backs up changed files under `~/.local/share/llm-wiki-kit/backups/`
@@ -113,6 +113,8 @@ If the proxy performs TLS inspection, install the organization's CA and set `caf
113
113
 
114
114
  If `npm install -g llm-wiki-kit@latest` succeeds but `llm-wiki --help` does not show newer commands such as `projects`, `context`, `lint`, or `consolidate`, the shell is probably executing an older source checkout or stale shim before the npm global binary.
115
115
 
116
+ Newer `llm-wiki update` releases diagnose this before claiming success. If the npm global root does not match the active runtime root, or if `npm install -g` exits 0 but the active runtime is still old, `update` exits nonzero with details such as `active runtime root`, `npm global root`, `runtime before`, `runtime after`, and `command matches runtime`. On root-owned Linux prefixes, run the printed `sudo npm install -g llm-wiki-kit@<target>` command, then run the printed `llm-wiki post-update ...` command as the normal Codex/Claude user.
117
+
116
118
  Confirm what is actually running:
117
119
 
118
120
  ```bash
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "llm-wiki-kit",
3
- "version": "0.2.11",
4
- "description": "Hook-first living LLM Wiki runtime for Codex and Claude Code.",
3
+ "version": "0.2.13",
4
+ "description": "Hook-first living Markdown wiki runtime for Codex and Claude Code with Korean/English prompt-aware guidance.",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "bin/",
@@ -15,6 +15,16 @@
15
15
  "bin": {
16
16
  "llm-wiki": "bin/llm-wiki.js"
17
17
  },
18
+ "keywords": [
19
+ "codex",
20
+ "claude-code",
21
+ "llm-wiki",
22
+ "agent-memory",
23
+ "markdown",
24
+ "hooks",
25
+ "korean",
26
+ "english"
27
+ ],
18
28
  "scripts": {
19
29
  "test": "node --test",
20
30
  "doctor": "node bin/llm-wiki.js doctor",
@@ -143,14 +143,21 @@ export function classifyTurn(entry, eventName = '') {
143
143
  };
144
144
  }
145
145
 
146
- export function formatDurableCaptureGuidance(query) {
146
+ export function formatDurableCaptureGuidance(query, options = {}) {
147
147
  const text = capturedText(query);
148
148
  if (!text) return '';
149
149
  if (explicitDurableRequested(text)) {
150
+ if (options.language === 'ko') {
151
+ return [
152
+ 'LLM Wiki 문서화 요청 감지:',
153
+ '- 현재 사용자 요청을 먼저 처리하되, 사용자가 기록/문서화를 요청했으므로 기존 durable wiki 문서를 찾아 갱신한다.',
154
+ '- 새 문서는 관련 기존 문서가 없을 때만 만들고, credentials/private data는 저장하지 않는다.',
155
+ ].join('\n');
156
+ }
150
157
  return [
151
- 'LLM Wiki 문서화 요청 감지:',
152
- '- 현재 사용자 요청을 먼저 처리하되, 사용자가 기록/문서화를 요청했으므로 기존 durable wiki 문서를 찾아 갱신한다.',
153
- '- 문서는 관련 기존 문서가 없을 때만 만들고, credentials/private data는 저장하지 않는다.',
158
+ 'LLM Wiki documentation request detected:',
159
+ '- Answer the current user request first, then update the existing durable wiki page because the user asked to record or document this.',
160
+ '- Create a new page only when no related durable page exists, and never store credentials or private data.',
154
161
  ].join('\n');
155
162
  }
156
163
  return '';
package/src/cli.js CHANGED
@@ -324,7 +324,7 @@ function formatUpdate(value) {
324
324
  `- installed: ${value.installedVersion}`,
325
325
  `- latest: ${value.latestVersion}`,
326
326
  `- update available: ${value.updateAvailable ? 'yes' : 'no'}`,
327
- ...(value.updateAvailable ? [`- 권장 실행: ${commandForProject('update', value.workspace)}`] : []),
327
+ ...(value.updateAvailable ? [`- recommended command: ${commandForProject('update', value.workspace)}`] : []),
328
328
  `- project applied runtime: ${value.project?.lastRuntimeVersionApplied || 'unknown'}`,
329
329
  ].join('\n');
330
330
  }
@@ -341,20 +341,33 @@ function formatUpdate(value) {
341
341
  `- installed: ${value.installedVersion}`,
342
342
  `- latest: ${value.latestVersion}`,
343
343
  `- update available: ${value.updateAvailable ? 'yes' : 'no'}`,
344
- ...(value.updateAvailable ? [`- 권장 실행: ${commandForProject('update', value.workspace)}`] : []),
344
+ ...(value.updateAvailable ? [`- recommended command: ${commandForProject('update', value.workspace)}`] : []),
345
345
  `- scope: ${value.scope || 'current'}`,
346
346
  ...(projects.length > 0 ? [`- projects checked: ${projects.length}`] : []),
347
347
  `- project template changes: ${changed}`,
348
348
  `- project template skipped: ${skipped}`,
349
349
  ].join('\n');
350
350
  }
351
+ const title = value.updateApplied
352
+ ? 'llm-wiki-kit updated'
353
+ : 'llm-wiki-kit already current; post-update complete';
351
354
  return [
352
- 'llm-wiki-kit updated',
355
+ title,
353
356
  `- workspace: ${value.workspace}`,
354
- `- before: ${value.before}`,
357
+ `- runtime before: ${value.runtimeBefore || value.before}`,
358
+ `- registry target: ${value.latest || value.target}`,
359
+ `- runtime after: ${value.runtimeAfter || 'unknown'}`,
360
+ ...(value.postUpdateRuntime ? [`- post-update runtime: ${value.postUpdateRuntime}`] : []),
361
+ `- runtime verified: ${value.runtimeVerified ? 'yes' : 'no'}`,
362
+ `- npm install: ${value.npmInstall?.skipped ? 'skipped' : 'ran'}`,
355
363
  `- target: ${value.target}`,
364
+ `- active runtime root: ${value.activePackageRoot || 'unknown'}`,
365
+ `- npm global root: ${value.npmGlobal?.root || 'unknown'}`,
366
+ `- command: ${value.command?.path || 'not found'}`,
367
+ `- command matches runtime: ${value.command?.matchesRuntime ? 'yes' : 'no'}`,
356
368
  `- scope: ${value.scope || 'current'}`,
357
369
  ...(value.projects ? [`- projects updated: ${value.projects.length}`] : []),
370
+ ...((value.warnings || []).map((warning) => `- warning: ${warning}`)),
358
371
  ].join('\n');
359
372
  }
360
373
 
package/src/hook.js CHANGED
@@ -9,6 +9,7 @@ import { summarizeForStorage } from './redaction.js';
9
9
  import { buildEntryFromState, clearTurnState, rememberQuestion, rememberTool } from './state.js';
10
10
  import { updateNoticeContext } from './update-notice.js';
11
11
  import { removeLegacyOmxWikiSurfaces } from './legacy-omx-wiki.js';
12
+ import { resolveLanguage } from './language.js';
12
13
  import { relative } from 'path';
13
14
 
14
15
  async function readStdinJson() {
@@ -53,9 +54,10 @@ function contextOutput(provider, eventName, context) {
53
54
  };
54
55
  }
55
56
 
56
- async function hookContext(projectRoot, eventName, context, payload) {
57
+ async function hookContext(projectRoot, eventName, context, payload, language) {
57
58
  const notice = await updateNoticeContext(projectRoot, eventName, {
58
59
  session: payload.session_id || payload.sessionId,
60
+ language,
59
61
  });
60
62
  return [context, notice].filter(Boolean).join('\n\n');
61
63
  }
@@ -85,7 +87,7 @@ async function handleAnswerFirstStop(projectRoot, eventName, payload, entry) {
85
87
  const archiveEntry = {
86
88
  ...entry,
87
89
  followUp: classification.suggestDurable
88
- ? ' turn 저장할 만한 내용이 있어 보입니다. 사용자가 승인하면 기존 durable wiki 문서에 합친다.'
90
+ ? 'This turn may be worth preserving. If the user approves, merge it into an existing durable wiki page.'
89
91
  : entry.followUp,
90
92
  };
91
93
  const liveQaPath = await appendLiveQa(projectRoot, archiveEntry);
@@ -128,6 +130,10 @@ export async function handleHook(provider, explicitEvent) {
128
130
  const cwd = payload.cwd || process.cwd();
129
131
  const projectRoot = await findProjectRoot(cwd);
130
132
  await bootstrapProject(projectRoot);
133
+ const requestLanguage = await resolveLanguage(projectRoot, {
134
+ provider,
135
+ prompt: eventName === 'UserPromptSubmit' ? promptText(payload) : '',
136
+ }).catch(() => 'en');
131
137
  await recordProject(projectRoot, 'hook').catch(() => {});
132
138
  await autoUpdateManagedProject(projectRoot, eventName).catch(() => {});
133
139
  await removeLegacyWikiSurfaces(projectRoot, eventName).catch(() => {});
@@ -143,8 +149,9 @@ export async function handleHook(provider, explicitEvent) {
143
149
  const context = await hookContext(
144
150
  projectRoot,
145
151
  eventName,
146
- [recovery, await buildContextBrief(projectRoot, 'SessionStart')].filter(Boolean).join('\n\n'),
147
- payload
152
+ [recovery, await buildContextBrief(projectRoot, 'SessionStart', '', { language: requestLanguage })].filter(Boolean).join('\n\n'),
153
+ payload,
154
+ requestLanguage
148
155
  );
149
156
  return contextOutput(provider, eventName, context);
150
157
  }
@@ -152,13 +159,14 @@ export async function handleHook(provider, explicitEvent) {
152
159
  if (eventName === 'UserPromptSubmit') {
153
160
  const prompt = promptText(payload);
154
161
  await rememberQuestion(projectRoot, payload, prompt);
155
- const guidance = formatDurableCaptureGuidance(prompt);
162
+ const guidance = formatDurableCaptureGuidance(prompt, { language: requestLanguage });
156
163
  const recovery = await consumeCompactRecoveryContext(projectRoot, payload).catch(() => '');
157
164
  const context = await hookContext(
158
165
  projectRoot,
159
166
  eventName,
160
- [recovery, await buildContextBrief(projectRoot, eventName, prompt), guidance].filter(Boolean).join('\n\n'),
161
- payload
167
+ [recovery, await buildContextBrief(projectRoot, eventName, prompt, { language: requestLanguage }), guidance].filter(Boolean).join('\n\n'),
168
+ payload,
169
+ requestLanguage
162
170
  );
163
171
  return contextOutput(provider, eventName, context);
164
172
  }
@@ -0,0 +1,73 @@
1
+ import { join } from 'path';
2
+ import { homeDir, readJson, readText } from './fs-utils.js';
3
+
4
+ const HANGUL_RE = /[\u3131-\u318e\uac00-\ud7a3]/g;
5
+ const LATIN_RE = /[A-Za-z]/g;
6
+
7
+ export function normalizeLanguage(value) {
8
+ const text = String(value || '').trim().toLowerCase();
9
+ if (!text) return null;
10
+ if (/^(ko|kor|korean|한국어|한국|한글)$/.test(text)) return 'ko';
11
+ if (/^(en|eng|english|영어)$/.test(text)) return 'en';
12
+ if (text.includes('korean') || text.includes('한국')) return 'ko';
13
+ if (text.includes('english') || text.includes('영어')) return 'en';
14
+ return null;
15
+ }
16
+
17
+ export function detectTextLanguage(value) {
18
+ const text = String(value || '').normalize('NFC');
19
+ if (!text.trim()) return null;
20
+ const hangul = (text.match(HANGUL_RE) || []).length;
21
+ const latin = (text.match(LATIN_RE) || []).length;
22
+ if (hangul >= 2) return 'ko';
23
+ if (hangul === 1 && latin < 12) return 'ko';
24
+ if (latin >= 3 && hangul === 0) return 'en';
25
+ return null;
26
+ }
27
+
28
+ async function claudeSettingsLanguage(projectRoot) {
29
+ const paths = [
30
+ join(homeDir(), '.claude', 'settings.json'),
31
+ join(projectRoot, '.claude', 'settings.json'),
32
+ join(projectRoot, '.claude', 'settings.local.json'),
33
+ ];
34
+ let language = null;
35
+ for (const path of paths) {
36
+ const settings = await readJson(path, null);
37
+ const next = normalizeLanguage(settings?.language);
38
+ if (next) language = next;
39
+ }
40
+ return language;
41
+ }
42
+
43
+ async function instructionFileLanguage(projectRoot) {
44
+ const paths = [
45
+ join(projectRoot, 'CLAUDE.md'),
46
+ join(projectRoot, '.claude', 'CLAUDE.md'),
47
+ join(projectRoot, 'AGENTS.md'),
48
+ join(projectRoot, 'llm-wiki', 'AGENTS.md'),
49
+ ];
50
+ for (const path of paths) {
51
+ const text = await readText(path, '');
52
+ const explicit = text.match(/(?:preferred\s+language|response\s+language|language|언어|응답\s*언어)\s*[::-]\s*([^\n]+)/i);
53
+ const normalized = normalizeLanguage(explicit?.[1]);
54
+ if (normalized) return normalized;
55
+ const detected = detectTextLanguage(text.slice(0, 4000));
56
+ if (detected) return detected;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ export async function resolveLanguage(projectRoot, options = {}) {
62
+ const promptLanguage = detectTextLanguage(options.prompt || '');
63
+ if (promptLanguage) return promptLanguage;
64
+ if (String(options.provider || '').toLowerCase() === 'claude') {
65
+ const settingsLanguage = await claudeSettingsLanguage(projectRoot);
66
+ if (settingsLanguage) return settingsLanguage;
67
+ }
68
+ return (await instructionFileLanguage(projectRoot)) || 'en';
69
+ }
70
+
71
+ export function languageName(language) {
72
+ return language === 'ko' ? 'Korean' : 'English';
73
+ }
package/src/live-qa.js CHANGED
@@ -114,7 +114,7 @@ export function formatLiveQaBlock(entry) {
114
114
  entry.verification || '(not captured)',
115
115
  '',
116
116
  '### Follow-up',
117
- entry.followUp || '작업/결정 중심으로 저장된 live Q&A 기록이다. 재사용 가능한 사실은 사용자가 원하거나 명확히 중요할 때만 기존 durable wiki 문서에 합친다.',
117
+ entry.followUp || 'Work/decision-focused live Q&A record. Merge reusable facts into existing durable wiki pages only when the user asks or the fact is clearly important.',
118
118
  '',
119
119
  ].join('\n');
120
120
  }
@@ -22,7 +22,7 @@ function queueHeader() {
22
22
  return [
23
23
  '# LLM Wiki Maintenance Queue',
24
24
  '',
25
- '정식 wiki 문서에 합칠 후보를 보관한다. Hook은 후보만 만들고, agent 기존 durable wiki 문서를 찾아 병합한다.',
25
+ 'Candidates to merge into durable wiki pages. Hooks only create candidates; the active agent reviews and merges them into existing durable wiki documents.',
26
26
  '',
27
27
  'Status values: pending, done, skipped.',
28
28
  '',
@@ -309,6 +309,7 @@ export async function recoverStaleTurnStates(projectRoot, options = {}) {
309
309
 
310
310
  export function formatMaintenanceContext(summary, options = {}) {
311
311
  if (!summary.reviewDue) return '';
312
+ const language = options.language === 'ko' ? 'ko' : 'en';
312
313
  const eventName = options.eventName || '';
313
314
  const defaultLimit = eventName === 'SessionStart' || eventName === 'InstructionsLoaded' ? 1 : 5;
314
315
  const limit = options.limit || defaultLimit;
@@ -321,13 +322,17 @@ export function formatMaintenanceContext(summary, options = {}) {
321
322
  return '';
322
323
  }
323
324
 
324
- const lines = [
325
- eventName === 'UserPromptSubmit'
326
- ? 'LLM Wiki maintenance status:'
327
- : 'LLM Wiki maintenance status:',
328
- `- review due: yes (${(summary.reviewReasons || []).slice(0, 2).join('; ') || 'periodic review threshold met'}).`,
329
- `- pending review items: ${summary.pendingCount}. 현재 요청이 우선이며, 관련 있을 때만 durable wiki 정리에 사용한다.`,
330
- ];
325
+ const lines = language === 'ko'
326
+ ? [
327
+ 'LLM Wiki maintenance status:',
328
+ `- review due: yes (${(summary.reviewReasons || []).slice(0, 2).join('; ') || 'periodic review threshold met'}).`,
329
+ `- pending review items: ${summary.pendingCount}. 현재 요청이 우선이며, 관련 있을 때만 durable wiki 정리에 사용한다.`,
330
+ ]
331
+ : [
332
+ 'LLM Wiki maintenance status:',
333
+ `- review due: yes (${(summary.reviewReasons || []).slice(0, 2).join('; ') || 'periodic review threshold met'}).`,
334
+ `- pending review items: ${summary.pendingCount}. The current request comes first; use this only when it is relevant to durable wiki cleanup.`,
335
+ ];
331
336
  for (const item of pending) {
332
337
  lines.push(`- ${item.topic || item.id}: ${item.suggested_target}; source=${item.source}${item.result_missing ? '; result missing' : ''}`);
333
338
  }
@@ -436,22 +436,33 @@ export async function inspectProjectState(projectRoot) {
436
436
  };
437
437
  }
438
438
 
439
- export function formatProjectMaintenanceContext(inspection) {
439
+ export function formatProjectMaintenanceContext(inspection, options = {}) {
440
440
  const files = inspection?.managedFiles || [];
441
441
  const attention = files.filter((file) => file.needsAttention);
442
442
  const outdated = files.filter((file) => !file.current && file.patchable);
443
443
  if (attention.length === 0 && outdated.length === 0) return '';
444
-
445
- const lines = [
446
- 'LLM Wiki maintenance note:',
447
- '- 이전 버전의 llm-wiki-kit 규칙/문서가 남아 있을 수 있다.',
448
- '- 확실히 kit가 관리하는 영역은 자동 갱신된다. 사용자 편집 가능성이 있는 문서는 덮어쓰지 말고 기존 내용을 보존한 채 현재 규칙에 맞게 자연스럽게 정리한다.',
449
- ];
444
+ const language = options.language === 'ko' ? 'ko' : 'en';
445
+
446
+ const lines = language === 'ko'
447
+ ? [
448
+ 'LLM Wiki maintenance note:',
449
+ '- 이전 버전의 llm-wiki-kit 규칙/문서가 남아 있을 수 있다.',
450
+ '- 확실히 kit가 관리하는 영역은 자동 갱신된다. 사용자 편집 가능성이 있는 문서는 덮어쓰지 말고 기존 내용을 보존한 채 현재 규칙에 맞게 자연스럽게 정리한다.',
451
+ ]
452
+ : [
453
+ 'LLM Wiki maintenance note:',
454
+ '- Older llm-wiki-kit rules or docs may still be present.',
455
+ '- Clearly managed kit areas are refreshed automatically. Preserve user-edited files and align them with current rules without overwriting local content.',
456
+ ];
450
457
  if (outdated.length > 0) {
451
- lines.push(`- 자동 갱신 대상: ${outdated.map((file) => file.path).join(', ')}`);
458
+ lines.push(language === 'ko'
459
+ ? `- 자동 갱신 대상: ${outdated.map((file) => file.path).join(', ')}`
460
+ : `- auto-refresh candidates: ${outdated.map((file) => file.path).join(', ')}`);
452
461
  }
453
462
  if (attention.length > 0) {
454
- lines.push(`- agent 확인 필요: ${attention.map((file) => file.path).join(', ')}`);
463
+ lines.push(language === 'ko'
464
+ ? `- agent 확인 필요: ${attention.map((file) => file.path).join(', ')}`
465
+ : `- needs agent review: ${attention.map((file) => file.path).join(', ')}`);
455
466
  }
456
467
  return lines.join('\n');
457
468
  }
package/src/project.js CHANGED
@@ -136,19 +136,21 @@ export async function searchWiki(projectRoot, query, limit = 5) {
136
136
  return searchWikiWithIndex(projectRoot, query, typeof limit === 'number' ? { limit } : limit);
137
137
  }
138
138
 
139
- export async function buildContextBrief(projectRoot, eventName, query = '') {
139
+ export async function buildContextBrief(projectRoot, eventName, query = '', options = {}) {
140
+ const language = options.language === 'ko' ? 'ko' : 'en';
140
141
  const pack = await buildContextPack(projectRoot, query, {
141
142
  includeLog: eventName === 'SessionStart',
142
143
  limit: 5,
143
144
  });
144
145
  const maintenance = await inspectProjectState(projectRoot)
145
- .then(formatProjectMaintenanceContext)
146
+ .then((inspection) => formatProjectMaintenanceContext(inspection, { language }))
146
147
  .catch(() => '');
147
148
  const wikiMaintenance = await maintenanceSummary(projectRoot)
148
- .then((summary) => formatMaintenanceContext(summary, { eventName, query }))
149
+ .then((summary) => formatMaintenanceContext(summary, { eventName, query, language }))
149
150
  .catch(() => '');
150
151
  return [formatHookContextPack(pack, {
151
152
  eventName,
153
+ language,
152
154
  hitLimit: eventName === 'SessionStart' ? 2 : 3,
153
155
  logLineLimit: eventName === 'SessionStart' ? 2 : 0,
154
156
  }), maintenance, wikiMaintenance].filter(Boolean).join('\n\n');