@fugui200/llmwiki 0.1.2-beta.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,403 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const CONFIG_FILE = '.llmwiki.json';
8
+ const DEFAULT_WIKI_ROOT = '~/.llmwiki';
9
+ const LOCAL_WIKI_ROOT = '.llmwiki-local';
10
+ const DEFAULT_PROFILE = 'personal';
11
+ const DEFAULT_SYNC_BRANCH = 'main';
12
+
13
+ function normalizePath(filePath) {
14
+ return filePath.replaceAll(path.sep, '/');
15
+ }
16
+
17
+ function normalizeProjectName(name) {
18
+ if (name === '.llmwiki') return 'llmwiki';
19
+ if (name.startsWith('.') && name.length > 1) return name.slice(1);
20
+ return name;
21
+ }
22
+
23
+ function projectNameFromPackage(root) {
24
+ const packagePath = path.join(root, 'package.json');
25
+ if (!fs.existsSync(packagePath)) return null;
26
+
27
+ try {
28
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
29
+ const name = typeof pkg.name === 'string' ? pkg.name.split('/').pop() : null;
30
+ return name ? normalizeProjectName(name) : null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function defaultNameFromRoot(root) {
37
+ const absoluteRoot = path.resolve(root);
38
+ return projectNameFromPackage(absoluteRoot) || normalizeProjectName(path.basename(absoluteRoot));
39
+ }
40
+
41
+ function discoverProjectRoots(workspaceRoot) {
42
+ const out = {};
43
+ if (!fs.existsSync(workspaceRoot)) return out;
44
+
45
+ for (const entry of fs.readdirSync(workspaceRoot, { withFileTypes: true })) {
46
+ if (!entry.isDirectory()) continue;
47
+ if (entry.name.startsWith('.')) continue;
48
+
49
+ const child = path.join(workspaceRoot, entry.name);
50
+ if (fs.existsSync(path.join(child, '.git'))) {
51
+ out[entry.name] = entry.name;
52
+ }
53
+ }
54
+
55
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
56
+ }
57
+
58
+ function normalizeProfile(profile) {
59
+ const value = profile || DEFAULT_PROFILE;
60
+ if (['personal', 'team', 'local'].includes(value)) return value;
61
+ throw new Error(`unsupported profile: ${value}`);
62
+ }
63
+
64
+ function hasOwn(object, key) {
65
+ return Object.prototype.hasOwnProperty.call(object, key);
66
+ }
67
+
68
+ function createSyncConfig(profile, options = {}) {
69
+ if (options.sync) return options.sync;
70
+ if (profile === 'local') return { type: 'none' };
71
+ return {
72
+ type: 'git',
73
+ branch: options.branch || DEFAULT_SYNC_BRANCH,
74
+ };
75
+ }
76
+
77
+ function createWikiRoot(profile, options = {}) {
78
+ const hasWikiRoot = hasOwn(options, 'wikiRoot') && options.wikiRoot;
79
+ if (profile === 'team' && !hasWikiRoot) {
80
+ throw new Error('team profile requires --wiki-root');
81
+ }
82
+ if (hasWikiRoot) return options.wikiRoot;
83
+ if (profile === 'local') return LOCAL_WIKI_ROOT;
84
+ return DEFAULT_WIKI_ROOT;
85
+ }
86
+
87
+ function createWorkspaceConfig(workspaceRoot, options = {}) {
88
+ const name = options.name || defaultNameFromRoot(workspaceRoot);
89
+ const profile = normalizeProfile(options.profile);
90
+ return {
91
+ version: 1,
92
+ type: 'workspace',
93
+ name,
94
+ profile,
95
+ wikiRoot: createWikiRoot(profile, options),
96
+ sync: createSyncConfig(profile, options),
97
+ projectRoots: options.projectRoots || discoverProjectRoots(workspaceRoot),
98
+ projectAliases: options.projectAliases || {},
99
+ defaultProject: options.defaultProject || name,
100
+ };
101
+ }
102
+
103
+ function createProjectConfig(projectRoot, options = {}) {
104
+ const name = options.name || defaultNameFromRoot(projectRoot);
105
+ const profile = normalizeProfile(options.profile);
106
+ return {
107
+ version: 1,
108
+ type: 'project',
109
+ name,
110
+ profile,
111
+ wikiRoot: createWikiRoot(profile, options),
112
+ sync: createSyncConfig(profile, options),
113
+ };
114
+ }
115
+
116
+ function expandHome(filePath) {
117
+ const home = process.env.HOME || '';
118
+ if (filePath === '~' || filePath === '$HOME') return home;
119
+ if (filePath && filePath.startsWith('~/')) {
120
+ return path.join(home, filePath.slice(2));
121
+ }
122
+ if (filePath && filePath.startsWith('$HOME/')) {
123
+ return path.join(home, filePath.slice('$HOME/'.length));
124
+ }
125
+ return filePath;
126
+ }
127
+
128
+ function resolveWikiRoot(config, configDir = process.cwd()) {
129
+ const rawRoot = createWikiRoot(normalizeProfile(config.profile), config);
130
+ const expanded = expandHome(rawRoot);
131
+ return path.isAbsolute(expanded)
132
+ ? path.resolve(expanded)
133
+ : path.resolve(configDir, expanded);
134
+ }
135
+
136
+ function findConfig(startDir = process.cwd()) {
137
+ const match = findEffectiveConfig(startDir);
138
+ return match ? match.path : null;
139
+ }
140
+
141
+ function readConfigMatch(configPath) {
142
+ const config = loadConfig(configPath);
143
+ return {
144
+ path: configPath,
145
+ dir: path.dirname(configPath),
146
+ config,
147
+ };
148
+ }
149
+
150
+ function isStandaloneProjectConfig(config) {
151
+ return config && config.type === 'project' && config.scope === 'standalone';
152
+ }
153
+
154
+ function findConfigChain(startDir = process.cwd()) {
155
+ let current = path.resolve(startDir);
156
+ const matches = [];
157
+ while (true) {
158
+ const candidate = path.join(current, CONFIG_FILE);
159
+ if (fs.existsSync(candidate)) {
160
+ matches.push(readConfigMatch(candidate));
161
+ }
162
+
163
+ const parent = path.dirname(current);
164
+ if (parent === current) return matches;
165
+ current = parent;
166
+ }
167
+ }
168
+
169
+ function loadConfig(configPath) {
170
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
171
+ }
172
+
173
+ function findEffectiveConfig(startDir = process.cwd()) {
174
+ const chain = findConfigChain(startDir);
175
+ if (!chain.length) return null;
176
+
177
+ const nearest = chain[0];
178
+ if (isStandaloneProjectConfig(nearest.config)) {
179
+ return nearest;
180
+ }
181
+
182
+ return chain.find(match => match.config.type === 'workspace') || nearest;
183
+ }
184
+
185
+ function loadConfigIfExists(configPath) {
186
+ if (!fs.existsSync(configPath)) return {};
187
+ return loadConfig(configPath);
188
+ }
189
+
190
+ function writeWorkspaceConfig(workspaceRoot, options = {}) {
191
+ const configPath = path.join(workspaceRoot, CONFIG_FILE);
192
+ const existing = loadConfigIfExists(configPath);
193
+ const config = {
194
+ ...existing,
195
+ ...createWorkspaceConfig(workspaceRoot, { ...existing, ...options }),
196
+ type: 'workspace',
197
+ };
198
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
199
+ return { configPath, config };
200
+ }
201
+
202
+ function writeProjectConfig(projectRoot, options = {}) {
203
+ const configPath = path.join(projectRoot, CONFIG_FILE);
204
+ const existing = loadConfigIfExists(configPath);
205
+ const config = {
206
+ ...existing,
207
+ ...createProjectConfig(projectRoot, { ...existing, ...options }),
208
+ type: 'project',
209
+ };
210
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
211
+ return { configPath, config };
212
+ }
213
+
214
+ function pathMatchesProject(targetPath, workspaceRoot, projectRoot) {
215
+ const absoluteTarget = path.resolve(workspaceRoot, targetPath);
216
+ const absoluteProject = path.resolve(workspaceRoot, projectRoot);
217
+ const relative = path.relative(absoluteProject, absoluteTarget);
218
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
219
+ }
220
+
221
+ function directoryForGitLookup(inputPath) {
222
+ if (fs.existsSync(inputPath)) {
223
+ const stat = fs.statSync(inputPath);
224
+ return stat.isDirectory() ? inputPath : path.dirname(inputPath);
225
+ }
226
+
227
+ return path.extname(inputPath) ? path.dirname(inputPath) : inputPath;
228
+ }
229
+
230
+ function findGitProjectRoot(startPath, boundaryDir) {
231
+ let current = path.resolve(directoryForGitLookup(startPath));
232
+ const boundary = path.resolve(boundaryDir);
233
+
234
+ while (true) {
235
+ const relativeToBoundary = path.relative(boundary, current);
236
+ if (relativeToBoundary.startsWith('..') || path.isAbsolute(relativeToBoundary)) {
237
+ return null;
238
+ }
239
+
240
+ if (current !== boundary && fs.existsSync(path.join(current, '.git'))) {
241
+ return current;
242
+ }
243
+
244
+ if (current === boundary) return null;
245
+ const parent = path.dirname(current);
246
+ if (parent === current) return null;
247
+ current = parent;
248
+ }
249
+ }
250
+
251
+ function projectNameFromRoot(projectRoot) {
252
+ return defaultNameFromRoot(projectRoot);
253
+ }
254
+
255
+ function classifyWorkspace(config, configDir, options = {}) {
256
+ const cwd = path.resolve(options.cwd || process.cwd());
257
+ const paths = options.paths && options.paths.length ? options.paths : [cwd];
258
+ const matched = new Set();
259
+
260
+ for (const inputPath of paths) {
261
+ const absoluteInput = path.isAbsolute(inputPath)
262
+ ? inputPath
263
+ : path.resolve(cwd, inputPath);
264
+ const relativeToWorkspace = normalizePath(path.relative(configDir, absoluteInput));
265
+ let inputMatched = false;
266
+
267
+ for (const [projectName, projectRoot] of Object.entries(config.projectRoots || {})) {
268
+ if (pathMatchesProject(relativeToWorkspace, configDir, projectRoot)) {
269
+ matched.add(projectName);
270
+ inputMatched = true;
271
+ }
272
+ }
273
+
274
+ if (!inputMatched) {
275
+ const gitProjectRoot = findGitProjectRoot(absoluteInput, configDir);
276
+ if (gitProjectRoot) {
277
+ matched.add(projectNameFromRoot(gitProjectRoot));
278
+ }
279
+ }
280
+ }
281
+
282
+ if (matched.size === 0) {
283
+ const defaultProject = config.defaultProject || config.name;
284
+ const archiveProject = resolveArchiveProject(config, defaultProject);
285
+ return {
286
+ workspace: config.name,
287
+ matchedProjects: [defaultProject],
288
+ archiveProjects: [archiveProject],
289
+ archiveType: 'workspace',
290
+ suggestedPath: `wiki/projects/${archiveProject}/`,
291
+ };
292
+ }
293
+
294
+ const matchedProjects = [...matched].sort((a, b) => a.localeCompare(b));
295
+ const archiveProjects = uniqueSorted(matchedProjects.map(project => resolveArchiveProject(config, project)));
296
+ if (archiveProjects.length === 1) {
297
+ return {
298
+ workspace: config.name,
299
+ matchedProjects,
300
+ archiveProjects,
301
+ archiveType: 'project',
302
+ suggestedPath: `wiki/projects/${archiveProjects[0]}/`,
303
+ };
304
+ }
305
+
306
+ return {
307
+ workspace: config.name,
308
+ matchedProjects,
309
+ archiveProjects,
310
+ archiveType: 'synthesis',
311
+ suggestedPath: `wiki/synthesis/${archiveProjects.join('-') || config.name}-<topic>.md`,
312
+ };
313
+ }
314
+
315
+ function uniqueSorted(values) {
316
+ return [...new Set(values)].sort((a, b) => a.localeCompare(b));
317
+ }
318
+
319
+ function resolveArchiveProject(config, projectName) {
320
+ const aliases = config.projectAliases || {};
321
+ return aliases[projectName] || projectName;
322
+ }
323
+
324
+ function classifyProject(config) {
325
+ const projectName = config.name;
326
+ return {
327
+ workspace: null,
328
+ matchedProjects: [projectName],
329
+ archiveProjects: [projectName],
330
+ archiveType: 'project',
331
+ suggestedPath: `wiki/projects/${projectName}/`,
332
+ };
333
+ }
334
+
335
+ function classifyConfig(config, configDir, options = {}) {
336
+ if (config.type === 'workspace') {
337
+ return classifyWorkspace(config, configDir, options);
338
+ }
339
+
340
+ if (config.type === 'project') {
341
+ return classifyProject(config, configDir, options);
342
+ }
343
+
344
+ return {
345
+ workspace: null,
346
+ matchedProjects: [],
347
+ archiveProjects: [],
348
+ archiveType: 'unknown-config',
349
+ suggestedPath: null,
350
+ };
351
+ }
352
+
353
+ function classifyFromCwd(options = {}) {
354
+ const cwd = path.resolve(options.cwd || process.cwd());
355
+ const match = findEffectiveConfig(cwd);
356
+ if (!match) {
357
+ return {
358
+ workspace: null,
359
+ matchedProjects: [],
360
+ archiveProjects: [],
361
+ archiveType: 'unconfigured',
362
+ suggestedPath: null,
363
+ };
364
+ }
365
+
366
+ return classifyConfig(match.config, match.dir, {
367
+ cwd,
368
+ paths: options.paths || [],
369
+ });
370
+ }
371
+
372
+ function main() {
373
+ const result = classifyFromCwd({ paths: process.argv.slice(2) });
374
+ console.log(JSON.stringify(result, null, 2));
375
+ }
376
+
377
+ if (require.main === module) {
378
+ main();
379
+ }
380
+
381
+ module.exports = {
382
+ CONFIG_FILE,
383
+ DEFAULT_PROFILE,
384
+ DEFAULT_SYNC_BRANCH,
385
+ DEFAULT_WIKI_ROOT,
386
+ classifyConfig,
387
+ classifyFromCwd,
388
+ classifyProject,
389
+ classifyWorkspace,
390
+ createProjectConfig,
391
+ createWorkspaceConfig,
392
+ defaultNameFromRoot,
393
+ discoverProjectRoots,
394
+ findConfigChain,
395
+ findEffectiveConfig,
396
+ findGitProjectRoot,
397
+ findConfig,
398
+ loadConfigIfExists,
399
+ loadConfig,
400
+ resolveWikiRoot,
401
+ writeProjectConfig,
402
+ writeWorkspaceConfig,
403
+ };
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const {
6
+ CONFIG_FILE,
7
+ classifyConfig,
8
+ findConfig,
9
+ loadConfig,
10
+ resolveWikiRoot,
11
+ } = require('./classify-project');
12
+
13
+ function safeObject(value) {
14
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
15
+ }
16
+
17
+ function readStdin() {
18
+ return new Promise(resolve => {
19
+ const chunks = [];
20
+ process.stdin.on('data', chunk => chunks.push(Buffer.from(chunk)));
21
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8').trim()));
22
+ process.stdin.resume();
23
+ });
24
+ }
25
+
26
+ async function readPayload() {
27
+ const raw = await readStdin();
28
+ if (!raw) return {};
29
+ try {
30
+ return safeObject(JSON.parse(raw));
31
+ } catch {
32
+ return {};
33
+ }
34
+ }
35
+
36
+ function readHookEventName(payload) {
37
+ const raw = String(payload.hook_event_name || payload.hookEventName || payload.event || payload.name || '').trim();
38
+ return raw || null;
39
+ }
40
+
41
+ function resolveCwd(payload) {
42
+ const cwd = String(payload.cwd || payload.project_path || payload.projectPath || '').trim();
43
+ return cwd ? path.resolve(cwd) : process.cwd();
44
+ }
45
+
46
+ function formatJson(value) {
47
+ return JSON.stringify(value, null, 2);
48
+ }
49
+
50
+ function readLegacyProjectTargets(classification) {
51
+ const archiveProjects = new Set(classification.archiveProjects || []);
52
+ return (classification.matchedProjects || [])
53
+ .filter(project => !archiveProjects.has(project))
54
+ .map(project => `wiki/projects/${project}/`);
55
+ }
56
+
57
+ function buildAdditionalContext(cwd) {
58
+ const configPath = findConfig(cwd);
59
+ if (!configPath) return null;
60
+
61
+ const config = loadConfig(configPath);
62
+ const configDir = path.dirname(configPath);
63
+ const classification = classifyConfig(config, configDir, { cwd });
64
+ const wikiRoot = resolveWikiRoot(config, configDir);
65
+ const legacyTargets = readLegacyProjectTargets(classification);
66
+
67
+ const sections = [
68
+ '[LLM Wiki]',
69
+ `- config: ${CONFIG_FILE}`,
70
+ `- wikiRoot: ${wikiRoot}`,
71
+ `- profile: ${config.profile || 'personal'}`,
72
+ `- sync: ${config.sync && config.sync.type === 'none' ? 'none' : `git ${(config.sync && config.sync.branch) || 'main'}`}`,
73
+ `- matchedProjects: ${classification.matchedProjects.join(', ') || '(none)'}`,
74
+ `- archiveProjects: ${(classification.archiveProjects || []).join(', ') || '(none)'}`,
75
+ `- archiveTarget: ${classification.suggestedPath || '(none)'}`,
76
+ '',
77
+ 'Use this as routing metadata only. Do not read or load global LLM Wiki pages at startup, after /clear, or for ordinary prompts.',
78
+ 'Search/read wiki pages only when the user asks to recall/archive knowledge, or when the current task clearly needs prior project context.',
79
+ 'When recall is needed, prefer current archiveProjects first; use shared patterns/tools/concepts only after keyword matching.',
80
+ ];
81
+
82
+ if (legacyTargets.length) {
83
+ sections.push(
84
+ '',
85
+ `Alias note: before writing archiveTarget, also check existing legacy project pages under ${legacyTargets.join(', ')} so earlier child-repo knowledge is not orphaned.`,
86
+ );
87
+ }
88
+
89
+ sections.push('', '## classification', formatJson(classification));
90
+ return sections.join('\n');
91
+ }
92
+
93
+ async function main() {
94
+ const payload = await readPayload();
95
+ const hookEventName = readHookEventName(payload);
96
+
97
+ if (hookEventName !== 'SessionStart') {
98
+ process.stdout.write('{}\n');
99
+ return;
100
+ }
101
+
102
+ const additionalContext = buildAdditionalContext(resolveCwd(payload));
103
+ if (!additionalContext) {
104
+ process.stdout.write('{}\n');
105
+ return;
106
+ }
107
+
108
+ process.stdout.write(JSON.stringify({
109
+ hookSpecificOutput: {
110
+ hookEventName,
111
+ additionalContext,
112
+ },
113
+ }) + '\n');
114
+ }
115
+
116
+ if (require.main === module) {
117
+ main().catch(error => {
118
+ process.stderr.write(`[llmwiki codex hook] ${error.message}\n`);
119
+ process.stdout.write('{}\n');
120
+ });
121
+ }
122
+
123
+ module.exports = {
124
+ buildAdditionalContext,
125
+ readLegacyProjectTargets,
126
+ resolveCwd,
127
+ };