knowy-cli 0.1.2

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/src/i18n.js ADDED
@@ -0,0 +1,172 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { KNOWY_CONFIG } from './constants.js';
4
+
5
+ /**
6
+ * Detect the user's language from system environment.
7
+ * Returns a BCP-47 language tag like 'en', 'zh-TW', 'ja', etc.
8
+ */
9
+ export function detectLanguage() {
10
+ // Try LANG, LC_ALL, LC_MESSAGES
11
+ const raw = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '';
12
+ // e.g. "zh_TW.UTF-8" → "zh-TW", "en_US.UTF-8" → "en"
13
+ const match = raw.match(/^([a-z]{2})(?:_([A-Z]{2}))?/);
14
+ if (match) {
15
+ const lang = match[1];
16
+ const region = match[2];
17
+ if (region) return `${lang}-${region}`;
18
+ return lang;
19
+ }
20
+
21
+ // Fallback: try Intl
22
+ try {
23
+ const locale = Intl.DateTimeFormat().resolvedOptions().locale;
24
+ if (locale) return locale;
25
+ } catch { /* ignore */ }
26
+
27
+ return 'en';
28
+ }
29
+
30
+ /**
31
+ * Resolve language: .knowy.json override > detected system language.
32
+ * Normalizes to a supported language key.
33
+ */
34
+ export async function resolveLanguage(projectRoot) {
35
+ // Check .knowy.json override
36
+ try {
37
+ const config = JSON.parse(await readFile(join(projectRoot, KNOWY_CONFIG), 'utf-8'));
38
+ if (config.language) return normalizeLanguage(config.language);
39
+ } catch { /* no config yet */ }
40
+
41
+ return normalizeLanguage(detectLanguage());
42
+ }
43
+
44
+ /**
45
+ * Normalize a locale string to a supported language key.
46
+ * Supported: 'en', 'zh-TW'
47
+ * Add more as needed.
48
+ */
49
+ export function normalizeLanguage(locale) {
50
+ if (!locale) return 'en';
51
+ const lower = locale.toLowerCase();
52
+ if (lower.startsWith('zh')) return 'zh-TW';
53
+ // Add more mappings here:
54
+ // if (lower.startsWith('ja')) return 'ja';
55
+ // if (lower.startsWith('ko')) return 'ko';
56
+ return 'en';
57
+ }
58
+
59
+ // ── Message Catalog ────────────────────────────────────────────────
60
+
61
+ const messages = {
62
+ en: {
63
+ // CLI
64
+ 'cli.init.exists': '.knowledge/ already exists.',
65
+ 'cli.init.continue': 'Continue? (Knowledge files will not be overwritten)',
66
+ 'cli.init.aborted': 'Aborted.',
67
+ 'cli.init.created': (f) => ` ✓ Created ${f}`,
68
+ 'cli.init.skipped': (f) => ` · Skipped ${f} (already exists)`,
69
+ 'cli.init.templates': (n) => ` ✓ Installed ${n} templates to .knowledge/.templates/`,
70
+ 'cli.init.detected': (names) => ` Detected: ${names}`,
71
+ 'cli.init.selectTools': 'Select tools to connect:',
72
+ 'cli.init.selectHint': ' Enter numbers to toggle, then press Enter to confirm.',
73
+ 'cli.init.handshake.created': (file, tool) => ` ✓ Created ${file} (${tool})`,
74
+ 'cli.init.handshake.updated': (file, tool) => ` ✓ Updated ${file} (${tool})`,
75
+ 'cli.init.handshake.appended': (file, tool) => ` ✓ Added to ${file} (${tool})`,
76
+ 'cli.init.skills': (n) => ` ✓ Installed ${n} skills to .claude/skills/knowy/`,
77
+ 'cli.init.done': '✅ Done!',
78
+ 'cli.init.nextStep': 'Next step: run /knowy init in your AI tool to populate your knowledge files.',
79
+ 'cli.init.selectLanguage': 'Select language for templates:',
80
+ 'cli.init.langDetected': (lang) => ` Detected language: ${lang}`,
81
+
82
+ 'cli.update.title': 'update',
83
+ 'cli.update.noConfig': '.knowy.json not found. Run "knowy init" first.',
84
+ 'cli.update.badConfig': 'Failed to read .knowy.json. Run "knowy init" to re-initialize.',
85
+ 'cli.update.templates': (n) => ` ✓ Updated ${n} templates`,
86
+ 'cli.update.skills': (n) => ` ✓ Updated ${n} skills`,
87
+ 'cli.update.newTools': (names) => ` New tools detected: ${names}`,
88
+ 'cli.update.addTools': ' Add knowledge references to these tools?',
89
+ 'cli.update.refreshed': (n) => ` ✓ Refreshed ${n} tool connection(s)`,
90
+ 'cli.update.done': '✅ Update complete.',
91
+
92
+ 'cli.mcp.title': 'MCP setup',
93
+ 'cli.mcp.selectTools': 'Which tools should use the Knowy MCP server?',
94
+ 'cli.mcp.noSelection': 'No tools selected. Aborted.',
95
+ 'cli.mcp.alreadyConfigured': (name) => ` · ${name}: already configured`,
96
+ 'cli.mcp.added': (name) => ` ✓ ${name}: added Knowy MCP server`,
97
+ 'cli.mcp.done': '✅ MCP setup complete.',
98
+ 'cli.mcp.restart': ' Restart your AI tool to activate the Knowy MCP server.',
99
+
100
+ 'ui.toggle': 'Toggle (e.g., 1,3,5) or press Enter to confirm: ',
101
+
102
+ 'cli.help.tagline': 'Give your AI a structured project brain',
103
+ 'cli.help.usage': 'Usage',
104
+ 'cli.help.commands': 'Commands',
105
+ 'cli.help.options': 'Options',
106
+ 'cli.help.init': 'Scaffold .knowledge/ structure and connect AI tools',
107
+ 'cli.help.update': 'Update skills, templates, and tool connections',
108
+ 'cli.help.setupMcp': 'Configure MCP server for your AI tool',
109
+ 'cli.help.help': 'Show this help message',
110
+ 'cli.help.version': 'Show version number',
111
+ 'cli.unknownCommand': (cmd) => `Unknown command: ${cmd}`,
112
+ },
113
+
114
+ 'zh-TW': {
115
+ 'cli.init.exists': '.knowledge/ 已存在。',
116
+ 'cli.init.continue': '繼續?(知識文件不會被覆蓋)',
117
+ 'cli.init.aborted': '已取消。',
118
+ 'cli.init.created': (f) => ` ✓ 已建立 ${f}`,
119
+ 'cli.init.skipped': (f) => ` · 已跳過 ${f}(已存在)`,
120
+ 'cli.init.templates': (n) => ` ✓ 已安裝 ${n} 個模板到 .knowledge/.templates/`,
121
+ 'cli.init.detected': (names) => ` 偵測到:${names}`,
122
+ 'cli.init.selectTools': '選擇要連結的工具:',
123
+ 'cli.init.selectHint': ' 輸入編號切換選擇,按 Enter 確認。',
124
+ 'cli.init.handshake.created': (file, tool) => ` ✓ 已建立 ${file}(${tool})`,
125
+ 'cli.init.handshake.updated': (file, tool) => ` ✓ 已更新 ${file}(${tool})`,
126
+ 'cli.init.handshake.appended': (file, tool) => ` ✓ 已加入 ${file}(${tool})`,
127
+ 'cli.init.skills': (n) => ` ✓ 已安裝 ${n} 個 skills 到 .claude/skills/knowy/`,
128
+ 'cli.init.done': '✅ 完成!',
129
+ 'cli.init.nextStep': '下一步:在你的 AI 工具中執行 /knowy init 來填寫知識文件。',
130
+ 'cli.init.selectLanguage': '選擇模板語言:',
131
+ 'cli.init.langDetected': (lang) => ` 偵測到語言:${lang}`,
132
+
133
+ 'cli.update.title': '更新',
134
+ 'cli.update.noConfig': '找不到 .knowy.json。請先執行 "knowy init"。',
135
+ 'cli.update.badConfig': '無法讀取 .knowy.json。請執行 "knowy init" 重新初始化。',
136
+ 'cli.update.templates': (n) => ` ✓ 已更新 ${n} 個模板`,
137
+ 'cli.update.skills': (n) => ` ✓ 已更新 ${n} 個 skills`,
138
+ 'cli.update.newTools': (names) => ` 偵測到新工具:${names}`,
139
+ 'cli.update.addTools': ' 要為這些工具加入知識引用嗎?',
140
+ 'cli.update.refreshed': (n) => ` ✓ 已刷新 ${n} 個工具連結`,
141
+ 'cli.update.done': '✅ 更新完成。',
142
+
143
+ 'cli.mcp.title': 'MCP 設定',
144
+ 'cli.mcp.selectTools': '哪些工具要使用 Knowy MCP server?',
145
+ 'cli.mcp.noSelection': '未選擇任何工具。已取消。',
146
+ 'cli.mcp.alreadyConfigured': (name) => ` · ${name}:已設定`,
147
+ 'cli.mcp.added': (name) => ` ✓ ${name}:已加入 Knowy MCP server`,
148
+ 'cli.mcp.done': '✅ MCP 設定完成。',
149
+ 'cli.mcp.restart': ' 請重啟你的 AI 工具以啟用 Knowy MCP server。',
150
+
151
+ 'ui.toggle': '切換(例如 1,3,5)或按 Enter 確認:',
152
+
153
+ 'cli.help.tagline': '給你的 AI 一個結構化的專案大腦',
154
+ 'cli.help.usage': '用法',
155
+ 'cli.help.commands': '指令',
156
+ 'cli.help.options': '選項',
157
+ 'cli.help.init': '建立 .knowledge/ 結構並連結 AI 工具',
158
+ 'cli.help.update': '更新 skills、模板和工具連結',
159
+ 'cli.help.setupMcp': '為你的 AI 工具設定 MCP server',
160
+ 'cli.help.help': '顯示此說明',
161
+ 'cli.help.version': '顯示版本號',
162
+ 'cli.unknownCommand': (cmd) => `未知的指令:${cmd}`,
163
+ },
164
+ };
165
+
166
+ /**
167
+ * Get a message function/string for the given language and key.
168
+ */
169
+ export function t(lang, key) {
170
+ const catalog = messages[lang] || messages['en'];
171
+ return catalog[key] || messages['en'][key] || key;
172
+ }
@@ -0,0 +1,471 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { VERSION, PACKAGE_ROOT } from './constants.js';
3
+
4
+ const PROTOCOL_VERSION = '2025-03-26';
5
+
6
+ // ── Tool Definitions ───────────────────────────────────────────────
7
+
8
+ const TOOLS = [
9
+ {
10
+ name: 'knowy_init',
11
+ description: 'Scaffold .knowledge/ structure, detect AI tools, inject references, and install skills. Returns a report of what was created.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ project_path: {
16
+ type: 'string',
17
+ description: 'Absolute path to the project root. Defaults to current working directory.',
18
+ },
19
+ tools: {
20
+ type: 'array',
21
+ items: { type: 'string' },
22
+ description: 'Tool IDs to connect (e.g., ["agents-md", "claude-code"]). If omitted, auto-detects and connects all detected tools plus AGENTS.md.',
23
+ },
24
+ },
25
+ },
26
+ },
27
+ {
28
+ name: 'knowy_update',
29
+ description: 'Update Knowy skills and templates (managed files), re-detect tools, and refresh handshakes. Never overwrites knowledge files.',
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ project_path: {
34
+ type: 'string',
35
+ description: 'Absolute path to the project root. Defaults to current working directory.',
36
+ },
37
+ },
38
+ },
39
+ },
40
+ {
41
+ name: 'knowy_judge',
42
+ description: 'Cross-check .knowledge/ files for consistency and coherence. Returns a structured health check report with traffic-light indicators.',
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ project_path: {
47
+ type: 'string',
48
+ description: 'Absolute path to the project root.',
49
+ },
50
+ scope: {
51
+ type: 'string',
52
+ description: 'What to check: empty for full check, a file name (e.g., "experience"), a pair (e.g., "principles vision"), or an event description.',
53
+ },
54
+ },
55
+ },
56
+ },
57
+ {
58
+ name: 'knowy_next',
59
+ description: 'Suggest what to work on next based on .knowledge/ files. Returns a feature brief grounded in principles, vision, and experience.',
60
+ inputSchema: {
61
+ type: 'object',
62
+ properties: {
63
+ project_path: {
64
+ type: 'string',
65
+ description: 'Absolute path to the project root.',
66
+ },
67
+ direction: {
68
+ type: 'string',
69
+ description: 'Optional direction or feature area to explore. If omitted, suggests the next logical step from the roadmap.',
70
+ },
71
+ },
72
+ },
73
+ },
74
+ ];
75
+
76
+ // ── Tool Handlers ──────────────────────────────────────────────────
77
+
78
+ async function handleKnowyInit(args) {
79
+ const projectPath = args.project_path || process.cwd();
80
+ const { scaffoldKnowledge } = await import('./scaffold.js');
81
+ const { installTemplates } = await import('./templates.js');
82
+ const { installSkills } = await import('./skills.js');
83
+ const { detectTools } = await import('./adapters/detect.js');
84
+ const { getToolById } = await import('./adapters/registry.js');
85
+ const { injectHandshake } = await import('./adapters/handshake.js');
86
+ const { readFile, writeFile } = await import('node:fs/promises');
87
+ const { join } = await import('node:path');
88
+ const { KNOWY_CONFIG } = await import('./constants.js');
89
+ const { resolveLanguage } = await import('./i18n.js');
90
+
91
+ const lang = await resolveLanguage(projectPath);
92
+ const report = [];
93
+
94
+ // Scaffold
95
+ const scaffoldResult = await scaffoldKnowledge(projectPath, lang);
96
+ for (const f of scaffoldResult.created) report.push(`✓ Created ${f}`);
97
+ for (const f of scaffoldResult.skipped) report.push(`· Skipped ${f} (already exists)`);
98
+
99
+ // Templates
100
+ const templates = await installTemplates(projectPath, lang);
101
+ report.push(`✓ Installed ${templates.length} templates`);
102
+
103
+ // Detect & handshake
104
+ const { detected } = await detectTools(projectPath);
105
+ let toolIds = args.tools;
106
+ if (!toolIds || toolIds.length === 0) {
107
+ toolIds = ['agents-md', ...detected];
108
+ }
109
+
110
+ const writtenFiles = new Set();
111
+ for (const id of toolIds) {
112
+ const tool = getToolById(id);
113
+ if (!tool) continue;
114
+ for (const target of tool.targets) {
115
+ if (writtenFiles.has(target.file)) continue;
116
+ const result = await injectHandshake(projectPath, target);
117
+ writtenFiles.add(target.file);
118
+ report.push(`✓ ${result.action} ${result.file} (${tool.name})`);
119
+ }
120
+ }
121
+
122
+ // Skills
123
+ const skills = await installSkills(projectPath);
124
+ report.push(`✓ Installed ${skills.length} skills`);
125
+
126
+ // Update config
127
+ const configPath = join(projectPath, KNOWY_CONFIG);
128
+ let config;
129
+ try {
130
+ config = JSON.parse(await readFile(configPath, 'utf-8'));
131
+ } catch {
132
+ config = { version: VERSION, createdAt: new Date().toISOString() };
133
+ }
134
+ config.version = VERSION;
135
+ config.language = config.language || lang;
136
+ config.tools = [...new Set(toolIds)];
137
+ config.updatedAt = new Date().toISOString();
138
+ await writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
139
+
140
+ report.push('');
141
+ report.push('Done! Run /knowy init in your AI tool to populate knowledge files.');
142
+
143
+ return report.join('\n');
144
+ }
145
+
146
+ async function handleKnowyUpdate(args) {
147
+ const projectPath = args.project_path || process.cwd();
148
+ const { installTemplates } = await import('./templates.js');
149
+ const { installSkills } = await import('./skills.js');
150
+ const { detectTools } = await import('./adapters/detect.js');
151
+ const { getToolById } = await import('./adapters/registry.js');
152
+ const { injectHandshake } = await import('./adapters/handshake.js');
153
+ const { readFile, writeFile, access } = await import('node:fs/promises');
154
+ const { join } = await import('node:path');
155
+ const { KNOWY_CONFIG } = await import('./constants.js');
156
+
157
+ const configPath = join(projectPath, KNOWY_CONFIG);
158
+ let config;
159
+ try {
160
+ config = JSON.parse(await readFile(configPath, 'utf-8'));
161
+ } catch {
162
+ return 'Error: .knowy.json not found. Run knowy_init first.';
163
+ }
164
+
165
+ const report = [];
166
+
167
+ // Update managed files
168
+ const templates = await installTemplates(projectPath);
169
+ report.push(`✓ Updated ${templates.length} templates`);
170
+
171
+ const skills = await installSkills(projectPath);
172
+ report.push(`✓ Updated ${skills.length} skills`);
173
+
174
+ // Re-detect and handshake
175
+ const { detected } = await detectTools(projectPath);
176
+ const existingTools = new Set(config.tools || []);
177
+ const newTools = detected.filter(id => !existingTools.has(id));
178
+
179
+ if (newTools.length > 0) {
180
+ const names = newTools.map(id => getToolById(id)?.name).filter(Boolean);
181
+ report.push(`New tools detected: ${names.join(', ')}`);
182
+ for (const id of newTools) {
183
+ existingTools.add(id);
184
+ }
185
+ }
186
+
187
+ // Refresh all handshakes
188
+ const writtenFiles = new Set();
189
+ for (const id of existingTools) {
190
+ const tool = getToolById(id);
191
+ if (!tool) continue;
192
+ for (const target of tool.targets) {
193
+ if (writtenFiles.has(target.file)) continue;
194
+ await injectHandshake(projectPath, target);
195
+ writtenFiles.add(target.file);
196
+ }
197
+ }
198
+ report.push(`✓ Refreshed ${writtenFiles.size} tool connection(s)`);
199
+
200
+ // Update config
201
+ config.version = VERSION;
202
+ config.tools = [...existingTools];
203
+ config.updatedAt = new Date().toISOString();
204
+ await writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
205
+
206
+ report.push('\nUpdate complete.');
207
+ return report.join('\n');
208
+ }
209
+
210
+ async function handleKnowyJudge(args) {
211
+ const projectPath = args.project_path || process.cwd();
212
+ const { readFile } = await import('node:fs/promises');
213
+ const { join } = await import('node:path');
214
+ const { KNOWLEDGE_DIR } = await import('./constants.js');
215
+
216
+ // Read knowledge files
217
+ const files = {};
218
+ for (const name of ['principles.md', 'vision.md', 'experience.md']) {
219
+ try {
220
+ files[name] = await readFile(join(projectPath, KNOWLEDGE_DIR, name), 'utf-8');
221
+ } catch {
222
+ files[name] = null;
223
+ }
224
+ }
225
+
226
+ const missing = Object.entries(files).filter(([, v]) => v === null).map(([k]) => k);
227
+ if (missing.length > 0) {
228
+ return `Cannot run judge — missing files: ${missing.join(', ')}\nRun knowy_init first.`;
229
+ }
230
+
231
+ // Return files content for AI to analyze
232
+ const scope = args.scope || 'full';
233
+ const lines = [
234
+ `## Knowy Judge — Scope: ${scope}`,
235
+ '',
236
+ 'Below are the current knowledge files. Please perform the cross-check analysis.',
237
+ '',
238
+ '### principles.md',
239
+ '```markdown',
240
+ files['principles.md'],
241
+ '```',
242
+ '',
243
+ '### vision.md',
244
+ '```markdown',
245
+ files['vision.md'],
246
+ '```',
247
+ '',
248
+ '### experience.md',
249
+ '```markdown',
250
+ files['experience.md'],
251
+ '```',
252
+ '',
253
+ '### Instructions',
254
+ '',
255
+ ];
256
+
257
+ if (scope === 'full') {
258
+ lines.push('Perform a **full check** with all 11 sections:');
259
+ lines.push('1. Internal Coherence (3): check each file for self-contradictions');
260
+ lines.push('2. Cross-references (6 directional):');
261
+ lines.push(' - Principles → Vision: can vision be derived from principles?');
262
+ lines.push(' - Vision → Principles: does vision require unstated principles?');
263
+ lines.push(' - Principles → Experience: do principles predict observed patterns?');
264
+ lines.push(' - Experience → Principles: does experience challenge principles?');
265
+ lines.push(' - Vision → Experience: does experience support the planned direction?');
266
+ lines.push(' - Experience → Vision: are there lessons suggesting new opportunities?');
267
+ lines.push('3. Overall (1): synthesize — where is the most pressure?');
268
+ lines.push('4. Beyond Scope (1): content that doesn\'t belong');
269
+ lines.push('5. Suggested Actions: numbered, prioritized');
270
+ } else {
271
+ lines.push(`Perform a **scoped check** for: "${scope}"`);
272
+ lines.push('Show only the relevant sections from the full 11-section check.');
273
+ }
274
+
275
+ lines.push('');
276
+ lines.push('Use 🟢 (one line) for healthy, 🟡 (expanded) for tensions, 🔴 (expanded) for conflicts.');
277
+ lines.push('Always quote specific text from the files to support findings.');
278
+
279
+ return lines.join('\n');
280
+ }
281
+
282
+ async function handleKnowyNext(args) {
283
+ const projectPath = args.project_path || process.cwd();
284
+ const { readFile, readdir } = await import('node:fs/promises');
285
+ const { join } = await import('node:path');
286
+ const { KNOWLEDGE_DIR } = await import('./constants.js');
287
+
288
+ // Read knowledge files
289
+ const files = {};
290
+ for (const name of ['principles.md', 'vision.md', 'experience.md']) {
291
+ try {
292
+ files[name] = await readFile(join(projectPath, KNOWLEDGE_DIR, name), 'utf-8');
293
+ } catch {
294
+ files[name] = null;
295
+ }
296
+ }
297
+
298
+ const missing = Object.entries(files).filter(([, v]) => v === null).map(([k]) => k);
299
+ if (missing.length > 0) {
300
+ return `Cannot plan next step — missing files: ${missing.join(', ')}\nRun knowy_init first.`;
301
+ }
302
+
303
+ // Scan for spec tools
304
+ let specTools = [];
305
+ try {
306
+ const skillsDir = join(projectPath, '.claude', 'skills');
307
+ const skills = await readdir(skillsDir);
308
+ specTools = skills.filter(s =>
309
+ s.startsWith('speckit') || s.startsWith('openspec') || s.includes('spec')
310
+ );
311
+ } catch { /* no skills dir */ }
312
+
313
+ const direction = args.direction || '';
314
+ const lines = [
315
+ `## Knowy Next${direction ? ` — Direction: ${direction}` : ''}`,
316
+ '',
317
+ 'Below are the current knowledge files. Help the user plan their next step.',
318
+ '',
319
+ '### principles.md',
320
+ '```markdown',
321
+ files['principles.md'],
322
+ '```',
323
+ '',
324
+ '### vision.md',
325
+ '```markdown',
326
+ files['vision.md'],
327
+ '```',
328
+ '',
329
+ '### experience.md',
330
+ '```markdown',
331
+ files['experience.md'],
332
+ '```',
333
+ '',
334
+ '### Instructions',
335
+ '',
336
+ ];
337
+
338
+ if (direction) {
339
+ lines.push(`The user wants to explore: "${direction}"`);
340
+ lines.push('1. Locate this direction in the vision roadmap');
341
+ lines.push('2. Check prerequisites');
342
+ lines.push('3. Find relevant experience lessons');
343
+ } else {
344
+ lines.push('The user has no specific direction. Suggest the next logical step:');
345
+ lines.push('1. Find the next incomplete milestone in the roadmap');
346
+ lines.push('2. Consider experience lessons that affect priority');
347
+ }
348
+
349
+ lines.push('');
350
+ lines.push('Converge on a feature brief: name, description, scope, grounding in principles, informed by experience, risks.');
351
+
352
+ if (specTools.length > 0) {
353
+ lines.push('');
354
+ lines.push(`Spec tools detected in this project: ${specTools.join(', ')}`);
355
+ lines.push('Suggest the user invoke the appropriate spec tool to flesh out the details.');
356
+ } else {
357
+ lines.push('');
358
+ lines.push('No spec tools detected. Suggest: "You can now use your preferred specification tool to flesh out the details, or start implementing directly."');
359
+ }
360
+
361
+ return lines.join('\n');
362
+ }
363
+
364
+ // ── MCP Protocol Handler ───────────────────────────────────────────
365
+
366
+ function makeResponse(id, result) {
367
+ return JSON.stringify({ jsonrpc: '2.0', id, result });
368
+ }
369
+
370
+ function makeError(id, code, message) {
371
+ return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
372
+ }
373
+
374
+ async function handleMessage(msg) {
375
+ const { id, method, params } = msg;
376
+
377
+ // Notifications (no id) — just acknowledge silently
378
+ if (id === undefined) return null;
379
+
380
+ switch (method) {
381
+ case 'initialize':
382
+ return makeResponse(id, {
383
+ protocolVersion: PROTOCOL_VERSION,
384
+ capabilities: { tools: {} },
385
+ serverInfo: { name: 'knowy', version: VERSION },
386
+ });
387
+
388
+ case 'ping':
389
+ return makeResponse(id, {});
390
+
391
+ case 'tools/list':
392
+ return makeResponse(id, { tools: TOOLS });
393
+
394
+ case 'tools/call': {
395
+ const toolName = params?.name;
396
+ const args = params?.arguments || {};
397
+
398
+ try {
399
+ let result;
400
+ switch (toolName) {
401
+ case 'knowy_init':
402
+ result = await handleKnowyInit(args);
403
+ break;
404
+ case 'knowy_update':
405
+ result = await handleKnowyUpdate(args);
406
+ break;
407
+ case 'knowy_judge':
408
+ result = await handleKnowyJudge(args);
409
+ break;
410
+ case 'knowy_next':
411
+ result = await handleKnowyNext(args);
412
+ break;
413
+ default:
414
+ return makeError(id, -32602, `Unknown tool: ${toolName}`);
415
+ }
416
+ return makeResponse(id, {
417
+ content: [{ type: 'text', text: result }],
418
+ isError: false,
419
+ });
420
+ } catch (err) {
421
+ return makeResponse(id, {
422
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
423
+ isError: true,
424
+ });
425
+ }
426
+ }
427
+
428
+ default:
429
+ return makeError(id, -32601, `Method not found: ${method}`);
430
+ }
431
+ }
432
+
433
+ // ── Entry Point ────────────────────────────────────────────────────
434
+
435
+ export async function startMcpServer() {
436
+ const rl = createInterface({
437
+ input: process.stdin,
438
+ terminal: false,
439
+ });
440
+
441
+ const pending = [];
442
+
443
+ rl.on('line', (line) => {
444
+ if (!line.trim()) return;
445
+
446
+ let msg;
447
+ try {
448
+ msg = JSON.parse(line);
449
+ } catch {
450
+ const err = makeError(null, -32700, 'Parse error');
451
+ process.stdout.write(err + '\n');
452
+ return;
453
+ }
454
+
455
+ const task = handleMessage(msg).then((response) => {
456
+ if (response) {
457
+ process.stdout.write(response + '\n');
458
+ }
459
+ }).catch((err) => {
460
+ process.stderr.write(`Error: ${err.message}\n`);
461
+ });
462
+ pending.push(task);
463
+ });
464
+
465
+ rl.on('close', async () => {
466
+ await Promise.all(pending);
467
+ process.exit(0);
468
+ });
469
+
470
+ process.stderr.write(`knowy MCP server v${VERSION} started\n`);
471
+ }
@@ -0,0 +1,53 @@
1
+ import { mkdir, copyFile, writeFile, access } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import {
4
+ KNOWLEDGE_DIR, KNOWY_CONFIG, CORE_FILES, SUBDIRS,
5
+ PACKAGE_ROOT, VERSION
6
+ } from './constants.js';
7
+
8
+ async function exists(p) {
9
+ try { await access(p); return true; } catch { return false; }
10
+ }
11
+
12
+ export async function scaffoldKnowledge(projectRoot, language = 'en') {
13
+ const knowledgeDir = join(projectRoot, KNOWLEDGE_DIR);
14
+ const report = { created: [], skipped: [] };
15
+
16
+ // Create .knowledge/ and subdirectories
17
+ await mkdir(knowledgeDir, { recursive: true });
18
+ for (const sub of SUBDIRS) {
19
+ await mkdir(join(knowledgeDir, sub), { recursive: true });
20
+ }
21
+
22
+ // Copy core files from language-specific templates (never overwrite)
23
+ for (const file of CORE_FILES) {
24
+ const dest = join(knowledgeDir, file);
25
+ if (await exists(dest)) {
26
+ report.skipped.push(file);
27
+ } else {
28
+ // Try language-specific template first, fallback to English
29
+ let src = join(PACKAGE_ROOT, 'templates', language, `${file}.tmpl`);
30
+ if (!await exists(src)) {
31
+ src = join(PACKAGE_ROOT, 'templates', 'en', `${file}.tmpl`);
32
+ }
33
+ await copyFile(src, dest);
34
+ report.created.push(file);
35
+ }
36
+ }
37
+
38
+ // Create .knowy.json
39
+ const configPath = join(projectRoot, KNOWY_CONFIG);
40
+ if (await exists(configPath)) {
41
+ report.skipped.push(KNOWY_CONFIG);
42
+ } else {
43
+ await writeFile(configPath, JSON.stringify({
44
+ version: VERSION,
45
+ language,
46
+ createdAt: new Date().toISOString(),
47
+ tools: []
48
+ }, null, 2) + '\n');
49
+ report.created.push(KNOWY_CONFIG);
50
+ }
51
+
52
+ return report;
53
+ }