prompt-forge-studio-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/package.json +25 -0
- package/src/api.js +69 -0
- package/src/auth.js +69 -0
- package/src/config.js +28 -0
- package/src/health.js +38 -0
- package/src/index.js +37 -0
- package/src/init.js +101 -0
- package/src/studio.js +149 -0
- package/src/ui.js +126 -0
- package/test-project/README.md +9 -0
- package/test-project/forge.config.json +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# forge
|
|
2
|
+
|
|
3
|
+
Forge CLI Studio is a professional, premium terminal-native Prompt Engineering studio designed for rapid prompt generation, testing, and AI model evaluation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
You can link this package globally to try it out:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd forge-cli
|
|
11
|
+
npm link
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
### Initialize a new project
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
forge init my-project
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This will create a new directory, initialize a git repository, set up the required structure, and create a `forge.config.json` file.
|
|
23
|
+
|
|
24
|
+
### Studio Mode
|
|
25
|
+
|
|
26
|
+
Run the studio interactive REPL:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
forge
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Commands available in Studio Mode:
|
|
33
|
+
- `:model` - Switch to a specific model.
|
|
34
|
+
- `:models` - List available models and configure them.
|
|
35
|
+
- `:test` - Test the current model.
|
|
36
|
+
- `:test all` - Test all available models and display connection latency.
|
|
37
|
+
- `:auto` - Toggle auto-failover mode.
|
|
38
|
+
- `:key` - Configure API keys.
|
|
39
|
+
- `:clear` - Clear the console.
|
|
40
|
+
- `:exit` - Exit the studio.
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prompt-forge-studio-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Professional terminal-native prompt engineering studio",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"forge": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"boxen": "^5.1.2",
|
|
17
|
+
"chalk": "^4.1.2",
|
|
18
|
+
"cli-table3": "^0.6.5",
|
|
19
|
+
"commander": "^14.0.3",
|
|
20
|
+
"figlet": "^1.10.0",
|
|
21
|
+
"gradient-string": "^3.0.0",
|
|
22
|
+
"inquirer": "^8.2.7",
|
|
23
|
+
"ora": "^5.4.1"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const ora = require('ora');
|
|
2
|
+
const { getHealthStatus } = require('./health');
|
|
3
|
+
const { requireAuth } = require('./auth');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
// Simulated AI responses based on prompt input
|
|
7
|
+
async function generateResponse(prompt, model) {
|
|
8
|
+
const spinner = ora(`Generating response with ${chalk.cyan(model)}...`).start();
|
|
9
|
+
|
|
10
|
+
// Simulate network latency
|
|
11
|
+
const latency = Math.floor(Math.random() * 500) + 500;
|
|
12
|
+
await new Promise(resolve => setTimeout(resolve, latency));
|
|
13
|
+
|
|
14
|
+
// Simulated failure logic: pretend gemini is currently having issues occasionally
|
|
15
|
+
if (model === 'gemini' && Math.random() > 0.5) {
|
|
16
|
+
spinner.fail(`Failed to get response from ${chalk.cyan(model)}`);
|
|
17
|
+
throw new Error('Provider connection timeout');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
spinner.succeed(`Received response from ${chalk.cyan(model)} in ${latency}ms`);
|
|
21
|
+
|
|
22
|
+
let responseText;
|
|
23
|
+
if (prompt.toLowerCase().includes('hello') || prompt.toLowerCase().includes('hi')) {
|
|
24
|
+
responseText = `Hello! I am the ${model} model. How can I help you in Forge Studio today?`;
|
|
25
|
+
} else {
|
|
26
|
+
responseText = `This is a generated response from ${model} for your prompt: "${prompt}".\n\n- Point 1: Simulated insight\n- Point 2: Prompt engineering tip\n- Point 3: Evaluation matrix.`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
text: responseText,
|
|
31
|
+
metadata: {
|
|
32
|
+
model,
|
|
33
|
+
latency
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function executeWithFailover(prompt, primaryModel, autoFailover) {
|
|
39
|
+
const healthManager = require('./health');
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return await generateResponse(prompt, primaryModel);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (!autoFailover) {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Auto-failover logic
|
|
49
|
+
console.log(chalk.yellow(`\n⚠ Auto-failover triggered. ${primaryModel} failed.`));
|
|
50
|
+
const allModels = ['gpt-4o', 'claude', 'gemini'].filter(m => m !== primaryModel);
|
|
51
|
+
|
|
52
|
+
// Find next healthy model (simulated)
|
|
53
|
+
for (const backup of allModels) {
|
|
54
|
+
try {
|
|
55
|
+
console.log(chalk.dim(`Attempting fallback to ${backup}...`));
|
|
56
|
+
return await generateResponse(prompt, backup);
|
|
57
|
+
} catch (backupError) {
|
|
58
|
+
// continue
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw new Error('All failover attempts exhausted.');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
generateResponse,
|
|
68
|
+
executeWithFailover
|
|
69
|
+
};
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
const FORGE_HOME = path.join(os.homedir(), '.forge');
|
|
8
|
+
const AUTH_FILE = path.join(FORGE_HOME, 'auth.json');
|
|
9
|
+
|
|
10
|
+
function ensureForgeHomeDir() {
|
|
11
|
+
if (!fs.existsSync(FORGE_HOME)) {
|
|
12
|
+
fs.mkdirSync(FORGE_HOME, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getAuth() {
|
|
17
|
+
if (!fs.existsSync(AUTH_FILE)) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const rawData = fs.readFileSync(AUTH_FILE, 'utf8');
|
|
22
|
+
return JSON.parse(rawData);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function setAuth(provider, key) {
|
|
29
|
+
ensureForgeHomeDir();
|
|
30
|
+
const currentAuth = getAuth();
|
|
31
|
+
currentAuth[provider] = key;
|
|
32
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(currentAuth, null, 2), { mode: 0o600 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function requireAuth() {
|
|
36
|
+
const auth = getAuth();
|
|
37
|
+
if (Object.keys(auth).length === 0) {
|
|
38
|
+
console.log(chalk.yellow('\nNo API keys found. Let\'s set up your primary provider.'));
|
|
39
|
+
|
|
40
|
+
const { provider } = await inquirer.prompt([
|
|
41
|
+
{
|
|
42
|
+
type: 'list',
|
|
43
|
+
name: 'provider',
|
|
44
|
+
message: 'Select an AI provider to configure:',
|
|
45
|
+
choices: ['openai', 'anthropic', 'gemini']
|
|
46
|
+
}
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const { key } = await inquirer.prompt([
|
|
50
|
+
{
|
|
51
|
+
type: 'password',
|
|
52
|
+
name: 'key',
|
|
53
|
+
message: `Enter your ${provider.toUpperCase()} API Key:`,
|
|
54
|
+
mask: '*'
|
|
55
|
+
}
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
setAuth(provider, key);
|
|
59
|
+
console.log(chalk.green('\n✔ API key saved securely locally.'));
|
|
60
|
+
return getAuth();
|
|
61
|
+
}
|
|
62
|
+
return auth;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
getAuth,
|
|
67
|
+
setAuth,
|
|
68
|
+
requireAuth
|
|
69
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const CONFIG_FILE = 'forge.config.json';
|
|
5
|
+
|
|
6
|
+
function getConfig() {
|
|
7
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE);
|
|
8
|
+
if (!fs.existsSync(configPath)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const rawData = fs.readFileSync(configPath, 'utf8');
|
|
13
|
+
return JSON.parse(rawData);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error('Error reading forge.config.json', error.message);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function updateConfig(newConfig) {
|
|
21
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE);
|
|
22
|
+
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
getConfig,
|
|
27
|
+
updateConfig,
|
|
28
|
+
};
|
package/src/health.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const ora = require('ora');
|
|
2
|
+
|
|
3
|
+
const MODELS = ['gpt-4o', 'claude', 'gemini'];
|
|
4
|
+
|
|
5
|
+
// Simulates checking latency and status for available models
|
|
6
|
+
async function checkAllModelsHealth() {
|
|
7
|
+
const results = [];
|
|
8
|
+
const spinner = ora('Pinging AI providers...').start();
|
|
9
|
+
|
|
10
|
+
for (const model of MODELS) {
|
|
11
|
+
// Simulate latency and status randomly
|
|
12
|
+
const latency = Math.floor(Math.random() * 1000) + 100;
|
|
13
|
+
let status = 'Active';
|
|
14
|
+
|
|
15
|
+
// Randomize some issues for realism
|
|
16
|
+
if (model === 'gemini') {
|
|
17
|
+
status = 'Offline';
|
|
18
|
+
} else if (latency > 800) {
|
|
19
|
+
status = 'Degraded';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
results.push({
|
|
23
|
+
name: model,
|
|
24
|
+
status: status,
|
|
25
|
+
latency: status === 'Offline' ? null : latency
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Small delay to make spinner look realistic
|
|
29
|
+
await new Promise(r => setTimeout(r, 400));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
spinner.succeed('Health check complete');
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
checkAllModelsHealth
|
|
38
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const { initProject } = require('./init');
|
|
5
|
+
const { startStudio } = require('./studio');
|
|
6
|
+
const { showBanner } = require('./ui');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('forge')
|
|
12
|
+
.description('Prompt Forge Studio - Professional Terminal-Native Prompt Engineering Studio')
|
|
13
|
+
.version('1.0.0');
|
|
14
|
+
|
|
15
|
+
// Command: init
|
|
16
|
+
program
|
|
17
|
+
.command('init [project]')
|
|
18
|
+
.description('Initialize a new Forge project')
|
|
19
|
+
.action((project) => {
|
|
20
|
+
initProject(project || 'prompt-forge-project');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Default behavior: Studio mode
|
|
24
|
+
program
|
|
25
|
+
.action(() => {
|
|
26
|
+
const isProjectDir = fs.existsSync(path.join(process.cwd(), 'forge.config.json'));
|
|
27
|
+
|
|
28
|
+
showBanner();
|
|
29
|
+
|
|
30
|
+
if (!isProjectDir) {
|
|
31
|
+
console.log('\nWarning: Not inside a Forge project. Run `forge init <project>` to set one up.\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
startStudio();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
program.parse(process.argv);
|
package/src/init.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const boxen = require('boxen');
|
|
7
|
+
|
|
8
|
+
function initProject(projectName) {
|
|
9
|
+
const projectPath = path.join(process.cwd(), projectName);
|
|
10
|
+
|
|
11
|
+
console.log(
|
|
12
|
+
boxen(chalk.bold.cyan(`Initializing Forge Studio Project: ${projectName}`), {
|
|
13
|
+
padding: 1,
|
|
14
|
+
margin: 1,
|
|
15
|
+
borderStyle: 'round',
|
|
16
|
+
borderColor: 'cyan'
|
|
17
|
+
})
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const spinner = ora('Creating directories...').start();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.existsSync(projectPath)) {
|
|
24
|
+
fs.mkdirSync(projectPath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Create folder structure
|
|
28
|
+
const dirs = ['prompts', 'sessions', 'logs', '.forge'];
|
|
29
|
+
dirs.forEach(dir => {
|
|
30
|
+
fs.mkdirSync(path.join(projectPath, dir), { recursive: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
spinner.succeed('Directories created');
|
|
34
|
+
|
|
35
|
+
// Create files
|
|
36
|
+
spinner.start('Generating configuration files...');
|
|
37
|
+
|
|
38
|
+
const configContent = {
|
|
39
|
+
project: projectName,
|
|
40
|
+
version: '1.0.0',
|
|
41
|
+
defaultModel: 'gpt-4o',
|
|
42
|
+
autoFailover: true
|
|
43
|
+
};
|
|
44
|
+
fs.writeFileSync(
|
|
45
|
+
path.join(projectPath, 'forge.config.json'),
|
|
46
|
+
JSON.stringify(configContent, null, 2)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const gitignoreContent = `.forge/
|
|
50
|
+
logs/
|
|
51
|
+
.env
|
|
52
|
+
*.log
|
|
53
|
+
node_modules/
|
|
54
|
+
`;
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(projectPath, '.gitignore'),
|
|
57
|
+
gitignoreContent
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const readmeContent = `# ${projectName}
|
|
61
|
+
|
|
62
|
+
A Prompt Forge Studio project.
|
|
63
|
+
|
|
64
|
+
## Directory Structure
|
|
65
|
+
- \`/prompts\` - Your prompt templates
|
|
66
|
+
- \`/sessions\` - Saved studio sessions
|
|
67
|
+
- \`/logs\` - Execution logs
|
|
68
|
+
- \`/.forge\` - Local project cache (git-ignored)
|
|
69
|
+
`;
|
|
70
|
+
fs.writeFileSync(
|
|
71
|
+
path.join(projectPath, 'README.md'),
|
|
72
|
+
readmeContent
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
spinner.succeed('Configuration files generated');
|
|
76
|
+
|
|
77
|
+
// Initialize git repo if not exists
|
|
78
|
+
spinner.start('Initializing git repository...');
|
|
79
|
+
try {
|
|
80
|
+
if (!fs.existsSync(path.join(projectPath, '.git'))) {
|
|
81
|
+
execSync('git init', { cwd: projectPath, stdio: 'ignore' });
|
|
82
|
+
spinner.succeed('Git repository initialized');
|
|
83
|
+
} else {
|
|
84
|
+
spinner.info('Git repository already exists');
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
spinner.warn('Git is not installed or failed to initialize');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log('\n' + chalk.green('✔ Project setup complete!'));
|
|
91
|
+
console.log(`\nNext steps:\n ${chalk.cyan(`cd ${projectName}`)}\n ${chalk.cyan('forge')}`);
|
|
92
|
+
|
|
93
|
+
} catch (error) {
|
|
94
|
+
spinner.fail('Failed to initialize project');
|
|
95
|
+
console.error(chalk.red(error.message));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
initProject
|
|
101
|
+
};
|
package/src/studio.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const inquirer = require('inquirer');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { requireAuth, setAuth } = require('./auth');
|
|
4
|
+
const { executeWithFailover } = require('./api');
|
|
5
|
+
const { checkAllModelsHealth } = require('./health');
|
|
6
|
+
const { showSessionPanel, showModelComparison, formatAIResponse, showInfoBox, showErrorBox } = require('./ui');
|
|
7
|
+
const { getConfig } = require('./config');
|
|
8
|
+
|
|
9
|
+
const AVAILABLE_MODELS = ['gpt-4o', 'claude', 'gemini'];
|
|
10
|
+
|
|
11
|
+
let state = {
|
|
12
|
+
model: 'gpt-4o',
|
|
13
|
+
status: 'Active',
|
|
14
|
+
autoFailover: true
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async function startStudio() {
|
|
18
|
+
await requireAuth();
|
|
19
|
+
|
|
20
|
+
// Load project config if available
|
|
21
|
+
const config = getConfig();
|
|
22
|
+
if (config) {
|
|
23
|
+
if (config.defaultModel) state.model = config.defaultModel;
|
|
24
|
+
if (config.autoFailover !== undefined) state.autoFailover = config.autoFailover;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
showSessionPanel(state.model, state.status, state.autoFailover);
|
|
28
|
+
await studioLoop();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function handleCommand(input) {
|
|
32
|
+
const cmd = input.trim().toLowerCase();
|
|
33
|
+
|
|
34
|
+
switch (cmd) {
|
|
35
|
+
case ':exit':
|
|
36
|
+
console.log(chalk.cyan('\nExiting Forge Studio. Goodbye!\n'));
|
|
37
|
+
process.exit(0);
|
|
38
|
+
break;
|
|
39
|
+
|
|
40
|
+
case ':clear':
|
|
41
|
+
console.clear();
|
|
42
|
+
showSessionPanel(state.model, state.status, state.autoFailover);
|
|
43
|
+
break;
|
|
44
|
+
|
|
45
|
+
case ':auto':
|
|
46
|
+
state.autoFailover = !state.autoFailover;
|
|
47
|
+
showInfoBox(`Auto-failover is now ${state.autoFailover ? 'ON' : 'OFF'}`);
|
|
48
|
+
showSessionPanel(state.model, state.status, state.autoFailover);
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case ':model':
|
|
52
|
+
const { selectedModel } = await inquirer.prompt([{
|
|
53
|
+
type: 'list',
|
|
54
|
+
name: 'selectedModel',
|
|
55
|
+
message: 'Select a primary model:',
|
|
56
|
+
choices: AVAILABLE_MODELS
|
|
57
|
+
}]);
|
|
58
|
+
state.model = selectedModel;
|
|
59
|
+
showSessionPanel(state.model, state.status, state.autoFailover);
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case ':models':
|
|
63
|
+
console.log(chalk.cyan('\nAvailable models configuration goes here.\n'));
|
|
64
|
+
break;
|
|
65
|
+
|
|
66
|
+
case ':test all':
|
|
67
|
+
const results = await checkAllModelsHealth();
|
|
68
|
+
showModelComparison(results);
|
|
69
|
+
break;
|
|
70
|
+
|
|
71
|
+
case ':test':
|
|
72
|
+
state.status = 'Testing...';
|
|
73
|
+
showSessionPanel(state.model, state.status, state.autoFailover);
|
|
74
|
+
try {
|
|
75
|
+
await executeWithFailover('hello', state.model, false);
|
|
76
|
+
state.status = 'Active';
|
|
77
|
+
} catch (e) {
|
|
78
|
+
state.status = 'Offline';
|
|
79
|
+
}
|
|
80
|
+
showSessionPanel(state.model, state.status, state.autoFailover);
|
|
81
|
+
break;
|
|
82
|
+
|
|
83
|
+
case ':key':
|
|
84
|
+
const { provider } = await inquirer.prompt([{
|
|
85
|
+
type: 'list',
|
|
86
|
+
name: 'provider',
|
|
87
|
+
message: 'Which provider to configure?',
|
|
88
|
+
choices: AVAILABLE_MODELS.map(m => m === 'gpt-4o' ? 'openai' : (m === 'claude' ? 'anthropic' : 'gemini'))
|
|
89
|
+
}]);
|
|
90
|
+
const { key } = await inquirer.prompt([{
|
|
91
|
+
type: 'password',
|
|
92
|
+
name: 'key',
|
|
93
|
+
message: `Enter new API key for ${provider}:`,
|
|
94
|
+
mask: '*'
|
|
95
|
+
}]);
|
|
96
|
+
setAuth(provider, key);
|
|
97
|
+
showInfoBox(`Key updated for ${provider}`);
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
default:
|
|
101
|
+
if (cmd.startsWith(':')) {
|
|
102
|
+
showErrorBox('Unknown Command', `Command ${cmd} not recognized. Type :exit to quit.`);
|
|
103
|
+
} else {
|
|
104
|
+
// Handle prompt generation
|
|
105
|
+
try {
|
|
106
|
+
const result = await executeWithFailover(input, state.model, state.autoFailover);
|
|
107
|
+
|
|
108
|
+
if (result.metadata.model !== state.model) {
|
|
109
|
+
// It failed over to a different model!
|
|
110
|
+
showInfoBox(`Response was generated by fallback model: ${result.metadata.model}`);
|
|
111
|
+
if (state.autoFailover) {
|
|
112
|
+
state.status = 'Degraded';
|
|
113
|
+
showSessionPanel(state.model, state.status, state.autoFailover);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
formatAIResponse(result.text, result.metadata);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
showErrorBox('Generation Failed', error.message);
|
|
120
|
+
if (!state.autoFailover) {
|
|
121
|
+
console.log(chalk.cyan('Hint: Try running `:test all` to check model health, or enable auto-failover with `:auto`.\n'));
|
|
122
|
+
}
|
|
123
|
+
state.status = 'Offline';
|
|
124
|
+
showSessionPanel(state.model, state.status, state.autoFailover);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function studioLoop() {
|
|
131
|
+
while (true) {
|
|
132
|
+
const { promptInput } = await inquirer.prompt([
|
|
133
|
+
{
|
|
134
|
+
type: 'input',
|
|
135
|
+
name: 'promptInput',
|
|
136
|
+
message: chalk.cyan('forge> '),
|
|
137
|
+
prefix: ''
|
|
138
|
+
}
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
if (!promptInput.trim()) continue;
|
|
142
|
+
|
|
143
|
+
await handleCommand(promptInput);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
startStudio
|
|
149
|
+
};
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const figlet = require('figlet');
|
|
2
|
+
const gradient = require('gradient-string');
|
|
3
|
+
const boxen = require('boxen');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const Table = require('cli-table3');
|
|
6
|
+
|
|
7
|
+
function showBanner() {
|
|
8
|
+
const bannerText = figlet.textSync('FORGE STUDIO', { font: 'Standard', horizontalLayout: 'fitted' });
|
|
9
|
+
console.log(gradient.pastel.multiline(bannerText));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function showSessionPanel(model, status, autoFailover) {
|
|
13
|
+
let statusColor;
|
|
14
|
+
switch (status.toLowerCase()) {
|
|
15
|
+
case 'active':
|
|
16
|
+
statusColor = chalk.green(status);
|
|
17
|
+
break;
|
|
18
|
+
case 'degraded':
|
|
19
|
+
statusColor = chalk.yellow(status);
|
|
20
|
+
break;
|
|
21
|
+
case 'offline':
|
|
22
|
+
statusColor = chalk.red(status);
|
|
23
|
+
break;
|
|
24
|
+
default:
|
|
25
|
+
statusColor = chalk.white(status);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const autoText = autoFailover ? chalk.green('ON') : chalk.red('OFF');
|
|
29
|
+
|
|
30
|
+
const content = [
|
|
31
|
+
chalk.bold.cyan('Forge Studio (beta)'),
|
|
32
|
+
`Model: ${chalk.white(model)}`,
|
|
33
|
+
`Status: ${statusColor}`,
|
|
34
|
+
`Auto-Failover: ${autoText}`
|
|
35
|
+
].join('\n');
|
|
36
|
+
|
|
37
|
+
console.log(
|
|
38
|
+
boxen(content, {
|
|
39
|
+
padding: 1,
|
|
40
|
+
margin: { top: 1, bottom: 1 },
|
|
41
|
+
borderStyle: 'bold',
|
|
42
|
+
borderColor: 'blue',
|
|
43
|
+
dimBorder: true
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function showModelComparison(modelsInfo) {
|
|
49
|
+
const table = new Table({
|
|
50
|
+
head: [chalk.cyan('Model'), chalk.cyan('Status'), chalk.cyan('Latency')],
|
|
51
|
+
chars: {
|
|
52
|
+
'top': '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐',
|
|
53
|
+
'bottom': '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘',
|
|
54
|
+
'left': '│', 'left-mid': '├', 'mid': '─', 'mid-mid': '┼',
|
|
55
|
+
'right': '│', 'right-mid': '┤', 'middle': '│'
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
modelsInfo.forEach(info => {
|
|
60
|
+
let statusColor, latencyDisplay;
|
|
61
|
+
|
|
62
|
+
if (info.status === 'Active') {
|
|
63
|
+
statusColor = chalk.green(info.status);
|
|
64
|
+
latencyDisplay = info.latency ? chalk.white(`${info.latency}ms`) : '-';
|
|
65
|
+
} else if (info.status === 'Degraded') {
|
|
66
|
+
statusColor = chalk.yellow(info.status);
|
|
67
|
+
latencyDisplay = info.latency ? chalk.yellow(`${info.latency}ms`) : '-';
|
|
68
|
+
} else {
|
|
69
|
+
statusColor = chalk.red(info.status);
|
|
70
|
+
latencyDisplay = chalk.dim('-');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
table.push([info.name, statusColor, latencyDisplay]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
console.log('\n' + table.toString() + '\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatAIResponse(text, metadata = {}) {
|
|
80
|
+
const metaText = Object.keys(metadata).length > 0
|
|
81
|
+
? chalk.dim(`\n\n---\n[Model: ${metadata.model || 'unknown'} | Latency: ${metadata.latency || 0}ms]`)
|
|
82
|
+
: '';
|
|
83
|
+
|
|
84
|
+
const boxedResponse = boxen(chalk.white(text) + metaText, {
|
|
85
|
+
padding: 1,
|
|
86
|
+
margin: { top: 1, bottom: 1 },
|
|
87
|
+
borderStyle: 'round',
|
|
88
|
+
borderColor: 'magenta',
|
|
89
|
+
title: chalk.magenta.bold(' AI Response '),
|
|
90
|
+
titleAlignment: 'left'
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
console.log(boxedResponse);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function showErrorBox(title, message) {
|
|
97
|
+
console.log(
|
|
98
|
+
boxen(chalk.red(message), {
|
|
99
|
+
padding: 1,
|
|
100
|
+
margin: 1,
|
|
101
|
+
borderStyle: 'double',
|
|
102
|
+
borderColor: 'red',
|
|
103
|
+
title: chalk.red.bold(` Error: ${title} `)
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function showInfoBox(message) {
|
|
109
|
+
console.log(
|
|
110
|
+
boxen(chalk.cyan(message), {
|
|
111
|
+
padding: 1,
|
|
112
|
+
margin: 1,
|
|
113
|
+
borderStyle: 'round',
|
|
114
|
+
borderColor: 'cyan'
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
showBanner,
|
|
121
|
+
showSessionPanel,
|
|
122
|
+
showModelComparison,
|
|
123
|
+
formatAIResponse,
|
|
124
|
+
showErrorBox,
|
|
125
|
+
showInfoBox
|
|
126
|
+
};
|