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.
- package/README.md +32 -2
- package/docs/concepts.md +12 -2
- package/docs/integrations/claude-code.md +2 -0
- package/docs/integrations/codex.md +2 -2
- package/docs/operations.md +92 -3
- package/docs/research/baseline.md +3 -1
- package/docs/security.md +2 -0
- package/docs/troubleshooting.md +55 -0
- package/package.json +4 -1
- package/src/cli.js +50 -3
- package/src/consolidate.js +203 -0
- package/src/doctor.js +2 -0
- package/src/install.js +30 -2
- package/src/project-state.js +76 -11
- package/src/project.js +11 -55
- package/src/templates.js +90 -7
- package/src/update.js +10 -4
- package/src/wiki-lint.js +271 -0
- package/src/wiki-model.js +263 -0
- package/src/wiki-search.js +243 -0
|
@@ -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
|
-
|
|
63
|
-
|
|
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,
|
package/src/project-state.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
85
|
-
|
|
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
|
|
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)
|
|
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 =
|
|
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
|
|
124
|
-
const
|
|
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
|
-
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
}
|