llm-wiki-kit 0.1.0

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,68 @@
1
+ const SECRET_PATTERNS = [
2
+ {
3
+ name: 'private-key',
4
+ pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
5
+ },
6
+ {
7
+ name: 'env-assignment-secret',
8
+ pattern: /\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|PRIVATE_KEY|ACCESS_KEY)[A-Z0-9_]*)\s*=\s*([^\s"'`]{6,}|["'`][^"'`]{6,}["'`])/gi,
9
+ },
10
+ {
11
+ name: 'bearer-token',
12
+ pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/g,
13
+ },
14
+ {
15
+ name: 'generic-token',
16
+ pattern: /\b(?:sk|pk|ghp|gho|github_pat|xoxb|xoxp|AKIA)[A-Za-z0-9._-]{16,}\b/g,
17
+ },
18
+ {
19
+ name: 'email',
20
+ pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
21
+ },
22
+ {
23
+ name: 'phone',
24
+ pattern: /(?<![\w])(?!(?:\d{4}-\d{2}-\d{2})(?!\d))(?:\+?\d(?:[\d .()-]*\d){9,})(?![\w])/g,
25
+ },
26
+ ];
27
+
28
+ const SENSITIVE_PATH_PATTERNS = [
29
+ /(^|\/)\.env(\.|$)/i,
30
+ /(^|\/)(?:id_rsa|id_dsa|id_ecdsa|id_ed25519)$/i,
31
+ /\.(?:pem|key|p12|pfx)$/i,
32
+ /(^|\/)(?:secrets?|credentials?|raw_private)(\/|$)/i,
33
+ /(?:token|password|passwd|credential|private[_-]?key)/i,
34
+ ];
35
+
36
+ export function redactText(value, maxLength = 6000) {
37
+ let text = typeof value === 'string' ? value : JSON.stringify(value ?? '');
38
+ for (const rule of SECRET_PATTERNS) {
39
+ text = text.replace(rule.pattern, `[REDACTED:${rule.name}]`);
40
+ }
41
+ if (text.length > maxLength) {
42
+ text = `${text.slice(0, maxLength)}\n[TRUNCATED:${text.length - maxLength}]`;
43
+ }
44
+ return text;
45
+ }
46
+
47
+ export function hasSecretLikeText(value) {
48
+ const text = typeof value === 'string' ? value : JSON.stringify(value ?? '');
49
+ return SECRET_PATTERNS.some((rule) => {
50
+ rule.pattern.lastIndex = 0;
51
+ return rule.pattern.test(text);
52
+ });
53
+ }
54
+
55
+ export function isSensitivePath(path) {
56
+ if (!path || typeof path !== 'string') return false;
57
+ return SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(path));
58
+ }
59
+
60
+ export function summarizeForStorage(value, maxLength = 1200) {
61
+ return redactText(value, maxLength).replace(/\r/g, '').trim();
62
+ }
63
+
64
+ export function extractPathsFromText(text) {
65
+ if (!text || typeof text !== 'string') return [];
66
+ const matches = text.match(/(?:^|\s)([.~\/A-Za-z0-9_@:-][^\s"'`<>|;]{1,220})/g) || [];
67
+ return matches.map((item) => item.trim()).filter(Boolean);
68
+ }
package/src/state.js ADDED
@@ -0,0 +1,73 @@
1
+ import { join } from 'path';
2
+ import { kitDataDir, readJson, sha256, writeJson } from './fs-utils.js';
3
+ import { summarizeForStorage } from './redaction.js';
4
+
5
+ export function sessionKey(projectRoot, payload) {
6
+ const sessionId =
7
+ payload.session_id ||
8
+ payload.sessionId ||
9
+ payload.conversation_id ||
10
+ payload.transcript_path ||
11
+ 'default';
12
+ return sha256(`${projectRoot}:${sessionId}`).slice(0, 32);
13
+ }
14
+
15
+ export function statePath(projectRoot, payload) {
16
+ return join(kitDataDir(), 'state', `${sessionKey(projectRoot, payload)}.json`);
17
+ }
18
+
19
+ export async function readTurnState(projectRoot, payload) {
20
+ return (await readJson(statePath(projectRoot, payload), null)) || {
21
+ projectRoot,
22
+ session: sessionKey(projectRoot, payload),
23
+ questions: [],
24
+ tools: [],
25
+ files: [],
26
+ verification: [],
27
+ updated_at: new Date().toISOString(),
28
+ };
29
+ }
30
+
31
+ export async function writeTurnState(projectRoot, payload, state) {
32
+ state.updated_at = new Date().toISOString();
33
+ await writeJson(statePath(projectRoot, payload), state);
34
+ }
35
+
36
+ export async function rememberQuestion(projectRoot, payload, prompt) {
37
+ const state = await readTurnState(projectRoot, payload);
38
+ const clean = summarizeForStorage(prompt, 3000);
39
+ if (clean) state.questions.push({ at: new Date().toISOString(), text: clean });
40
+ state.questions = state.questions.slice(-5);
41
+ await writeTurnState(projectRoot, payload, state);
42
+ }
43
+
44
+ export async function rememberTool(projectRoot, payload, summary) {
45
+ const state = await readTurnState(projectRoot, payload);
46
+ const clean = summarizeForStorage(summary, 1500);
47
+ if (clean) state.tools.push({ at: new Date().toISOString(), text: clean });
48
+ state.tools = state.tools.slice(-20);
49
+ const fileMatches = clean.match(/(?:^|\s)([A-Za-z0-9_./@:-]+\.[A-Za-z0-9_./@:-]+)/g) || [];
50
+ for (const match of fileMatches) state.files.push(match.trim());
51
+ state.files = [...new Set(state.files)].slice(-30);
52
+ if (/\b(test|pytest|vitest|jest|playwright|lint|typecheck|build|doctor)\b/i.test(clean)) {
53
+ state.verification.push(clean);
54
+ state.verification = state.verification.slice(-10);
55
+ }
56
+ await writeTurnState(projectRoot, payload, state);
57
+ }
58
+
59
+ export async function buildEntryFromState(projectRoot, payload, assistantText) {
60
+ const state = await readTurnState(projectRoot, payload);
61
+ const question = state.questions.at(-1)?.text || summarizeForStorage(payload.prompt || payload.user_prompt || '', 2000);
62
+ const result = summarizeForStorage(assistantText || payload.last_assistant_message || payload.response || '', 4000);
63
+ const topic = question ? question.replace(/\s+/g, ' ').slice(0, 80) : 'session turn';
64
+ return {
65
+ topic,
66
+ question: question || '(not captured)',
67
+ work: state.tools.map((item) => `- ${item.text}`).join('\n') || '(not captured)',
68
+ result: result || '(not captured)',
69
+ changedFiles: [...new Set(state.files)].map((item) => `- ${item}`).join('\n') || '(not captured)',
70
+ verification: state.verification.map((item) => `- ${item}`).join('\n') || '(not captured)',
71
+ followUp: '',
72
+ };
73
+ }
@@ -0,0 +1,31 @@
1
+ import { runtimeVersion } from './version.js';
2
+
3
+ export function rootAgentsPolicy() {
4
+ return `\n<!-- llm-wiki-kit:start -->\n## LLM Wiki Policy\n\nThis repository uses llm-wiki-kit as a hook-first living Markdown wiki for Codex and Claude Code.\n\n- This block supersedes older OMX/OMC/\`omx_wiki/\` LLM Wiki instructions for this repository.\n- Treat chat memory as temporary; durable project knowledge belongs in \`llm-wiki/\`.\n- \`llm-wiki/raw/\` stores immutable or redacted source material. Do not edit raw source files.\n- \`llm-wiki/wiki/\` stores LLM-maintained knowledge pages such as decisions, architecture, debugging, context, concepts, and queries.\n- Before non-trivial work, use the injected LLM Wiki context and consult \`llm-wiki/wiki/index.md\` when present.\n- Capture reusable decisions, debugging findings, verification commands, and open questions into the wiki.\n- Never store credentials, tokens, private keys, \`.env\` contents, customer identifiers, or private personal data.\n- Mark inference explicitly and preserve contradictions instead of silently overwriting them.\n\n<!-- llm-wiki-kit:end -->\n`;
5
+ }
6
+
7
+ export function llmWikiAgents() {
8
+ return `# LLM Wiki Agent Rules\n\nGenerated by llm-wiki-kit ${runtimeVersion()}.\n\n## Purpose\nMaintain a living Markdown LLM Wiki from immutable source files and redacted Codex/Claude Code session events.\nThese rules supersede older OMX/OMC/\`omx_wiki/\` LLM Wiki rules for this project.\n\n## Directories\n- \`raw/\`: immutable or redacted source material. Never edit original source captures.\n- \`wiki/\`: AI-maintained knowledge pages.\n- \`outputs/\`: live Q&A summaries, reports, and generated briefs.\n- \`procedures/\`: detailed operating rules for ingest, query, lint, and security.\n\n## Core Rules\n- Never modify \`raw/\` source material except to append redacted session envelopes generated by hooks.\n- Do not state unsupported claims as facts.\n- Mark inference explicitly.\n- Add \`source_ids\` or file references for important claims.\n- Update \`wiki/index.md\` and \`wiki/log.md\` whenever new durable knowledge is created.\n- Preserve contradictions in \`Contradictions\` or \`Open Questions\` instead of overwriting them.\n- Ask before reading private or secret-looking files.\n\n## Page Format\nUse YAML frontmatter when creating wiki pages:\n\n\`\`\`yaml\n---\ntitle: \"\"\ntype: \"source | concept | entity | decision | architecture | debugging | context | query | session-log | convention\"\nsource_ids: []\nstatus: \"draft | reviewed | stale\"\nlast_updated: \"YYYY-MM-DD\"\nconfidence: \"high | medium | low\"\n---\n\`\`\`\n\n## Operations\n- ingest: read new raw files, create or update wiki pages, then update \`wiki/index.md\` and \`wiki/log.md\`.\n- query: start from \`wiki/index.md\`, read relevant wiki pages, answer with source references, and save reusable answers.\n- lint: find stale pages, orphan pages, broken wiki links, missing sources, duplicate concepts, contradictions, and missing links.\n`;
9
+ }
10
+
11
+ export function indexPage() {
12
+ return `# LLM Wiki Index\n\nGenerated by llm-wiki-kit.\n\n## Overview\n\nThis is the navigation map for the project living wiki. Keep it short and useful.\n\n## Main Areas\n\n- [Sources](sources/) - source summaries and source IDs.\n- [Concepts](concepts/) - reusable concepts and terminology.\n- [Entities](entities/) - people, systems, modules, vendors, services, and tools.\n- [Decisions](decisions/) - decisions and rationale.\n- [Architecture](architecture/) - system structure and data/control flow.\n- [Debugging](debugging/) - root causes, fixes, verification evidence.\n- [Context](context/) - session continuity and project memory.\n- [Queries](queries/) - durable answers to useful questions.\n\n## Operating Notes\n\n- Start from this index before broad project questions.\n- Read 3-7 relevant pages first; inspect raw sources only when precision matters.\n- Add links using \`[[page-or-topic]]\` when pages become related.\n`;
13
+ }
14
+
15
+ export function logPage() {
16
+ return `# LLM Wiki Log\n\nAppend-only operating history for this project wiki.\n`;
17
+ }
18
+
19
+ export function procedure(name) {
20
+ const procedures = {
21
+ 'ingest.md': `# Ingest Procedure\n\n1. Read \`wiki/index.md\` first.\n2. Inspect new material under \`raw/inbox/\` or \`raw/sources/\`.\n3. Create or update \`wiki/sources/<slug>.md\` for each source.\n4. Update related concept, entity, decision, architecture, debugging, or context pages.\n5. Prefer updating existing pages over creating duplicates.\n6. Add source references and confidence.\n7. Update \`wiki/index.md\` and append to \`wiki/log.md\`.\n`,
22
+ 'query.md': `# Query Procedure\n\n1. Start from \`wiki/index.md\`.\n2. Search \`wiki/\` for relevant pages.\n3. Read the smallest useful set of pages first.\n4. Use raw sources only when exact evidence matters.\n5. Separate verified facts from inference.\n6. Save reusable answers into \`wiki/queries/\` and link them from related pages.\n`,
23
+ 'lint.md': `# Lint Procedure\n\nCheck for stale pages, orphan pages, broken wiki links, missing sources, duplicate concepts, unsupported claims, and unresolved contradictions. Prefer producing a review report before automatic edits.\n`,
24
+ 'security.md': `# Security Procedure\n\n- Never store credentials, tokens, private keys, \`.env\` contents, customer identifiers, or private personal data.\n- Redact before writing hook payloads or summaries.\n- Full raw transcript capture is disabled by default and must be explicitly enabled by project policy.\n- If a file or prompt looks secret-bearing, do not persist it and ask for confirmation before reading further.\n`,
25
+ };
26
+ return procedures[name] || '';
27
+ }
28
+
29
+ export function gitignore() {
30
+ return `# llm-wiki-kit safety defaults\nraw/sessions/*.jsonl\nraw/sessions/*.ndjson\nraw/private/\nraw_private/\nsecrets/\n*.key\n*.pem\n*.p12\n*.pfx\n.env\n.env.*\n.cache/\n`;
31
+ }
package/src/update.js ADDED
@@ -0,0 +1,133 @@
1
+ import { spawnSync } from 'child_process';
2
+ import { resolve } from 'path';
3
+ import { appendWikiLog, bootstrapProject } from './project.js';
4
+ import { install } from './install.js';
5
+ import { applyProjectTemplateUpdate, inspectProjectState } from './project-state.js';
6
+ import { binPath, detectInstallSource, packageName, runtimeVersion } from './version.js';
7
+
8
+ function runCommand(command, args, options = {}) {
9
+ const result = spawnSync(command, args, {
10
+ encoding: 'utf8',
11
+ env: options.env || process.env,
12
+ timeout: options.timeout || 120000,
13
+ });
14
+ return {
15
+ status: result.status,
16
+ stdout: result.stdout || '',
17
+ stderr: result.stderr || '',
18
+ error: result.error || null,
19
+ };
20
+ }
21
+
22
+ function npmCommand() {
23
+ return process.env.LLM_WIKI_KIT_NPM || 'npm';
24
+ }
25
+
26
+ function binCommand() {
27
+ return process.env.LLM_WIKI_KIT_BIN || binPath;
28
+ }
29
+
30
+ function assertCommandOk(result, label) {
31
+ if (result.status === 0 && !result.error) return;
32
+ const detail = result.stderr.trim() || result.stdout.trim() || result.error?.message || 'unknown error';
33
+ throw new Error(`${label} failed: ${detail}`);
34
+ }
35
+
36
+ export function parseRegistryVersion(output) {
37
+ return String(output || '').trim().split(/\s+/).at(-1) || '';
38
+ }
39
+
40
+ export async function checkForUpdate(options = {}) {
41
+ const target = options.to || 'latest';
42
+ const installedVersion = runtimeVersion();
43
+ const result = runCommand(npmCommand(), ['view', `${packageName()}@${target}`, 'version'], {
44
+ timeout: options.timeout || 30000,
45
+ });
46
+ assertCommandOk(result, 'npm view');
47
+ const latestVersion = parseRegistryVersion(result.stdout);
48
+ return {
49
+ installedVersion,
50
+ latestVersion,
51
+ target,
52
+ updateAvailable: latestVersion !== installedVersion,
53
+ };
54
+ }
55
+
56
+ export async function postUpdate(options = {}) {
57
+ const workspace = resolve(options.workspace || process.cwd());
58
+ const installResult = await install({
59
+ ...options,
60
+ workspace,
61
+ replaceHooks: true,
62
+ });
63
+ const projectResult = options.noProject
64
+ ? null
65
+ : await applyProjectTemplateUpdate(workspace);
66
+
67
+ if (!options.noProject) {
68
+ await appendWikiLog(workspace, `llm-wiki-kit post-update applied runtime ${runtimeVersion()}; changed templates: ${projectResult.changed.length}`);
69
+ }
70
+
71
+ return {
72
+ workspace,
73
+ runtimeVersion: runtimeVersion(),
74
+ install: installResult,
75
+ project: projectResult,
76
+ };
77
+ }
78
+
79
+ export async function update(options = {}) {
80
+ const workspace = resolve(options.workspace || process.cwd());
81
+ if (!options.check && !options.dryRun && detectInstallSource() === 'source' && process.env.LLM_WIKI_KIT_ALLOW_SOURCE_UPDATE !== '1') {
82
+ throw new Error('llm-wiki update cannot self-update from a source checkout. Install with `npm install -g llm-wiki-kit`, then run `llm-wiki update --workspace <project>`.');
83
+ }
84
+
85
+ const check = await checkForUpdate(options);
86
+
87
+ if (options.check) {
88
+ return {
89
+ mode: 'check',
90
+ workspace,
91
+ ...check,
92
+ project: options.noProject ? null : await inspectProjectState(workspace),
93
+ };
94
+ }
95
+
96
+ if (options.dryRun) {
97
+ return {
98
+ mode: 'dry-run',
99
+ workspace,
100
+ ...check,
101
+ installSource: detectInstallSource(),
102
+ project: options.noProject ? null : await applyProjectTemplateUpdate(workspace, { dryRun: true }),
103
+ };
104
+ }
105
+
106
+ const target = options.to || 'latest';
107
+ const installResult = runCommand(npmCommand(), ['install', '-g', `${packageName()}@${target}`], {
108
+ timeout: options.timeout || 120000,
109
+ });
110
+ assertCommandOk(installResult, 'npm install -g');
111
+
112
+ const postArgs = [binCommand(), 'post-update', '--workspace', workspace];
113
+ if (options.profile) postArgs.push('--profile', options.profile);
114
+ if (options.noProject) postArgs.push('--no-project');
115
+ if (options.codex === false) postArgs.push('--no-codex');
116
+ if (options.claude === false) postArgs.push('--no-claude');
117
+ const postResult = runCommand(process.execPath, postArgs, {
118
+ timeout: options.timeout || 120000,
119
+ });
120
+ assertCommandOk(postResult, 'post-update');
121
+
122
+ return {
123
+ mode: 'update',
124
+ workspace,
125
+ before: check.installedVersion,
126
+ target,
127
+ npmInstall: {
128
+ stdout: installResult.stdout.trim(),
129
+ stderr: installResult.stderr.trim(),
130
+ },
131
+ postUpdate: postResult.stdout.trim(),
132
+ };
133
+ }
package/src/version.js ADDED
@@ -0,0 +1,27 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { dirname, join, resolve, sep } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const here = dirname(fileURLToPath(import.meta.url));
6
+
7
+ export const packageRoot = resolve(here, '..');
8
+ export const packageJsonPath = join(packageRoot, 'package.json');
9
+ export const binPath = join(packageRoot, 'bin', 'llm-wiki.js');
10
+
11
+ export function packageMetadata() {
12
+ return JSON.parse(readFileSync(packageJsonPath, 'utf8'));
13
+ }
14
+
15
+ export function runtimeVersion() {
16
+ return packageMetadata().version;
17
+ }
18
+
19
+ export function packageName() {
20
+ return packageMetadata().name;
21
+ }
22
+
23
+ export function detectInstallSource() {
24
+ if (existsSync(join(packageRoot, '.git'))) return 'source';
25
+ if (packageRoot.includes(`${sep}node_modules${sep}`)) return 'npm';
26
+ return 'unknown';
27
+ }