knowzcode 0.3.7 → 0.5.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/bin/knowzcode.mjs CHANGED
@@ -43,7 +43,7 @@ const PLATFORMS = {
43
43
  },
44
44
  codex: {
45
45
  name: 'OpenAI Codex',
46
- detect: (dir) => existsSync(join(dir, 'AGENTS.md')) || existsSync(join(dir, 'AGENTS.override.md')) || existsSync(join(dir, '.codex')),
46
+ detect: (dir) => existsSync(join(dir, 'AGENTS.md')) || existsSync(join(dir, 'AGENTS.override.md')) || existsSync(join(dir, '.codex')) || existsSync(join(dir, '.agents')),
47
47
  adapterPath: (dir) => join(dir, 'AGENTS.md'),
48
48
  templateHeader: '## OpenAI Codex (AGENTS.md)',
49
49
  },
@@ -63,7 +63,7 @@ const PLATFORMS = {
63
63
  name: 'GitHub Copilot',
64
64
  detect: (dir) => existsSync(join(dir, '.github', 'copilot-instructions.md')) || existsSync(join(dir, '.github')),
65
65
  adapterPath: (dir) => join(dir, '.github', 'copilot-instructions.md'),
66
- templateHeader: '## GitHub Copilot (.github/copilot-instructions.md)',
66
+ templateHeader: '## GitHub Copilot',
67
67
  },
68
68
  windsurf: {
69
69
  name: 'Windsurf',
@@ -128,6 +128,185 @@ function detectPlatforms(dir) {
128
128
  }
129
129
 
130
130
  // ─── Adapter Template Parser ─────────────────────────────────────────────────
131
+ // Returns Map<platformId, { primary: string, files: Map<relativePath, { content, lang }> }>
132
+
133
+ function injectVersion(content) {
134
+ return content.replace(/vX\.Y\.Z/g, `v${VERSION}`);
135
+ }
136
+
137
+ function extractSection(content, headerIdx) {
138
+ const afterHeader = content.slice(headerIdx);
139
+ const nextSection = afterHeader.search(/\r?\n---\r?\n\r?\n## /);
140
+ return nextSection !== -1 ? afterHeader.slice(0, nextSection) : afterHeader;
141
+ }
142
+
143
+ function extractFence(text, lang, startFrom = 0) {
144
+ const marker = '```' + lang;
145
+ const fenceStart = text.indexOf(marker, startFrom);
146
+ if (fenceStart === -1) return null;
147
+ const contentStart = text.indexOf('\n', fenceStart) + 1;
148
+ // Track nested fences to find the matching closing fence
149
+ let depth = 0;
150
+ let pos = contentStart;
151
+ while (pos < text.length) {
152
+ const nextFence = text.indexOf('\n```', pos);
153
+ if (nextFence === -1) return null;
154
+ const afterBackticks = nextFence + 4;
155
+ const charAfter = afterBackticks < text.length ? text[afterBackticks] : undefined;
156
+ if (charAfter && /\w/.test(charAfter)) {
157
+ // Opening fence (```bash, ```json, etc.)
158
+ depth++;
159
+ } else {
160
+ // Closing fence (``` followed by whitespace/newline/EOF)
161
+ if (depth === 0) {
162
+ return { content: text.slice(contentStart, nextFence), endIdx: afterBackticks };
163
+ }
164
+ depth--;
165
+ }
166
+ pos = afterBackticks;
167
+ }
168
+ return null;
169
+ }
170
+
171
+ function parseCopilotSection(section) {
172
+ const files = new Map();
173
+
174
+ // Section A: copilot-instructions.md (first ```markdown before ### B.)
175
+ const sectionBIdx = section.indexOf('### B.');
176
+ const sectionA = sectionBIdx !== -1 ? section.slice(0, sectionBIdx) : section;
177
+ const primaryFence = extractFence(sectionA, 'markdown');
178
+ if (!primaryFence) return null;
179
+
180
+ // Section B: prompt files (#### kc-*.prompt.md headers)
181
+ const headerRegex = /#### (kc-[\w-]+\.prompt\.md)/g;
182
+ const headers = [];
183
+ let match;
184
+ while ((match = headerRegex.exec(section)) !== null) {
185
+ headers.push({ filename: match[1], index: match.index });
186
+ }
187
+
188
+ const sectionCIdx = section.indexOf('### C.');
189
+ for (let i = 0; i < headers.length; i++) {
190
+ const start = headers[i].index;
191
+ const end = i + 1 < headers.length
192
+ ? headers[i + 1].index
193
+ : (sectionCIdx !== -1 && sectionCIdx > start ? sectionCIdx : section.length);
194
+ const subSection = section.slice(start, end);
195
+
196
+ const fenceOpen = subSection.indexOf('```markdown');
197
+ if (fenceOpen === -1) continue;
198
+ const contentStart = subSection.indexOf('\n', fenceOpen) + 1;
199
+ // Use lastIndexOf to handle prompt files that contain inner code fences
200
+ const lastFenceClose = subSection.lastIndexOf('\n```');
201
+ if (lastFenceClose <= contentStart) continue;
202
+
203
+ files.set(`.github/prompts/${headers[i].filename}`, {
204
+ content: subSection.slice(contentStart, lastFenceClose),
205
+ lang: 'markdown',
206
+ });
207
+ }
208
+
209
+ // Section C: .vscode/mcp.json
210
+ if (sectionCIdx !== -1) {
211
+ const sectionDIdx = section.indexOf('### D.', sectionCIdx);
212
+ const sectionC = section.slice(sectionCIdx, sectionDIdx !== -1 ? sectionDIdx : section.length);
213
+ const jsonFence = extractFence(sectionC, 'json');
214
+ if (jsonFence) {
215
+ files.set('.vscode/mcp.json', { content: jsonFence.content, lang: 'json' });
216
+ }
217
+ }
218
+
219
+ return { primary: primaryFence.content, files };
220
+ }
221
+
222
+ function parseGeminiSection(section) {
223
+ const files = new Map();
224
+
225
+ // Extract TOML blocks: ```toml fences with # .gemini/commands/kc/{name}.toml comment
226
+ let searchFrom = 0;
227
+ while (true) {
228
+ const fenceStart = section.indexOf('```toml', searchFrom);
229
+ if (fenceStart === -1) break;
230
+ const contentStart = section.indexOf('\n', fenceStart) + 1;
231
+ const fenceEnd = section.indexOf('\n```', contentStart);
232
+ if (fenceEnd === -1) break;
233
+ const tomlContent = section.slice(contentStart, fenceEnd);
234
+ const pathMatch = tomlContent.match(/^# (\.gemini\/commands\/kc\/[\w-]+\.toml)/);
235
+ if (pathMatch) {
236
+ files.set(pathMatch[1], { content: tomlContent, lang: 'toml' });
237
+ }
238
+ searchFrom = fenceEnd + 4;
239
+ }
240
+
241
+ // Skill files: #### .gemini/skills/kc-{name}/SKILL.md headers
242
+ const skillRegex = /#### (\.gemini\/skills\/kc-[\w-]+\/SKILL\.md)/g;
243
+ const skillHeaders = [];
244
+ let skillMatch;
245
+ while ((skillMatch = skillRegex.exec(section)) !== null) {
246
+ skillHeaders.push({ filepath: skillMatch[1], index: skillMatch.index });
247
+ }
248
+ // Subagent files: #### .gemini/agents/kc-{name}.md headers
249
+ const agentRegex = /#### (\.gemini\/agents\/kc-[\w-]+\.md)/g;
250
+ const agentHeaders = [];
251
+ let agentMatch;
252
+ while ((agentMatch = agentRegex.exec(section)) !== null) {
253
+ agentHeaders.push({ filepath: agentMatch[1], index: agentMatch.index });
254
+ }
255
+ // Combine all subsection headers for boundary detection
256
+ const allSubHeaders = [...skillHeaders, ...agentHeaders].sort((a, b) => a.index - b.index);
257
+
258
+ for (let i = 0; i < allSubHeaders.length; i++) {
259
+ const start = allSubHeaders[i].index;
260
+ const end = i + 1 < allSubHeaders.length ? allSubHeaders[i + 1].index : section.length;
261
+ const subSection = section.slice(start, end);
262
+ const fence = extractFence(subSection, 'markdown');
263
+ if (fence) {
264
+ files.set(allSubHeaders[i].filepath, { content: fence.content, lang: 'markdown' });
265
+ }
266
+ }
267
+
268
+ // Primary: ```markdown fence (GEMINI.md) — extract from content BEFORE first skill/subagent header
269
+ const firstSubHeader = allSubHeaders.length > 0 ? allSubHeaders[0].index : section.length;
270
+ const primarySection = section.slice(0, firstSubHeader);
271
+ const primaryFence = extractFence(primarySection, 'markdown');
272
+ if (!primaryFence) return null;
273
+
274
+ return { primary: primaryFence.content, files };
275
+ }
276
+
277
+ function parseCodexSection(section) {
278
+ const files = new Map();
279
+
280
+ // Primary: first ```markdown fence (AGENTS.md)
281
+ const primaryFence = extractFence(section, 'markdown');
282
+ if (!primaryFence) return null;
283
+
284
+ // Skill files: #### .agents/skills/kc-{name}/SKILL.md headers
285
+ const headerRegex = /#### (\.agents\/skills\/kc-[\w-]+\/SKILL\.md)/g;
286
+ const headers = [];
287
+ let match;
288
+ while ((match = headerRegex.exec(section)) !== null) {
289
+ headers.push({ filepath: match[1], index: match.index });
290
+ }
291
+
292
+ for (let i = 0; i < headers.length; i++) {
293
+ const start = headers[i].index;
294
+ const end = i + 1 < headers.length ? headers[i + 1].index : section.length;
295
+ const subSection = section.slice(start, end);
296
+ const fence = extractFence(subSection, 'markdown');
297
+ if (fence) {
298
+ files.set(headers[i].filepath, { content: fence.content, lang: 'markdown' });
299
+ }
300
+ }
301
+
302
+ return { primary: primaryFence.content, files };
303
+ }
304
+
305
+ function parseSimpleSection(section) {
306
+ const primaryFence = extractFence(section, 'markdown');
307
+ if (!primaryFence) return null;
308
+ return { primary: primaryFence.content, files: new Map() };
309
+ }
131
310
 
132
311
  function parseAdapterTemplates() {
133
312
  const adaptersPath = join(PKG_ROOT, 'knowzcode', 'platform_adapters.md');
@@ -145,16 +324,15 @@ function parseAdapterTemplates() {
145
324
  const headerIdx = content.indexOf(platform.templateHeader);
146
325
  if (headerIdx === -1) continue;
147
326
 
148
- // Find the code fence after this header
149
- const afterHeader = content.slice(headerIdx);
150
- const fenceStart = afterHeader.indexOf('```markdown');
151
- if (fenceStart === -1) continue;
152
-
153
- const contentStart = afterHeader.indexOf('\n', fenceStart) + 1;
154
- const fenceEnd = afterHeader.indexOf('\n```', contentStart);
155
- if (fenceEnd === -1) continue;
156
-
157
- templates.set(id, afterHeader.slice(contentStart, fenceEnd));
327
+ const section = extractSection(content, headerIdx);
328
+ let result;
329
+ switch (id) {
330
+ case 'copilot': result = parseCopilotSection(section); break;
331
+ case 'gemini': result = parseGeminiSection(section); break;
332
+ case 'codex': result = parseCodexSection(section); break;
333
+ default: result = parseSimpleSection(section); break;
334
+ }
335
+ if (result) templates.set(id, result);
158
336
  }
159
337
 
160
338
  return templates;
@@ -233,6 +411,57 @@ function removeMarketplaceConfig(claudeDir) {
233
411
  }
234
412
  }
235
413
 
414
+ // ─── Gemini MCP Config Helpers ────────────────────────────────────────────────
415
+
416
+ function writeGeminiMcpConfig(settingsPath, apiKey, projectPath, endpoint = 'https://mcp.knowz.io/mcp') {
417
+ ensureDir(dirname(settingsPath));
418
+ let settings = {};
419
+ if (existsSync(settingsPath)) {
420
+ try {
421
+ settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
422
+ } catch {
423
+ settings = {};
424
+ }
425
+ }
426
+ if (!settings.mcpServers) settings.mcpServers = {};
427
+ settings.mcpServers.knowz = {
428
+ url: endpoint,
429
+ headers: {
430
+ 'Authorization': `Bearer ${apiKey}`,
431
+ 'X-Project-Path': projectPath,
432
+ },
433
+ };
434
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
435
+ }
436
+
437
+ function removeGeminiMcpConfig(settingsPath) {
438
+ if (!existsSync(settingsPath)) return false;
439
+ try {
440
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
441
+ if (settings.mcpServers && settings.mcpServers.knowz) {
442
+ delete settings.mcpServers.knowz;
443
+ if (Object.keys(settings.mcpServers).length === 0) {
444
+ delete settings.mcpServers;
445
+ }
446
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
447
+ return true;
448
+ }
449
+ } catch {
450
+ // Ignore parse errors
451
+ }
452
+ return false;
453
+ }
454
+
455
+ function hasGeminiMcpConfig(settingsPath) {
456
+ if (!existsSync(settingsPath)) return false;
457
+ try {
458
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
459
+ return !!(settings.mcpServers && settings.mcpServers.knowz);
460
+ } catch {
461
+ return false;
462
+ }
463
+ }
464
+
236
465
  // ─── Stale File Cleanup ─────────────────────────────────────────────────────
237
466
 
238
467
  function removeStaleFiles(sourceDir, targetDir) {
@@ -550,18 +779,76 @@ async function cmdInstall(opts) {
550
779
 
551
780
  adapterFiles.push(claudeDir + '/commands/', claudeDir + '/agents/', claudeDir + '/skills/');
552
781
  } else {
553
- // Other platforms: extract template and write adapter file
554
- const template = templates.get(platformId);
555
- if (!template) {
782
+ // Other platforms: extract template and write adapter + additional files
783
+ const templateSet = templates.get(platformId);
784
+ if (!templateSet) {
556
785
  log.warn(`No adapter template found for ${platform.name} — skipping`);
557
786
  continue;
558
787
  }
559
788
 
789
+ // Write primary adapter file
560
790
  const adapterFile = platform.adapterPath(dir);
561
791
  ensureDir(dirname(adapterFile));
562
- writeFileSync(adapterFile, template);
792
+ writeFileSync(adapterFile, injectVersion(templateSet.primary));
563
793
  adapterFiles.push(adapterFile);
564
794
  log.ok(`${platform.name} adapter: ${adapterFile}`);
795
+
796
+ // Write additional files (prompts, TOMLs, skills, subagents)
797
+ for (const [relativePath, { content }] of templateSet.files) {
798
+ let filePath;
799
+ if (platformId === 'codex' && opts.global && relativePath.startsWith('.agents/skills/')) {
800
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '~';
801
+ filePath = join(homeDir, relativePath);
802
+ } else if (platformId === 'gemini' && opts.global && relativePath.startsWith('.gemini/skills/')) {
803
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '~';
804
+ filePath = join(homeDir, relativePath);
805
+ } else {
806
+ filePath = join(dir, relativePath);
807
+ }
808
+ ensureDir(dirname(filePath));
809
+ writeFileSync(filePath, injectVersion(content));
810
+ adapterFiles.push(filePath);
811
+ }
812
+ if (templateSet.files.size > 0) {
813
+ log.ok(` + ${templateSet.files.size} additional file(s)`);
814
+ }
815
+
816
+ // Clean up legacy .codex/skills/kc/ if present (migrated to .agents/skills/)
817
+ if (platformId === 'codex') {
818
+ const legacySkillDir = join(dir, '.codex', 'skills', 'kc');
819
+ if (existsSync(legacySkillDir)) {
820
+ log.info('Removing legacy .codex/skills/kc/ (migrated to .agents/skills/)');
821
+ rmSync(legacySkillDir, { recursive: true, force: true });
822
+ }
823
+ if (opts.global) {
824
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '~';
825
+ const legacyGlobal = join(homeDir, '.codex', 'skills', 'kc');
826
+ if (existsSync(legacyGlobal)) {
827
+ log.info('Removing legacy global ~/.codex/skills/kc/');
828
+ rmSync(legacyGlobal, { recursive: true, force: true });
829
+ }
830
+ }
831
+ }
832
+ }
833
+ }
834
+
835
+ // 3.5. Gemini MCP config offer (when Gemini is selected and not --force)
836
+ if (selectedPlatforms.includes('gemini') && !opts.global && !opts.force) {
837
+ console.log('');
838
+ console.log(`${c.bold}Gemini MCP Configuration${c.reset}`);
839
+ console.log(`MCP enables vector search, vault access, and AI-powered Q&A.`);
840
+ const wantMcp = await promptConfirm('Configure MCP for Gemini CLI? (requires API key)');
841
+ if (wantMcp) {
842
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
843
+ const apiKey = await rl.question('Enter your KnowzCode API key: ');
844
+ rl.close();
845
+ if (apiKey.trim()) {
846
+ const settingsPath = join(dir, '.gemini', 'settings.json');
847
+ writeGeminiMcpConfig(settingsPath, apiKey.trim(), dir);
848
+ log.ok('Gemini MCP configured in .gemini/settings.json');
849
+ } else {
850
+ log.warn('No API key provided — skipping MCP config. Use /kc:connect-mcp later.');
851
+ }
565
852
  }
566
853
  }
567
854
 
@@ -657,6 +944,76 @@ async function cmdUninstall(opts) {
657
944
  }
658
945
  }
659
946
 
947
+ // Additional platform-specific files/directories
948
+ const copilotPromptsDir = join(dir, '.github', 'prompts');
949
+ if (existsSync(copilotPromptsDir)) {
950
+ for (const f of readdirSync(copilotPromptsDir)) {
951
+ if (f.startsWith('kc-') && f.endsWith('.prompt.md')) {
952
+ components.push({ label: `Copilot prompt: ${f}`, path: join(copilotPromptsDir, f) });
953
+ }
954
+ }
955
+ }
956
+ const vscodeMcp = join(dir, '.vscode', 'mcp.json');
957
+ if (existsSync(vscodeMcp)) {
958
+ components.push({ label: 'VS Code MCP config', path: vscodeMcp });
959
+ }
960
+ const geminiCmdDir = join(dir, '.gemini', 'commands', 'kc');
961
+ if (existsSync(geminiCmdDir)) {
962
+ components.push({ label: 'Gemini commands (kc/)', path: geminiCmdDir });
963
+ }
964
+ // Gemini skills: .gemini/skills/kc-*
965
+ const geminiSkillDir = join(dir, '.gemini', 'skills');
966
+ if (existsSync(geminiSkillDir)) {
967
+ for (const entry of readdirSync(geminiSkillDir)) {
968
+ if (entry.startsWith('kc-')) {
969
+ components.push({ label: `Gemini skill (${entry}/)`, path: join(geminiSkillDir, entry) });
970
+ }
971
+ }
972
+ }
973
+ const globalGeminiSkillDir = join(process.env.HOME || process.env.USERPROFILE || '~', '.gemini', 'skills');
974
+ if (existsSync(globalGeminiSkillDir)) {
975
+ for (const entry of readdirSync(globalGeminiSkillDir)) {
976
+ if (entry.startsWith('kc-')) {
977
+ components.push({ label: `Gemini skill — global (~/.gemini/skills/${entry}/)`, path: join(globalGeminiSkillDir, entry) });
978
+ }
979
+ }
980
+ }
981
+ // Gemini subagents: .gemini/agents/kc-*.md
982
+ const geminiAgentDir = join(dir, '.gemini', 'agents');
983
+ if (existsSync(geminiAgentDir)) {
984
+ for (const entry of readdirSync(geminiAgentDir)) {
985
+ if (entry.startsWith('kc-') && entry.endsWith('.md')) {
986
+ components.push({ label: `Gemini subagent (${entry})`, path: join(geminiAgentDir, entry) });
987
+ }
988
+ }
989
+ }
990
+ // New path: .agents/skills/kc-*
991
+ const agentsSkillDir = join(dir, '.agents', 'skills');
992
+ if (existsSync(agentsSkillDir)) {
993
+ for (const entry of readdirSync(agentsSkillDir)) {
994
+ if (entry.startsWith('kc-')) {
995
+ components.push({ label: `Codex skill (${entry}/)`, path: join(agentsSkillDir, entry) });
996
+ }
997
+ }
998
+ }
999
+ const globalAgentsSkillDir = join(process.env.HOME || process.env.USERPROFILE || '~', '.agents', 'skills');
1000
+ if (existsSync(globalAgentsSkillDir)) {
1001
+ for (const entry of readdirSync(globalAgentsSkillDir)) {
1002
+ if (entry.startsWith('kc-')) {
1003
+ components.push({ label: `Codex skill — global (~/.agents/skills/${entry}/)`, path: join(globalAgentsSkillDir, entry) });
1004
+ }
1005
+ }
1006
+ }
1007
+ // Legacy path: .codex/skills/kc (remove on uninstall)
1008
+ const legacyCodexSkillDir = join(dir, '.codex', 'skills', 'kc');
1009
+ if (existsSync(legacyCodexSkillDir)) {
1010
+ components.push({ label: 'Codex skills — legacy (.codex/skills/kc/)', path: legacyCodexSkillDir });
1011
+ }
1012
+ const legacyGlobalCodexSkillDir = join(process.env.HOME || process.env.USERPROFILE || '~', '.codex', 'skills', 'kc');
1013
+ if (existsSync(legacyGlobalCodexSkillDir)) {
1014
+ components.push({ label: 'Codex skills — legacy global (~/.codex/skills/kc/)', path: legacyGlobalCodexSkillDir });
1015
+ }
1016
+
660
1017
  if (components.length === 0) {
661
1018
  log.info('No KnowzCode installation found.');
662
1019
  return;
@@ -705,6 +1062,17 @@ async function cmdUninstall(opts) {
705
1062
  // Clean up marketplace config from settings.json
706
1063
  removeMarketplaceConfig(claudeDir);
707
1064
 
1065
+ // Clean up Gemini MCP config (remove only knowz entry, preserve other settings)
1066
+ const geminiSettingsProject = join(dir, '.gemini', 'settings.json');
1067
+ if (removeGeminiMcpConfig(geminiSettingsProject)) {
1068
+ removed.push('Gemini MCP config (.gemini/settings.json)');
1069
+ }
1070
+ const homeDir2 = process.env.HOME || process.env.USERPROFILE || '~';
1071
+ const geminiSettingsUser = join(homeDir2, '.gemini', 'settings.json');
1072
+ if (removeGeminiMcpConfig(geminiSettingsUser)) {
1073
+ removed.push('Gemini MCP config (~/.gemini/settings.json)');
1074
+ }
1075
+
708
1076
  console.log('');
709
1077
  log.ok('Uninstall complete');
710
1078
  console.log(' Removed:');
@@ -821,11 +1189,144 @@ async function cmdUpgrade(opts) {
821
1189
  const adapterFile = platform.adapterPath(dir);
822
1190
  if (!existsSync(adapterFile)) continue; // Only update existing adapters
823
1191
 
824
- const template = templates.get(platformId);
825
- if (!template) continue;
1192
+ const templateSet = templates.get(platformId);
1193
+ if (!templateSet) continue;
826
1194
 
827
- writeFileSync(adapterFile, template);
1195
+ // Update primary adapter file
1196
+ writeFileSync(adapterFile, injectVersion(templateSet.primary));
828
1197
  regenerated.push(platform.name);
1198
+
1199
+ // Regenerate additional files
1200
+ const currentPaths = new Set();
1201
+ for (const [relativePath, { content }] of templateSet.files) {
1202
+ const filePath = join(dir, relativePath);
1203
+ ensureDir(dirname(filePath));
1204
+ writeFileSync(filePath, injectVersion(content));
1205
+ currentPaths.add(relativePath);
1206
+ }
1207
+
1208
+ // Stale file cleanup for platform-owned directories
1209
+ if (platformId === 'copilot') {
1210
+ const promptsDir = join(dir, '.github', 'prompts');
1211
+ if (existsSync(promptsDir)) {
1212
+ for (const f of readdirSync(promptsDir)) {
1213
+ if (f.startsWith('kc-') && f.endsWith('.prompt.md') && !currentPaths.has(`.github/prompts/${f}`)) {
1214
+ log.info(`Removing stale prompt: ${f}`);
1215
+ rmSync(join(promptsDir, f), { force: true });
1216
+ }
1217
+ }
1218
+ }
1219
+ } else if (platformId === 'gemini') {
1220
+ const tomlDir = join(dir, '.gemini', 'commands', 'kc');
1221
+ if (existsSync(tomlDir)) {
1222
+ for (const f of readdirSync(tomlDir)) {
1223
+ if (f.endsWith('.toml') && !currentPaths.has(`.gemini/commands/kc/${f}`)) {
1224
+ log.info(`Removing stale command: ${f}`);
1225
+ rmSync(join(tomlDir, f), { force: true });
1226
+ }
1227
+ }
1228
+ }
1229
+ // Stale skill cleanup: .gemini/skills/kc-*
1230
+ const geminiSkillDir = join(dir, '.gemini', 'skills');
1231
+ if (existsSync(geminiSkillDir)) {
1232
+ for (const entry of readdirSync(geminiSkillDir)) {
1233
+ if (entry.startsWith('kc-') && !currentPaths.has(`.gemini/skills/${entry}/SKILL.md`)) {
1234
+ log.info(`Removing stale Gemini skill: ${entry}/`);
1235
+ rmSync(join(geminiSkillDir, entry), { recursive: true, force: true });
1236
+ }
1237
+ }
1238
+ }
1239
+ // Stale subagent cleanup: .gemini/agents/kc-*.md
1240
+ const geminiAgentDir = join(dir, '.gemini', 'agents');
1241
+ if (existsSync(geminiAgentDir)) {
1242
+ for (const entry of readdirSync(geminiAgentDir)) {
1243
+ if (entry.startsWith('kc-') && entry.endsWith('.md') && !currentPaths.has(`.gemini/agents/${entry}`)) {
1244
+ log.info(`Removing stale Gemini subagent: ${entry}`);
1245
+ rmSync(join(geminiAgentDir, entry), { force: true });
1246
+ }
1247
+ }
1248
+ }
1249
+ } else if (platformId === 'codex') {
1250
+ const skillDir = join(dir, '.agents', 'skills');
1251
+ if (existsSync(skillDir)) {
1252
+ for (const entry of readdirSync(skillDir)) {
1253
+ if (entry.startsWith('kc-') && !currentPaths.has(`.agents/skills/${entry}/SKILL.md`)) {
1254
+ log.info(`Removing stale skill: ${entry}/`);
1255
+ rmSync(join(skillDir, entry), { recursive: true, force: true });
1256
+ }
1257
+ }
1258
+ }
1259
+ // Migration: remove legacy .codex/skills/kc/ if present
1260
+ const legacySkillDir = join(dir, '.codex', 'skills', 'kc');
1261
+ if (existsSync(legacySkillDir)) {
1262
+ log.info('Removing legacy .codex/skills/kc/ (migrated to .agents/skills/)');
1263
+ rmSync(legacySkillDir, { recursive: true, force: true });
1264
+ }
1265
+ }
1266
+ }
1267
+
1268
+ // Check for global codex skills
1269
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '~';
1270
+ const globalAgentsSkillDir = join(homeDir, '.agents', 'skills');
1271
+ if (existsSync(globalAgentsSkillDir)) {
1272
+ const codexTemplateSet = templates.get('codex');
1273
+ if (codexTemplateSet) {
1274
+ const currentPaths = new Set([...codexTemplateSet.files.keys()]);
1275
+ // Stale cleanup
1276
+ for (const entry of readdirSync(globalAgentsSkillDir)) {
1277
+ if (entry.startsWith('kc-') && !currentPaths.has(`.agents/skills/${entry}/SKILL.md`)) {
1278
+ log.info(`Removing stale global skill: ${entry}/`);
1279
+ rmSync(join(globalAgentsSkillDir, entry), { recursive: true, force: true });
1280
+ }
1281
+ }
1282
+ // Regenerate global skills
1283
+ for (const [relativePath, { content }] of codexTemplateSet.files) {
1284
+ if (relativePath.startsWith('.agents/skills/')) {
1285
+ const filePath = join(homeDir, relativePath);
1286
+ ensureDir(dirname(filePath));
1287
+ writeFileSync(filePath, injectVersion(content));
1288
+ }
1289
+ }
1290
+ log.info('Updated global Codex skills');
1291
+ }
1292
+ }
1293
+ // Migration: remove legacy global .codex/skills/kc/ if present
1294
+ const legacyGlobalSkillDir = join(homeDir, '.codex', 'skills', 'kc');
1295
+ if (existsSync(legacyGlobalSkillDir)) {
1296
+ log.info('Removing legacy global ~/.codex/skills/kc/ (migrated to ~/.agents/skills/)');
1297
+ rmSync(legacyGlobalSkillDir, { recursive: true, force: true });
1298
+ }
1299
+
1300
+ // Check for global Gemini skills
1301
+ const globalGeminiSkillDir = join(homeDir, '.gemini', 'skills');
1302
+ if (existsSync(globalGeminiSkillDir)) {
1303
+ const geminiTemplateSet = templates.get('gemini');
1304
+ if (geminiTemplateSet) {
1305
+ const currentPaths = new Set([...geminiTemplateSet.files.keys()]);
1306
+ // Stale cleanup
1307
+ for (const entry of readdirSync(globalGeminiSkillDir)) {
1308
+ if (entry.startsWith('kc-') && !currentPaths.has(`.gemini/skills/${entry}/SKILL.md`)) {
1309
+ log.info(`Removing stale global Gemini skill: ${entry}/`);
1310
+ rmSync(join(globalGeminiSkillDir, entry), { recursive: true, force: true });
1311
+ }
1312
+ }
1313
+ // Regenerate global skills
1314
+ for (const [relativePath, { content }] of geminiTemplateSet.files) {
1315
+ if (relativePath.startsWith('.gemini/skills/')) {
1316
+ const filePath = join(homeDir, relativePath);
1317
+ ensureDir(dirname(filePath));
1318
+ writeFileSync(filePath, injectVersion(content));
1319
+ }
1320
+ }
1321
+ log.info('Updated global Gemini skills');
1322
+ }
1323
+ }
1324
+
1325
+ // Preserve Gemini MCP config during upgrade (don't overwrite user's API key)
1326
+ const geminiSettingsPath = join(dir, '.gemini', 'settings.json');
1327
+ const geminiMcpPreserved = hasGeminiMcpConfig(geminiSettingsPath);
1328
+ if (geminiMcpPreserved && opts.verbose) {
1329
+ log.info('Preserved: Gemini MCP config (.gemini/settings.json)');
829
1330
  }
830
1331
 
831
1332
  // Write new version
@@ -834,7 +1335,7 @@ async function cmdUpgrade(opts) {
834
1335
  console.log('');
835
1336
  log.ok(`Upgraded to ${VERSION}`);
836
1337
  console.log('');
837
- console.log(` ${c.bold}Preserved:${c.reset} specs/, tracker, log, architecture, project config`);
1338
+ console.log(` ${c.bold}Preserved:${c.reset} specs/, tracker, log, architecture, project config${geminiMcpPreserved ? ', Gemini MCP config' : ''}`);
838
1339
  console.log(` ${c.bold}Updated:${c.reset} loop, prompts, adapters, enterprise templates`);
839
1340
  if (regenerated.length > 0) {
840
1341
  console.log(` ${c.bold}Adapters:${c.reset} ${regenerated.join(', ')}`);
@@ -859,7 +1360,7 @@ ${c.bold}Options:${c.reset}
859
1360
  --target <path> Target directory (default: current directory)
860
1361
  --platforms <list> Comma-separated: claude,codex,gemini,cursor,copilot,windsurf,all
861
1362
  --force Skip confirmation prompts
862
- --global Install Claude Code components to ~/.claude/
1363
+ --global Install Claude Code to ~/.claude/, Codex skills to ~/.agents/skills/, Gemini skills to ~/.gemini/skills/
863
1364
  --agent-teams Enable Agent Teams in .claude/settings.local.json
864
1365
  --verbose Show detailed output
865
1366
  -h, --help Show this help
@@ -87,6 +87,30 @@ Configure the KnowzCode MCP server using Claude Code's built-in MCP management.
87
87
  - Parse `--configure-vaults` flag (forces vault prompts)
88
88
  - Store parsed values for use in configuration
89
89
 
90
+ 1.5. **Smart Config Discovery (if no API key in arguments)**
91
+
92
+ Before prompting for an API key, check known config sources:
93
+
94
+ a. **Environment variable**: Check `KNOWZ_API_KEY`
95
+ - If set: use as the API key, display "Using API key from KNOWZ_API_KEY (ending ...{last4})"
96
+
97
+ b. **Project config**: Read `knowzcode/mcp_config.md`
98
+ - If `Connected: Yes` and endpoint set: pre-populate endpoint for Step 5
99
+ - If `API Key (last 4)` set: note for confirmation prompt
100
+
101
+ c. **Vault config**: Read `knowzcode/knowzcode_vaults.md`
102
+ - If vaults have non-empty IDs: note for Step 6 (skip vault prompts unless `--configure-vaults`)
103
+
104
+ d. **Cross-platform config files** (check for API key in other platforms):
105
+ - `.gemini/settings.json` → `mcpServers.knowz.headers.Authorization`
106
+ - `~/.gemini/settings.json` → same
107
+ - `.vscode/mcp.json` → `servers.knowz.headers`
108
+ - If found: extract Bearer token, offer to reuse:
109
+ "Found existing API key (ending ...{last4}) in {source}. Use this key? [Yes/No]"
110
+
111
+ If a key was discovered, skip Step 3 (interactive API key prompt).
112
+ If vaults were discovered, skip vault prompts in Step 6.
113
+
90
114
  2. **Check for existing configuration**
91
115
  - Check if MCP server already configured: `CLAUDECODE= claude mcp get knowz`
92
116
  - If already configured, ask if user wants to reconfigure
@@ -324,6 +348,7 @@ Configure the KnowzCode MCP server using Claude Code's built-in MCP management.
324
348
  - Set `Connected: Yes`
325
349
  - Set `Endpoint: <endpoint-url>`
326
350
  - Set `Last Verified: <timestamp>`
351
+ - Set `API Key (last 4): <last 4 characters of the API key>`
327
352
  - Set Ecosystem Vault ID and name (if provided or already set)
328
353
  - Set Code Vault ID and name (if provided)
329
354
  - Set `Auto-configured: No` (to distinguish from /kc:register setup)