fraim-framework 2.0.126 → 2.0.128
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/dist/src/ai-hub/catalog.js +280 -44
- package/dist/src/ai-hub/desktop-main.js +2 -2
- package/dist/src/ai-hub/hosts.js +384 -10
- package/dist/src/ai-hub/server.js +255 -9
- package/dist/src/cli/commands/add-ide.js +4 -3
- package/dist/src/cli/commands/first-run.js +61 -0
- package/dist/src/cli/commands/hub.js +4 -4
- package/dist/src/cli/commands/init-project.js +8 -4
- package/dist/src/cli/commands/setup.js +4 -3
- package/dist/src/cli/commands/sync.js +32 -6
- package/dist/src/cli/doctor/checks/ide-config-checks.js +20 -2
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/mcp/ide-formats.js +29 -1
- package/dist/src/cli/mcp/mcp-server-registry.js +1 -0
- package/dist/src/cli/setup/auto-mcp-setup.js +14 -8
- package/dist/src/cli/setup/ide-detector.js +32 -1
- package/dist/src/cli/setup/ide-global-integration.js +5 -1
- package/dist/src/cli/setup/ide-invocation-surfaces.js +14 -0
- package/dist/src/cli/setup/mcp-config-generator.js +12 -1
- package/dist/src/cli/utils/agent-adapters.js +10 -0
- package/dist/src/core/utils/git-utils.js +14 -6
- package/dist/src/first-run/install-state.js +70 -0
- package/dist/src/first-run/server.js +158 -0
- package/dist/src/first-run/session-service.js +746 -0
- package/dist/src/first-run/types.js +97 -0
- package/dist/src/local-mcp-server/otlp-metrics-receiver.js +7 -1
- package/dist/src/local-mcp-server/stdio-server.js +41 -9
- package/package.json +3 -1
- package/public/ai-hub/index.html +149 -102
- package/public/ai-hub/script.js +1154 -271
- package/public/ai-hub/styles.css +753 -450
- package/public/first-run/error-frame.js +89 -0
- package/public/first-run/index.html +35 -0
- package/public/first-run/script.js +417 -0
- package/public/first-run/styles.css +386 -0
|
@@ -60,6 +60,8 @@ function checkMCPConfigsExist() {
|
|
|
60
60
|
let existCount = 0;
|
|
61
61
|
let missingCount = 0;
|
|
62
62
|
const missing = [];
|
|
63
|
+
const bootstrapableMissing = [];
|
|
64
|
+
const blockingMissing = [];
|
|
63
65
|
for (const ide of installedIDEs) {
|
|
64
66
|
const configPath = (0, ide_detector_1.expandPath)(ide.configPath);
|
|
65
67
|
if (fs_1.default.existsSync(configPath)) {
|
|
@@ -68,6 +70,12 @@ function checkMCPConfigsExist() {
|
|
|
68
70
|
else {
|
|
69
71
|
missingCount++;
|
|
70
72
|
missing.push(ide.name);
|
|
73
|
+
if (ide.supportsConfigBootstrap) {
|
|
74
|
+
bootstrapableMissing.push(ide.name);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
blockingMissing.push(ide.name);
|
|
78
|
+
}
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
81
|
if (missingCount === 0) {
|
|
@@ -86,11 +94,21 @@ function checkMCPConfigsExist() {
|
|
|
86
94
|
details: { existCount, missingCount, missing }
|
|
87
95
|
};
|
|
88
96
|
}
|
|
97
|
+
if (blockingMissing.length === 0 && bootstrapableMissing.length > 0) {
|
|
98
|
+
return {
|
|
99
|
+
status: 'warning',
|
|
100
|
+
message: `Detected IDEs still need initial MCP config bootstrap: ${bootstrapableMissing.join(', ')}`,
|
|
101
|
+
suggestion: 'Run fraim add-ide to create the initial MCP configuration',
|
|
102
|
+
command: 'fraim add-ide',
|
|
103
|
+
details: { missingCount, missing, bootstrapableMissing }
|
|
104
|
+
};
|
|
105
|
+
}
|
|
89
106
|
return {
|
|
90
107
|
status: 'error',
|
|
91
|
-
message:
|
|
108
|
+
message: `No MCP configs found for detected IDEs: ${blockingMissing.join(', ')}`,
|
|
92
109
|
suggestion: 'Run fraim add-ide to configure MCP servers',
|
|
93
|
-
command: 'fraim add-ide'
|
|
110
|
+
command: 'fraim add-ide',
|
|
111
|
+
details: { missingCount, missing, blockingMissing, bootstrapableMissing }
|
|
94
112
|
};
|
|
95
113
|
}
|
|
96
114
|
};
|
package/dist/src/cli/fraim.js
CHANGED
|
@@ -51,6 +51,7 @@ const login_1 = require("./commands/login");
|
|
|
51
51
|
const mcp_1 = require("./commands/mcp");
|
|
52
52
|
const migrate_project_fraim_1 = require("./commands/migrate-project-fraim");
|
|
53
53
|
const hub_1 = require("./commands/hub");
|
|
54
|
+
const first_run_1 = require("./commands/first-run");
|
|
54
55
|
const fs_1 = __importDefault(require("fs"));
|
|
55
56
|
const path_1 = __importDefault(require("path"));
|
|
56
57
|
const program = new commander_1.Command();
|
|
@@ -91,6 +92,7 @@ program.addCommand(login_1.loginCommand);
|
|
|
91
92
|
program.addCommand(mcp_1.mcpCommand);
|
|
92
93
|
program.addCommand(migrate_project_fraim_1.migrateProjectFraimCommand);
|
|
93
94
|
program.addCommand(hub_1.hubCommand);
|
|
95
|
+
program.addCommand(first_run_1.firstRunCommand);
|
|
94
96
|
// Wait for async command initialization before parsing
|
|
95
97
|
(async () => {
|
|
96
98
|
// Import the initialization promise from setup command
|
|
@@ -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(),
|
|
@@ -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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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)');
|
|
@@ -9,6 +9,7 @@ exports.buildCursorMentionRuleContent = buildCursorMentionRuleContent;
|
|
|
9
9
|
exports.buildCodexSkillContent = buildCodexSkillContent;
|
|
10
10
|
exports.buildWindsurfCommandContent = buildWindsurfCommandContent;
|
|
11
11
|
exports.buildKiroCommandContent = buildKiroCommandContent;
|
|
12
|
+
exports.buildGeminiCommandContent = buildGeminiCommandContent;
|
|
12
13
|
exports.describeInvocationSurface = describeInvocationSurface;
|
|
13
14
|
exports.FRAIM_LAUNCH_PHRASE = 'Use FRAIM for <job or task>';
|
|
14
15
|
exports.CURSOR_MDC_FRONTMATTER = `---
|
|
@@ -107,6 +108,17 @@ function buildKiroCommandContent() {
|
|
|
107
108
|
|
|
108
109
|
${buildFraimInvocationBody('generic-tool-discovery')}`;
|
|
109
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
|
+
"""`;
|
|
121
|
+
}
|
|
110
122
|
function describeInvocationSurface(ideName, invocationProfile) {
|
|
111
123
|
switch (invocationProfile) {
|
|
112
124
|
case 'claude-slash':
|
|
@@ -125,6 +137,8 @@ function describeInvocationSurface(ideName, invocationProfile) {
|
|
|
125
137
|
return `${ideName}: /fraim`;
|
|
126
138
|
case 'kiro-hashtag':
|
|
127
139
|
return `${ideName}: #fraim`;
|
|
140
|
+
case 'gemini-command':
|
|
141
|
+
return `${ideName}: /fraim`;
|
|
128
142
|
case 'instructions-only':
|
|
129
143
|
default:
|
|
130
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) {
|
|
@@ -102,6 +104,10 @@ Use the stubs here to discover which FRAIM job, skill, or rule is relevant, then
|
|
|
102
104
|
const vscodePrompt = `# FRAIM
|
|
103
105
|
|
|
104
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 @@ ${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discover
|
|
|
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
|
|
@@ -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;
|
|
@@ -19,6 +20,18 @@ function extractLocalFolderLabel(rawPath) {
|
|
|
19
20
|
const candidate = parts[parts.length - 1];
|
|
20
21
|
return candidate || undefined;
|
|
21
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
|
+
}
|
|
22
35
|
/**
|
|
23
36
|
* Gets a unique port based on the current git branch name (if it's an issue branch)
|
|
24
37
|
* Default to 15302 if not on an issue branch
|
|
@@ -119,12 +132,7 @@ function sanitizeRepoIdentifier(repoUrl) {
|
|
|
119
132
|
}
|
|
120
133
|
const trimmed = repoUrl.trim();
|
|
121
134
|
// Convert local paths to just the folder name for privacy-safe workspace labeling.
|
|
122
|
-
if (trimmed
|
|
123
|
-
trimmed.startsWith('\\') ||
|
|
124
|
-
/^[a-zA-Z]:[\\/]/.test(trimmed) ||
|
|
125
|
-
trimmed.startsWith('./') ||
|
|
126
|
-
trimmed.startsWith('../') ||
|
|
127
|
-
trimmed.startsWith('file://')) {
|
|
135
|
+
if (isExplicitLocalRepoPath(trimmed)) {
|
|
128
136
|
return extractLocalFolderLabel(trimmed);
|
|
129
137
|
}
|
|
130
138
|
// Normalize GitHub/GitLab SSH and HTTPS URLs
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createInitialFirstRunState = createInitialFirstRunState;
|
|
7
|
+
exports.loadFirstRunState = loadFirstRunState;
|
|
8
|
+
exports.saveFirstRunState = saveFirstRunState;
|
|
9
|
+
exports.clearFirstRunState = clearFirstRunState;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const script_sync_utils_1 = require("../cli/utils/script-sync-utils");
|
|
13
|
+
const types_1 = require("./types");
|
|
14
|
+
function getStatePath() {
|
|
15
|
+
return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'install-state.json');
|
|
16
|
+
}
|
|
17
|
+
function ensureUserDir() {
|
|
18
|
+
fs_1.default.mkdirSync((0, script_sync_utils_1.getUserFraimDir)(), { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
function maskInstallKey(key) {
|
|
21
|
+
const normalized = key.trim();
|
|
22
|
+
if (normalized.length <= 8) {
|
|
23
|
+
return '<redacted>';
|
|
24
|
+
}
|
|
25
|
+
return `${normalized.slice(0, 4)}...${normalized.slice(-4)}`;
|
|
26
|
+
}
|
|
27
|
+
function createInitialFirstRunState(key) {
|
|
28
|
+
const now = new Date().toISOString();
|
|
29
|
+
return {
|
|
30
|
+
version: 2,
|
|
31
|
+
installKeyRef: maskInstallKey(key),
|
|
32
|
+
platform: process.platform,
|
|
33
|
+
agentId: 'claude-code',
|
|
34
|
+
rows: (0, types_1.createInitialRows)(),
|
|
35
|
+
resourcesUrl: types_1.FIRST_RUN_RESOURCES_URL,
|
|
36
|
+
createdAt: now,
|
|
37
|
+
updatedAt: now,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function loadFirstRunState() {
|
|
41
|
+
const statePath = getStatePath();
|
|
42
|
+
if (!fs_1.default.existsSync(statePath)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const state = JSON.parse(fs_1.default.readFileSync(statePath, 'utf8'));
|
|
47
|
+
// Reject persisted v1 state — schema changed materially in #352 v1.
|
|
48
|
+
if (state.version !== 2) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (typeof state.installKeyRef === 'string' && !state.installKeyRef.includes('...')) {
|
|
52
|
+
state.installKeyRef = maskInstallKey(state.installKeyRef);
|
|
53
|
+
}
|
|
54
|
+
return state;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function saveFirstRunState(state) {
|
|
61
|
+
ensureUserDir();
|
|
62
|
+
state.updatedAt = new Date().toISOString();
|
|
63
|
+
fs_1.default.writeFileSync(getStatePath(), JSON.stringify(state, null, 2));
|
|
64
|
+
}
|
|
65
|
+
function clearFirstRunState() {
|
|
66
|
+
const statePath = getStatePath();
|
|
67
|
+
if (fs_1.default.existsSync(statePath)) {
|
|
68
|
+
fs_1.default.unlinkSync(statePath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.FirstRunServer = void 0;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const session_service_1 = require("./session-service");
|
|
12
|
+
function resolveFirstRunPublicDir() {
|
|
13
|
+
const candidates = [
|
|
14
|
+
path_1.default.resolve(process.cwd(), 'public/first-run'),
|
|
15
|
+
path_1.default.resolve(__dirname, '..', '..', 'public/first-run'),
|
|
16
|
+
path_1.default.resolve(__dirname, '..', '..', '..', 'public/first-run'),
|
|
17
|
+
];
|
|
18
|
+
for (const candidate of candidates) {
|
|
19
|
+
if (fs_1.default.existsSync(candidate)) {
|
|
20
|
+
return candidate;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new Error('Could not locate public/first-run assets.');
|
|
24
|
+
}
|
|
25
|
+
function pickProjectPath() {
|
|
26
|
+
if (process.platform === 'win32') {
|
|
27
|
+
const script = [
|
|
28
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
29
|
+
'$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
|
|
30
|
+
'$dialog.ShowNewFolderButton = $true',
|
|
31
|
+
'if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {',
|
|
32
|
+
' Write-Output $dialog.SelectedPath',
|
|
33
|
+
'}',
|
|
34
|
+
].join('; ');
|
|
35
|
+
const result = (0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-Command', script], {
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
});
|
|
38
|
+
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
39
|
+
}
|
|
40
|
+
if (process.platform === 'darwin') {
|
|
41
|
+
const result = (0, child_process_1.spawnSync)('osascript', ['-e', 'POSIX path of (choose folder with prompt "Select a FRAIM project folder")'], {
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
});
|
|
44
|
+
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
45
|
+
}
|
|
46
|
+
const result = (0, child_process_1.spawnSync)('bash', ['-lc', 'zenity --file-selection --directory 2>/dev/null || kdialog --getexistingdirectory 2>/dev/null'], {
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
});
|
|
49
|
+
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
50
|
+
}
|
|
51
|
+
function isCanonicalRowId(value) {
|
|
52
|
+
return typeof value === 'string' && session_service_1.FIRST_RUN_ROW_IDS.includes(value);
|
|
53
|
+
}
|
|
54
|
+
class FirstRunServer {
|
|
55
|
+
constructor(options) {
|
|
56
|
+
this.app = (0, express_1.default)();
|
|
57
|
+
this.sessionService = options.sessionService;
|
|
58
|
+
this.finishPromise = new Promise((resolve) => {
|
|
59
|
+
this.finishResolver = resolve;
|
|
60
|
+
});
|
|
61
|
+
this.app.use(express_1.default.json());
|
|
62
|
+
this.app.use('/api/first-run', (req, res, next) => {
|
|
63
|
+
if (req.method === 'GET') {
|
|
64
|
+
return next();
|
|
65
|
+
}
|
|
66
|
+
const requestToken = req.header('x-fraim-first-run-token');
|
|
67
|
+
if (requestToken !== this.sessionService.getRequestToken()) {
|
|
68
|
+
return res.status(403).json({ error: 'Invalid first-run session token.' });
|
|
69
|
+
}
|
|
70
|
+
return next();
|
|
71
|
+
});
|
|
72
|
+
this.app.use('/first-run', express_1.default.static(resolveFirstRunPublicDir()));
|
|
73
|
+
this.app.get('/health', (_req, res) => {
|
|
74
|
+
res.json({ status: 'ok', service: 'fraim-first-run' });
|
|
75
|
+
});
|
|
76
|
+
this.registerRoutes();
|
|
77
|
+
}
|
|
78
|
+
async start(port) {
|
|
79
|
+
await new Promise((resolve, reject) => {
|
|
80
|
+
this.httpServer = this.app.listen(port, '127.0.0.1');
|
|
81
|
+
this.httpServer.once('listening', () => resolve());
|
|
82
|
+
this.httpServer.once('error', (error) => reject(error));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async waitForFinish() {
|
|
86
|
+
await this.finishPromise;
|
|
87
|
+
}
|
|
88
|
+
async stop() {
|
|
89
|
+
if (!this.httpServer) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await new Promise((resolve, reject) => {
|
|
93
|
+
this.httpServer.close((error) => {
|
|
94
|
+
if (error) {
|
|
95
|
+
reject(error);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
resolve();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
this.httpServer = undefined;
|
|
102
|
+
}
|
|
103
|
+
registerRoutes() {
|
|
104
|
+
this.app.get('/api/first-run/session', (_req, res) => {
|
|
105
|
+
res.json(this.sessionService.getSession());
|
|
106
|
+
});
|
|
107
|
+
this.app.post('/api/first-run/rows/:rowId/run', async (req, res) => {
|
|
108
|
+
const { rowId } = req.params;
|
|
109
|
+
if (!isCanonicalRowId(rowId)) {
|
|
110
|
+
return res.status(400).json({ error: `Unknown row id: ${rowId}` });
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const result = await this.sessionService.runRow(rowId, req.body || {});
|
|
114
|
+
return res.json(result);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not run row.' });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
this.app.post('/api/first-run/agent/change', (req, res) => {
|
|
121
|
+
try {
|
|
122
|
+
return res.json(this.sessionService.changeAgent(req.body || {}));
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not change agent.' });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
this.app.post('/api/first-run/project-path/pick', (_req, res) => {
|
|
129
|
+
try {
|
|
130
|
+
const selectedPath = pickProjectPath();
|
|
131
|
+
if (!selectedPath) {
|
|
132
|
+
return res.status(204).end();
|
|
133
|
+
}
|
|
134
|
+
return res.json({ path: selectedPath });
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not open the folder picker.' });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
this.app.post('/api/first-run/finish', (_req, res) => {
|
|
141
|
+
const result = this.sessionService.finish();
|
|
142
|
+
this.finishResolver?.();
|
|
143
|
+
return res.json(result);
|
|
144
|
+
});
|
|
145
|
+
// Hub-launch helper — starts an AiHubServer for the chosen project and
|
|
146
|
+
// opens the user's browser. v2 (#355) replaces the in-process spawn with
|
|
147
|
+
// a durable launcher binary.
|
|
148
|
+
this.app.post('/api/first-run/open-hub', async (_req, res) => {
|
|
149
|
+
try {
|
|
150
|
+
return res.json(await this.sessionService.openHub());
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not open Hub.' });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
exports.FirstRunServer = FirstRunServer;
|