llm-wiki-kit 0.2.11 → 0.2.12
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 +18 -6
- package/docs/integrations/claude-code.md +2 -0
- package/docs/integrations/codex.md +1 -0
- package/docs/manual.md +132 -396
- package/package.json +12 -2
- package/src/capture-policy.js +11 -4
- package/src/cli.js +2 -2
- package/src/hook.js +15 -7
- package/src/language.js +73 -0
- package/src/live-qa.js +1 -1
- package/src/maintenance.js +13 -8
- package/src/project-state.js +20 -9
- package/src/project.js +5 -3
- package/src/templates.js +89 -82
- package/src/update-notice.js +14 -4
- package/src/wiki-search.js +35 -13
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-wiki-kit",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Hook-first living
|
|
3
|
+
"version": "0.2.12",
|
|
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",
|
package/src/capture-policy.js
CHANGED
|
@@ -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
|
-
'-
|
|
153
|
-
'-
|
|
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 ? [`-
|
|
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,7 +341,7 @@ function formatUpdate(value) {
|
|
|
341
341
|
`- installed: ${value.installedVersion}`,
|
|
342
342
|
`- latest: ${value.latestVersion}`,
|
|
343
343
|
`- update available: ${value.updateAvailable ? 'yes' : 'no'}`,
|
|
344
|
-
...(value.updateAvailable ? [`-
|
|
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}`,
|
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
|
-
? '
|
|
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
|
}
|
package/src/language.js
ADDED
|
@@ -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 || '
|
|
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
|
}
|
package/src/maintenance.js
CHANGED
|
@@ -22,7 +22,7 @@ function queueHeader() {
|
|
|
22
22
|
return [
|
|
23
23
|
'# LLM Wiki Maintenance Queue',
|
|
24
24
|
'',
|
|
25
|
-
'
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
}
|
package/src/project-state.js
CHANGED
|
@@ -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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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(
|
|
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(
|
|
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');
|