llm-wiki-kit 0.2.17 → 0.2.18
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 +3 -1
- package/docs/integrations/claude-code.md +1 -1
- package/docs/integrations/codex.md +1 -1
- package/docs/manual.md +3 -1
- package/docs/operations.md +2 -0
- package/package.json +1 -1
- package/src/consolidate.js +19 -13
- package/src/hook.js +26 -8
- package/src/language.js +15 -0
- package/src/live-qa.js +34 -11
- package/src/project.js +48 -2
- package/src/state.js +43 -1
- package/src/templates.js +3 -2
- package/src/wiki-search.js +8 -8
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ The goal is not to make users run `ingest` or `record` commands. After install,
|
|
|
10
10
|
|
|
11
11
|
- Korean user prompt -> Korean hook guidance.
|
|
12
12
|
- English user prompt -> English hook guidance.
|
|
13
|
+
- Codex synthetic plan execution prompts such as `Implement the plan.` are treated as neutral, so they reuse the last real session language or project preference instead of forcing English.
|
|
14
|
+
- Project `.kit-state.json` may set `preferredLanguage` to `ko` or `en` for neutral prompts.
|
|
13
15
|
- If there is no clear current prompt language, Claude Code `settings.json` `language` is used when present.
|
|
14
16
|
- If that setting is missing, local `CLAUDE.md` and `AGENTS.md` language signals are used.
|
|
15
17
|
- The fallback is English.
|
|
@@ -100,7 +102,7 @@ Use Claude Code or Codex normally.
|
|
|
100
102
|
The installed hooks:
|
|
101
103
|
|
|
102
104
|
- inject functional compact context at session start, instructions loaded, and prompt submit. The hook still uses `wiki/memory.md`, `wiki/index.md`, relevant wiki search results, maintenance signals, update status, and any compact recovery packet; it formats only the useful parts so user-visible hook context does not look like a raw debug dump.
|
|
103
|
-
- automatically choose Korean or English hook guidance from the current user prompt, then fall back to Claude Code `language`, local `CLAUDE.md`/`AGENTS.md`, and English.
|
|
105
|
+
- automatically choose Korean or English hook guidance from the current real user prompt, keep Codex synthetic plan execution prompts neutral, then fall back to remembered session language, project `preferredLanguage`, Claude Code `language`, local `CLAUDE.md`/`AGENTS.md`, and English.
|
|
104
106
|
- remove Codex-facing legacy `oh-my-codex:wiki`/`omx_wiki` surfaces at session start so `llm-wiki/` remains the active wiki implementation
|
|
105
107
|
- record small redacted raw event envelopes and per-turn state
|
|
106
108
|
- capture meaningful work and structured decision points, including tool evidence, changed files, verification notes, and reusable durable-candidate signals
|
|
@@ -44,7 +44,7 @@ The hook records redacted turn summaries but does not deny tool calls only becau
|
|
|
44
44
|
|
|
45
45
|
At `SessionStart`/`InstructionsLoaded`, the hook first attempts a safe managed-template refresh, recovers stale turn state into `outputs/maintenance/queue.md`, applies deterministic maintenance lifecycle hygiene for the current workspace, performs a cached npm update notice check for npm installs, then injects functional compact context. The context still uses `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, relevant wiki/search state, operating rules, maintenance signals, passive runtime update status, and managed-template cleanup notes; the hook formats those signals so they are usable if shown in the Claude Code UI. 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 functional compact context set. Verbose `llm-wiki context` can explain `why selected`, `rankReason`, `matchedFields`, and `evidenceRefs`, but hook context keeps those details compact. Update notice cache is scoped by npm command, and maintenance reminders are shown for wiki/maintenance prompts, queue topic matches, approved items, durable candidates, stale/recovered items, or review-threshold pressure.
|
|
46
46
|
|
|
47
|
-
Hook-visible language is selected from the current user prompt first. Korean prompts get Korean guidance, English prompts get English guidance.
|
|
47
|
+
Hook-visible language is selected from the current real user prompt first. Korean prompts get Korean guidance, English prompts get English guidance. Neutral prompts can reuse remembered session language or project `preferredLanguage`; then the hook checks Claude Code `settings.json` `language` when it exists, local `CLAUDE.md`/`AGENTS.md` language signals, and finally English. The kit does not require Claude Code to expose a language setting.
|
|
48
48
|
|
|
49
49
|
`PostToolUse` and `PostToolBatch` record redacted tool summaries in the same turn buffer. `PreCompact` classifies the current turn before compaction: simple turns record only a context note, work-evidence, structured-decision, explicit durable, or hook-suggested durable turns write a chunked live Q&A checkpoint, and durable candidates write a maintenance queue item only when no durable wiki update is detected. Queue items carry `signal_level` and may carry safe `evidence_refs` candidates from actual repo files and verification commands. The checkpoint can include only a bounded redacted transcript tail, never the full raw transcript or raw `transcript_path`. Compaction is not blocked; if checkpoint storage fails, the hook records a compact recovery packet for the next legal context-injection event. `PostCompact` stores the redacted compact summary as a context note and prepares any pending recovery packet without returning model-visible context directly. In the default `answer-first` mode, `SubagentStop` does not create live Q&A, query, decision, or maintenance files. `Stop` and `SessionEnd` append chunked live Q&A only for work-evidence, structured-decision, or durable-candidate turns and do not auto-create `wiki/queries/` or `wiki/decisions/`. If the user explicitly asked to record durable knowledge, or the turn contains reusable architecture/debugging/policy/procedure/decision signals, and no durable wiki update is detected, `Stop`/`SessionEnd` queue a pending maintenance item for agent review. The lifecycle can move explicit/high-signal items to `approved`, skip stale low-signal items, and archive old reviewed items, but it never creates durable wiki pages automatically. Approved and durable-candidate maintenance items are surfaced as compact soft reminders. `Stop` and `SessionEnd` then clear the per-session turn buffer; `SubagentStop` does not.
|
|
50
50
|
|
|
@@ -31,7 +31,7 @@ Expected behavior:
|
|
|
31
31
|
|
|
32
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`, applies deterministic maintenance lifecycle hygiene for the current workspace, performs a cached npm update notice check for npm installs, then injects functional compact context. The context still uses `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, relevant wiki/search state, operating rules, maintenance signals, passive runtime update status, and managed-template cleanup notes; the hook formats those signals so they are usable if shown in the Codex UI.
|
|
33
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 functional compact context set. Verbose `llm-wiki context` can explain `why selected`, `rankReason`, `matchedFields`, and `evidenceRefs`, but hook context keeps those details compact. Update notice cache is scoped by npm command, and maintenance reminders are shown for wiki/maintenance prompts, queue topic matches, approved items, durable candidates, stale/recovered items, or review-threshold pressure.
|
|
34
|
-
- Hook-visible language is selected from the current user prompt first. Korean prompts get Korean guidance, English prompts get English guidance. If no prompt language is clear, Codex falls back to local `CLAUDE.md`/`AGENTS.md` language signals, then English.
|
|
34
|
+
- Hook-visible language is selected from the current real user prompt first. Korean prompts get Korean guidance, English prompts get English guidance. Codex synthetic plan execution prompts such as `Implement the plan.` are treated as neutral, so they reuse remembered session language or project `preferredLanguage` instead of forcing English. If no prompt language is clear, Codex falls back to local `CLAUDE.md`/`AGENTS.md` language signals, then English.
|
|
35
35
|
- `PreToolUse` records redacted tool summaries without blocking tool calls.
|
|
36
36
|
- `PostToolUse` records redacted tool summaries in a turn buffer.
|
|
37
37
|
- `PreCompact` classifies the current turn before compaction. Simple turns record only a context note; work-evidence, structured-decision, explicit durable, or hook-suggested durable turns write a chunked live Q&A checkpoint; durable candidates write a maintenance queue item only when no durable wiki update is detected. The checkpoint can include only a bounded redacted transcript tail, never the full raw transcript or raw `transcript_path`. Compaction is not blocked; if checkpoint storage fails, the hook records a compact recovery packet for the next legal context-injection event.
|
package/docs/manual.md
CHANGED
|
@@ -10,6 +10,8 @@ The runtime supports seamless Korean/English use:
|
|
|
10
10
|
|
|
11
11
|
- If the current user prompt is clearly Korean, hook-visible guidance is Korean.
|
|
12
12
|
- If the current user prompt is clearly English, hook-visible guidance is English.
|
|
13
|
+
- Codex synthetic plan execution prompts such as `Implement the plan.` are not treated as English; they reuse the last real session language or project preference.
|
|
14
|
+
- A project can set `preferredLanguage` in `llm-wiki/.kit-state.json` for neutral prompts.
|
|
13
15
|
- When there is no clear prompt language, Claude Code may use its `settings.json` `language` value when present.
|
|
14
16
|
- If no setting exists, the runtime checks local `CLAUDE.md` and `AGENTS.md` language signals.
|
|
15
17
|
- The fallback is English.
|
|
@@ -68,7 +70,7 @@ llm-wiki/
|
|
|
68
70
|
Use Codex or Claude Code normally. Installed hooks:
|
|
69
71
|
|
|
70
72
|
- inject functional compact context at session start, instructions loaded, and prompt submit;
|
|
71
|
-
- select Korean or English hook guidance from the current user prompt and local instruction files;
|
|
73
|
+
- select Korean or English hook guidance from the current real user prompt, remembered session language, project preference, and local instruction files;
|
|
72
74
|
- use `wiki/memory.md`, `wiki/index.md`, relevant wiki search, maintenance signals, update notices, and compact recovery packets;
|
|
73
75
|
- record redacted prompt/tool/result summaries in per-turn state;
|
|
74
76
|
- preserve safe evidence pointers as `evidence_refs` when changed files or verification commands are available;
|
package/docs/operations.md
CHANGED
|
@@ -296,6 +296,8 @@ Managed project files are conservative:
|
|
|
296
296
|
|
|
297
297
|
Real registry checks require `llm-wiki-kit` to be published to npm. Before publication, `llm-wiki update --check` returns npm 404. Use fake npm in tests or local tarball install for server smoke checks.
|
|
298
298
|
|
|
299
|
+
For Korean-first Codex projects, set `preferredLanguage: "ko"` in `llm-wiki/.kit-state.json` when neutral prompts should stay Korean. This is especially useful after Codex plan approval, because Codex may submit `Implement the plan.` as the next prompt and the runtime deliberately treats that exact synthetic prompt as language-neutral.
|
|
300
|
+
|
|
299
301
|
## Release Checklist
|
|
300
302
|
|
|
301
303
|
Before publishing:
|
package/package.json
CHANGED
package/src/consolidate.js
CHANGED
|
@@ -3,6 +3,7 @@ import { appendWikiLog } from './project.js';
|
|
|
3
3
|
import { backupFile, exists, readText, writeText } from './fs-utils.js';
|
|
4
4
|
import { hasSecretLikeText, isSensitivePath, normalizeForStorage } from './redaction.js';
|
|
5
5
|
import { indexPage, memoryPage } from './templates.js';
|
|
6
|
+
import { resolveLanguage } from './language.js';
|
|
6
7
|
import { collectWikiPages } from './wiki-model.js';
|
|
7
8
|
import { runLint } from './wiki-lint.js';
|
|
8
9
|
import {
|
|
@@ -99,22 +100,25 @@ function sortedPages(pages) {
|
|
|
99
100
|
});
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
function buildGeneratedMemoryBlock(pages, stats) {
|
|
103
|
+
function buildGeneratedMemoryBlock(pages, stats, options = {}) {
|
|
104
|
+
const ko = options.language === 'ko';
|
|
103
105
|
const allCandidates = sortedPages(pages);
|
|
104
106
|
const candidates = allCandidates.slice(0, 25);
|
|
105
107
|
stats.memoryPages = candidates.length;
|
|
106
108
|
const lines = [
|
|
107
|
-
'## Generated Memory Map',
|
|
109
|
+
ko ? '## 생성된 Memory Map' : '## Generated Memory Map',
|
|
108
110
|
'',
|
|
109
|
-
`- Durable pages indexed: ${allCandidates.length}`,
|
|
110
|
-
`- Hidden episodic pages: ${stats.hiddenEpisodicPages}`,
|
|
111
|
-
|
|
111
|
+
ko ? `- 색인된 durable 문서: ${allCandidates.length}` : `- Durable pages indexed: ${allCandidates.length}`,
|
|
112
|
+
ko ? `- 숨긴 episodic 문서: ${stats.hiddenEpisodicPages}` : `- Hidden episodic pages: ${stats.hiddenEpisodicPages}`,
|
|
113
|
+
ko
|
|
114
|
+
? `- 건너뛴 archived/stale/superseded 문서: ${stats.archivedSkippedPages}/${stats.staleSkippedPages}/${stats.supersededSkippedPages}`
|
|
115
|
+
: `- Skipped archived/stale/superseded pages: ${stats.archivedSkippedPages}/${stats.staleSkippedPages}/${stats.supersededSkippedPages}`,
|
|
112
116
|
];
|
|
113
117
|
if (candidates.length === 0) {
|
|
114
|
-
lines.push('- No durable pages found yet.');
|
|
118
|
+
lines.push(ko ? '- 아직 durable 문서가 없습니다.' : '- No durable pages found yet.');
|
|
115
119
|
return lines.join('\n');
|
|
116
120
|
}
|
|
117
|
-
lines.push('', '### High-Value Pages');
|
|
121
|
+
lines.push('', ko ? '### 핵심 문서' : '### High-Value Pages');
|
|
118
122
|
for (const page of candidates) {
|
|
119
123
|
const metadata = [
|
|
120
124
|
page.type || 'unknown',
|
|
@@ -127,7 +131,8 @@ function buildGeneratedMemoryBlock(pages, stats) {
|
|
|
127
131
|
return lines.join('\n');
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
function buildGeneratedIndexBlock(pages) {
|
|
134
|
+
function buildGeneratedIndexBlock(pages, options = {}) {
|
|
135
|
+
const ko = options.language === 'ko';
|
|
131
136
|
const groups = new Map();
|
|
132
137
|
const candidates = sortedPages(pages);
|
|
133
138
|
for (const page of candidates) {
|
|
@@ -137,12 +142,12 @@ function buildGeneratedIndexBlock(pages) {
|
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
const lines = [
|
|
140
|
-
'## Generated Page Map',
|
|
145
|
+
ko ? '## 생성된 문서 지도' : '## Generated Page Map',
|
|
141
146
|
'',
|
|
142
|
-
`- Durable pages indexed: ${candidates.length}`,
|
|
147
|
+
ko ? `- 색인된 durable 문서: ${candidates.length}` : `- Durable pages indexed: ${candidates.length}`,
|
|
143
148
|
];
|
|
144
149
|
if (groups.size === 0) {
|
|
145
|
-
lines.push('- No durable pages found yet.');
|
|
150
|
+
lines.push(ko ? '- 아직 durable 문서가 없습니다.' : '- No durable pages found yet.');
|
|
146
151
|
return lines.join('\n');
|
|
147
152
|
}
|
|
148
153
|
for (const type of [...groups.keys()].sort()) {
|
|
@@ -171,6 +176,7 @@ export async function runConsolidate(projectRoot, options = {}) {
|
|
|
171
176
|
const lintResult = await runLint(projectRoot, { maxFiles: options.maxFiles || 1000 });
|
|
172
177
|
const pages = await collectWikiPages(projectRoot, { maxFiles: options.maxFiles || 1000 });
|
|
173
178
|
const { candidates, stats } = classifyGeneratedPages(pages);
|
|
179
|
+
const language = await resolveLanguage(projectRoot, {}).catch(() => 'en');
|
|
174
180
|
const changed = [];
|
|
175
181
|
const skipped = [];
|
|
176
182
|
|
|
@@ -180,7 +186,7 @@ export async function runConsolidate(projectRoot, options = {}) {
|
|
|
180
186
|
memoryPage(),
|
|
181
187
|
MEMORY_START,
|
|
182
188
|
MEMORY_END,
|
|
183
|
-
buildGeneratedMemoryBlock(candidates, stats),
|
|
189
|
+
buildGeneratedMemoryBlock(candidates, stats, { language }),
|
|
184
190
|
options,
|
|
185
191
|
);
|
|
186
192
|
if (memoryChange?.changed) changed.push(memoryChange.changed);
|
|
@@ -192,7 +198,7 @@ export async function runConsolidate(projectRoot, options = {}) {
|
|
|
192
198
|
indexPage(),
|
|
193
199
|
INDEX_START,
|
|
194
200
|
INDEX_END,
|
|
195
|
-
buildGeneratedIndexBlock(candidates),
|
|
201
|
+
buildGeneratedIndexBlock(candidates, { language }),
|
|
196
202
|
options,
|
|
197
203
|
);
|
|
198
204
|
if (indexChange?.changed) changed.push(indexChange.changed);
|
package/src/hook.js
CHANGED
|
@@ -6,10 +6,10 @@ import { applyMaintenanceLifecycle, recoverStaleTurnStates, recordMaintenanceFor
|
|
|
6
6
|
import { applyProjectTemplateUpdate, inspectProjectState } from './project-state.js';
|
|
7
7
|
import { recordProject } from './projects.js';
|
|
8
8
|
import { summarizeForStorage } from './redaction.js';
|
|
9
|
-
import { buildEntryFromState, clearTurnState, rememberQuestion, rememberTool } from './state.js';
|
|
9
|
+
import { buildEntryFromState, clearTurnState, readRememberedLanguage, rememberLanguage, 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
|
+
import { detectTextLanguage, isSyntheticPlanPrompt, resolveLanguage } from './language.js';
|
|
13
13
|
import { relative } from 'path';
|
|
14
14
|
|
|
15
15
|
async function readStdinJson() {
|
|
@@ -30,6 +30,11 @@ function promptText(payload) {
|
|
|
30
30
|
return payload.prompt || payload.user_prompt || payload.userPrompt || payload.message || payload.input || '';
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function storagePromptText(prompt, language) {
|
|
34
|
+
if (!isSyntheticPlanPrompt(prompt)) return prompt;
|
|
35
|
+
return language === 'ko' ? '승인된 계획 실행' : 'Approved plan implementation.';
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
function toolSummary(payload) {
|
|
34
39
|
const toolName = payload.tool_name || payload.toolName || payload.name || payload.tool?.name || 'tool';
|
|
35
40
|
const input = payload.tool_input || payload.toolInput || payload.input || payload.arguments || payload.tool?.input || {};
|
|
@@ -145,10 +150,17 @@ export async function handleHook(provider, explicitEvent) {
|
|
|
145
150
|
const cwd = payload.cwd || process.cwd();
|
|
146
151
|
const projectRoot = await findProjectRoot(cwd);
|
|
147
152
|
await bootstrapProject(projectRoot);
|
|
153
|
+
const prompt = eventName === 'UserPromptSubmit' ? promptText(payload) : '';
|
|
154
|
+
const promptLanguage = eventName === 'UserPromptSubmit' ? detectTextLanguage(prompt) : null;
|
|
155
|
+
const rememberedLanguage = await readRememberedLanguage(projectRoot, payload).catch(() => null);
|
|
148
156
|
const requestLanguage = await resolveLanguage(projectRoot, {
|
|
149
157
|
provider,
|
|
150
|
-
prompt
|
|
158
|
+
prompt,
|
|
159
|
+
rememberedLanguage,
|
|
151
160
|
}).catch(() => 'en');
|
|
161
|
+
if (eventName === 'UserPromptSubmit' && promptLanguage) {
|
|
162
|
+
await rememberLanguage(projectRoot, payload, promptLanguage, 'prompt').catch(() => null);
|
|
163
|
+
}
|
|
152
164
|
await recordProject(projectRoot, 'hook').catch(() => {});
|
|
153
165
|
await autoUpdateManagedProject(projectRoot, eventName).catch(() => {});
|
|
154
166
|
await removeLegacyWikiSurfaces(projectRoot, eventName).catch(() => {});
|
|
@@ -175,14 +187,17 @@ export async function handleHook(provider, explicitEvent) {
|
|
|
175
187
|
}
|
|
176
188
|
|
|
177
189
|
if (eventName === 'UserPromptSubmit') {
|
|
178
|
-
const
|
|
179
|
-
await rememberQuestion(projectRoot, payload,
|
|
180
|
-
|
|
190
|
+
const storedPrompt = storagePromptText(prompt, requestLanguage);
|
|
191
|
+
await rememberQuestion(projectRoot, payload, storedPrompt, {
|
|
192
|
+
language: requestLanguage,
|
|
193
|
+
skipIfExisting: isSyntheticPlanPrompt(prompt),
|
|
194
|
+
});
|
|
195
|
+
const guidance = formatDurableCaptureGuidance(storedPrompt, { language: requestLanguage });
|
|
181
196
|
const recovery = await consumeCompactRecoveryContext(projectRoot, payload).catch(() => '');
|
|
182
197
|
const context = await hookContext(
|
|
183
198
|
projectRoot,
|
|
184
199
|
eventName,
|
|
185
|
-
[recovery, await buildContextBrief(projectRoot, eventName,
|
|
200
|
+
[recovery, await buildContextBrief(projectRoot, eventName, storedPrompt, { language: requestLanguage }), guidance].filter(Boolean).join('\n\n'),
|
|
186
201
|
payload,
|
|
187
202
|
requestLanguage
|
|
188
203
|
);
|
|
@@ -210,7 +225,10 @@ export async function handleHook(provider, explicitEvent) {
|
|
|
210
225
|
|
|
211
226
|
if (eventName === 'Stop' || eventName === 'SubagentStop' || eventName === 'SessionEnd') {
|
|
212
227
|
const assistantText = payload.last_assistant_message || payload.response || payload.assistant_response || '';
|
|
213
|
-
const entry =
|
|
228
|
+
const entry = {
|
|
229
|
+
...await buildEntryFromState(projectRoot, payload, assistantText),
|
|
230
|
+
language: requestLanguage,
|
|
231
|
+
};
|
|
214
232
|
if (isLegacyEagerCaptureMode()) {
|
|
215
233
|
await handleLegacyEagerStop(projectRoot, eventName, payload, entry);
|
|
216
234
|
} else {
|
package/src/language.js
CHANGED
|
@@ -3,6 +3,7 @@ import { homeDir, readJson, readText } from './fs-utils.js';
|
|
|
3
3
|
|
|
4
4
|
const HANGUL_RE = /[\u3131-\u318e\uac00-\ud7a3]/g;
|
|
5
5
|
const LATIN_RE = /[A-Za-z]/g;
|
|
6
|
+
const SYNTHETIC_PLAN_PROMPT_RE = /^\s*implement\s+the\s+plan\.?\s*$/i;
|
|
6
7
|
|
|
7
8
|
export function normalizeLanguage(value) {
|
|
8
9
|
const text = String(value || '').trim().toLowerCase();
|
|
@@ -14,9 +15,14 @@ export function normalizeLanguage(value) {
|
|
|
14
15
|
return null;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
export function isSyntheticPlanPrompt(value) {
|
|
19
|
+
return SYNTHETIC_PLAN_PROMPT_RE.test(String(value || ''));
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
export function detectTextLanguage(value) {
|
|
18
23
|
const text = String(value || '').normalize('NFC');
|
|
19
24
|
if (!text.trim()) return null;
|
|
25
|
+
if (isSyntheticPlanPrompt(text)) return null;
|
|
20
26
|
const hangul = (text.match(HANGUL_RE) || []).length;
|
|
21
27
|
const latin = (text.match(LATIN_RE) || []).length;
|
|
22
28
|
if (hangul >= 2) return 'ko';
|
|
@@ -25,6 +31,11 @@ export function detectTextLanguage(value) {
|
|
|
25
31
|
return null;
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
async function projectPreferredLanguage(projectRoot) {
|
|
35
|
+
const state = await readJson(join(projectRoot, 'llm-wiki', '.kit-state.json'), null);
|
|
36
|
+
return normalizeLanguage(state?.preferredLanguage || state?.preferred_language);
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
async function claudeSettingsLanguage(projectRoot) {
|
|
29
40
|
const paths = [
|
|
30
41
|
join(homeDir(), '.claude', 'settings.json'),
|
|
@@ -61,6 +72,10 @@ async function instructionFileLanguage(projectRoot) {
|
|
|
61
72
|
export async function resolveLanguage(projectRoot, options = {}) {
|
|
62
73
|
const promptLanguage = detectTextLanguage(options.prompt || '');
|
|
63
74
|
if (promptLanguage) return promptLanguage;
|
|
75
|
+
const rememberedLanguage = normalizeLanguage(options.rememberedLanguage);
|
|
76
|
+
if (rememberedLanguage) return rememberedLanguage;
|
|
77
|
+
const preferredLanguage = await projectPreferredLanguage(projectRoot);
|
|
78
|
+
if (preferredLanguage) return preferredLanguage;
|
|
64
79
|
if (String(options.provider || '').toLowerCase() === 'claude') {
|
|
65
80
|
const settingsLanguage = await claudeSettingsLanguage(projectRoot);
|
|
66
81
|
if (settingsLanguage) return settingsLanguage;
|
package/src/live-qa.js
CHANGED
|
@@ -95,31 +95,52 @@ async function targetChunkPath(projectRoot, day, block, options = {}) {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
export function formatLiveQaBlock(entry) {
|
|
98
|
+
const korean = entry.language === 'ko';
|
|
99
|
+
const labels = korean
|
|
100
|
+
? {
|
|
101
|
+
question: '### 질문',
|
|
102
|
+
work: '### 작업',
|
|
103
|
+
result: '### 결과',
|
|
104
|
+
changedFiles: '### 변경 파일',
|
|
105
|
+
verification: '### 검증',
|
|
106
|
+
followUp: '### 후속 조치',
|
|
107
|
+
fallback: '작업/결정 중심 live Q&A 기록입니다. 재사용할 사실은 사용자가 요청했거나 명확히 중요할 때만 기존 durable wiki 문서에 병합합니다.',
|
|
108
|
+
}
|
|
109
|
+
: {
|
|
110
|
+
question: '### Question',
|
|
111
|
+
work: '### Work',
|
|
112
|
+
result: '### Result',
|
|
113
|
+
changedFiles: '### Changed files',
|
|
114
|
+
verification: '### Verification',
|
|
115
|
+
followUp: '### Follow-up',
|
|
116
|
+
fallback: '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.',
|
|
117
|
+
};
|
|
98
118
|
return [
|
|
99
119
|
`\n## ${timeKst()} KST - ${entry.topic || 'session turn'}`,
|
|
100
120
|
'',
|
|
101
|
-
|
|
121
|
+
labels.question,
|
|
102
122
|
entry.question || '(not captured)',
|
|
103
123
|
'',
|
|
104
|
-
|
|
124
|
+
labels.work,
|
|
105
125
|
entry.work || '(not captured)',
|
|
106
126
|
'',
|
|
107
|
-
|
|
127
|
+
labels.result,
|
|
108
128
|
entry.result || '(not captured)',
|
|
109
129
|
'',
|
|
110
|
-
|
|
130
|
+
labels.changedFiles,
|
|
111
131
|
entry.changedFiles || '(not captured)',
|
|
112
132
|
'',
|
|
113
|
-
|
|
133
|
+
labels.verification,
|
|
114
134
|
entry.verification || '(not captured)',
|
|
115
135
|
'',
|
|
116
|
-
|
|
117
|
-
entry.followUp ||
|
|
136
|
+
labels.followUp,
|
|
137
|
+
entry.followUp || labels.fallback,
|
|
118
138
|
'',
|
|
119
139
|
].join('\n');
|
|
120
140
|
}
|
|
121
141
|
|
|
122
|
-
export async function refreshLiveQaIndex(projectRoot, day = todayKst()) {
|
|
142
|
+
export async function refreshLiveQaIndex(projectRoot, day = todayKst(), options = {}) {
|
|
143
|
+
const korean = options.language === 'ko';
|
|
123
144
|
const dayDir = liveQaDayDir(projectRoot, day);
|
|
124
145
|
const files = await listChunkFiles(dayDir);
|
|
125
146
|
const rows = [];
|
|
@@ -134,13 +155,15 @@ export async function refreshLiveQaIndex(projectRoot, day = todayKst()) {
|
|
|
134
155
|
const content = [
|
|
135
156
|
`# Live Q&A ${day}`,
|
|
136
157
|
'',
|
|
137
|
-
|
|
158
|
+
korean
|
|
159
|
+
? '의미 있는 작업과 결정 turn을 날짜별 chunk로 보관하는 live Q&A archive입니다.'
|
|
160
|
+
: 'Chunked live Q&A archive for meaningful work and decision turns.',
|
|
138
161
|
'',
|
|
139
162
|
`- chunks: ${files.length}`,
|
|
140
163
|
`- turns: ${totalBlocks}`,
|
|
141
164
|
`- updated_at: ${new Date().toISOString()}`,
|
|
142
165
|
'',
|
|
143
|
-
'## Chunks',
|
|
166
|
+
korean ? '## 청크' : '## Chunks',
|
|
144
167
|
'',
|
|
145
168
|
rows.length > 0 ? rows.join('\n') : '(none)',
|
|
146
169
|
'',
|
|
@@ -153,7 +176,7 @@ export async function appendLiveQa(projectRoot, entry, options = {}) {
|
|
|
153
176
|
const block = redactText(formatLiveQaBlock(entry), 12000);
|
|
154
177
|
const path = await targetChunkPath(projectRoot, day, block, options);
|
|
155
178
|
await appendText(path, block);
|
|
156
|
-
await refreshLiveQaIndex(projectRoot, day);
|
|
179
|
+
await refreshLiveQaIndex(projectRoot, day, { language: entry.language });
|
|
157
180
|
return path;
|
|
158
181
|
}
|
|
159
182
|
|
package/src/project.js
CHANGED
|
@@ -109,7 +109,27 @@ export async function writeQueryPage(projectRoot, entry) {
|
|
|
109
109
|
const path = join(projectRoot, 'llm-wiki', 'wiki', 'queries', `${day}-${slug}.md`);
|
|
110
110
|
if (await exists(path)) return path;
|
|
111
111
|
const evidenceRefs = frontmatterEvidenceRefs(evidenceRefsFromEntry(entry, { projectRoot }));
|
|
112
|
-
const
|
|
112
|
+
const ko = entry.language === 'ko';
|
|
113
|
+
const labels = ko
|
|
114
|
+
? {
|
|
115
|
+
question: '질문',
|
|
116
|
+
answer: '답변 요약',
|
|
117
|
+
work: '작업 메모',
|
|
118
|
+
verification: '검증',
|
|
119
|
+
related: '관련 문서',
|
|
120
|
+
changeLog: '변경 기록',
|
|
121
|
+
captured: 'llm-wiki-kit hook이 자동 기록함.',
|
|
122
|
+
}
|
|
123
|
+
: {
|
|
124
|
+
question: 'Question',
|
|
125
|
+
answer: 'Answer Summary',
|
|
126
|
+
work: 'Work Notes',
|
|
127
|
+
verification: 'Verification',
|
|
128
|
+
related: 'Related Pages',
|
|
129
|
+
changeLog: 'Change Log',
|
|
130
|
+
captured: 'Captured automatically by llm-wiki-kit hook.',
|
|
131
|
+
};
|
|
132
|
+
const content = `---\ntitle: "${entry.topic || slug}"\ntype: "query"\nsource_ids: []\n${evidenceRefs}\nstatus: "draft"\nlast_updated: "${day}"\nconfidence: "medium"\nmemory_type: "episodic"\nimportance: 2\nlast_verified: "unknown"\nsupersedes: []\nsuperseded_by: []\n---\n\n# ${entry.topic || entry.question.slice(0, 80)}\n\n## ${labels.question}\n${entry.question}\n\n## ${labels.answer}\n${entry.result || '(not captured)'}\n\n## ${labels.work}\n${entry.work || '(not captured)'}\n\n## ${labels.verification}\n${entry.verification || '(not captured)'}\n\n## ${labels.related}\n- [[index]]\n\n## ${labels.changeLog}\n- ${day}: ${labels.captured}\n`;
|
|
113
133
|
await writeTextIfMissing(path, redactText(content, 12000));
|
|
114
134
|
return path;
|
|
115
135
|
}
|
|
@@ -123,7 +143,33 @@ export async function writeDecisionPage(projectRoot, entry) {
|
|
|
123
143
|
const path = join(projectRoot, 'llm-wiki', 'wiki', 'decisions', `${day}-${slug}.md`);
|
|
124
144
|
if (await exists(path)) return path;
|
|
125
145
|
const evidenceRefs = frontmatterEvidenceRefs(evidenceRefsFromEntry(entry, { projectRoot }));
|
|
126
|
-
const
|
|
146
|
+
const ko = entry.language === 'ko';
|
|
147
|
+
const labels = ko
|
|
148
|
+
? {
|
|
149
|
+
title: '결정',
|
|
150
|
+
decision: '결정',
|
|
151
|
+
context: '맥락',
|
|
152
|
+
evidence: '근거',
|
|
153
|
+
verification: '검증',
|
|
154
|
+
openQuestions: '열린 질문',
|
|
155
|
+
changeLog: '변경 기록',
|
|
156
|
+
fallbackDecision: '(assistant 응답에서 캡처됨; 검토 필요)',
|
|
157
|
+
fallbackOpen: '(기록 없음)',
|
|
158
|
+
captured: 'llm-wiki-kit hook이 자동 기록함.',
|
|
159
|
+
}
|
|
160
|
+
: {
|
|
161
|
+
title: 'Decision',
|
|
162
|
+
decision: 'Decision',
|
|
163
|
+
context: 'Context',
|
|
164
|
+
evidence: 'Evidence',
|
|
165
|
+
verification: 'Verification',
|
|
166
|
+
openQuestions: 'Open Questions',
|
|
167
|
+
changeLog: 'Change Log',
|
|
168
|
+
fallbackDecision: '(captured from assistant response; review needed)',
|
|
169
|
+
fallbackOpen: '(none captured)',
|
|
170
|
+
captured: 'Captured automatically by llm-wiki-kit hook.',
|
|
171
|
+
};
|
|
172
|
+
const content = `---\ntitle: "${entry.topic || slug}"\ntype: "decision"\nsource_ids: []\n${evidenceRefs}\nstatus: "draft"\nlast_updated: "${day}"\nconfidence: "medium"\nmemory_type: "semantic"\nimportance: 4\nlast_verified: "unknown"\nsupersedes: []\nsuperseded_by: []\n---\n\n# ${entry.topic || labels.title}\n\n## ${labels.decision}\n${entry.result || labels.fallbackDecision}\n\n## ${labels.context}\n${entry.question || '(not captured)'}\n\n## ${labels.evidence}\n${entry.work || '(not captured)'}\n\n## ${labels.verification}\n${entry.verification || '(not captured)'}\n\n## ${labels.openQuestions}\n${entry.followUp || labels.fallbackOpen}\n\n## ${labels.changeLog}\n- ${day}: ${labels.captured}\n`;
|
|
127
173
|
await writeTextIfMissing(path, redactText(content, 12000));
|
|
128
174
|
return path;
|
|
129
175
|
}
|
package/src/state.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
2
|
import { unlink } from 'fs/promises';
|
|
3
3
|
import { kitDataDir, readJson, sha256, writeJson } from './fs-utils.js';
|
|
4
|
+
import { normalizeLanguage } from './language.js';
|
|
4
5
|
import { summarizeForStorage } from './redaction.js';
|
|
5
6
|
|
|
6
7
|
export function sessionKey(projectRoot, payload) {
|
|
@@ -17,6 +18,18 @@ export function statePath(projectRoot, payload) {
|
|
|
17
18
|
return join(kitDataDir(), 'state', `${sessionKey(projectRoot, payload)}.json`);
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function projectLanguageKey(projectRoot) {
|
|
22
|
+
return sha256(projectRoot).slice(0, 32);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function sessionLanguagePath(projectRoot, payload) {
|
|
26
|
+
return join(kitDataDir(), 'language', `${sessionKey(projectRoot, payload)}.json`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function projectLanguagePath(projectRoot) {
|
|
30
|
+
return join(kitDataDir(), 'language', `${projectLanguageKey(projectRoot)}-project.json`);
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
export async function readTurnState(projectRoot, payload) {
|
|
21
34
|
return (await readJson(statePath(projectRoot, payload), null)) || {
|
|
22
35
|
projectRoot,
|
|
@@ -54,12 +67,40 @@ export async function clearCompactRecovery(projectRoot, payload) {
|
|
|
54
67
|
await unlink(compactRecoveryPath(projectRoot, payload)).catch(() => {});
|
|
55
68
|
}
|
|
56
69
|
|
|
57
|
-
export async function
|
|
70
|
+
export async function rememberLanguage(projectRoot, payload, language, source = 'prompt') {
|
|
71
|
+
const normalized = normalizeLanguage(language);
|
|
72
|
+
if (!normalized) return null;
|
|
73
|
+
const record = {
|
|
74
|
+
language: normalized,
|
|
75
|
+
source,
|
|
76
|
+
updated_at: new Date().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
await writeJson(sessionLanguagePath(projectRoot, payload), record);
|
|
79
|
+
await writeJson(projectLanguagePath(projectRoot), record);
|
|
80
|
+
return record;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function readRememberedLanguage(projectRoot, payload) {
|
|
84
|
+
const session = await readJson(sessionLanguagePath(projectRoot, payload), null);
|
|
85
|
+
const sessionLanguage = normalizeLanguage(session?.language);
|
|
86
|
+
if (sessionLanguage) return sessionLanguage;
|
|
87
|
+
const project = await readJson(projectLanguagePath(projectRoot), null);
|
|
88
|
+
return normalizeLanguage(project?.language);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function rememberQuestion(projectRoot, payload, prompt, options = {}) {
|
|
58
92
|
const state = await readTurnState(projectRoot, payload);
|
|
93
|
+
if (options.skipIfExisting && state.questions.length > 0) {
|
|
94
|
+
if (options.language) state.language = normalizeLanguage(options.language) || state.language;
|
|
95
|
+
await writeTurnState(projectRoot, payload, state);
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
59
98
|
const clean = summarizeForStorage(prompt, 3000);
|
|
60
99
|
if (clean) state.questions.push({ at: new Date().toISOString(), text: clean });
|
|
61
100
|
state.questions = state.questions.slice(-5);
|
|
101
|
+
if (options.language) state.language = normalizeLanguage(options.language) || state.language;
|
|
62
102
|
await writeTurnState(projectRoot, payload, state);
|
|
103
|
+
return state;
|
|
63
104
|
}
|
|
64
105
|
|
|
65
106
|
export async function rememberTool(projectRoot, payload, summary) {
|
|
@@ -112,5 +153,6 @@ export function buildEntryFromTurnState(state, assistantText = '') {
|
|
|
112
153
|
followUp: '',
|
|
113
154
|
firstTimestamp: questions[0]?.at || tools[0]?.at || state.updated_at,
|
|
114
155
|
session: state.session,
|
|
156
|
+
language: normalizeLanguage(state.language),
|
|
115
157
|
};
|
|
116
158
|
}
|
package/src/templates.js
CHANGED
|
@@ -9,7 +9,7 @@ This repository uses llm-wiki-kit as a hook-first living Markdown wiki for Codex
|
|
|
9
9
|
|
|
10
10
|
- This block supersedes older OMX/OMC/\`omx_wiki/\` LLM Wiki instructions for this repository.
|
|
11
11
|
- Use Codex or Claude Code normally. Do not create a workflow where users must remember extra \`llm-wiki\` commands during ordinary work.
|
|
12
|
-
- Language: respond in the user's current prompt language when it is clearly Korean or English. Keep technical identifiers, commands, paths, code, and original error text unchanged. If no prompt language is clear, follow local \`CLAUDE.md\`/\`AGENTS.md\` guidance, then fall back to English.
|
|
12
|
+
- Language: respond in the user's current real prompt language when it is clearly Korean or English. Codex synthetic plan execution prompts such as \`Implement the plan.\` are neutral and should reuse the last real session language or project preference. Keep technical identifiers, commands, paths, code, and original error text unchanged. If no prompt language is clear, follow local \`CLAUDE.md\`/\`AGENTS.md\` guidance, then fall back to English.
|
|
13
13
|
- Treat chat memory as temporary. Durable project knowledge belongs in repository Markdown under \`llm-wiki/\`.
|
|
14
14
|
- \`llm-wiki/raw/\` is the immutable or redacted evidence layer. Do not modify raw captures except safe hook envelope append.
|
|
15
15
|
- \`llm-wiki/wiki/\` is the curated knowledge layer managed by the agent. Put decisions, architecture, debugging findings, concepts, procedures, and context there.
|
|
@@ -46,7 +46,8 @@ The user should not need to run many \`llm-wiki\` commands manually. The agent c
|
|
|
46
46
|
These rules replace older OMX/OMC/\`omx_wiki/\` rules for this project.
|
|
47
47
|
|
|
48
48
|
## Language
|
|
49
|
-
- Follow the user's current prompt language when it is clearly Korean or English.
|
|
49
|
+
- Follow the user's current real prompt language when it is clearly Korean or English.
|
|
50
|
+
- Treat Codex synthetic plan execution prompts such as \`Implement the plan.\` as neutral; reuse the last real session language or project preference when available.
|
|
50
51
|
- If no current prompt language is clear, follow local \`CLAUDE.md\`/\`AGENTS.md\` guidance, then fall back to English.
|
|
51
52
|
- Keep commands, paths, code identifiers, API names, package names, logs, and original error text unchanged.
|
|
52
53
|
- Korean and English users should both be able to use Codex/Claude Code naturally without changing \`llm-wiki\` commands.
|
package/src/wiki-search.js
CHANGED
|
@@ -470,16 +470,16 @@ export function formatHookContextPack(pack, options = {}) {
|
|
|
470
470
|
const language = options.language === 'ko' ? 'ko' : 'en';
|
|
471
471
|
const lines = language === 'ko'
|
|
472
472
|
? [
|
|
473
|
-
'LLM Wiki context (
|
|
473
|
+
'LLM Wiki context (기능 요약):',
|
|
474
474
|
'- 우선순위: 현재 사용자 요청에 먼저 답하고, wiki context는 도움이 될 때만 사용한다.',
|
|
475
475
|
'- 언어: 현재 사용자 입력 언어를 따른다. 경로, 명령어, 코드 식별자, 오류 원문은 원래 언어를 유지한다.',
|
|
476
|
-
'- 기능 유지: hook memory/search/capture/maintenance/update
|
|
476
|
+
'- 기능 유지: hook memory/search/capture/maintenance/update 신호가 활성화되어 있다.',
|
|
477
477
|
'- 오래 남길 프로젝트 지식은 llm-wiki/wiki에 둔다. credentials, tokens, private keys, .env contents는 저장하지 않는다.',
|
|
478
478
|
]
|
|
479
479
|
: [
|
|
480
480
|
'LLM Wiki context (functional compact):',
|
|
481
481
|
'- Priority: answer the current user request first; use wiki context when it helps.',
|
|
482
|
-
'- Language: follow the current user prompt language. Keep paths, commands, code identifiers, and original error text unchanged.',
|
|
482
|
+
'- Language: follow the current real user prompt language. Keep paths, commands, code identifiers, and original error text unchanged.',
|
|
483
483
|
'- Functionality: hook memory/search/capture/maintenance/update signals are active.',
|
|
484
484
|
'- Durable project knowledge belongs in llm-wiki/wiki; never store credentials, tokens, private keys, or .env contents.',
|
|
485
485
|
];
|
|
@@ -490,12 +490,12 @@ export function formatHookContextPack(pack, options = {}) {
|
|
|
490
490
|
|
|
491
491
|
const memoryLines = memoryFocusLines(pack.memoryExcerpt, options.memoryLineLimit ?? 6);
|
|
492
492
|
if (memoryLines.length > 0) {
|
|
493
|
-
lines.push('', language === 'ko' ? '
|
|
493
|
+
lines.push('', language === 'ko' ? '메모리 핵심:' : 'Memory focus:');
|
|
494
494
|
lines.push(...memoryLines.map((line) => `- ${line.replace(/^-\s+/, '')}`));
|
|
495
495
|
} else {
|
|
496
496
|
lines.push(
|
|
497
497
|
'',
|
|
498
|
-
'Memory focus:',
|
|
498
|
+
language === 'ko' ? '메모리 핵심:' : 'Memory focus:',
|
|
499
499
|
language === 'ko'
|
|
500
500
|
? '- 프로젝트 맥락이 필요하면 llm-wiki/wiki/memory.md와 llm-wiki/wiki/index.md를 확인한다.'
|
|
501
501
|
: '- See llm-wiki/wiki/memory.md and llm-wiki/wiki/index.md when project context is needed.'
|
|
@@ -504,7 +504,7 @@ export function formatHookContextPack(pack, options = {}) {
|
|
|
504
504
|
|
|
505
505
|
const hits = hookHits(pack, options);
|
|
506
506
|
if (hits.length > 0) {
|
|
507
|
-
lines.push('', language === 'ko' ? '
|
|
507
|
+
lines.push('', language === 'ko' ? '관련 wiki 문서:' : 'Relevant wiki pages:');
|
|
508
508
|
for (const hit of hits) {
|
|
509
509
|
const suffix = hit.source === 'linked' && hit.via.length > 0
|
|
510
510
|
? `, via ${hit.via.join(', ')}`
|
|
@@ -514,7 +514,7 @@ export function formatHookContextPack(pack, options = {}) {
|
|
|
514
514
|
} else if (pack.query) {
|
|
515
515
|
lines.push(
|
|
516
516
|
'',
|
|
517
|
-
'Relevant wiki pages:',
|
|
517
|
+
language === 'ko' ? '관련 wiki 문서:' : 'Relevant wiki pages:',
|
|
518
518
|
language === 'ko'
|
|
519
519
|
? '- 직접 관련된 durable wiki 문서를 찾지 못했다. 정상적으로 답하고, 필요할 때만 파일/wiki를 확인한다.'
|
|
520
520
|
: '- No directly relevant durable wiki page found; answer normally and inspect files/wiki only if needed.'
|
|
@@ -523,7 +523,7 @@ export function formatHookContextPack(pack, options = {}) {
|
|
|
523
523
|
|
|
524
524
|
const logLines = logFocusLines(pack.logExcerpt, options.logLineLimit ?? 2);
|
|
525
525
|
if (logLines.length > 0) {
|
|
526
|
-
lines.push('', 'Recent log:');
|
|
526
|
+
lines.push('', language === 'ko' ? '최근 로그:' : 'Recent log:');
|
|
527
527
|
lines.push(...logLines.map((line) => `- ${line.replace(/^-\s+/, '')}`));
|
|
528
528
|
}
|
|
529
529
|
|