@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,704 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * llmwiki init - configure project-level LLM Wiki integration.
6
+ *
7
+ * Examples:
8
+ * llmwiki init --agent claude-code
9
+ * llmwiki init --agent codex
10
+ * llmwiki init --agent gemini
11
+ * llmwiki init --agent all
12
+ * llmwiki init --profile local --agent codex
13
+ * llmwiki init --workspace --profile team --wiki-root /path/to/team-wiki --agent claude-code
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+ const path = require('path');
19
+ const { writeProjectConfig, writeWorkspaceConfig } = require('./classify-project');
20
+
21
+ const LLMWIKI_CONTEXT_BLOCK_START = '<!-- LLMWIKI:START -->';
22
+ const LLMWIKI_CONTEXT_BLOCK_END = '<!-- LLMWIKI:END -->';
23
+ const STOP_HOOK_COMMAND = [
24
+ 'if [ -d ~/.llmwiki/.git ]; then',
25
+ 'echo \'[LLM Wiki] 如果本次工作产出了新知识',
26
+ '(架构决策、经验模式、踩坑记录等),',
27
+ '可以说「归档到 llmwiki」来沉淀。\'; fi',
28
+ ].join(' ');
29
+
30
+ const AGENT_OPTIONS = [
31
+ { id: 'claude-code', name: 'Claude Code', description: '.claude/settings.json hooks' },
32
+ { id: 'codex', name: 'Codex CLI', description: 'global hooks + marker' },
33
+ { id: 'gemini', name: 'Gemini CLI', description: 'GEMINI.md context' },
34
+ { id: 'all', name: 'All agents', description: 'install every integration' },
35
+ ];
36
+
37
+ function parseArgs(argv) {
38
+ if (argv[0] === 'init') argv = argv.slice(1);
39
+ const out = {
40
+ agent: null,
41
+ workspace: false,
42
+ workspaceAsked: false,
43
+ profile: null,
44
+ wikiRoot: null,
45
+ branch: null,
46
+ help: false,
47
+ };
48
+
49
+ for (let i = 0; i < argv.length; i += 1) {
50
+ const arg = argv[i];
51
+ if (arg === 'help' || arg === '--help' || arg === '-h') {
52
+ out.help = true;
53
+ } else if (arg === '--workspace') {
54
+ out.workspace = true;
55
+ out.workspaceAsked = true;
56
+ } else if (arg === '--profile') {
57
+ out.profile = argv[i + 1] || null;
58
+ i += 1;
59
+ } else if (arg.startsWith('--profile=')) {
60
+ out.profile = arg.slice('--profile='.length);
61
+ } else if (arg === '--wiki-root') {
62
+ out.wikiRoot = argv[i + 1] || null;
63
+ i += 1;
64
+ } else if (arg.startsWith('--wiki-root=')) {
65
+ out.wikiRoot = arg.slice('--wiki-root='.length);
66
+ } else if (arg === '--branch') {
67
+ out.branch = argv[i + 1] || null;
68
+ i += 1;
69
+ } else if (arg.startsWith('--branch=')) {
70
+ out.branch = arg.slice('--branch='.length);
71
+ } else if (arg === '--agent') {
72
+ out.agent = argv[i + 1] || null;
73
+ i += 1;
74
+ } else if (arg.startsWith('--agent=')) {
75
+ out.agent = arg.slice('--agent='.length);
76
+ } else if (!arg.startsWith('-') && !out.agent) {
77
+ out.agent = arg;
78
+ }
79
+ }
80
+
81
+ out.agent = normalizeAgent(out.agent);
82
+ return out;
83
+ }
84
+
85
+ function normalizeAgent(agent) {
86
+ if (!agent) return null;
87
+ const value = agent.toLowerCase();
88
+ if (['claude', 'claude-code', 'claudecode'].includes(value)) return 'claude-code';
89
+ if (['code', 'codex', 'openai-code'].includes(value)) return 'codex';
90
+ if (value === 'gemini') return 'gemini';
91
+ if (value === 'all') return 'all';
92
+ return value;
93
+ }
94
+
95
+ function showInitHelp() {
96
+ console.log('用法: llmwiki init [--workspace] [--profile personal|team|local] [--wiki-root <path>] [--agent <agent>]');
97
+ console.log('');
98
+ console.log('说明:');
99
+ console.log(' 为当前项目导入指定 Agent 的 LLM Wiki 接入配置。');
100
+ console.log('');
101
+ console.log('Agent:');
102
+ console.log(' claude-code 写入 .claude/settings.json hooks');
103
+ console.log(' codex 写入 .llmwiki.json 项目标记,并安装全局 Codex hooks');
104
+ console.log(' gemini 写入 GEMINI.md 项目上下文入口');
105
+ console.log(' all 同时导入 claude-code、codex、gemini');
106
+ console.log('');
107
+ console.log('选项:');
108
+ console.log(' --workspace 生成 workspace 配置 .llmwiki.json,用于多子项目归属判断');
109
+ console.log(' --profile 设置知识库同步边界: personal | team | local');
110
+ console.log(' --wiki-root 设置当前项目使用的物理 wiki 根目录');
111
+ console.log(' --branch 设置 git 同步分支,默认 main');
112
+ console.log(' --help, -h 显示 init 帮助信息');
113
+ }
114
+
115
+ function createSelectionState() {
116
+ return {
117
+ cursor: 0,
118
+ selected: new Set(),
119
+ done: false,
120
+ cancelled: false,
121
+ };
122
+ }
123
+
124
+ function applySelectionKey(state, key) {
125
+ const next = {
126
+ cursor: state.cursor,
127
+ selected: new Set(state.selected),
128
+ done: state.done,
129
+ cancelled: state.cancelled,
130
+ };
131
+
132
+ if (key === 'up') {
133
+ next.cursor = (next.cursor - 1 + AGENT_OPTIONS.length) % AGENT_OPTIONS.length;
134
+ } else if (key === 'down') {
135
+ next.cursor = (next.cursor + 1) % AGENT_OPTIONS.length;
136
+ } else if (key === 'space') {
137
+ const id = AGENT_OPTIONS[next.cursor].id;
138
+ if (next.selected.has(id)) {
139
+ next.selected.delete(id);
140
+ } else if (id === 'all') {
141
+ next.selected = new Set(['all']);
142
+ } else {
143
+ next.selected.delete('all');
144
+ next.selected.add(id);
145
+ }
146
+ } else if (key === 'enter') {
147
+ next.done = true;
148
+ } else if (key === 'escape') {
149
+ next.cancelled = true;
150
+ next.done = true;
151
+ }
152
+
153
+ return next;
154
+ }
155
+
156
+ function keyFromChunk(chunk) {
157
+ return keysFromChunk(chunk)[0] || null;
158
+ }
159
+
160
+ function keysFromChunk(chunk) {
161
+ const value = chunk.toString('utf8');
162
+ const keys = [];
163
+ for (let i = 0; i < value.length; i += 1) {
164
+ const char = value[i];
165
+ const triple = value.slice(i, i + 3);
166
+ if (triple === '\u001b[A') {
167
+ keys.push('up');
168
+ i += 2;
169
+ } else if (triple === '\u001b[B') {
170
+ keys.push('down');
171
+ i += 2;
172
+ } else if (char === '\u0003' || char === '\u001b') {
173
+ keys.push('escape');
174
+ } else if (char === ' ') {
175
+ keys.push('space');
176
+ } else if (char === '\r' || char === '\n') {
177
+ keys.push('enter');
178
+ }
179
+ }
180
+ return keys;
181
+ }
182
+
183
+ function renderAgentPicker(output, state, message = '') {
184
+ const lines = [
185
+ '',
186
+ '╭─ Choose your AI assistant ─────────────────────────╮',
187
+ ...AGENT_OPTIONS.map((agent, index) => {
188
+ const cursor = index === state.cursor ? '›' : ' ';
189
+ const mark = state.selected.has(agent.id) ? '●' : '○';
190
+ return `│ ${cursor} ${mark} ${agent.id.padEnd(12)} ${agent.name.padEnd(14)} ${agent.description.padEnd(27)} │`;
191
+ }),
192
+ '│ │',
193
+ `│ ${message || 'Use ↑/↓ to navigate, Space to select, Enter to submit'.padEnd(54)} │`,
194
+ '╰────────────────────────────────────────────────────────╯',
195
+ ];
196
+
197
+ output.write('\u001b[2J\u001b[H');
198
+ output.write(lines.join('\n'));
199
+ }
200
+
201
+ function promptAgentSelection(input = process.stdin, output = process.stdout) {
202
+ if (!input.isTTY || !output.isTTY) return Promise.resolve(null);
203
+
204
+ let state = createSelectionState();
205
+ let message = '';
206
+
207
+ return new Promise(resolve => {
208
+ const cleanup = () => {
209
+ input.setRawMode(false);
210
+ input.pause();
211
+ output.write('\u001b[?25h\n');
212
+ input.removeListener('data', onData);
213
+ };
214
+
215
+ const finish = value => {
216
+ cleanup();
217
+ resolve(value);
218
+ };
219
+
220
+ const onData = chunk => {
221
+ const keys = keysFromChunk(chunk);
222
+ for (const key of keys) {
223
+ const next = applySelectionKey(state, key);
224
+ if (next.cancelled) {
225
+ finish([]);
226
+ return;
227
+ }
228
+ if (next.done && next.selected.size === 0) {
229
+ state = { ...next, done: false };
230
+ message = 'Select at least one agent with Space, or Esc to cancel'.padEnd(54);
231
+ renderAgentPicker(output, state, message);
232
+ return;
233
+ }
234
+ state = next;
235
+
236
+ if (state.done) {
237
+ finish([...state.selected]);
238
+ return;
239
+ }
240
+ }
241
+
242
+ message = '';
243
+ renderAgentPicker(output, state, message);
244
+ };
245
+
246
+ output.write('\u001b[?25l');
247
+ renderAgentPicker(output, state, message);
248
+ input.setRawMode(true);
249
+ input.resume();
250
+ input.on('data', onData);
251
+ });
252
+ }
253
+
254
+ function renderSinglePicker(output, title, options, cursor, hint = '') {
255
+ const lines = [
256
+ '',
257
+ `╭─ ${title} ─────────────────────────╮`,
258
+ ...options.map((option, index) => {
259
+ const pointer = index === cursor ? '›' : ' ';
260
+ return `│ ${pointer} ${option.label.padEnd(16)} ${option.description || ''}`.padEnd(54) + '│';
261
+ }),
262
+ '│'.padEnd(54) + '│',
263
+ `│ ${hint || 'Use ↑/↓ to navigate, Enter to submit'.padEnd(50)} │`,
264
+ '╰────────────────────────────────────╯',
265
+ ];
266
+
267
+ output.write('\u001b[2J\u001b[H');
268
+ output.write(lines.join('\n'));
269
+ }
270
+
271
+ function promptSingleSelection(title, options, input = process.stdin, output = process.stdout) {
272
+ if (!input.isTTY || !output.isTTY) return Promise.resolve(null);
273
+
274
+ let cursor = 0;
275
+ return new Promise(resolve => {
276
+ const cleanup = () => {
277
+ input.setRawMode(false);
278
+ input.pause();
279
+ output.write('\u001b[?25h\n');
280
+ input.removeListener('data', onData);
281
+ };
282
+
283
+ const finish = value => {
284
+ cleanup();
285
+ resolve(value);
286
+ };
287
+
288
+ const onData = chunk => {
289
+ for (const key of keysFromChunk(chunk)) {
290
+ if (key === 'escape') {
291
+ finish(null);
292
+ return;
293
+ }
294
+ if (key === 'up') cursor = (cursor - 1 + options.length) % options.length;
295
+ if (key === 'down') cursor = (cursor + 1) % options.length;
296
+ if (key === 'space' || key === 'enter') {
297
+ finish(options[cursor].value);
298
+ return;
299
+ }
300
+ }
301
+ renderSinglePicker(output, title, options, cursor);
302
+ };
303
+
304
+ output.write('\u001b[?25l');
305
+ renderSinglePicker(output, title, options, cursor);
306
+ input.setRawMode(true);
307
+ input.resume();
308
+ input.on('data', onData);
309
+ });
310
+ }
311
+
312
+ function promptLine(question, defaultValue = '', input = process.stdin, output = process.stdout) {
313
+ if (!input.isTTY || !output.isTTY) return Promise.resolve(null);
314
+
315
+ const suffix = defaultValue ? ` (${defaultValue})` : '';
316
+ output.write(`${question}${suffix}: `);
317
+ return new Promise(resolve => {
318
+ const onLine = line => {
319
+ input.pause();
320
+ input.removeListener('data', onData);
321
+ resolve(line.trim());
322
+ };
323
+ let buffer = '';
324
+ const onData = chunk => {
325
+ const text = chunk.toString('utf8');
326
+ buffer += text;
327
+ if (buffer.includes('\n') || buffer.includes('\r')) {
328
+ onLine(buffer.replace(/[\r\n].*$/s, ''));
329
+ }
330
+ };
331
+ input.resume();
332
+ input.on('data', onData);
333
+ });
334
+ }
335
+
336
+ function buildInteractiveConfigOptions(args, answers = {}) {
337
+ return {
338
+ workspace: args.workspaceAsked ? args.workspace : Boolean(answers.workspace),
339
+ profile: args.profile || answers.profile || null,
340
+ wikiRoot: args.wikiRoot || answers.wikiRoot || null,
341
+ branch: args.branch || null,
342
+ };
343
+ }
344
+
345
+ function defaultWikiRootForProfile(profile) {
346
+ if (profile === 'local') return '.llmwiki-local';
347
+ if (profile === 'team') return null;
348
+ return '~/.llmwiki';
349
+ }
350
+
351
+ async function promptInteractiveConfig(args, input = process.stdin, output = process.stdout) {
352
+ if (!input.isTTY || !output.isTTY) return args;
353
+
354
+ const workspace = args.workspaceAsked
355
+ ? args.workspace
356
+ : await promptSingleSelection('LLM Wiki Scope', [
357
+ { label: 'project', value: false, description: 'single repo' },
358
+ { label: 'workspace', value: true, description: 'multi repo root' },
359
+ ], input, output);
360
+
361
+ const profile = args.profile || await promptSingleSelection('LLM Wiki Profile', [
362
+ { label: 'personal', value: 'personal', description: 'personal knowledge' },
363
+ { label: 'team', value: 'team', description: 'company/team shared wiki' },
364
+ { label: 'local', value: 'local', description: 'local only, no push/pull' },
365
+ ], input, output);
366
+
367
+ const defaultRoot = defaultWikiRootForProfile(profile);
368
+ let wikiRoot = args.wikiRoot || await promptLine('wikiRoot', defaultRoot || '', input, output);
369
+ if (!wikiRoot && defaultRoot) wikiRoot = null;
370
+
371
+ if (profile === 'team') {
372
+ while (!wikiRoot) {
373
+ wikiRoot = await promptLine('team profile requires wikiRoot', '', input, output);
374
+ }
375
+ }
376
+
377
+ return {
378
+ ...args,
379
+ ...buildInteractiveConfigOptions(args, { workspace, profile, wikiRoot }),
380
+ };
381
+ }
382
+
383
+ function ensureGitProject(projectRoot, isWorkspace) {
384
+ if (!isWorkspace && !fs.existsSync(path.join(projectRoot, '.git'))) {
385
+ console.error('错误:当前目录不是 git 仓库根目录');
386
+ process.exit(1);
387
+ }
388
+ }
389
+
390
+ function ensureArrayHook(settings, eventName) {
391
+ if (!settings.hooks) settings.hooks = {};
392
+ if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
393
+ }
394
+
395
+ function upsertCommandHook(hookList, command, options = {}) {
396
+ const matchTexts = options.matchTexts || [options.matchText || 'llmwiki', 'LLM Wiki'];
397
+ for (const entry of hookList) {
398
+ for (const hook of entry.hooks || []) {
399
+ if (hook.command && matchTexts.some(text => hook.command.includes(text))) {
400
+ hook.command = command;
401
+ if (options.timeout) hook.timeout = options.timeout;
402
+ return 'updated';
403
+ }
404
+ }
405
+ }
406
+
407
+ const hook = {
408
+ type: 'command',
409
+ command,
410
+ };
411
+ if (options.timeout) hook.timeout = options.timeout;
412
+
413
+ hookList.push({
414
+ matcher: options.matcher || '',
415
+ hooks: [hook],
416
+ });
417
+ return 'installed';
418
+ }
419
+
420
+ function removeCommandHooks(hookList, matchTexts = ['llmwiki', 'LLM Wiki']) {
421
+ let removed = false;
422
+ for (let i = hookList.length - 1; i >= 0; i -= 1) {
423
+ const entry = hookList[i];
424
+ const nextHooks = (entry.hooks || []).filter(hook => {
425
+ const shouldRemove = hook.command && matchTexts.some(text => hook.command.includes(text));
426
+ if (shouldRemove) removed = true;
427
+ return !shouldRemove;
428
+ });
429
+ if (nextHooks.length) {
430
+ entry.hooks = nextHooks;
431
+ } else {
432
+ hookList.splice(i, 1);
433
+ }
434
+ }
435
+ return removed;
436
+ }
437
+
438
+ function buildAgentContextBlock() {
439
+ return [
440
+ LLMWIKI_CONTEXT_BLOCK_START,
441
+ '# LLM Wiki',
442
+ '',
443
+ '本项目已接入 LLM Wiki。默认只使用 `.llmwiki.json` 做项目归属和归档路由判断。',
444
+ '',
445
+ '- 普通对话、启动和 clear 后不要自动读取全局 wiki 页面。',
446
+ '- 用户明确要求召回/归档,或任务明显需要历史项目知识时,再按需召回。',
447
+ '- 召回时先运行 `llmwiki classify` 判断当前项目归属。',
448
+ '- 优先读取当前 archiveProjects 对应页面;公共 patterns/tools/concepts 只在关键词命中后读取。',
449
+ '- 归档时更新对应 wiki 页面,并维护索引和日志。',
450
+ LLMWIKI_CONTEXT_BLOCK_END,
451
+ ].join('\n');
452
+ }
453
+
454
+ function upsertContextBlock(filePath) {
455
+ const block = buildAgentContextBlock();
456
+ let content = '';
457
+ if (fs.existsSync(filePath)) {
458
+ content = fs.readFileSync(filePath, 'utf8').trimEnd();
459
+ }
460
+
461
+ const pattern = new RegExp(`${LLMWIKI_CONTEXT_BLOCK_START}[\\s\\S]*?${LLMWIKI_CONTEXT_BLOCK_END}`);
462
+ const nextContent = pattern.test(content)
463
+ ? `${content.replace(pattern, block)}\n`
464
+ : `${content ? `${content}\n\n` : ''}${block}\n`;
465
+
466
+ fs.writeFileSync(filePath, nextContent);
467
+ }
468
+
469
+ function installClaudeCode(projectRoot) {
470
+ const claudeDir = path.join(projectRoot, '.claude');
471
+ const settingsPath = path.join(claudeDir, 'settings.json');
472
+ const claudePath = path.join(projectRoot, 'CLAUDE.md');
473
+ fs.mkdirSync(claudeDir, { recursive: true });
474
+
475
+ let settings = {};
476
+ if (fs.existsSync(settingsPath)) {
477
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
478
+ }
479
+
480
+ ensureArrayHook(settings, 'Stop');
481
+ if (settings.hooks.UserPromptSubmit) {
482
+ removeCommandHooks(settings.hooks.UserPromptSubmit, ['llmwiki', 'LLM Wiki', 'yahallmwiki', 'Yaha Wiki']);
483
+ }
484
+ const stopStatus = upsertCommandHook(settings.hooks.Stop, STOP_HOOK_COMMAND);
485
+ upsertContextBlock(claudePath);
486
+
487
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, '\t') + '\n');
488
+ console.log(`✓ Claude Code hook/context 已${stopStatus === 'updated' ? '更新' : '导入'}: .claude/settings.json, CLAUDE.md`);
489
+ }
490
+
491
+ function installCodex(projectRoot) {
492
+ const result = installCodexGlobalHooks();
493
+ console.log('✓ Codex 接入已标记: .llmwiki.json');
494
+ if (result.status === 'installed') {
495
+ console.log(`✓ Codex 全局 hook 已写入: ${result.hooksPath}`);
496
+ } else if (result.status === 'updated') {
497
+ console.log(`✓ Codex 全局 hook 已更新: ${result.hooksPath}`);
498
+ } else if (result.status === 'unchanged') {
499
+ console.log(`✓ Codex 全局 hook 已存在: ${result.hooksPath}`);
500
+ } else {
501
+ console.log(`⚠ Codex 全局 hook 未写入: ${result.reason}`);
502
+ }
503
+ console.log(' Codex hook 只注入项目归属元数据;需要召回时再按需读取 wiki。');
504
+ }
505
+
506
+ function resolveCodexHome() {
507
+ return process.env.CODEX_HOME
508
+ ? path.resolve(process.env.CODEX_HOME)
509
+ : path.join(os.homedir(), '.codex');
510
+ }
511
+
512
+ function buildCodexHookCommand() {
513
+ return `"${process.execPath}" "${path.join(__dirname, 'codex-hook.js')}"`;
514
+ }
515
+
516
+ function ensureCodexEventHook(settings, eventName, hookCommand, options = {}) {
517
+ if (!settings.hooks) settings.hooks = {};
518
+ if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
519
+
520
+ let changed = false;
521
+ for (const entry of settings.hooks[eventName]) {
522
+ for (const hook of entry.hooks || []) {
523
+ if (hook.command && hook.command.includes('codex-hook.js')) {
524
+ if (hook.command !== hookCommand) {
525
+ hook.command = hookCommand;
526
+ changed = true;
527
+ }
528
+ return changed ? 'updated' : 'unchanged';
529
+ }
530
+ }
531
+ }
532
+
533
+ const entry = {
534
+ hooks: [
535
+ {
536
+ type: 'command',
537
+ command: hookCommand,
538
+ },
539
+ ],
540
+ };
541
+ if (options.matcher) entry.matcher = options.matcher;
542
+ settings.hooks[eventName].push(entry);
543
+ return 'installed';
544
+ }
545
+
546
+ function removeCodexEventHook(settings, eventName) {
547
+ if (!settings.hooks || !settings.hooks[eventName]) return false;
548
+ return removeCommandHooks(settings.hooks[eventName], ['codex-hook.js']);
549
+ }
550
+
551
+ function installCodexGlobalHooks() {
552
+ const codexHome = resolveCodexHome();
553
+ const hooksPath = path.join(codexHome, 'hooks.json');
554
+ const hookCommand = buildCodexHookCommand();
555
+ let settings = {};
556
+
557
+ try {
558
+ fs.mkdirSync(codexHome, { recursive: true });
559
+ if (fs.existsSync(hooksPath)) {
560
+ settings = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
561
+ }
562
+
563
+ const sessionStatus = ensureCodexEventHook(settings, 'SessionStart', hookCommand, {
564
+ matcher: 'startup|resume|clear',
565
+ });
566
+ const promptRemoved = removeCodexEventHook(settings, 'UserPromptSubmit');
567
+
568
+ if (sessionStatus === 'unchanged' && !promptRemoved) {
569
+ return { status: 'unchanged', hooksPath };
570
+ }
571
+
572
+ fs.writeFileSync(hooksPath, JSON.stringify(settings, null, 2) + '\n');
573
+ return {
574
+ status: sessionStatus === 'installed' ? 'installed' : 'updated',
575
+ hooksPath,
576
+ };
577
+ } catch (error) {
578
+ return {
579
+ status: 'failed',
580
+ hooksPath,
581
+ reason: error.message,
582
+ };
583
+ }
584
+ }
585
+
586
+ function installGemini(projectRoot) {
587
+ const geminiPath = path.join(projectRoot, 'GEMINI.md');
588
+ upsertContextBlock(geminiPath);
589
+ console.log('✓ Gemini context 已导入: GEMINI.md');
590
+ }
591
+
592
+ function expandAgent(agent) {
593
+ return agent === 'all'
594
+ ? AGENT_OPTIONS.filter(option => option.id !== 'all').map(option => option.id)
595
+ : [agent];
596
+ }
597
+
598
+ function installAgent(projectRoot, agent) {
599
+ if (agent === 'claude-code') {
600
+ installClaudeCode(projectRoot);
601
+ } else if (agent === 'codex') {
602
+ installCodex(projectRoot);
603
+ } else if (agent === 'gemini') {
604
+ installGemini(projectRoot);
605
+ } else {
606
+ console.error(`错误:不支持的 agent: ${agent}`);
607
+ showInitHelp();
608
+ process.exit(1);
609
+ }
610
+ }
611
+
612
+ function installAgents(projectRoot, agents) {
613
+ for (const agent of agents) {
614
+ installAgent(projectRoot, agent);
615
+ }
616
+ }
617
+
618
+ async function main() {
619
+ const projectRoot = process.cwd();
620
+ let args = parseArgs(process.argv.slice(2));
621
+
622
+ if (args.help) {
623
+ showInitHelp();
624
+ return;
625
+ }
626
+
627
+ let wikiConfigWritten = false;
628
+ const writeWikiConfig = () => {
629
+ const configOptions = {
630
+ profile: args.profile || (args.workspace && args.wikiRoot ? 'team' : undefined),
631
+ wikiRoot: args.wikiRoot || undefined,
632
+ branch: args.branch || undefined,
633
+ };
634
+
635
+ if (args.workspace) {
636
+ const { configPath, config } = writeWorkspaceConfig(projectRoot, configOptions);
637
+ console.log(`✓ workspace 配置已写入 ${path.relative(projectRoot, configPath)}`);
638
+ console.log(` - workspace: ${config.name}`);
639
+ console.log(` - profile: ${config.profile}`);
640
+ console.log(` - wikiRoot: ${config.wikiRoot}`);
641
+ console.log(` - sync: ${config.sync.type}${config.sync.branch ? ` ${config.sync.branch}` : ''}`);
642
+ console.log(` - projects: ${Object.keys(config.projectRoots).join(', ') || '(none detected)'}`);
643
+ wikiConfigWritten = true;
644
+ return;
645
+ }
646
+
647
+ const { configPath, config } = writeProjectConfig(projectRoot, configOptions);
648
+ console.log(`✓ project 配置已写入 ${path.relative(projectRoot, configPath)}`);
649
+ console.log(` - project: ${config.name}`);
650
+ console.log(` - profile: ${config.profile}`);
651
+ console.log(` - wikiRoot: ${config.wikiRoot}`);
652
+ console.log(` - sync: ${config.sync.type}${config.sync.branch ? ` ${config.sync.branch}` : ''}`);
653
+ wikiConfigWritten = true;
654
+ };
655
+
656
+ if (!args.agent) {
657
+ const selectedAgents = await promptAgentSelection();
658
+ if (selectedAgents && selectedAgents.length) {
659
+ args = await promptInteractiveConfig(args);
660
+ ensureGitProject(projectRoot, args.workspace);
661
+ if (args.workspace && !args.profile && !args.wikiRoot) {
662
+ throw new Error('workspace init requires --profile or --wiki-root');
663
+ }
664
+ if (!wikiConfigWritten) writeWikiConfig();
665
+ installAgents(projectRoot, selectedAgents);
666
+ return;
667
+ }
668
+
669
+ console.log('请选择 agent 后再导入对应接入:');
670
+ console.log(' llmwiki init --agent claude-code');
671
+ console.log(' llmwiki init --agent codex');
672
+ console.log(' llmwiki init --agent gemini');
673
+ console.log(' llmwiki init --agent all');
674
+ return;
675
+ }
676
+
677
+ ensureGitProject(projectRoot, args.workspace);
678
+ if (args.workspace && !args.profile && !args.wikiRoot) {
679
+ throw new Error('workspace init requires --profile or --wiki-root');
680
+ }
681
+
682
+ if (args.workspace) {
683
+ writeWikiConfig();
684
+ }
685
+ if (!wikiConfigWritten) writeWikiConfig();
686
+ installAgents(projectRoot, expandAgent(args.agent));
687
+ }
688
+
689
+ if (require.main === module) {
690
+ main().catch(error => {
691
+ console.error(error.message);
692
+ process.exit(1);
693
+ });
694
+ }
695
+
696
+ module.exports = {
697
+ applySelectionKey,
698
+ buildInteractiveConfigOptions,
699
+ createSelectionState,
700
+ keyFromChunk,
701
+ keysFromChunk,
702
+ installCodexGlobalHooks,
703
+ main,
704
+ };
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "llmwiki-connector",
3
+ "version": "0.1.0",
4
+ "description": "连接 LLM Wiki 与当前项目,提供知识召回、归档能力"
5
+ }