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.
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/bin/llm-wiki.js +7 -0
- package/docs/concepts.md +29 -0
- package/docs/integrations/claude-code.md +48 -0
- package/docs/integrations/codex.md +45 -0
- package/docs/operations.md +137 -0
- package/docs/research/baseline.md +18 -0
- package/docs/security.md +27 -0
- package/docs/troubleshooting.md +101 -0
- package/examples/hook-fixtures/stop.json +6 -0
- package/examples/hook-fixtures/user-prompt-submit.json +7 -0
- package/examples/minimal-project/AGENTS.md +10 -0
- package/install.sh +5 -0
- package/package.json +27 -0
- package/src/cli.js +183 -0
- package/src/constants.js +69 -0
- package/src/doctor.js +89 -0
- package/src/fs-utils.js +173 -0
- package/src/hook.js +133 -0
- package/src/install.js +164 -0
- package/src/migrate.js +57 -0
- package/src/project-state.js +231 -0
- package/src/project.js +204 -0
- package/src/redaction.js +68 -0
- package/src/state.js +73 -0
- package/src/templates.js +31 -0
- package/src/update.js +133 -0
- package/src/version.js +27 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { handleHook } from './hook.js';
|
|
3
|
+
import { install, status, uninstall } from './install.js';
|
|
4
|
+
import { bootstrapProject } from './project.js';
|
|
5
|
+
import { formatDoctor, runDoctor } from './doctor.js';
|
|
6
|
+
import { migrate } from './migrate.js';
|
|
7
|
+
import { postUpdate, update } from './update.js';
|
|
8
|
+
|
|
9
|
+
function parseOptions(args) {
|
|
10
|
+
const options = {};
|
|
11
|
+
const rest = [];
|
|
12
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
13
|
+
const arg = args[i];
|
|
14
|
+
if (arg === '--workspace' || arg === '--cwd') {
|
|
15
|
+
options.workspace = args[++i];
|
|
16
|
+
} else if (arg === '--profile') {
|
|
17
|
+
options.profile = args[++i];
|
|
18
|
+
} else if (arg === '--to') {
|
|
19
|
+
options.to = args[++i];
|
|
20
|
+
} else if (arg === '--no-codex') {
|
|
21
|
+
options.codex = false;
|
|
22
|
+
} else if (arg === '--no-claude') {
|
|
23
|
+
options.claude = false;
|
|
24
|
+
} else if (arg === '--no-project') {
|
|
25
|
+
options.noProject = true;
|
|
26
|
+
} else if (arg === '--check') {
|
|
27
|
+
options.check = true;
|
|
28
|
+
} else if (arg === '--dry-run') {
|
|
29
|
+
options.dryRun = true;
|
|
30
|
+
} else if (arg === '--json') {
|
|
31
|
+
options.json = true;
|
|
32
|
+
} else {
|
|
33
|
+
rest.push(arg);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { options, rest };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printJsonOrText(value, options, formatter = null) {
|
|
40
|
+
if (options.json) {
|
|
41
|
+
console.log(JSON.stringify(value, null, 2));
|
|
42
|
+
} else if (formatter) {
|
|
43
|
+
console.log(formatter(value));
|
|
44
|
+
} else {
|
|
45
|
+
console.log(typeof value === 'string' ? value : JSON.stringify(value, null, 2));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function runCli(args) {
|
|
50
|
+
const [command, ...tail] = args;
|
|
51
|
+
const { options, rest } = parseOptions(tail);
|
|
52
|
+
|
|
53
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
54
|
+
console.log(`llm-wiki-kit
|
|
55
|
+
|
|
56
|
+
Usage:
|
|
57
|
+
llm-wiki install --workspace /apps [--profile standard]
|
|
58
|
+
llm-wiki update --workspace <project> [--check|--dry-run|--to <version-or-tag>]
|
|
59
|
+
llm-wiki doctor
|
|
60
|
+
llm-wiki status
|
|
61
|
+
llm-wiki uninstall
|
|
62
|
+
llm-wiki hook codex <EventName>
|
|
63
|
+
llm-wiki hook claude <EventName>
|
|
64
|
+
llm-wiki bootstrap --workspace <project>
|
|
65
|
+
llm-wiki migrate --workspace <project>
|
|
66
|
+
`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (command === 'install') {
|
|
71
|
+
const result = await install(options);
|
|
72
|
+
printJsonOrText(result, options, (value) => [
|
|
73
|
+
'llm-wiki-kit installed',
|
|
74
|
+
`- workspace: ${value.workspace}`,
|
|
75
|
+
`- bin: ${value.localBin}`,
|
|
76
|
+
`- changed hooks: ${value.changed.length ? value.changed.join(', ') : 'none (already installed)'}`,
|
|
77
|
+
'Restart Codex/Claude Code sessions so new hooks are loaded.',
|
|
78
|
+
].join('\n'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (command === 'uninstall') {
|
|
83
|
+
const result = await uninstall(options);
|
|
84
|
+
printJsonOrText(result, options, (value) => `llm-wiki-kit uninstalled hooks: ${value.changed.length ? value.changed.join(', ') : 'none'}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (command === 'status') {
|
|
89
|
+
printJsonOrText(await status(options), options, formatStatus);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (command === 'update') {
|
|
94
|
+
printJsonOrText(await update(options), options, formatUpdate);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (command === 'post-update') {
|
|
99
|
+
printJsonOrText(await postUpdate(options), options, formatPostUpdate);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (command === 'doctor') {
|
|
104
|
+
const result = await runDoctor();
|
|
105
|
+
printJsonOrText(result, options, formatDoctor);
|
|
106
|
+
if (!result.ok) process.exitCode = 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (command === 'bootstrap') {
|
|
111
|
+
const projectRoot = resolve(options.workspace || process.cwd());
|
|
112
|
+
printJsonOrText(await bootstrapProject(projectRoot, options), options);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (command === 'migrate') {
|
|
117
|
+
const projectRoot = resolve(options.workspace || process.cwd());
|
|
118
|
+
printJsonOrText(await migrate(projectRoot), options);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (command === 'hook') {
|
|
123
|
+
const provider = rest[0] || 'codex';
|
|
124
|
+
const eventName = rest[1];
|
|
125
|
+
const output = await handleHook(provider, eventName);
|
|
126
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
throw new Error(`unknown command: ${command}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatStatus(value) {
|
|
134
|
+
const project = value.project || {};
|
|
135
|
+
return [
|
|
136
|
+
'llm-wiki-kit status',
|
|
137
|
+
`- runtime: ${value.runtimeVersion} (${value.installSource})`,
|
|
138
|
+
`- bin: ${value.binPath}`,
|
|
139
|
+
`- hooks current: ${value.hooksCurrent ? 'yes' : 'no'}`,
|
|
140
|
+
`- codex hook: ${value.codexInstalled ? 'current' : 'missing/outdated'}`,
|
|
141
|
+
`- claude hook: ${value.claudeInstalled ? 'current' : 'missing/outdated'}`,
|
|
142
|
+
`- project applied runtime: ${project.lastRuntimeVersionApplied || 'unknown'}`,
|
|
143
|
+
`- project templates current: ${project.managedFilesCurrent ? 'yes' : 'no'}`,
|
|
144
|
+
].join('\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatUpdate(value) {
|
|
148
|
+
if (value.mode === 'check') {
|
|
149
|
+
return [
|
|
150
|
+
'llm-wiki-kit update check',
|
|
151
|
+
`- installed: ${value.installedVersion}`,
|
|
152
|
+
`- latest: ${value.latestVersion}`,
|
|
153
|
+
`- update available: ${value.updateAvailable ? 'yes' : 'no'}`,
|
|
154
|
+
`- project applied runtime: ${value.project?.lastRuntimeVersionApplied || 'unknown'}`,
|
|
155
|
+
].join('\n');
|
|
156
|
+
}
|
|
157
|
+
if (value.mode === 'dry-run') {
|
|
158
|
+
return [
|
|
159
|
+
'llm-wiki-kit update dry-run',
|
|
160
|
+
`- installed: ${value.installedVersion}`,
|
|
161
|
+
`- latest: ${value.latestVersion}`,
|
|
162
|
+
`- update available: ${value.updateAvailable ? 'yes' : 'no'}`,
|
|
163
|
+
`- project template changes: ${value.project?.changed?.length || 0}`,
|
|
164
|
+
`- project template skipped: ${value.project?.skipped?.length || 0}`,
|
|
165
|
+
].join('\n');
|
|
166
|
+
}
|
|
167
|
+
return [
|
|
168
|
+
'llm-wiki-kit updated',
|
|
169
|
+
`- workspace: ${value.workspace}`,
|
|
170
|
+
`- before: ${value.before}`,
|
|
171
|
+
`- target: ${value.target}`,
|
|
172
|
+
].join('\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function formatPostUpdate(value) {
|
|
176
|
+
return [
|
|
177
|
+
'llm-wiki-kit post-update complete',
|
|
178
|
+
`- workspace: ${value.workspace}`,
|
|
179
|
+
`- runtime: ${value.runtimeVersion}`,
|
|
180
|
+
`- changed hooks: ${value.install.changed.length ? value.install.changed.join(', ') : 'none'}`,
|
|
181
|
+
`- changed project templates: ${value.project?.changed?.length || 0}`,
|
|
182
|
+
].join('\n');
|
|
183
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const KIT_NAME = 'llm-wiki-kit';
|
|
2
|
+
export const DEFAULT_PROFILE = 'standard';
|
|
3
|
+
|
|
4
|
+
export const CODEX_EVENTS = [
|
|
5
|
+
'SessionStart',
|
|
6
|
+
'UserPromptSubmit',
|
|
7
|
+
'PreToolUse',
|
|
8
|
+
'PostToolUse',
|
|
9
|
+
'PreCompact',
|
|
10
|
+
'PostCompact',
|
|
11
|
+
'SubagentStop',
|
|
12
|
+
'Stop',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export const CLAUDE_EVENTS = [
|
|
16
|
+
'SessionStart',
|
|
17
|
+
'InstructionsLoaded',
|
|
18
|
+
'UserPromptSubmit',
|
|
19
|
+
'PreToolUse',
|
|
20
|
+
'PostToolUse',
|
|
21
|
+
'PostToolBatch',
|
|
22
|
+
'PreCompact',
|
|
23
|
+
'PostCompact',
|
|
24
|
+
'SubagentStop',
|
|
25
|
+
'Stop',
|
|
26
|
+
'SessionEnd',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export const PROJECT_MARKERS = [
|
|
30
|
+
'llm-wiki',
|
|
31
|
+
'.git',
|
|
32
|
+
'AGENTS.md',
|
|
33
|
+
'CLAUDE.md',
|
|
34
|
+
'package.json',
|
|
35
|
+
'pyproject.toml',
|
|
36
|
+
'go.mod',
|
|
37
|
+
'Cargo.toml',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const LLM_WIKI_DIRS = [
|
|
41
|
+
'raw/inbox',
|
|
42
|
+
'raw/sessions',
|
|
43
|
+
'raw/sources',
|
|
44
|
+
'raw/assets',
|
|
45
|
+
'wiki/sources',
|
|
46
|
+
'wiki/concepts',
|
|
47
|
+
'wiki/entities',
|
|
48
|
+
'wiki/decisions',
|
|
49
|
+
'wiki/architecture',
|
|
50
|
+
'wiki/debugging',
|
|
51
|
+
'wiki/context',
|
|
52
|
+
'wiki/queries',
|
|
53
|
+
'outputs/questions',
|
|
54
|
+
'outputs/reports',
|
|
55
|
+
'procedures',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export const WIKI_CATEGORIES = [
|
|
59
|
+
'source',
|
|
60
|
+
'concept',
|
|
61
|
+
'entity',
|
|
62
|
+
'decision',
|
|
63
|
+
'architecture',
|
|
64
|
+
'debugging',
|
|
65
|
+
'context',
|
|
66
|
+
'query',
|
|
67
|
+
'session-log',
|
|
68
|
+
'convention',
|
|
69
|
+
];
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { mkdtemp } from 'fs/promises';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { exists, kitDataDir } from './fs-utils.js';
|
|
6
|
+
import { status } from './install.js';
|
|
7
|
+
|
|
8
|
+
function nodeMajor() {
|
|
9
|
+
return Number.parseInt(process.versions.node.split('.')[0], 10);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runDoctor() {
|
|
13
|
+
const checks = [];
|
|
14
|
+
const add = (name, ok, detail = '') => checks.push({ name, ok, detail });
|
|
15
|
+
const stat = await status();
|
|
16
|
+
|
|
17
|
+
add('node >= 20', nodeMajor() >= 20, process.version);
|
|
18
|
+
add('runtime version detected', Boolean(stat.runtimeVersion), stat.runtimeVersion || 'unknown');
|
|
19
|
+
add('llm-wiki bin exists', await exists(stat.binPath), stat.binPath);
|
|
20
|
+
add('Codex hook installed', stat.codexInstalled, stat.codexHooksPath);
|
|
21
|
+
add('Claude hook installed', stat.claudeInstalled, stat.claudeSettingsPath);
|
|
22
|
+
add('project templates current', stat.project.managedFilesCurrent, stat.project.statePath);
|
|
23
|
+
add('codex command available', spawnSync('codex', ['--version'], { encoding: 'utf8' }).status === 0, 'codex --version');
|
|
24
|
+
add('claude command available', spawnSync('claude', ['--version'], { encoding: 'utf8' }).status === 0, 'claude --version');
|
|
25
|
+
add('state directory writable', await canWrite(join(kitDataDir(), '.doctor')), kitDataDir());
|
|
26
|
+
add('docs present', await docsPresent(), 'README.md and docs/');
|
|
27
|
+
add('sample hook roundtrip', await sampleHookRoundtrip(stat.binPath), 'UserPromptSubmit fixture');
|
|
28
|
+
|
|
29
|
+
const allOk = checks.every((check) => check.ok);
|
|
30
|
+
return { ok: allOk, checks };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function docsPresent() {
|
|
34
|
+
const root = new URL('..', import.meta.url).pathname;
|
|
35
|
+
return (await exists(join(root, 'README.md'))) &&
|
|
36
|
+
(await exists(join(root, 'docs', 'concepts.md'))) &&
|
|
37
|
+
(await exists(join(root, 'docs', 'security.md')));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function sampleHookRoundtrip(binPath) {
|
|
41
|
+
try {
|
|
42
|
+
const root = await mkdtemp(join(tmpdir(), 'llm-wiki-kit-doctor-'));
|
|
43
|
+
const env = {
|
|
44
|
+
...process.env,
|
|
45
|
+
XDG_DATA_HOME: join(root, '.data'),
|
|
46
|
+
XDG_CACHE_HOME: join(root, '.cache'),
|
|
47
|
+
};
|
|
48
|
+
const payload = {
|
|
49
|
+
hook_event_name: 'UserPromptSubmit',
|
|
50
|
+
cwd: root,
|
|
51
|
+
session_id: 'doctor',
|
|
52
|
+
prompt: 'doctor sample prompt',
|
|
53
|
+
};
|
|
54
|
+
const result = spawnSync(process.execPath, [binPath, 'hook', 'codex', 'UserPromptSubmit'], {
|
|
55
|
+
input: JSON.stringify(payload),
|
|
56
|
+
encoding: 'utf8',
|
|
57
|
+
env,
|
|
58
|
+
timeout: 10000,
|
|
59
|
+
});
|
|
60
|
+
return result.status === 0 && result.stdout.includes('additionalContext');
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function canWrite(path) {
|
|
67
|
+
try {
|
|
68
|
+
const { ensureDir } = await import('./fs-utils.js');
|
|
69
|
+
const { writeFile, unlink } = await import('fs/promises');
|
|
70
|
+
await ensureDir(join(path, '..'));
|
|
71
|
+
await writeFile(path, 'ok', 'utf8');
|
|
72
|
+
await unlink(path);
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatDoctor(result) {
|
|
80
|
+
const lines = [`llm-wiki-kit doctor: ${result.ok ? 'PASS' : 'WARN'}`];
|
|
81
|
+
for (const check of result.checks) {
|
|
82
|
+
lines.push(`- ${check.ok ? 'PASS' : 'WARN'} ${check.name}${check.detail ? ` (${check.detail})` : ''}`);
|
|
83
|
+
}
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push('Run ./install.sh --workspace /apps --profile standard to install hooks, then restart Codex/Claude Code sessions.');
|
|
87
|
+
}
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
package/src/fs-utils.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { constants as fsConstants } from 'fs';
|
|
3
|
+
import {
|
|
4
|
+
access,
|
|
5
|
+
appendFile,
|
|
6
|
+
copyFile,
|
|
7
|
+
mkdir,
|
|
8
|
+
readFile,
|
|
9
|
+
readdir,
|
|
10
|
+
stat,
|
|
11
|
+
symlink,
|
|
12
|
+
unlink,
|
|
13
|
+
writeFile,
|
|
14
|
+
} from 'fs/promises';
|
|
15
|
+
import { dirname, join, parse, resolve } from 'path';
|
|
16
|
+
import { PROJECT_MARKERS } from './constants.js';
|
|
17
|
+
|
|
18
|
+
export function homeDir() {
|
|
19
|
+
return process.env.HOME || process.env.USERPROFILE || process.cwd();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function dataHome() {
|
|
23
|
+
return process.env.XDG_DATA_HOME || join(homeDir(), '.local', 'share');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function cacheHome() {
|
|
27
|
+
return process.env.XDG_CACHE_HOME || join(homeDir(), '.cache');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function kitDataDir() {
|
|
31
|
+
return join(dataHome(), 'llm-wiki-kit');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function exists(path) {
|
|
35
|
+
try {
|
|
36
|
+
await access(path, fsConstants.F_OK);
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function isDirectory(path) {
|
|
44
|
+
try {
|
|
45
|
+
return (await stat(path)).isDirectory();
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function ensureDir(path) {
|
|
52
|
+
await mkdir(path, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function readText(path, fallback = '') {
|
|
56
|
+
try {
|
|
57
|
+
return await readFile(path, 'utf8');
|
|
58
|
+
} catch {
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function writeTextIfMissing(path, content) {
|
|
64
|
+
if (await exists(path)) return false;
|
|
65
|
+
await ensureDir(dirname(path));
|
|
66
|
+
await writeFile(path, content, 'utf8');
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function writeText(path, content) {
|
|
71
|
+
await ensureDir(dirname(path));
|
|
72
|
+
await writeFile(path, content, 'utf8');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function appendText(path, content) {
|
|
76
|
+
await ensureDir(dirname(path));
|
|
77
|
+
await appendFile(path, content, 'utf8');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function readJson(path, fallback = null) {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
83
|
+
} catch {
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function writeJson(path, value) {
|
|
89
|
+
await ensureDir(dirname(path));
|
|
90
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function backupFile(path, label) {
|
|
94
|
+
if (!(await exists(path))) return null;
|
|
95
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '');
|
|
96
|
+
const destDir = join(kitDataDir(), 'backups', stamp);
|
|
97
|
+
await ensureDir(destDir);
|
|
98
|
+
const safeLabel = label.replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
99
|
+
const dest = join(destDir, safeLabel || parse(path).base);
|
|
100
|
+
await copyFile(path, dest);
|
|
101
|
+
return dest;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function safeSymlink(target, linkPath) {
|
|
105
|
+
await ensureDir(dirname(linkPath));
|
|
106
|
+
try {
|
|
107
|
+
await unlink(linkPath);
|
|
108
|
+
} catch {
|
|
109
|
+
// No existing link.
|
|
110
|
+
}
|
|
111
|
+
await symlink(target, linkPath);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function findProjectRoot(startDir = process.cwd()) {
|
|
115
|
+
let current = resolve(startDir);
|
|
116
|
+
while (true) {
|
|
117
|
+
for (const marker of PROJECT_MARKERS) {
|
|
118
|
+
if (await exists(join(current, marker))) return current;
|
|
119
|
+
}
|
|
120
|
+
const parent = dirname(current);
|
|
121
|
+
if (parent === current) return resolve(startDir);
|
|
122
|
+
current = parent;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function listMarkdownFiles(root, maxFiles = 500) {
|
|
127
|
+
const output = [];
|
|
128
|
+
async function walk(dir) {
|
|
129
|
+
if (output.length >= maxFiles) return;
|
|
130
|
+
let entries = [];
|
|
131
|
+
try {
|
|
132
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
133
|
+
} catch {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
if (output.length >= maxFiles) return;
|
|
138
|
+
const full = join(dir, entry.name);
|
|
139
|
+
if (entry.isDirectory()) {
|
|
140
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
141
|
+
await walk(full);
|
|
142
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
143
|
+
output.push(full);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
await walk(root);
|
|
148
|
+
return output;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function sha256(input) {
|
|
152
|
+
return createHash('sha256').update(input).digest('hex');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function todayKst() {
|
|
156
|
+
const formatter = new Intl.DateTimeFormat('en-CA', {
|
|
157
|
+
timeZone: 'Asia/Seoul',
|
|
158
|
+
year: 'numeric',
|
|
159
|
+
month: '2-digit',
|
|
160
|
+
day: '2-digit',
|
|
161
|
+
});
|
|
162
|
+
return formatter.format(new Date());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function timeKst() {
|
|
166
|
+
const formatter = new Intl.DateTimeFormat('en-GB', {
|
|
167
|
+
timeZone: 'Asia/Seoul',
|
|
168
|
+
hour: '2-digit',
|
|
169
|
+
minute: '2-digit',
|
|
170
|
+
hour12: false,
|
|
171
|
+
});
|
|
172
|
+
return formatter.format(new Date());
|
|
173
|
+
}
|
package/src/hook.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { findProjectRoot } from './fs-utils.js';
|
|
2
|
+
import { bootstrapProject, appendContextNote, appendLiveQa, appendSessionEnvelope, appendWikiLog, buildContextBrief, writeDecisionPage, writeQueryPage } from './project.js';
|
|
3
|
+
import { extractPathsFromText, hasSecretLikeText, isSensitivePath, summarizeForStorage } from './redaction.js';
|
|
4
|
+
import { buildEntryFromState, rememberQuestion, rememberTool } from './state.js';
|
|
5
|
+
|
|
6
|
+
async function readStdinJson() {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
for await (const chunk of process.stdin) {
|
|
9
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
10
|
+
}
|
|
11
|
+
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
12
|
+
if (!raw) return {};
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeEvent(explicit, payload) {
|
|
17
|
+
return explicit || payload.hook_event_name || payload.hookEventName || payload.event || payload.name || 'Unknown';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function promptText(payload) {
|
|
21
|
+
return payload.prompt || payload.user_prompt || payload.userPrompt || payload.message || payload.input || '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toolSummary(payload) {
|
|
25
|
+
const toolName = payload.tool_name || payload.toolName || payload.name || payload.tool?.name || 'tool';
|
|
26
|
+
const input = payload.tool_input || payload.toolInput || payload.input || payload.arguments || payload.tool?.input || {};
|
|
27
|
+
return `${toolName}: ${summarizeForStorage(input, 1200)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toolInputText(payload) {
|
|
31
|
+
const input = payload.tool_input || payload.toolInput || payload.input || payload.arguments || payload.tool?.input || {};
|
|
32
|
+
return typeof input === 'string' ? input : JSON.stringify(input);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function contextOutput(eventName, context) {
|
|
36
|
+
if (!context) return {};
|
|
37
|
+
return {
|
|
38
|
+
hookSpecificOutput: {
|
|
39
|
+
hookEventName: eventName,
|
|
40
|
+
additionalContext: context,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function blockOutput(provider, reason) {
|
|
46
|
+
if (provider === 'claude') {
|
|
47
|
+
return {
|
|
48
|
+
permissionDecision: 'deny',
|
|
49
|
+
permissionDecisionReason: reason,
|
|
50
|
+
decision: 'block',
|
|
51
|
+
reason,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
decision: 'block',
|
|
56
|
+
reason,
|
|
57
|
+
hookSpecificOutput: {
|
|
58
|
+
hookEventName: 'PreToolUse',
|
|
59
|
+
additionalContext: reason,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function shouldBlockTool(payload) {
|
|
65
|
+
const input = toolInputText(payload);
|
|
66
|
+
const paths = extractPathsFromText(input);
|
|
67
|
+
if (paths.some(isSensitivePath)) {
|
|
68
|
+
return 'llm-wiki-kit blocked access to a secret-looking path. Do not read or store .env, keys, token files, credentials, raw_private, or secrets.';
|
|
69
|
+
}
|
|
70
|
+
if (hasSecretLikeText(input)) {
|
|
71
|
+
return 'llm-wiki-kit blocked a tool call containing secret-like text. Redact secrets before continuing.';
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function handleHook(provider, explicitEvent) {
|
|
77
|
+
const payload = await readStdinJson();
|
|
78
|
+
payload.__provider = provider;
|
|
79
|
+
const eventName = normalizeEvent(explicitEvent, payload);
|
|
80
|
+
const cwd = payload.cwd || process.cwd();
|
|
81
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
82
|
+
await bootstrapProject(projectRoot);
|
|
83
|
+
await appendSessionEnvelope(projectRoot, eventName, payload).catch(() => {});
|
|
84
|
+
|
|
85
|
+
if (eventName === 'SessionStart' || eventName === 'InstructionsLoaded') {
|
|
86
|
+
const context = await buildContextBrief(projectRoot, 'SessionStart');
|
|
87
|
+
return contextOutput(eventName, context);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (eventName === 'UserPromptSubmit') {
|
|
91
|
+
const prompt = promptText(payload);
|
|
92
|
+
await rememberQuestion(projectRoot, payload, prompt);
|
|
93
|
+
const context = await buildContextBrief(projectRoot, eventName, prompt);
|
|
94
|
+
const warning = hasSecretLikeText(prompt)
|
|
95
|
+
? '\n\nSecurity note: the prompt appears to contain secret-like text. Do not persist raw secret values; store only redacted summaries.'
|
|
96
|
+
: '';
|
|
97
|
+
return contextOutput(eventName, `${context}${warning}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (eventName === 'PreToolUse') {
|
|
101
|
+
const reason = shouldBlockTool(payload);
|
|
102
|
+
if (reason) return blockOutput(provider, reason);
|
|
103
|
+
await rememberTool(projectRoot, payload, `pre ${toolSummary(payload)}`);
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (eventName === 'PostToolUse' || eventName === 'PostToolBatch') {
|
|
108
|
+
await rememberTool(projectRoot, payload, `post ${toolSummary(payload)}`);
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (eventName === 'PreCompact' || eventName === 'PostCompact') {
|
|
113
|
+
await appendContextNote(projectRoot, eventName, 'Compaction lifecycle event captured by llm-wiki-kit.');
|
|
114
|
+
if (eventName === 'PostCompact') {
|
|
115
|
+
return contextOutput(eventName, await buildContextBrief(projectRoot, eventName));
|
|
116
|
+
}
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (eventName === 'Stop' || eventName === 'SubagentStop' || eventName === 'SessionEnd') {
|
|
121
|
+
const assistantText = payload.last_assistant_message || payload.response || payload.assistant_response || '';
|
|
122
|
+
const entry = await buildEntryFromState(projectRoot, payload, assistantText);
|
|
123
|
+
if (entry.question !== '(not captured)' || entry.result !== '(not captured)') {
|
|
124
|
+
await appendLiveQa(projectRoot, entry);
|
|
125
|
+
const queryPath = await writeQueryPage(projectRoot, entry);
|
|
126
|
+
const decisionPath = await writeDecisionPage(projectRoot, entry);
|
|
127
|
+
await appendWikiLog(projectRoot, `captured ${eventName}${queryPath ? `; query=${queryPath}` : ''}${decisionPath ? `; decision=${decisionPath}` : ''}`);
|
|
128
|
+
}
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {};
|
|
133
|
+
}
|