fraim-framework 2.0.124 → 2.0.127

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.
Files changed (46) hide show
  1. package/bin/fraim.js +1 -1
  2. package/dist/src/ai-hub/catalog.js +280 -44
  3. package/dist/src/ai-hub/desktop-main.js +2 -2
  4. package/dist/src/ai-hub/hosts.js +384 -10
  5. package/dist/src/ai-hub/server.js +255 -9
  6. package/dist/src/cli/commands/add-ide.js +4 -3
  7. package/dist/src/cli/commands/first-run.js +61 -0
  8. package/dist/src/cli/commands/hub.js +4 -4
  9. package/dist/src/cli/commands/init-project.js +4 -4
  10. package/dist/src/cli/commands/setup.js +4 -3
  11. package/dist/src/cli/commands/sync.js +21 -2
  12. package/dist/src/cli/doctor/checks/ide-config-checks.js +20 -2
  13. package/dist/src/cli/fraim.js +2 -0
  14. package/dist/src/cli/mcp/ide-formats.js +29 -1
  15. package/dist/src/cli/mcp/mcp-server-registry.js +1 -0
  16. package/dist/src/cli/setup/auto-mcp-setup.js +14 -8
  17. package/dist/src/cli/setup/ide-detector.js +32 -1
  18. package/dist/src/cli/setup/ide-global-integration.js +5 -1
  19. package/dist/src/cli/setup/ide-invocation-surfaces.js +70 -17
  20. package/dist/src/cli/setup/mcp-config-generator.js +12 -1
  21. package/dist/src/cli/utils/agent-adapters.js +12 -2
  22. package/dist/src/cli/utils/project-bootstrap.js +4 -3
  23. package/dist/src/core/quality-evidence.js +81 -8
  24. package/dist/src/core/utils/git-utils.js +32 -7
  25. package/dist/src/core/utils/job-aliases.js +47 -0
  26. package/dist/src/core/utils/workflow-parser.js +3 -5
  27. package/dist/src/first-run/install-state.js +68 -0
  28. package/dist/src/first-run/server.js +153 -0
  29. package/dist/src/first-run/session-service.js +302 -0
  30. package/dist/src/first-run/types.js +40 -0
  31. package/dist/src/local-mcp-server/agent-token-prices.js +114 -0
  32. package/dist/src/local-mcp-server/codex-token-adapter.js +232 -0
  33. package/dist/src/local-mcp-server/learning-context-builder.js +21 -8
  34. package/dist/src/local-mcp-server/otlp-metrics-receiver.js +7 -1
  35. package/dist/src/local-mcp-server/stdio-server.js +70 -17
  36. package/dist/src/local-mcp-server/token-adapter-registry.js +64 -0
  37. package/dist/src/local-mcp-server/usage-collector.js +25 -0
  38. package/index.js +83 -83
  39. package/package.json +7 -1
  40. package/public/ai-hub/index.html +149 -102
  41. package/public/ai-hub/script.js +1154 -271
  42. package/public/ai-hub/styles.css +753 -450
  43. package/public/first-run/index.html +221 -0
  44. package/public/first-run/script.js +361 -0
  45. package/dist/src/cli/services/device-flow-service.js +0 -83
  46. package/dist/src/local-mcp-server/prometheus-scraper.js +0 -152
@@ -2,7 +2,7 @@
2
2
  // IDE Format Adapters - transform logical server structure to IDE-specific formats
3
3
  // Uses the centralized registry to determine server types
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.IDE_FORMATS = exports.CodexFormat = exports.WindsurfFormat = exports.ClaudeCodeFormat = exports.ClaudeFormat = exports.VSCodeFormat = exports.KiroFormat = exports.StandardFormat = void 0;
5
+ exports.IDE_FORMATS = exports.CodexFormat = exports.WindsurfFormat = exports.ClaudeCodeFormat = exports.ClaudeFormat = exports.GeminiCliFormat = exports.VSCodeFormat = exports.KiroFormat = exports.StandardFormat = void 0;
6
6
  exports.getIDEFormat = getIDEFormat;
7
7
  const mcp_server_registry_1 = require("./mcp-server-registry");
8
8
  const provider_registry_1 = require("../providers/provider-registry");
@@ -93,6 +93,33 @@ class VSCodeFormat {
93
93
  }
94
94
  }
95
95
  exports.VSCodeFormat = VSCodeFormat;
96
+ class GeminiCliFormat {
97
+ constructor() {
98
+ this.name = 'gemini-cli';
99
+ }
100
+ transform(servers) {
101
+ const mcpServers = {};
102
+ for (const [key, server] of servers) {
103
+ if (server.url) {
104
+ mcpServers[key] = {
105
+ ...(server.type === 'sse'
106
+ ? { url: server.url }
107
+ : { httpUrl: server.httpUrl || server.url }),
108
+ ...(server.headers && { headers: server.headers })
109
+ };
110
+ }
111
+ else {
112
+ mcpServers[key] = {
113
+ command: server.command,
114
+ ...(server.args && { args: server.args }),
115
+ ...(server.env && { env: server.env })
116
+ };
117
+ }
118
+ }
119
+ return { mcpServers };
120
+ }
121
+ }
122
+ exports.GeminiCliFormat = GeminiCliFormat;
96
123
  // Claude Desktop format (excludes provider servers - Issue #132)
97
124
  class ClaudeFormat {
98
125
  constructor() {
@@ -230,6 +257,7 @@ exports.IDE_FORMATS = {
230
257
  standard: new StandardFormat(),
231
258
  kiro: new KiroFormat(),
232
259
  vscode: new VSCodeFormat(),
260
+ 'gemini-cli': new GeminiCliFormat(),
233
261
  claude: new ClaudeFormat(),
234
262
  'claude-code': new ClaudeCodeFormat(),
235
263
  windsurf: new WindsurfFormat(),
@@ -85,6 +85,7 @@ function buildHTTPServer(mcpConfig, token) {
85
85
  }
86
86
  const authHeader = mcpConfig.authHeaderTemplate?.replace('{token}', token) || `Bearer ${token}`;
87
87
  return {
88
+ type: 'http',
88
89
  url: mcpConfig.url,
89
90
  headers: {
90
91
  Authorization: authHeader
@@ -252,9 +252,9 @@ const configureIDEMCP = async (ide, fraimKey, tokenInput, providerConfigs) => {
252
252
  const autoConfigureMCP = async (fraimKey, tokenInput, selectedIDEs, providerConfigs) => {
253
253
  const tokens = normalizePlatformTokens(tokenInput);
254
254
  const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
255
- if (detectedIDEs.length === 0) {
255
+ if (detectedIDEs.length === 0 && (!selectedIDEs || selectedIDEs.length === 0)) {
256
256
  console.log(chalk_1.default.yellow('⚠️ No supported IDEs detected.'));
257
- console.log(chalk_1.default.gray('Supported IDEs: Claude, Antigravity, Kiro, Cursor, VSCode, Codex, Windsurf'));
257
+ console.log(chalk_1.default.gray('Supported IDEs: Claude, Claude Code, Antigravity, Gemini CLI, Kiro, Cursor, VSCode, Codex, Windsurf'));
258
258
  console.log(chalk_1.default.blue('\n💡 You can install an IDE and run setup again later.'));
259
259
  console.log(chalk_1.default.gray(' Or continue with manual MCP configuration.'));
260
260
  if (process.env.FRAIM_NON_INTERACTIVE) {
@@ -275,13 +275,19 @@ const autoConfigureMCP = async (fraimKey, tokenInput, selectedIDEs, providerConf
275
275
  }
276
276
  let idesToConfigure;
277
277
  if (selectedIDEs && selectedIDEs.length > 0) {
278
- // Use command line specified IDEs
279
- idesToConfigure = detectedIDEs.filter(ide => selectedIDEs.some(selected => ide.name.toLowerCase().includes(selected.toLowerCase())));
278
+ idesToConfigure = detectedIDEs.filter((ide) => selectedIDEs.some((selected) => ide.name.toLowerCase().includes(selected.toLowerCase())));
280
279
  if (idesToConfigure.length === 0) {
281
- console.log(chalk_1.default.yellow(`⚠️ No IDEs found matching: ${selectedIDEs.join(', ')}`));
282
- console.log(chalk_1.default.gray('Available IDEs:'));
283
- detectedIDEs.forEach(ide => console.log(chalk_1.default.gray(` • ${ide.name}`)));
284
- return;
280
+ idesToConfigure = (0, ide_detector_1.getAllSupportedIDEs)().filter((ide) => selectedIDEs.some((selected) => {
281
+ const normalized = selected.toLowerCase();
282
+ return ide.name.toLowerCase().includes(normalized)
283
+ || ide.aliases?.some((alias) => alias.includes(normalized));
284
+ }));
285
+ if (idesToConfigure.length === 0) {
286
+ console.log(chalk_1.default.yellow(`⚠️ No IDEs found matching: ${selectedIDEs.join(', ')}`));
287
+ console.log(chalk_1.default.gray('Available IDEs:'));
288
+ detectedIDEs.forEach((ide) => console.log(chalk_1.default.gray(` • ${ide.name}`)));
289
+ return;
290
+ }
285
291
  }
286
292
  }
287
293
  else {
@@ -7,6 +7,7 @@ exports.expandPath = exports.findIDEByName = exports.getAllSupportedIDEs = expor
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const os_1 = __importDefault(require("os"));
10
+ const child_process_1 = require("child_process");
10
11
  const expandPath = (filePath) => {
11
12
  if (filePath.startsWith('~/')) {
12
13
  return path_1.default.join(os_1.default.homedir(), filePath.slice(2));
@@ -17,6 +18,12 @@ exports.expandPath = expandPath;
17
18
  const checkMultiplePaths = (paths) => {
18
19
  return paths.some(p => fs_1.default.existsSync(expandPath(p)));
19
20
  };
21
+ const availableByVersionProbe = (command) => {
22
+ const result = process.platform === 'win32'
23
+ ? (0, child_process_1.spawnSync)('cmd.exe', ['/d', '/s', '/c', `${command} --version`], { encoding: 'utf8', timeout: 1500 })
24
+ : (0, child_process_1.spawnSync)(command, ['--version'], { encoding: 'utf8', timeout: 1500 });
25
+ return result.status === 0;
26
+ };
20
27
  const detectClaude = () => {
21
28
  const paths = [
22
29
  '~/.claude.json',
@@ -54,6 +61,15 @@ const detectWindsurf = () => {
54
61
  ];
55
62
  return checkMultiplePaths(paths);
56
63
  };
64
+ const detectGeminiCli = () => {
65
+ const paths = [
66
+ '~/.gemini/settings.json',
67
+ '~/.gemini',
68
+ '~/AppData/Roaming/gemini/settings.json',
69
+ '~/.config/gemini/settings.json'
70
+ ];
71
+ return checkMultiplePaths(paths) || availableByVersionProbe('gemini');
72
+ };
57
73
  exports.IDE_CONFIGS = [
58
74
  {
59
75
  name: 'Claude Code',
@@ -90,6 +106,21 @@ exports.IDE_CONFIGS = [
90
106
  detectMethod: () => fs_1.default.existsSync(expandPath('~/.gemini/antigravity')),
91
107
  description: 'Google Gemini Antigravity IDE'
92
108
  },
109
+ {
110
+ name: 'Gemini CLI',
111
+ configPath: '~/.gemini/settings.json',
112
+ configFormat: 'json',
113
+ configType: 'gemini-cli',
114
+ invocationProfile: 'gemini-command',
115
+ detectMethod: detectGeminiCli,
116
+ supportsConfigBootstrap: true,
117
+ aliases: ['gemini', 'gemini-cli', 'gemini cli'],
118
+ alternativePaths: [
119
+ '~/AppData/Roaming/gemini/settings.json',
120
+ '~/.config/gemini/settings.json'
121
+ ],
122
+ description: 'Google Gemini CLI local settings'
123
+ },
93
124
  {
94
125
  name: 'Kiro',
95
126
  configPath: '~/.kiro/settings/mcp.json',
@@ -186,6 +217,6 @@ const findIDEByName = (name) => {
186
217
  const normalized = name.toLowerCase();
187
218
  return exports.IDE_CONFIGS.find(ide => ide.name.toLowerCase().includes(normalized) ||
188
219
  normalized.includes(ide.name.toLowerCase()) ||
189
- ide.aliases?.some(alias => alias.includes(normalized) || normalized.includes(alias)));
220
+ ide.aliases?.some(alias => alias.toLowerCase().includes(normalized) || normalized.includes(alias.toLowerCase())));
190
221
  };
191
222
  exports.findIDEByName = findIDEByName;
@@ -36,7 +36,7 @@ async function installSlashCommands(homeDir) {
36
36
  }
37
37
  /**
38
38
  * Install FRAIM invocation artifacts for non-Claude IDEs.
39
- * Supports: Cursor, Codex, Windsurf, Kiro
39
+ * Supports: Cursor, Codex, Gemini CLI, Windsurf, Kiro
40
40
  * Does not overwrite existing files.
41
41
  */
42
42
  async function installGlobalRules(homeDir) {
@@ -49,6 +49,10 @@ async function installGlobalRules(homeDir) {
49
49
  if (fs_1.default.existsSync(codexDir)) {
50
50
  installFileIfMissing(path_1.default.join(codexDir, 'skills', 'fraim', 'SKILL.md'), (0, ide_invocation_surfaces_1.buildCodexSkillContent)(), 'Codex FRAIM skill (~/.codex/skills/fraim/SKILL.md)');
51
51
  }
52
+ const geminiDir = path_1.default.join(home, '.gemini');
53
+ if (fs_1.default.existsSync(geminiDir)) {
54
+ installFileIfMissing(path_1.default.join(geminiDir, 'commands', 'fraim.toml'), (0, ide_invocation_surfaces_1.buildGeminiCommandContent)(), 'Gemini CLI FRAIM command (~/.gemini/commands/fraim.toml)');
55
+ }
52
56
  const windsurfDir = path_1.default.join(home, '.codeium', 'windsurf');
53
57
  if (fs_1.default.existsSync(windsurfDir)) {
54
58
  installFileIfMissing(path_1.default.join(windsurfDir, 'commands', 'fraim.md'), (0, ide_invocation_surfaces_1.buildWindsurfCommandContent)(), 'Windsurf FRAIM command (~/.codeium/windsurf/commands/fraim.md)');
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FRAIM_INVOCATION_BODY = exports.CURSOR_MDC_FRONTMATTER = exports.FRAIM_LAUNCH_PHRASE = void 0;
3
+ exports.FRAIM_INVOCATION_BODY = exports.FRAIM_DEFERRED_TOOL_PRELOAD = exports.CURSOR_MDC_FRONTMATTER = exports.FRAIM_LAUNCH_PHRASE = void 0;
4
+ exports.buildFraimInvocationBody = buildFraimInvocationBody;
4
5
  exports.buildClaudeSkillContent = buildClaudeSkillContent;
5
6
  exports.buildClaudeCommandShimContent = buildClaudeCommandShimContent;
6
7
  exports.buildClaudeSlashCommandContent = buildClaudeSlashCommandContent;
@@ -8,15 +9,52 @@ exports.buildCursorMentionRuleContent = buildCursorMentionRuleContent;
8
9
  exports.buildCodexSkillContent = buildCodexSkillContent;
9
10
  exports.buildWindsurfCommandContent = buildWindsurfCommandContent;
10
11
  exports.buildKiroCommandContent = buildKiroCommandContent;
12
+ exports.buildGeminiCommandContent = buildGeminiCommandContent;
11
13
  exports.describeInvocationSurface = describeInvocationSurface;
12
14
  exports.FRAIM_LAUNCH_PHRASE = 'Use FRAIM for <job or task>';
13
15
  exports.CURSOR_MDC_FRONTMATTER = `---
14
16
  description: FRAIM discovery and execution contract
15
17
  alwaysApply: true
16
18
  ---`;
17
- exports.FRAIM_INVOCATION_BODY = `Follow this process:
19
+ exports.FRAIM_DEFERRED_TOOL_PRELOAD = [
20
+ 'fraim_connect',
21
+ 'list_fraim_jobs',
22
+ 'get_fraim_job',
23
+ 'get_fraim_file',
24
+ 'seekMentoring'
25
+ ];
26
+ function buildDeferredToolBootstrapSection(profile) {
27
+ if (profile === 'none') {
28
+ return '';
29
+ }
30
+ const preloadList = exports.FRAIM_DEFERRED_TOOL_PRELOAD.map((toolName) => `\`${toolName}\``).join(', ');
31
+ switch (profile) {
32
+ case 'claude-toolsearch':
33
+ return `0. **Preload deferred FRAIM tools when needed**:
34
+ - If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`ToolSearch\` once to load ${preloadList}.
35
+ - Do the preload as one batched discovery step, not one search per tool.
36
+
37
+ `;
38
+ case 'codex-tool-search':
39
+ return `0. **Preload deferred FRAIM tools when needed**:
40
+ - If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`tool_search\` once to load ${preloadList}.
41
+ - Do the preload as one batched discovery step, not one search per tool.
42
+
43
+ `;
44
+ case 'generic-tool-discovery':
45
+ return `0. **Preload deferred FRAIM tools when needed**:
46
+ - If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, use the host's tool discovery surface once to load ${preloadList}.
47
+ - Do the preload as one batched discovery step, not one search per tool.
48
+
49
+ `;
50
+ default:
51
+ return '';
52
+ }
53
+ }
54
+ function buildFraimInvocationBody(profile = 'none') {
55
+ return `Follow this process:
18
56
 
19
- 1. **If the user did not specify a FRAIM job or topic**:
57
+ ${buildDeferredToolBootstrapSection(profile)}1. **If the user did not specify a FRAIM job or topic**:
20
58
  Call \`list_fraim_jobs()\` to discover available jobs. Present the results grouped by the categories returned by the server. For each group, list 3-5 of the most relevant jobs with a one-line description.
21
59
 
22
60
  2. **Find the match**:
@@ -27,20 +65,22 @@ exports.FRAIM_INVOCATION_BODY = `Follow this process:
27
65
  - For skills, use the content returned by \`get_fraim_file(...)\`.
28
66
 
29
67
  4. **Execute**:
30
- - For jobs, follow the phased instructions and use \`seekMentoring\` when the job requires phase transitions.
31
- - For skills, apply the skill steps directly to the user's current context.
68
+ - For jobs, follow the phased instructions and use \`seekMentoring\` when the job requires phase transitions.
69
+ - For skills, apply the skill steps directly to the user's current context.
32
70
  `;
71
+ }
72
+ exports.FRAIM_INVOCATION_BODY = buildFraimInvocationBody();
33
73
  function buildClaudeSkillContent() {
34
- return `# FRAIM
35
-
36
- ${exports.FRAIM_INVOCATION_BODY}`;
74
+ return `# FRAIM
75
+
76
+ ${buildFraimInvocationBody('claude-toolsearch')}`;
37
77
  }
38
78
  function buildClaudeCommandShimContent() {
39
- return `# FRAIM Compatibility Command
40
-
41
- Use the FRAIM skill when Claude exposes skills directly. This compatibility command keeps \`/fraim\` working on surfaces that still discover legacy command files.
42
-
43
- ${exports.FRAIM_INVOCATION_BODY}`;
79
+ return `# FRAIM Compatibility Command
80
+
81
+ Use the FRAIM skill when Claude exposes skills directly. This compatibility command keeps \`/fraim\` working on surfaces that still discover legacy command files.
82
+
83
+ ${buildFraimInvocationBody('claude-toolsearch')}`;
44
84
  }
45
85
  function buildClaudeSlashCommandContent() {
46
86
  return buildClaudeCommandShimContent();
@@ -50,23 +90,34 @@ function buildCursorMentionRuleContent() {
50
90
 
51
91
  # FRAIM
52
92
 
53
- ${exports.FRAIM_INVOCATION_BODY}
93
+ ${buildFraimInvocationBody('generic-tool-discovery')}
54
94
  `;
55
95
  }
56
96
  function buildCodexSkillContent() {
57
97
  return `# FRAIM
58
98
 
59
- ${exports.FRAIM_INVOCATION_BODY}`;
99
+ ${buildFraimInvocationBody('codex-tool-search')}`;
60
100
  }
61
101
  function buildWindsurfCommandContent() {
62
102
  return `# FRAIM
63
103
 
64
- ${exports.FRAIM_INVOCATION_BODY}`;
104
+ ${buildFraimInvocationBody('generic-tool-discovery')}`;
65
105
  }
66
106
  function buildKiroCommandContent() {
67
107
  return `# FRAIM
68
108
 
69
- ${exports.FRAIM_INVOCATION_BODY}`;
109
+ ${buildFraimInvocationBody('generic-tool-discovery')}`;
110
+ }
111
+ function escapeTomlMultiline(value) {
112
+ return value.replace(/"""/g, '\\"""');
113
+ }
114
+ function buildGeminiCommandContent() {
115
+ return `description = "Discover and execute FRAIM jobs and skills"
116
+ prompt = """
117
+ # FRAIM
118
+
119
+ ${escapeTomlMultiline(buildFraimInvocationBody('generic-tool-discovery'))}
120
+ """`;
70
121
  }
71
122
  function describeInvocationSurface(ideName, invocationProfile) {
72
123
  switch (invocationProfile) {
@@ -86,6 +137,8 @@ function describeInvocationSurface(ideName, invocationProfile) {
86
137
  return `${ideName}: /fraim`;
87
138
  case 'kiro-hashtag':
88
139
  return `${ideName}: #fraim`;
140
+ case 'gemini-command':
141
+ return `${ideName}: /fraim`;
89
142
  case 'instructions-only':
90
143
  default:
91
144
  return `${ideName}: natural language`;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateMCPConfig = exports.generateWindsurfMCPServers = exports.generateVSCodeMCPServers = exports.generateCodexMCPServers = exports.generateKiroMCPServers = exports.generateClaudeCodeMCPServers = exports.generateClaudeMCPServers = exports.generateStandardMCPServers = exports.mergeTomlMCPServers = exports.extractTomlMcpServerBlock = void 0;
3
+ exports.generateMCPConfig = exports.generateWindsurfMCPServers = exports.generateGeminiCliMCPServers = exports.generateVSCodeMCPServers = exports.generateCodexMCPServers = exports.generateKiroMCPServers = exports.generateClaudeCodeMCPServers = exports.generateClaudeMCPServers = exports.generateStandardMCPServers = exports.mergeTomlMCPServers = exports.extractTomlMcpServerBlock = void 0;
4
4
  const mcp_server_builder_1 = require("../mcp/mcp-server-builder");
5
5
  const ide_formats_1 = require("../mcp/ide-formats");
6
6
  const normalizeTokens = (tokenInput) => {
@@ -160,6 +160,15 @@ const generateVSCodeMCPServers = async (fraimKey, tokenInput, providerConfigs) =
160
160
  return format.transform(builder.getServers());
161
161
  };
162
162
  exports.generateVSCodeMCPServers = generateVSCodeMCPServers;
163
+ const generateGeminiCliMCPServers = async (fraimKey, tokenInput, providerConfigs) => {
164
+ const tokens = normalizeTokens(tokenInput);
165
+ const builder = new mcp_server_builder_1.MCPServerBuilder();
166
+ builder.addBaseServers(fraimKey);
167
+ await addProviderServers(builder, tokens, providerConfigs);
168
+ const format = (0, ide_formats_1.getIDEFormat)('gemini-cli');
169
+ return format.transform(builder.getServers());
170
+ };
171
+ exports.generateGeminiCliMCPServers = generateGeminiCliMCPServers;
163
172
  const generateWindsurfMCPServers = async (fraimKey, tokenInput, providerConfigs) => {
164
173
  const tokens = normalizeTokens(tokenInput);
165
174
  const builder = new mcp_server_builder_1.MCPServerBuilder();
@@ -181,6 +190,8 @@ const generateMCPConfig = async (configType, fraimKey, tokenInput, providerConfi
181
190
  return await (0, exports.generateKiroMCPServers)(fraimKey, tokenInput, providerConfigs);
182
191
  case 'vscode':
183
192
  return await (0, exports.generateVSCodeMCPServers)(fraimKey, tokenInput, providerConfigs);
193
+ case 'gemini-cli':
194
+ return await (0, exports.generateGeminiCliMCPServers)(fraimKey, tokenInput, providerConfigs);
184
195
  case 'codex':
185
196
  return await (0, exports.generateCodexMCPServers)(fraimKey, tokenInput, providerConfigs);
186
197
  case 'windsurf':
@@ -15,6 +15,8 @@ const CLAUDE_FRAIM_COMMAND_PATH = path_1.default.join('.claude', 'commands', 'fr
15
15
  const CLAUDE_FRAIM_SKILL_PATH = path_1.default.join('.claude', 'skills', 'fraim', 'SKILL.md');
16
16
  const VSCODE_FRAIM_PROMPT_PATH = path_1.default.join('.github', 'prompts', 'fraim.prompt.md');
17
17
  const CODEX_FRAIM_SKILL_PATH = path_1.default.join('.codex', 'skills', 'fraim', 'SKILL.md');
18
+ const GEMINI_FRAIM_COMMAND_PATH = path_1.default.join('.gemini', 'commands', 'fraim.toml');
19
+ const GEMINI_PROJECT_INSTRUCTIONS_PATH = path_1.default.join('.gemini', 'GEMINI.md');
18
20
  const WINDSURF_FRAIM_COMMAND_PATH = path_1.default.join('.windsurf', 'commands', 'fraim.md');
19
21
  const KIRO_FRAIM_COMMAND_PATH = path_1.default.join('.kiro', 'commands', 'fraim.md');
20
22
  function buildManagedSection(body) {
@@ -74,7 +76,7 @@ This repository uses FRAIM.
74
76
  const cursorManagedBody = buildManagedSection(`
75
77
  # FRAIM
76
78
 
77
- ${ide_invocation_surfaces_1.FRAIM_INVOCATION_BODY}
79
+ ${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discovery')}
78
80
  `);
79
81
  const copilotBody = buildManagedSection(`
80
82
  ## FRAIM
@@ -101,7 +103,11 @@ Use the stubs here to discover which FRAIM job, skill, or rule is relevant, then
101
103
  `;
102
104
  const vscodePrompt = `# FRAIM
103
105
 
104
- ${ide_invocation_surfaces_1.FRAIM_INVOCATION_BODY}`;
106
+ ${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discovery')}`;
107
+ const geminiProjectInstructions = `# Gemini Project Instructions
108
+
109
+ @../AGENTS.md
110
+ `;
105
111
  return [
106
112
  { path: 'AGENTS.md', content: markdownBody },
107
113
  { path: 'CLAUDE.md', content: markdownBody },
@@ -112,6 +118,8 @@ ${ide_invocation_surfaces_1.FRAIM_INVOCATION_BODY}`;
112
118
  { path: CLAUDE_FRAIM_SKILL_PATH, content: (0, ide_invocation_surfaces_1.buildClaudeSkillContent)() },
113
119
  { path: CLAUDE_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildClaudeCommandShimContent)() },
114
120
  { path: CODEX_FRAIM_SKILL_PATH, content: (0, ide_invocation_surfaces_1.buildCodexSkillContent)() },
121
+ { path: GEMINI_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildGeminiCommandContent)() },
122
+ { path: GEMINI_PROJECT_INSTRUCTIONS_PATH, content: geminiProjectInstructions },
115
123
  { path: WINDSURF_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildWindsurfCommandContent)() },
116
124
  { path: KIRO_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildKiroCommandContent)() }
117
125
  ];
@@ -132,6 +140,8 @@ function ensureAgentAdapterFiles(projectRoot) {
132
140
  || file.path === CLAUDE_FRAIM_SKILL_PATH
133
141
  || file.path === CLAUDE_FRAIM_COMMAND_PATH
134
142
  || file.path === CODEX_FRAIM_SKILL_PATH
143
+ || file.path === GEMINI_FRAIM_COMMAND_PATH
144
+ || file.path === GEMINI_PROJECT_INSTRUCTIONS_PATH
135
145
  || file.path === WINDSURF_FRAIM_COMMAND_PATH
136
146
  || file.path === KIRO_FRAIM_COMMAND_PATH
137
147
  ? file.content
@@ -8,6 +8,7 @@ exports.recordPathStatus = recordPathStatus;
8
8
  exports.buildInitProjectSummary = buildInitProjectSummary;
9
9
  exports.printInitProjectSummary = printInitProjectSummary;
10
10
  const chalk_1 = __importDefault(require("chalk"));
11
+ const ONBOARDING_VIDEO_PLAYLIST_URL = 'https://fraimworks.ai/resources.html#videos';
11
12
  function formatModeLabel(mode) {
12
13
  switch (mode) {
13
14
  case 'conversational':
@@ -21,11 +22,11 @@ function formatModeLabel(mode) {
21
22
  function getModeSpecificNextStep(mode) {
22
23
  switch (mode) {
23
24
  case 'conversational':
24
- return 'The agent will focus on project context, validation commands, and durable repo rules.';
25
+ return `The agent will focus on project context, validation commands, and durable repo rules. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
25
26
  case 'split':
26
- return 'The agent will confirm the code-host and issue-tracker split, then ask only for the missing project details.';
27
+ return `The agent will confirm the code-host and issue-tracker split, then ask only for the missing project details. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
27
28
  default:
28
- return 'The agent will review the detected repo setup, then ask only for the highest-value missing project details.';
29
+ return `The agent will review the detected repo setup, then ask only for the highest-value missing project details. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
29
30
  }
30
31
  }
31
32
  function createInitProjectResult(projectName, mode) {
@@ -26,7 +26,11 @@ exports.validateQualityEvidence = validateQualityEvidence;
26
26
  exports.buildQualityRejectionMessage = buildQualityRejectionMessage;
27
27
  exports.QUALITY_REGISTRY = {
28
28
  // Customer Development
29
- 'process-interview-notes': { stage: 'customer-development', enforced: true, telemetryKind: 'score' },
29
+ // review-customer-development is the sole quality emitter for this stage.
30
+ // process-interview-notes is retained here for stage mapping only; it does
31
+ // not emit (enforced: false).
32
+ 'process-interview-notes': { stage: 'customer-development', enforced: false },
33
+ 'review-customer-development': { stage: 'customer-development', enforced: true, telemetryKind: 'score' },
30
34
  'triage-customer-needs': { stage: 'customer-development', enforced: true, telemetryKind: 'gate' },
31
35
  'interview-preparation': { stage: 'customer-discovery', enforced: false },
32
36
  // Business Strategy
@@ -39,6 +43,7 @@ exports.QUALITY_REGISTRY = {
39
43
  'test-quality-assessment': { stage: 'test-quality', enforced: true, telemetryKind: 'score' },
40
44
  // Security
41
45
  'security-review': { stage: 'security', enforced: true, telemetryKind: 'score' },
46
+ 'production-readiness-review': { stage: 'production-readiness', enforced: true, telemetryKind: 'score' },
42
47
  // Fundraising
43
48
  'investor-pitch-preparation': { stage: 'fundraising', enforced: false },
44
49
  // Go-to-Market
@@ -73,6 +78,7 @@ exports.STAGE_DISPLAY_NAMES = {
73
78
  'product-quality': 'Product Quality',
74
79
  'test-quality': 'Test Quality',
75
80
  'security': 'Security',
81
+ 'production-readiness': 'Production Readiness',
76
82
  'fundraising': 'Fundraising',
77
83
  'go-to-market': 'Go-to-Market',
78
84
  };
@@ -86,6 +92,7 @@ exports.ALL_STAGE_CATEGORIES = [
86
92
  'product-quality',
87
93
  'test-quality',
88
94
  'security',
95
+ 'production-readiness',
89
96
  'fundraising',
90
97
  'go-to-market',
91
98
  ];
@@ -114,20 +121,85 @@ const GATE_REQUIRED_FIELDS = [
114
121
  const UNIVERSAL_REQUIRED_FIELDS = [
115
122
  { path: 'composite', type: 'number' },
116
123
  ];
124
+ const SCORE_DIMENSION_REQUIRED_FIELDS = [
125
+ { suffix: 'score', type: 'number' },
126
+ { suffix: 'rationale', type: 'string' }
127
+ ];
128
+ const QUALITY_SCORE_DIMENSIONS = {
129
+ 'review-customer-development': [
130
+ 'icpCoherence',
131
+ 'interviewCoverage',
132
+ 'evidenceQuality',
133
+ 'patternSaturation',
134
+ 'signalToProduct'
135
+ ],
136
+ 'review-business-strategy': [
137
+ 'marketEvidence',
138
+ 'competitiveRigor',
139
+ 'unitEconomics',
140
+ 'strategicCoherence'
141
+ ],
142
+ 'branding-quality-audit': [
143
+ 'clarity',
144
+ 'differentiation',
145
+ 'coherence',
146
+ 'proof',
147
+ 'identityExpressiveness',
148
+ 'governanceReadiness'
149
+ ],
150
+ 'code-quality-assessment': [
151
+ 'typeSafety',
152
+ 'errorHandling',
153
+ 'architecture',
154
+ 'maintainability'
155
+ ],
156
+ 'test-quality-assessment': [
157
+ 'coverage',
158
+ 'testIntegrity',
159
+ 'testDesign',
160
+ 'reliability'
161
+ ],
162
+ 'security-review': [
163
+ 'findingSeverity',
164
+ 'remediationReadiness',
165
+ 'coverageCompleteness'
166
+ ],
167
+ 'production-readiness-review': [
168
+ 'securityPosture',
169
+ 'availabilityResilience',
170
+ 'backupRestore',
171
+ 'observabilityOps',
172
+ 'releaseSafety',
173
+ 'governanceRunbooks'
174
+ ]
175
+ };
176
+ function requiredDimensionFields(jobName) {
177
+ const dimensions = QUALITY_SCORE_DIMENSIONS[jobName] ?? [];
178
+ return dimensions.flatMap((dimension) => SCORE_DIMENSION_REQUIRED_FIELDS.map(({ suffix, type }) => ({
179
+ path: `${dimension}.${suffix}`,
180
+ type
181
+ })));
182
+ }
117
183
  /**
118
184
  * Returns the required quality fields for a given job.
119
- * Customer-development jobs retain the strict V1 schema.
120
- * All other jobs require only a composite score (principle-based, not prescriptive).
185
+ * Gate-only jobs use the gate schema. The strict V1 interview schema is
186
+ * pinned to process-interview-notes for defense-in-depth on the
187
+ * /api/analytics/quality-score endpoint. Scored quality review jobs require
188
+ * their stage-specific rubric dimensions as direct `{ score, rationale }`
189
+ * properties so the Quality view can render those dimensions dynamically.
121
190
  */
122
191
  function getRequiredFieldsForJob(jobName) {
123
192
  const entry = exports.QUALITY_REGISTRY[jobName];
124
193
  if (entry?.telemetryKind === 'gate') {
125
194
  return GATE_REQUIRED_FIELDS;
126
195
  }
127
- if (entry?.stage === 'customer-development') {
196
+ if (jobName === 'process-interview-notes') {
128
197
  return CUSTOMER_DEV_REQUIRED_FIELDS;
129
198
  }
130
- return UNIVERSAL_REQUIRED_FIELDS;
199
+ return [
200
+ ...UNIVERSAL_REQUIRED_FIELDS,
201
+ ...requiredDimensionFields(jobName)
202
+ ];
131
203
  }
132
204
  /**
133
205
  * @deprecated Use getRequiredFieldsForJob() for stage-aware validation.
@@ -188,6 +260,9 @@ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
188
260
  const entry = exports.QUALITY_REGISTRY[jobName];
189
261
  const isGateOnly = entry?.telemetryKind === 'gate';
190
262
  const isCustomerDev = entry?.stage === 'customer-development';
263
+ const dimensionExamples = (QUALITY_SCORE_DIMENSIONS[jobName] ?? ['marketEvidence', 'unitEconomics'])
264
+ .map((dimension) => ` ${dimension}: { score: <number>, rationale: "<string>" },`)
265
+ .join('\n');
191
266
  const schemaExample = isGateOnly
192
267
  ? [
193
268
  '```javascript',
@@ -226,9 +301,7 @@ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
226
301
  ' quality: {',
227
302
  ' composite: <number 0-10>,',
228
303
  ' coaching: "<actionable recommendation>",',
229
- ' marketEvidence: { score: <number>, rationale: "<string>" }, // flat, NO "dimensions" wrapper',
230
- ' unitEconomics: { score: <number>, rationale: "<string>" },',
231
- ' // add other sub-scores as direct properties',
304
+ dimensionExamples,
232
305
  ' }',
233
306
  '}',
234
307
  '```'
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isExplicitLocalRepoPath = isExplicitLocalRepoPath;
3
4
  exports.getPort = getPort;
4
5
  exports.determineDatabaseName = determineDatabaseName;
5
6
  exports.getCurrentGitBranch = getCurrentGitBranch;
@@ -7,6 +8,30 @@ exports.determineSchema = determineSchema;
7
8
  exports.getDefaultBranch = getDefaultBranch;
8
9
  exports.sanitizeRepoIdentifier = sanitizeRepoIdentifier;
9
10
  const child_process_1 = require("child_process");
11
+ function extractLocalFolderLabel(rawPath) {
12
+ const normalized = rawPath
13
+ .trim()
14
+ .replace(/^file:\/\//i, '')
15
+ .replace(/\\/g, '/')
16
+ .replace(/\/+$/, '');
17
+ if (!normalized)
18
+ return undefined;
19
+ const parts = normalized.split('/').filter(Boolean);
20
+ const candidate = parts[parts.length - 1];
21
+ return candidate || undefined;
22
+ }
23
+ function isExplicitLocalRepoPath(repoUrl) {
24
+ if (!repoUrl || typeof repoUrl !== 'string') {
25
+ return false;
26
+ }
27
+ const trimmed = repoUrl.trim();
28
+ return (trimmed.startsWith('/') ||
29
+ trimmed.startsWith('\\') ||
30
+ /^[a-zA-Z]:[\\/]/.test(trimmed) ||
31
+ trimmed.startsWith('./') ||
32
+ trimmed.startsWith('../') ||
33
+ trimmed.startsWith('file://'));
34
+ }
10
35
  /**
11
36
  * Gets a unique port based on the current git branch name (if it's an issue branch)
12
37
  * Default to 15302 if not on an issue branch
@@ -106,13 +131,9 @@ function sanitizeRepoIdentifier(repoUrl) {
106
131
  return undefined;
107
132
  }
108
133
  const trimmed = repoUrl.trim();
109
- // Reject local paths (Windows and POSIX)
110
- if (trimmed.startsWith('/') ||
111
- trimmed.startsWith('\\') ||
112
- /^[a-zA-Z]:\\/.test(trimmed) ||
113
- trimmed.startsWith('./') ||
114
- trimmed.startsWith('../')) {
115
- return undefined;
134
+ // Convert local paths to just the folder name for privacy-safe workspace labeling.
135
+ if (isExplicitLocalRepoPath(trimmed)) {
136
+ return extractLocalFolderLabel(trimmed);
116
137
  }
117
138
  // Normalize GitHub/GitLab SSH and HTTPS URLs
118
139
  // Patterns:
@@ -144,5 +165,9 @@ function sanitizeRepoIdentifier(repoUrl) {
144
165
  catch (e) {
145
166
  // Not a valid URL
146
167
  }
168
+ // Preserve plain folder labels that are already privacy-safe and not path-like.
169
+ if (!trimmed.includes('/') && !trimmed.includes('\\') && !trimmed.includes('://')) {
170
+ return trimmed;
171
+ }
147
172
  return undefined;
148
173
  }