llm-wiki-kit 0.1.4 → 0.1.6

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.
@@ -0,0 +1,203 @@
1
+ import { join } from 'path';
2
+ import { appendWikiLog } from './project.js';
3
+ import { backupFile, exists, readText, writeText } from './fs-utils.js';
4
+ import { hasSecretLikeText, isSensitivePath, normalizeForStorage } from './redaction.js';
5
+ import { indexPage, memoryPage } from './templates.js';
6
+ import { collectWikiPages } from './wiki-model.js';
7
+ import { runLint } from './wiki-lint.js';
8
+
9
+ export const MEMORY_START = '<!-- llm-wiki-kit:memory-start -->';
10
+ export const MEMORY_END = '<!-- llm-wiki-kit:memory-end -->';
11
+ export const INDEX_START = '<!-- llm-wiki-kit:index-start -->';
12
+ export const INDEX_END = '<!-- llm-wiki-kit:index-end -->';
13
+
14
+ function replaceOrAppendBlock(content, startMarker, endMarker, block) {
15
+ const replacement = `${startMarker}\n${block.trim()}\n${endMarker}`;
16
+ const firstStart = content.indexOf(startMarker);
17
+ const firstEnd = content.indexOf(endMarker);
18
+ if (firstStart === -1 && firstEnd === -1) {
19
+ return { next: `${content.trimEnd()}\n\n${replacement}\n`, malformed: false };
20
+ }
21
+ const lastStart = content.lastIndexOf(startMarker);
22
+ const lastEnd = content.lastIndexOf(endMarker);
23
+ if (firstStart === -1 || firstEnd === -1 || firstEnd < firstStart || lastStart > lastEnd) {
24
+ return { next: content, malformed: true };
25
+ }
26
+ return {
27
+ next: `${content.slice(0, firstStart)}${replacement}${content.slice(lastEnd + endMarker.length)}`,
28
+ malformed: false,
29
+ };
30
+ }
31
+
32
+ function pageTarget(page) {
33
+ return page.rel.replace(/^wiki\//, '').replace(/\.md$/i, '');
34
+ }
35
+
36
+ function pageReference(page) {
37
+ return `[[${pageTarget(page)}|${page.title}]]`;
38
+ }
39
+
40
+ function importance(page) {
41
+ const value = Number(page.frontmatter.importance || 0);
42
+ return Number.isFinite(value) ? value : 0;
43
+ }
44
+
45
+ function isPromotableEpisodicPage(page) {
46
+ return ['semantic', 'procedural'].includes(page.memoryType) && importance(page) >= 4;
47
+ }
48
+
49
+ function isGeneratedBlockCandidate(page) {
50
+ if (['wiki/index.md', 'wiki/log.md', 'wiki/memory.md'].includes(page.rel)) return false;
51
+ if (isSensitivePath(page.rel) || isSensitivePath(page.title)) return false;
52
+ if (hasSecretLikeText(`${page.rel}\n${page.title}\n${page.content}`)) return false;
53
+ const type = String(page.type || '').toLowerCase();
54
+ if (
55
+ type === 'query' ||
56
+ type === 'context' ||
57
+ type === 'session-log' ||
58
+ page.rel.startsWith('wiki/queries/') ||
59
+ page.rel.startsWith('wiki/context/')
60
+ ) {
61
+ return isPromotableEpisodicPage(page);
62
+ }
63
+ return true;
64
+ }
65
+
66
+ function sortedPages(pages) {
67
+ return pages
68
+ .filter(isGeneratedBlockCandidate)
69
+ .sort((a, b) => {
70
+ const importanceDiff = importance(b) - importance(a);
71
+ if (importanceDiff !== 0) return importanceDiff;
72
+ const typeDiff = String(a.type || '').localeCompare(String(b.type || ''));
73
+ if (typeDiff !== 0) return typeDiff;
74
+ return a.rel.localeCompare(b.rel);
75
+ });
76
+ }
77
+
78
+ function buildGeneratedMemoryBlock(pages) {
79
+ const candidates = sortedPages(pages).slice(0, 25);
80
+ const lines = [
81
+ '## Generated Memory Map',
82
+ '',
83
+ `- Pages indexed: ${sortedPages(pages).length}`,
84
+ ];
85
+ if (candidates.length === 0) {
86
+ lines.push('- No durable pages found yet.');
87
+ return lines.join('\n');
88
+ }
89
+ lines.push('', '### High-Value Pages');
90
+ for (const page of candidates) {
91
+ const metadata = [
92
+ page.type || 'unknown',
93
+ page.memoryType ? `memory:${page.memoryType}` : '',
94
+ importance(page) > 0 ? `importance:${importance(page)}` : '',
95
+ page.confidence ? `confidence:${page.confidence}` : '',
96
+ ].filter(Boolean).join(', ');
97
+ lines.push(`- ${pageReference(page)}${metadata ? ` - ${metadata}` : ''}`);
98
+ }
99
+ return lines.join('\n');
100
+ }
101
+
102
+ function buildGeneratedIndexBlock(pages) {
103
+ const groups = new Map();
104
+ const candidates = sortedPages(pages);
105
+ for (const page of candidates) {
106
+ const type = page.type || 'uncategorized';
107
+ if (!groups.has(type)) groups.set(type, []);
108
+ groups.get(type).push(page);
109
+ }
110
+
111
+ const lines = [
112
+ '## Generated Page Map',
113
+ '',
114
+ `- Pages indexed: ${candidates.length}`,
115
+ ];
116
+ if (groups.size === 0) {
117
+ lines.push('- No durable pages found yet.');
118
+ return lines.join('\n');
119
+ }
120
+ for (const type of [...groups.keys()].sort()) {
121
+ lines.push('', `### ${type}`);
122
+ for (const page of groups.get(type).slice(0, 50)) {
123
+ lines.push(`- ${pageReference(page)}`);
124
+ }
125
+ }
126
+ return lines.join('\n');
127
+ }
128
+
129
+ async function updateMarkedFile(projectRoot, rel, initialContent, startMarker, endMarker, block, options) {
130
+ const absolutePath = join(projectRoot, rel);
131
+ const current = await readText(absolutePath, initialContent);
132
+ const { next, malformed } = replaceOrAppendBlock(current || initialContent, startMarker, endMarker, block);
133
+ if (malformed) return { skipped: { path: rel, reason: 'malformed-marker' } };
134
+ if (normalizeForStorage(current) === normalizeForStorage(next)) return null;
135
+ if (!options.dryRun) {
136
+ if (await exists(absolutePath)) await backupFile(absolutePath, rel);
137
+ await writeText(absolutePath, next);
138
+ }
139
+ return { changed: { path: rel } };
140
+ }
141
+
142
+ export async function runConsolidate(projectRoot, options = {}) {
143
+ const lintResult = await runLint(projectRoot, { maxFiles: options.maxFiles || 1000 });
144
+ const pages = await collectWikiPages(projectRoot, { maxFiles: options.maxFiles || 1000 });
145
+ const changed = [];
146
+ const skipped = [];
147
+
148
+ const memoryChange = await updateMarkedFile(
149
+ projectRoot,
150
+ join('llm-wiki', 'wiki', 'memory.md'),
151
+ memoryPage(),
152
+ MEMORY_START,
153
+ MEMORY_END,
154
+ buildGeneratedMemoryBlock(pages),
155
+ options,
156
+ );
157
+ if (memoryChange?.changed) changed.push(memoryChange.changed);
158
+ if (memoryChange?.skipped) skipped.push(memoryChange.skipped);
159
+
160
+ const indexChange = await updateMarkedFile(
161
+ projectRoot,
162
+ join('llm-wiki', 'wiki', 'index.md'),
163
+ indexPage(),
164
+ INDEX_START,
165
+ INDEX_END,
166
+ buildGeneratedIndexBlock(pages),
167
+ options,
168
+ );
169
+ if (indexChange?.changed) changed.push(indexChange.changed);
170
+ if (indexChange?.skipped) skipped.push(indexChange.skipped);
171
+
172
+ const finalLintResult = options.dryRun || changed.length === 0
173
+ ? lintResult
174
+ : await runLint(projectRoot, { maxFiles: options.maxFiles || 1000 });
175
+
176
+ if (changed.length > 0 && !options.dryRun) {
177
+ await appendWikiLog(projectRoot, `consolidate: refreshed ${changed.map((item) => item.path).join(', ')}; lint errors=${finalLintResult.errorCount}; warnings=${finalLintResult.warningCount}`);
178
+ }
179
+
180
+ return {
181
+ workspace: projectRoot,
182
+ dryRun: Boolean(options.dryRun),
183
+ changed,
184
+ skipped,
185
+ lint: {
186
+ ok: finalLintResult.ok,
187
+ errorCount: finalLintResult.errorCount,
188
+ warningCount: finalLintResult.warningCount,
189
+ },
190
+ };
191
+ }
192
+
193
+ export function formatConsolidateResult(result) {
194
+ return [
195
+ 'llm-wiki consolidate',
196
+ `- workspace: ${result.workspace}`,
197
+ `- dry run: ${result.dryRun ? 'yes' : 'no'}`,
198
+ `- changed: ${result.changed.length ? result.changed.map((item) => item.path).join(', ') : 'none'}`,
199
+ `- skipped: ${result.skipped?.length ? result.skipped.map((item) => `${item.path} (${item.reason})`).join(', ') : 'none'}`,
200
+ `- lint errors: ${result.lint.errorCount}`,
201
+ `- lint warnings: ${result.lint.warningCount}`,
202
+ ].join('\n');
203
+ }
package/src/doctor.js CHANGED
@@ -17,6 +17,8 @@ export async function runDoctor() {
17
17
  add('node >= 20', nodeMajor() >= 20, process.version);
18
18
  add('runtime version detected', Boolean(stat.runtimeVersion), stat.runtimeVersion || 'unknown');
19
19
  add('llm-wiki bin exists', await exists(stat.binPath), stat.binPath);
20
+ add('runtime installed from npm', stat.installSource === 'npm', `${stat.installSource}; npm install -g does not update source checkouts`);
21
+ add('llm-wiki command resolves to current runtime', stat.commandMatchesRuntime, stat.commandPath ? `command=${stat.commandPath}; runtime=${stat.binPath}` : 'command not found on PATH');
20
22
  add('Codex hook installed', stat.codexInstalled, stat.codexHooksPath);
21
23
  add('Claude hook installed', stat.claudeInstalled, stat.claudeSettingsPath);
22
24
  add('project templates current', stat.project.managedFilesCurrent, stat.project.statePath);
package/src/install.js CHANGED
@@ -1,4 +1,6 @@
1
+ import { realpathSync } from 'fs';
1
2
  import { chmod } from 'fs/promises';
3
+ import { spawnSync } from 'child_process';
2
4
  import { join, resolve } from 'path';
3
5
  import { CLAUDE_EVENTS, CODEX_EVENTS, KIT_NAME } from './constants.js';
4
6
  import { backupFile, ensureDir, exists, homeDir, readJson, safeSymlink, writeJson } from './fs-utils.js';
@@ -15,6 +17,23 @@ export function hookCommand(provider, eventName) {
15
17
  return `${shellQuote(process.execPath)} ${shellQuote(binPath)} hook ${provider} ${eventName}`;
16
18
  }
17
19
 
20
+ function llmWikiCommandPaths() {
21
+ const result = spawnSync('sh', ['-lc', 'which -a llm-wiki 2>/dev/null || true'], {
22
+ encoding: 'utf8',
23
+ });
24
+ if (result.error) return [];
25
+ return [...new Set(result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean))];
26
+ }
27
+
28
+ function realpathOrOriginal(path) {
29
+ if (!path) return null;
30
+ try {
31
+ return realpathSync(path);
32
+ } catch {
33
+ return path;
34
+ }
35
+ }
36
+
18
37
  function addHook(hooks, eventName, command, options = {}) {
19
38
  hooks[eventName] = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
20
39
  const already = JSON.stringify(hooks[eventName]).includes(command);
@@ -59,8 +78,10 @@ export async function install(options = {}) {
59
78
  await backupFile(localBinPath, 'local-bin-llm-wiki');
60
79
  }
61
80
  await safeSymlink(binPath, localBinPath);
62
- await bootstrapProject(workspace, { profile: options.profile || 'standard', recordState: true });
63
- await recordProject(workspace, 'install');
81
+ if (!options.noProject) {
82
+ await bootstrapProject(workspace, { profile: options.profile || 'standard', recordState: true });
83
+ await recordProject(workspace, 'install');
84
+ }
64
85
 
65
86
  const codexHooksPath = join(homeDir(), '.codex', 'hooks.json');
66
87
  const claudeSettingsPath = join(homeDir(), '.claude', 'settings.json');
@@ -151,11 +172,18 @@ export async function status(options = {}) {
151
172
  const claude = await readJson(claudeSettingsPath, {});
152
173
  const codexInstalled = JSON.stringify(codex.hooks || {}).includes(binPath);
153
174
  const claudeInstalled = JSON.stringify(claude.hooks || {}).includes(binPath);
175
+ const commandPaths = llmWikiCommandPaths();
176
+ const commandPath = commandPaths[0] || null;
177
+ const resolvedCommandPath = realpathOrOriginal(commandPath);
178
+ const resolvedBinPath = realpathOrOriginal(binPath);
154
179
  return {
155
180
  runtimeVersion: runtimeVersion(),
156
181
  packageRoot,
157
182
  installSource: detectInstallSource(),
158
183
  binPath,
184
+ commandPath,
185
+ commandPaths,
186
+ commandMatchesRuntime: Boolean(resolvedCommandPath && resolvedBinPath && resolvedCommandPath === resolvedBinPath),
159
187
  codexInstalled,
160
188
  claudeInstalled,
161
189
  hooksCurrent: codexInstalled && claudeInstalled,
@@ -1,7 +1,7 @@
1
1
  import { dirname, join, relative } from 'path';
2
2
  import { backupFile, ensureDir, exists, readJson, readText, sha256, writeJson, writeText } from './fs-utils.js';
3
3
  import { runtimeVersion } from './version.js';
4
- import { llmWikiAgents, procedure, rootAgentsPolicy } from './templates.js';
4
+ import { llmWikiAgents, memoryPage, procedure, rootAgentsPolicy } from './templates.js';
5
5
 
6
6
  export const PROJECT_STATE_SCHEMA_VERSION = 1;
7
7
  export const PROJECT_STATE_FILE = '.kit-state.json';
@@ -27,7 +27,12 @@ function templateDescriptors() {
27
27
  path: join('llm-wiki', 'AGENTS.md'),
28
28
  content: llmWikiAgents(),
29
29
  mode: 'generated-file',
30
- marker: 'Generated by llm-wiki-kit',
30
+ },
31
+ {
32
+ id: 'wiki-memory',
33
+ path: join('llm-wiki', 'wiki', 'memory.md'),
34
+ content: memoryPage(),
35
+ mode: 'create-only',
31
36
  },
32
37
  ...PROCEDURE_NAMES.map((name) => ({
33
38
  id: `procedure-${name.replace(/\.md$/, '')}`,
@@ -81,32 +86,45 @@ function replaceMarkedBlock(current, replacement) {
81
86
  return `${current.slice(0, start)}${replacement.trimStart().trimEnd()}${current.slice(afterEnd)}`;
82
87
  }
83
88
 
84
- function hasGeneratedMarker(text, descriptor) {
85
- return descriptor.marker ? text.includes(descriptor.marker) : false;
89
+ function markerStatus(current) {
90
+ const start = current.indexOf(ROOT_POLICY_START);
91
+ const end = current.indexOf(ROOT_POLICY_END);
92
+ if (start === -1 && end === -1) return 'missing';
93
+ if (start === -1 || end === -1 || end < start) return 'malformed';
94
+ return 'complete';
86
95
  }
87
96
 
88
97
  function isRecordedManaged(state, descriptor, currentText) {
89
98
  const recorded = state.managedTemplates?.[descriptor.id];
90
99
  if (!recorded) return false;
100
+ if (descriptor.mode === 'create-only') return true;
91
101
  if (descriptor.mode === 'marker') return true;
92
- return !recorded.hash || recorded.hash === templateHash(currentText);
102
+ return Boolean(recorded.hash) && recorded.hash === templateHash(currentText);
93
103
  }
94
104
 
95
105
  function isKnownGeneratedContent(text, descriptor) {
106
+ if (descriptor.mode === 'create-only') return false;
96
107
  if (descriptor.mode === 'marker') return replaceMarkedBlock(text, descriptor.content) !== null;
97
- return templateHash(text) === templateHash(descriptor.content) || hasGeneratedMarker(text, descriptor);
108
+ return templateHash(text) === templateHash(descriptor.content);
98
109
  }
99
110
 
100
- function canPatchDescriptor(state, descriptor, currentText) {
111
+ function canPatchDescriptor(state, descriptor, currentText, fileExists = true) {
112
+ if (descriptor.mode === 'create-only') return !fileExists;
101
113
  if (descriptor.mode === 'marker') return replaceMarkedBlock(currentText, descriptor.content) !== null;
102
114
  return isRecordedManaged(state, descriptor, currentText) || isKnownGeneratedContent(currentText, descriptor);
103
115
  }
104
116
 
105
- function desiredTextForDescriptor(descriptor, currentText) {
117
+ function desiredTextForDescriptor(descriptor, currentText, fileExists = true) {
118
+ if (descriptor.mode === 'create-only') return fileExists ? currentText : descriptor.content;
106
119
  if (descriptor.mode === 'marker') return replaceMarkedBlock(currentText, descriptor.content);
107
120
  return descriptor.content;
108
121
  }
109
122
 
123
+ async function createOnlyParentExists(projectRoot, descriptor) {
124
+ if (descriptor.mode !== 'create-only') return true;
125
+ return exists(dirname(join(projectRoot, descriptor.path)));
126
+ }
127
+
110
128
  export async function inspectProjectState(projectRoot) {
111
129
  const state = await readProjectState(projectRoot);
112
130
  const runtime = runtimeVersion();
@@ -116,12 +134,18 @@ export async function inspectProjectState(projectRoot) {
116
134
  const absolutePath = join(projectRoot, descriptor.path);
117
135
  const fileExists = await exists(absolutePath);
118
136
  const currentText = fileExists ? await readText(absolutePath) : '';
119
- const desiredText = fileExists ? desiredTextForDescriptor(descriptor, currentText) : descriptor.content;
137
+ const desiredText = desiredTextForDescriptor(descriptor, currentText, fileExists);
120
138
  const desiredHash = templateHash(desiredText || descriptor.content);
121
139
  const currentHash = fileExists ? templateHash(currentText) : null;
122
140
  const recorded = state.managedTemplates[descriptor.id] || null;
123
- const patchable = fileExists ? canPatchDescriptor(state, descriptor, currentText) : false;
124
- const current = fileExists && desiredText !== null && currentHash === templateHash(desiredText);
141
+ const markerState = descriptor.mode === 'marker' && fileExists ? markerStatus(currentText) : null;
142
+ const parentExists = await createOnlyParentExists(projectRoot, descriptor);
143
+ const patchable = descriptor.mode === 'create-only' && !fileExists && !parentExists
144
+ ? false
145
+ : (markerState === 'malformed' || canPatchDescriptor(state, descriptor, currentText, fileExists));
146
+ const current = descriptor.mode === 'create-only'
147
+ ? fileExists
148
+ : fileExists && desiredText !== null && currentHash === templateHash(desiredText);
125
149
 
126
150
  files.push({
127
151
  id: descriptor.id,
@@ -133,6 +157,7 @@ export async function inspectProjectState(projectRoot) {
133
157
  desiredHash,
134
158
  recordedHash: recorded?.hash || null,
135
159
  recordedVersion: recorded?.version || null,
160
+ reason: markerState === 'malformed' ? 'malformed-marker' : null,
136
161
  });
137
162
  }
138
163
 
@@ -155,6 +180,14 @@ export async function recordManagedTemplates(projectRoot) {
155
180
  const absolutePath = join(projectRoot, descriptor.path);
156
181
  if (!(await exists(absolutePath))) continue;
157
182
  const currentText = await readText(absolutePath);
183
+ if (descriptor.mode === 'create-only') {
184
+ state.managedTemplates[descriptor.id] = {
185
+ path: descriptor.path,
186
+ version: runtime,
187
+ hash: templateHash(currentText),
188
+ };
189
+ continue;
190
+ }
158
191
  if (!canPatchDescriptor(state, descriptor, currentText)) continue;
159
192
  state.managedTemplates[descriptor.id] = {
160
193
  path: descriptor.path,
@@ -177,11 +210,43 @@ export async function applyProjectTemplateUpdate(projectRoot, options = {}) {
177
210
  for (const descriptor of templateDescriptors()) {
178
211
  const absolutePath = join(projectRoot, descriptor.path);
179
212
  if (!(await exists(absolutePath))) {
213
+ if (descriptor.mode === 'create-only') {
214
+ if (!(await createOnlyParentExists(projectRoot, descriptor))) {
215
+ skipped.push({ id: descriptor.id, path: descriptor.path, reason: 'missing-wiki' });
216
+ continue;
217
+ }
218
+ if (!options.dryRun) {
219
+ await ensureDir(dirname(absolutePath));
220
+ await writeText(absolutePath, descriptor.content, 'utf8');
221
+ }
222
+ changed.push({ id: descriptor.id, path: descriptor.path });
223
+ state.managedTemplates[descriptor.id] = {
224
+ path: descriptor.path,
225
+ version: runtime,
226
+ hash: templateHash(descriptor.content),
227
+ };
228
+ continue;
229
+ }
180
230
  skipped.push({ id: descriptor.id, path: descriptor.path, reason: 'missing' });
181
231
  continue;
182
232
  }
183
233
 
184
234
  const currentText = await readText(absolutePath);
235
+ if (descriptor.mode === 'marker' && markerStatus(currentText) === 'malformed') {
236
+ skipped.push({ id: descriptor.id, path: descriptor.path, reason: 'malformed-marker' });
237
+ continue;
238
+ }
239
+
240
+ if (descriptor.mode === 'create-only') {
241
+ unchanged.push({ id: descriptor.id, path: descriptor.path });
242
+ state.managedTemplates[descriptor.id] = {
243
+ path: descriptor.path,
244
+ version: runtime,
245
+ hash: templateHash(currentText),
246
+ };
247
+ continue;
248
+ }
249
+
185
250
  if (!canPatchDescriptor(state, descriptor, currentText)) {
186
251
  skipped.push({ id: descriptor.id, path: descriptor.path, reason: 'not-managed' });
187
252
  continue;
package/src/project.js CHANGED
@@ -3,7 +3,6 @@ import {
3
3
  appendText,
4
4
  ensureDir,
5
5
  exists,
6
- listMarkdownFiles,
7
6
  readText,
8
7
  timeKst,
9
8
  todayKst,
@@ -11,8 +10,9 @@ import {
11
10
  } from './fs-utils.js';
12
11
  import { LLM_WIKI_DIRS } from './constants.js';
13
12
  import { normalizeForStorage, redactText, summarizeForStorage } from './redaction.js';
14
- import { gitignore, indexPage, llmWikiAgents, logPage, procedure, rootAgentsPolicy } from './templates.js';
13
+ import { gitignore, indexPage, llmWikiAgents, logPage, memoryPage, procedure, rootAgentsPolicy } from './templates.js';
15
14
  import { recordManagedTemplates } from './project-state.js';
15
+ import { buildContextPack, formatContextPack, searchWiki as searchWikiWithIndex } from './wiki-search.js';
16
16
 
17
17
  export async function bootstrapProject(projectRoot, options = {}) {
18
18
  if (process.env.LLM_WIKI_KIT_DISABLE_BOOTSTRAP === '1') return { created: false };
@@ -28,6 +28,7 @@ export async function bootstrapProject(projectRoot, options = {}) {
28
28
 
29
29
  await maybeCreate(join(base, 'AGENTS.md'), llmWikiAgents());
30
30
  await maybeCreate(join(base, 'wiki', 'index.md'), indexPage());
31
+ await maybeCreate(join(base, 'wiki', 'memory.md'), memoryPage());
31
32
  await maybeCreate(join(base, 'wiki', 'log.md'), logPage());
32
33
  await maybeCreate(join(base, '.gitignore'), gitignore());
33
34
  for (const name of ['ingest.md', 'query.md', 'lint.md', 'security.md']) {
@@ -128,7 +129,7 @@ export async function writeQueryPage(projectRoot, entry) {
128
129
  const slug = slugify(entry.question, 'query');
129
130
  const path = join(projectRoot, 'llm-wiki', 'wiki', 'queries', `${day}-${slug}.md`);
130
131
  if (await exists(path)) return path;
131
- const content = `---\ntitle: "${entry.topic || slug}"\ntype: "query"\nsource_ids: []\nstatus: "draft"\nlast_updated: "${day}"\nconfidence: "medium"\n---\n\n# ${entry.topic || entry.question.slice(0, 80)}\n\n## Question\n${entry.question}\n\n## Answer Summary\n${entry.result || '(not captured)'}\n\n## Work Notes\n${entry.work || '(not captured)'}\n\n## Verification\n${entry.verification || '(not captured)'}\n\n## Related Pages\n- [[index]]\n\n## Change Log\n- ${day}: Captured automatically by llm-wiki-kit hook.\n`;
132
+ const content = `---\ntitle: "${entry.topic || slug}"\ntype: "query"\nsource_ids: []\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## Question\n${entry.question}\n\n## Answer Summary\n${entry.result || '(not captured)'}\n\n## Work Notes\n${entry.work || '(not captured)'}\n\n## Verification\n${entry.verification || '(not captured)'}\n\n## Related Pages\n- [[index]]\n\n## Change Log\n- ${day}: Captured automatically by llm-wiki-kit hook.\n`;
132
133
  await writeTextIfMissing(path, redactText(content, 12000));
133
134
  return path;
134
135
  }
@@ -141,7 +142,7 @@ export async function writeDecisionPage(projectRoot, entry) {
141
142
  const slug = slugify(entry.topic || entry.question || 'decision', 'decision');
142
143
  const path = join(projectRoot, 'llm-wiki', 'wiki', 'decisions', `${day}-${slug}.md`);
143
144
  if (await exists(path)) return path;
144
- const content = `---\ntitle: "${entry.topic || slug}"\ntype: "decision"\nsource_ids: []\nstatus: "draft"\nlast_updated: "${day}"\nconfidence: "medium"\n---\n\n# ${entry.topic || 'Decision'}\n\n## Decision\n${entry.result || '(captured from assistant response; review needed)'}\n\n## Context\n${entry.question || '(not captured)'}\n\n## Evidence\n${entry.work || '(not captured)'}\n\n## Verification\n${entry.verification || '(not captured)'}\n\n## Open Questions\n${entry.followUp || '(none captured)'}\n\n## Change Log\n- ${day}: Captured automatically by llm-wiki-kit hook.\n`;
145
+ const content = `---\ntitle: "${entry.topic || slug}"\ntype: "decision"\nsource_ids: []\nstatus: "draft"\nlast_updated: "${day}"\nconfidence: "medium"\nmemory_type: "semantic"\nimportance: 4\nlast_verified: "unknown"\nsupersedes: []\nsuperseded_by: []\n---\n\n# ${entry.topic || 'Decision'}\n\n## Decision\n${entry.result || '(captured from assistant response; review needed)'}\n\n## Context\n${entry.question || '(not captured)'}\n\n## Evidence\n${entry.work || '(not captured)'}\n\n## Verification\n${entry.verification || '(not captured)'}\n\n## Open Questions\n${entry.followUp || '(none captured)'}\n\n## Change Log\n- ${day}: Captured automatically by llm-wiki-kit hook.\n`;
145
146
  await writeTextIfMissing(path, redactText(content, 12000));
146
147
  return path;
147
148
  }
@@ -153,59 +154,14 @@ export async function appendContextNote(projectRoot, eventName, text) {
153
154
  await appendText(path, block);
154
155
  }
155
156
 
156
- function tokenize(text) {
157
- return String(text || '')
158
- .toLowerCase()
159
- .replace(/[^\p{Letter}\p{Number}]+/gu, ' ')
160
- .split(/\s+/)
161
- .filter((token) => token.length >= 2)
162
- .slice(0, 80);
163
- }
164
-
165
157
  export async function searchWiki(projectRoot, query, limit = 5) {
166
- const wikiRoot = join(projectRoot, 'llm-wiki', 'wiki');
167
- const files = await listMarkdownFiles(wikiRoot, 400);
168
- const terms = tokenize(query);
169
- if (terms.length === 0) return [];
170
- const scored = [];
171
- for (const file of files) {
172
- const raw = await readText(file);
173
- const content = raw.slice(0, 50000);
174
- const lower = content.toLowerCase();
175
- let score = 0;
176
- for (const term of terms) {
177
- if (lower.includes(term)) score += term.length > 3 ? 2 : 1;
178
- }
179
- if (score > 0) {
180
- const rel = relative(join(projectRoot, 'llm-wiki'), file);
181
- scored.push({
182
- score,
183
- path: rel,
184
- snippet: content.replace(/\s+/g, ' ').slice(0, 350),
185
- });
186
- }
187
- }
188
- return scored.sort((a, b) => b.score - a.score).slice(0, limit);
158
+ return searchWikiWithIndex(projectRoot, query, typeof limit === 'number' ? { limit } : limit);
189
159
  }
190
160
 
191
161
  export async function buildContextBrief(projectRoot, eventName, query = '') {
192
- const index = await readText(join(projectRoot, 'llm-wiki', 'wiki', 'index.md'));
193
- const log = await readText(join(projectRoot, 'llm-wiki', 'wiki', 'log.md'));
194
- const hits = query ? await searchWiki(projectRoot, query, 5) : [];
195
- const parts = [
196
- 'LLM Wiki context from llm-wiki-kit:',
197
- '- Treat chat memory as temporary; update project Markdown when knowledge should persist.',
198
- '- Preserve raw/wiki separation. Do not store secrets, tokens, .env contents, private keys, or personal/customer identifiers.',
199
- '- Prefer updating existing wiki pages over creating duplicate pages.',
200
- '',
201
- 'Index excerpt:',
202
- index.slice(0, 1200).trim(),
203
- ];
204
- if (hits.length > 0) {
205
- parts.push('', 'Relevant wiki pages:', ...hits.map((hit) => `- ${hit.path}: ${hit.snippet}`));
206
- }
207
- if (eventName === 'SessionStart') {
208
- parts.push('', 'Recent log excerpt:', log.slice(-1000).trim());
209
- }
210
- return parts.join('\n').trim();
162
+ const pack = await buildContextPack(projectRoot, query, {
163
+ includeLog: eventName === 'SessionStart',
164
+ limit: 5,
165
+ });
166
+ return formatContextPack(pack);
211
167
  }