berget 1.3.0 → 1.4.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/.github/workflows/publish.yml +56 -0
- package/.github/workflows/test.yml +38 -0
- package/README.md +177 -38
- package/dist/package.json +12 -2
- package/dist/src/commands/chat.js +183 -22
- package/dist/src/commands/models.js +2 -2
- package/dist/src/services/chat-service.js +10 -10
- package/dist/src/utils/markdown-renderer.js +73 -0
- package/dist/tests/commands/chat.test.js +107 -0
- package/dist/vitest.config.js +9 -0
- package/examples/README.md +95 -0
- package/examples/ai-review.sh +30 -0
- package/examples/install-global-security-hook.sh +170 -0
- package/examples/security-check.sh +102 -0
- package/examples/smart-commit.sh +26 -0
- package/package.json +12 -2
- package/src/commands/chat.ts +190 -23
- package/src/commands/models.ts +4 -4
- package/src/services/chat-service.ts +13 -23
- package/src/utils/markdown-renderer.ts +68 -0
- package/tests/commands/chat.test.ts +117 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Install global git security hook
|
|
3
|
+
# This script sets up a global pre-push hook that runs security checks on all repositories
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
echo "🔧 Installing global git security hook..."
|
|
8
|
+
|
|
9
|
+
# Create global git hooks directory
|
|
10
|
+
GLOBAL_HOOKS_DIR="$HOME/.git-hooks"
|
|
11
|
+
mkdir -p "$GLOBAL_HOOKS_DIR"
|
|
12
|
+
|
|
13
|
+
# Create the pre-push hook
|
|
14
|
+
cat > "$GLOBAL_HOOKS_DIR/pre-push" << 'EOF'
|
|
15
|
+
#!/bin/bash
|
|
16
|
+
# Global pre-push security hook using Berget AI
|
|
17
|
+
# This hook runs automatically before every git push
|
|
18
|
+
|
|
19
|
+
set -e
|
|
20
|
+
|
|
21
|
+
# Colors for output
|
|
22
|
+
RED='\033[0;31m'
|
|
23
|
+
GREEN='\033[0;32m'
|
|
24
|
+
YELLOW='\033[1;33m'
|
|
25
|
+
BLUE='\033[0;34m'
|
|
26
|
+
NC='\033[0m' # No Color
|
|
27
|
+
|
|
28
|
+
echo -e "${BLUE}🔒 Running security check before push...${NC}"
|
|
29
|
+
|
|
30
|
+
# Check if we're in a git repository
|
|
31
|
+
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
|
32
|
+
echo -e "${RED}Error: Not in a git repository${NC}"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Check if there are any commits to push
|
|
37
|
+
if [[ -z $(git log @{u}.. --oneline 2>/dev/null) ]]; then
|
|
38
|
+
echo -e "${GREEN}✅ No new commits to push${NC}"
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Get the diff of commits being pushed
|
|
43
|
+
DIFF=$(git diff @{u}.. 2>/dev/null || git diff HEAD~1)
|
|
44
|
+
|
|
45
|
+
if [[ -z "$DIFF" ]]; then
|
|
46
|
+
echo -e "${GREEN}✅ No changes to analyze${NC}"
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
echo -e "${BLUE}Analyzing security risks in commits being pushed...${NC}"
|
|
51
|
+
|
|
52
|
+
# Check if Berget CLI is available
|
|
53
|
+
if ! command -v npx > /dev/null 2>&1; then
|
|
54
|
+
echo -e "${YELLOW}⚠️ npx not found. Skipping security check.${NC}"
|
|
55
|
+
echo -e "${YELLOW}Install Node.js and npm to enable security checks.${NC}"
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Run security analysis
|
|
60
|
+
SECURITY_REPORT=$(echo "$DIFF" | npx berget chat run openai/gpt-oss "
|
|
61
|
+
Analyze this git diff for security vulnerabilities using OWASP Top 20 Code Review recommendations:
|
|
62
|
+
|
|
63
|
+
**OWASP Top 20 Security Categories to Check:**
|
|
64
|
+
|
|
65
|
+
1. **A01 - Broken Access Control**: Authorization bypasses, privilege escalation, insecure direct object references
|
|
66
|
+
2. **A02 - Cryptographic Failures**: Weak encryption, hardcoded keys, insecure random number generation, plain text storage
|
|
67
|
+
3. **A03 - Injection**: SQL injection, NoSQL injection, command injection, LDAP injection, XSS
|
|
68
|
+
4. **A04 - Insecure Design**: Missing security controls, threat modeling gaps, insecure architecture patterns
|
|
69
|
+
5. **A05 - Security Misconfiguration**: Default credentials, unnecessary features enabled, verbose error messages
|
|
70
|
+
6. **A06 - Vulnerable Components**: Outdated dependencies, known vulnerable libraries, unpatched components
|
|
71
|
+
7. **A07 - Authentication Failures**: Weak passwords, session management flaws, credential stuffing vulnerabilities
|
|
72
|
+
8. **A08 - Software Integrity Failures**: Unsigned code, insecure CI/CD pipelines, auto-update without verification
|
|
73
|
+
9. **A09 - Logging Failures**: Insufficient logging, sensitive data in logs, log injection
|
|
74
|
+
10. **A10 - Server-Side Request Forgery**: SSRF vulnerabilities, unvalidated URLs, internal service access
|
|
75
|
+
|
|
76
|
+
**Additional Critical Areas:**
|
|
77
|
+
11. **Input Validation**: Insufficient sanitization, buffer overflows, format string vulnerabilities
|
|
78
|
+
12. **Output Encoding**: XSS prevention, content type validation, encoding bypasses
|
|
79
|
+
13. **File Operations**: Path traversal, file upload vulnerabilities, insecure file permissions
|
|
80
|
+
14. **Network Security**: Insecure protocols, certificate validation, CSRF protection
|
|
81
|
+
15. **Session Management**: Session fixation, insecure cookies, session timeout issues
|
|
82
|
+
16. **Error Handling**: Information disclosure, stack traces in production, verbose error messages
|
|
83
|
+
17. **Business Logic**: Race conditions, workflow bypasses, price manipulation
|
|
84
|
+
18. **API Security**: Rate limiting, input validation, authentication on all endpoints
|
|
85
|
+
19. **Mobile Security**: Insecure data storage, weak encryption, certificate pinning
|
|
86
|
+
20. **Cloud Security**: Misconfigured permissions, exposed storage, insecure defaults
|
|
87
|
+
|
|
88
|
+
**Assessment Criteria:**
|
|
89
|
+
- 🟢 SAFE: No security risks identified according to OWASP guidelines
|
|
90
|
+
- 🟡 WARNING: Minor security risks that should be addressed (OWASP Medium risk)
|
|
91
|
+
- 🔴 CRITICAL: Serious security risks that MUST be addressed immediately (OWASP High/Critical risk)
|
|
92
|
+
|
|
93
|
+
**Required Response Format:**
|
|
94
|
+
**SECURITY ASSESSMENT: [🟢/🟡/🔴] [SAFE/WARNING/CRITICAL]**
|
|
95
|
+
|
|
96
|
+
**OWASP CATEGORIES AFFECTED:**
|
|
97
|
+
- [List specific OWASP categories if any vulnerabilities found]
|
|
98
|
+
|
|
99
|
+
**IDENTIFIED RISKS:**
|
|
100
|
+
- [List specific vulnerabilities with OWASP category references]
|
|
101
|
+
|
|
102
|
+
**RECOMMENDATIONS:**
|
|
103
|
+
- [Concrete remediation steps following OWASP secure coding practices]
|
|
104
|
+
|
|
105
|
+
**COMPLIANCE NOTES:**
|
|
106
|
+
- [Any additional security considerations or compliance requirements]
|
|
107
|
+
|
|
108
|
+
Diff to analyze:
|
|
109
|
+
\`\`\`diff
|
|
110
|
+
$DIFF
|
|
111
|
+
\`\`\`
|
|
112
|
+
" 2>/dev/null)
|
|
113
|
+
|
|
114
|
+
if [[ $? -ne 0 ]] || [[ -z "$SECURITY_REPORT" ]]; then
|
|
115
|
+
echo -e "${YELLOW}⚠️ Security analysis failed or unavailable. Proceeding with push.${NC}"
|
|
116
|
+
echo -e "${YELLOW}Make sure you have BERGET_API_KEY set or are logged in with 'npx berget auth login'${NC}"
|
|
117
|
+
exit 0
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
echo "$SECURITY_REPORT"
|
|
121
|
+
echo ""
|
|
122
|
+
|
|
123
|
+
# Extract security level from report
|
|
124
|
+
if echo "$SECURITY_REPORT" | grep -q "🔴.*CRITICAL"; then
|
|
125
|
+
echo -e "${RED}❌ CRITICAL security risks identified!${NC}"
|
|
126
|
+
echo -e "${RED}Push blocked. Address security issues before pushing.${NC}"
|
|
127
|
+
echo ""
|
|
128
|
+
echo -e "${YELLOW}To bypass this check (NOT RECOMMENDED):${NC}"
|
|
129
|
+
echo -e "${YELLOW}git push --no-verify${NC}"
|
|
130
|
+
exit 1
|
|
131
|
+
elif echo "$SECURITY_REPORT" | grep -q "🟡.*WARNING"; then
|
|
132
|
+
echo -e "${YELLOW}⚠️ Security warnings identified.${NC}"
|
|
133
|
+
read -p "Do you want to continue with push despite warnings? (y/N): " -n 1 -r
|
|
134
|
+
echo
|
|
135
|
+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
136
|
+
echo -e "${YELLOW}Push cancelled. Address security issues first.${NC}"
|
|
137
|
+
echo ""
|
|
138
|
+
echo -e "${YELLOW}To bypass this check (NOT RECOMMENDED):${NC}"
|
|
139
|
+
echo -e "${YELLOW}git push --no-verify${NC}"
|
|
140
|
+
exit 1
|
|
141
|
+
fi
|
|
142
|
+
elif echo "$SECURITY_REPORT" | grep -q "🟢.*SAFE"; then
|
|
143
|
+
echo -e "${GREEN}✅ No security risks identified. Safe to push!${NC}"
|
|
144
|
+
else
|
|
145
|
+
echo -e "${YELLOW}⚠️ Could not determine security status. Proceeding with caution.${NC}"
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
echo -e "${GREEN}Security check complete. Proceeding with push...${NC}"
|
|
149
|
+
EOF
|
|
150
|
+
|
|
151
|
+
# Make the hook executable
|
|
152
|
+
chmod +x "$GLOBAL_HOOKS_DIR/pre-push"
|
|
153
|
+
|
|
154
|
+
# Configure git to use the global hooks directory
|
|
155
|
+
git config --global core.hooksPath "$GLOBAL_HOOKS_DIR"
|
|
156
|
+
|
|
157
|
+
echo -e "${GREEN}✅ Global security hook installed successfully!${NC}"
|
|
158
|
+
echo ""
|
|
159
|
+
echo -e "${BLUE}The security hook will now run automatically before every 'git push' in all repositories.${NC}"
|
|
160
|
+
echo ""
|
|
161
|
+
echo -e "${YELLOW}Requirements:${NC}"
|
|
162
|
+
echo -e " • Node.js and npm installed"
|
|
163
|
+
echo -e " • Berget CLI configured (npx berget auth login or BERGET_API_KEY set)"
|
|
164
|
+
echo ""
|
|
165
|
+
echo -e "${YELLOW}To disable the hook temporarily:${NC}"
|
|
166
|
+
echo -e " git push --no-verify"
|
|
167
|
+
echo ""
|
|
168
|
+
echo -e "${YELLOW}To uninstall the global hook:${NC}"
|
|
169
|
+
echo -e " git config --global --unset core.hooksPath"
|
|
170
|
+
echo -e " rm -rf $GLOBAL_HOOKS_DIR"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Security check for git commits using Berget AI
|
|
3
|
+
# Usage: ./security-check.sh
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
echo "🔒 Security review of commits..."
|
|
7
|
+
echo "===================================="
|
|
8
|
+
|
|
9
|
+
# Check if there are staged changes
|
|
10
|
+
if [[ -z $(git diff --cached) ]]; then
|
|
11
|
+
echo "No staged changes found. Run 'git add' first."
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Get diff for security review
|
|
16
|
+
DIFF=$(git diff --cached)
|
|
17
|
+
|
|
18
|
+
echo "Analyzing security risks in staged changes..."
|
|
19
|
+
|
|
20
|
+
SECURITY_REPORT=$(echo "$DIFF" | npx berget chat run openai/gpt-oss "
|
|
21
|
+
Analyze this git diff for security vulnerabilities using OWASP Top 20 Code Review recommendations:
|
|
22
|
+
|
|
23
|
+
**OWASP Top 20 Security Categories to Check:**
|
|
24
|
+
|
|
25
|
+
1. **A01 - Broken Access Control**: Authorization bypasses, privilege escalation, insecure direct object references
|
|
26
|
+
2. **A02 - Cryptographic Failures**: Weak encryption, hardcoded keys, insecure random number generation, plain text storage
|
|
27
|
+
3. **A03 - Injection**: SQL injection, NoSQL injection, command injection, LDAP injection, XSS
|
|
28
|
+
4. **A04 - Insecure Design**: Missing security controls, threat modeling gaps, insecure architecture patterns
|
|
29
|
+
5. **A05 - Security Misconfiguration**: Default credentials, unnecessary features enabled, verbose error messages
|
|
30
|
+
6. **A06 - Vulnerable Components**: Outdated dependencies, known vulnerable libraries, unpatched components
|
|
31
|
+
7. **A07 - Authentication Failures**: Weak passwords, session management flaws, credential stuffing vulnerabilities
|
|
32
|
+
8. **A08 - Software Integrity Failures**: Unsigned code, insecure CI/CD pipelines, auto-update without verification
|
|
33
|
+
9. **A09 - Logging Failures**: Insufficient logging, sensitive data in logs, log injection
|
|
34
|
+
10. **A10 - Server-Side Request Forgery**: SSRF vulnerabilities, unvalidated URLs, internal service access
|
|
35
|
+
|
|
36
|
+
**Additional Critical Areas:**
|
|
37
|
+
11. **Input Validation**: Insufficient sanitization, buffer overflows, format string vulnerabilities
|
|
38
|
+
12. **Output Encoding**: XSS prevention, content type validation, encoding bypasses
|
|
39
|
+
13. **File Operations**: Path traversal, file upload vulnerabilities, insecure file permissions
|
|
40
|
+
14. **Network Security**: Insecure protocols, certificate validation, CSRF protection
|
|
41
|
+
15. **Session Management**: Session fixation, insecure cookies, session timeout issues
|
|
42
|
+
16. **Error Handling**: Information disclosure, stack traces in production, verbose error messages
|
|
43
|
+
17. **Business Logic**: Race conditions, workflow bypasses, price manipulation
|
|
44
|
+
18. **API Security**: Rate limiting, input validation, authentication on all endpoints
|
|
45
|
+
19. **Mobile Security**: Insecure data storage, weak encryption, certificate pinning
|
|
46
|
+
20. **Cloud Security**: Misconfigured permissions, exposed storage, insecure defaults
|
|
47
|
+
|
|
48
|
+
**Assessment Criteria:**
|
|
49
|
+
- 🟢 SAFE: No security risks identified according to OWASP guidelines
|
|
50
|
+
- 🟡 WARNING: Minor security risks that should be addressed (OWASP Medium risk)
|
|
51
|
+
- 🔴 CRITICAL: Serious security risks that MUST be addressed immediately (OWASP High/Critical risk)
|
|
52
|
+
|
|
53
|
+
**Required Response Format:**
|
|
54
|
+
**SECURITY ASSESSMENT: [🟢/🟡/🔴] [SAFE/WARNING/CRITICAL]**
|
|
55
|
+
|
|
56
|
+
**OWASP CATEGORIES AFFECTED:**
|
|
57
|
+
- [List specific OWASP categories if any vulnerabilities found]
|
|
58
|
+
|
|
59
|
+
**IDENTIFIED RISKS:**
|
|
60
|
+
- [List specific vulnerabilities with OWASP category references]
|
|
61
|
+
|
|
62
|
+
**RECOMMENDATIONS:**
|
|
63
|
+
- [Concrete remediation steps following OWASP secure coding practices]
|
|
64
|
+
|
|
65
|
+
**COMPLIANCE NOTES:**
|
|
66
|
+
- [Any additional security considerations or compliance requirements]
|
|
67
|
+
|
|
68
|
+
Diff to analyze:
|
|
69
|
+
\`\`\`diff
|
|
70
|
+
$DIFF
|
|
71
|
+
\`\`\`
|
|
72
|
+
")
|
|
73
|
+
|
|
74
|
+
echo "$SECURITY_REPORT"
|
|
75
|
+
echo ""
|
|
76
|
+
|
|
77
|
+
# Extract security level from report
|
|
78
|
+
if echo "$SECURITY_REPORT" | grep -q "🔴.*CRITICAL"; then
|
|
79
|
+
echo "❌ CRITICAL security risks identified!"
|
|
80
|
+
echo "Commit blocked. Address security issues before continuing."
|
|
81
|
+
exit 1
|
|
82
|
+
elif echo "$SECURITY_REPORT" | grep -q "🟡.*WARNING"; then
|
|
83
|
+
echo "⚠️ Security warnings identified."
|
|
84
|
+
read -p "Do you want to continue with commit despite warnings? (y/N): " -n 1 -r
|
|
85
|
+
echo
|
|
86
|
+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
87
|
+
echo "Commit cancelled. Address security issues first."
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
90
|
+
elif echo "$SECURITY_REPORT" | grep -q "🟢.*SAFE"; then
|
|
91
|
+
echo "✅ No security risks identified. Safe to continue!"
|
|
92
|
+
else
|
|
93
|
+
echo "⚠️ Could not determine security status. Review manually."
|
|
94
|
+
read -p "Do you want to continue with commit? (y/N): " -n 1 -r
|
|
95
|
+
echo
|
|
96
|
+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
97
|
+
echo "Commit cancelled."
|
|
98
|
+
exit 1
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
echo "Security review complete. You can now run 'git commit'."
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Smart commit generator using Berget AI
|
|
3
|
+
# Usage: ./smart-commit.sh
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
# Check if there are staged changes
|
|
7
|
+
if [[ -z $(git diff --cached) ]]; then
|
|
8
|
+
echo "No staged changes found. Run 'git add' first."
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# Generate commit message
|
|
13
|
+
COMMIT_MSG=$(git diff --cached | npx berget chat run openai/gpt-oss "Generate a conventional commit message for this staged diff. Reply with only the commit message, nothing else:")
|
|
14
|
+
|
|
15
|
+
echo "Suggested commit message:"
|
|
16
|
+
echo " $COMMIT_MSG"
|
|
17
|
+
echo
|
|
18
|
+
|
|
19
|
+
read -p "Do you want to use this message? (y/N): " -n 1 -r
|
|
20
|
+
echo
|
|
21
|
+
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
22
|
+
git commit -m "$COMMIT_MSG"
|
|
23
|
+
echo "✅ Commit created!"
|
|
24
|
+
else
|
|
25
|
+
echo "❌ Commit cancelled"
|
|
26
|
+
fi
|
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "berget",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"berget": "dist/index.js"
|
|
7
7
|
},
|
|
8
8
|
"private": false,
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
9
12
|
"scripts": {
|
|
10
13
|
"start": "node --import tsx ./index.ts --local",
|
|
11
14
|
"login": "node --import tsx ./index.ts --local auth login",
|
|
12
15
|
"logout": "node --import tsx ./index.ts --local auth logout",
|
|
13
16
|
"whoami": "node --import tsx ./index.ts --local auth whoami",
|
|
14
17
|
"build": "tsc",
|
|
18
|
+
"test": "vitest",
|
|
19
|
+
"test:run": "vitest run",
|
|
15
20
|
"prepublishOnly": "npm run build",
|
|
16
21
|
"generate-types": "openapi-typescript https://api.berget.ai/openapi.json -o src/types/api.d.ts"
|
|
17
22
|
},
|
|
@@ -19,14 +24,19 @@
|
|
|
19
24
|
"license": "MIT",
|
|
20
25
|
"description": "This is a cli command for interacting with the AI infrastructure provider Berget",
|
|
21
26
|
"devDependencies": {
|
|
27
|
+
"@types/marked": "^5.0.2",
|
|
28
|
+
"@types/marked-terminal": "^6.1.1",
|
|
22
29
|
"@types/node": "^20.11.20",
|
|
23
30
|
"tsx": "^4.19.3",
|
|
24
|
-
"typescript": "^5.3.3"
|
|
31
|
+
"typescript": "^5.3.3",
|
|
32
|
+
"vitest": "^1.0.0"
|
|
25
33
|
},
|
|
26
34
|
"dependencies": {
|
|
27
35
|
"chalk": "^4.1.2",
|
|
28
36
|
"commander": "^12.0.0",
|
|
29
37
|
"fs-extra": "^11.3.0",
|
|
38
|
+
"marked": "^9.1.6",
|
|
39
|
+
"marked-terminal": "^6.2.0",
|
|
30
40
|
"open": "^9.1.0",
|
|
31
41
|
"openapi-fetch": "^0.9.1",
|
|
32
42
|
"openapi-typescript": "^6.7.4",
|
package/src/commands/chat.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { ApiKeyService } from '../services/api-key-service'
|
|
|
7
7
|
import { AuthService } from '../services/auth-service'
|
|
8
8
|
import { handleError } from '../utils/error-handler'
|
|
9
9
|
import { DefaultApiKeyManager } from '../utils/default-api-key'
|
|
10
|
+
import { renderMarkdown, containsMarkdown } from '../utils/markdown-renderer'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Helper function to get user confirmation
|
|
@@ -36,7 +37,8 @@ export function registerChatCommands(program: Command): void {
|
|
|
36
37
|
chat
|
|
37
38
|
.command(SUBCOMMANDS.CHAT.RUN)
|
|
38
39
|
.description('Run a chat session with a specified model')
|
|
39
|
-
.argument('[model]', 'Model to use (default:
|
|
40
|
+
.argument('[model]', 'Model to use (default: openai/gpt-oss)')
|
|
41
|
+
.argument('[message]', 'Message to send directly (skips interactive mode)')
|
|
40
42
|
.option('-s, --system <message>', 'System message')
|
|
41
43
|
.option('-t, --temperature <temp>', 'Temperature (0-1)', parseFloat)
|
|
42
44
|
.option('-m, --max-tokens <tokens>', 'Maximum tokens to generate', parseInt)
|
|
@@ -45,8 +47,8 @@ export function registerChatCommands(program: Command): void {
|
|
|
45
47
|
'--api-key-id <id>',
|
|
46
48
|
'ID of the API key to use from your saved keys'
|
|
47
49
|
)
|
|
48
|
-
.option('--stream', '
|
|
49
|
-
.action(async (options) => {
|
|
50
|
+
.option('--no-stream', 'Disable streaming (streaming is enabled by default)')
|
|
51
|
+
.action(async (model, message, options) => {
|
|
50
52
|
try {
|
|
51
53
|
const chatService = ChatService.getInstance()
|
|
52
54
|
|
|
@@ -217,12 +219,6 @@ export function registerChatCommands(program: Command): void {
|
|
|
217
219
|
}
|
|
218
220
|
}
|
|
219
221
|
|
|
220
|
-
// Set up readline interface for user input
|
|
221
|
-
const rl = readline.createInterface({
|
|
222
|
-
input: process.stdin,
|
|
223
|
-
output: process.stdout,
|
|
224
|
-
})
|
|
225
|
-
|
|
226
222
|
// Prepare messages array
|
|
227
223
|
const messages: ChatMessage[] = []
|
|
228
224
|
|
|
@@ -234,6 +230,139 @@ export function registerChatCommands(program: Command): void {
|
|
|
234
230
|
})
|
|
235
231
|
}
|
|
236
232
|
|
|
233
|
+
// Check if input is being piped in
|
|
234
|
+
let inputMessage = message
|
|
235
|
+
let stdinContent = ''
|
|
236
|
+
|
|
237
|
+
if (!process.stdin.isTTY) {
|
|
238
|
+
// Read from stdin (piped input)
|
|
239
|
+
const chunks = []
|
|
240
|
+
for await (const chunk of process.stdin) {
|
|
241
|
+
chunks.push(chunk)
|
|
242
|
+
}
|
|
243
|
+
stdinContent = Buffer.concat(chunks).toString('utf8').trim()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Combine stdin content with message if both exist
|
|
247
|
+
if (stdinContent && message) {
|
|
248
|
+
inputMessage = `${stdinContent}\n\n${message}`
|
|
249
|
+
} else if (stdinContent && !message) {
|
|
250
|
+
inputMessage = stdinContent
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// If a message is provided (either as argument, from stdin, or both), send it directly and exit
|
|
254
|
+
if (inputMessage) {
|
|
255
|
+
// Add user message
|
|
256
|
+
messages.push({
|
|
257
|
+
role: 'user',
|
|
258
|
+
content: inputMessage,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
// Call the API
|
|
263
|
+
const completionOptions: ChatCompletionOptions = {
|
|
264
|
+
model: model || 'openai/gpt-oss',
|
|
265
|
+
messages: messages,
|
|
266
|
+
temperature:
|
|
267
|
+
options.temperature !== undefined ? options.temperature : 0.7,
|
|
268
|
+
max_tokens: options.maxTokens || 4096,
|
|
269
|
+
stream: options.stream !== false
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Only add apiKey if it actually exists
|
|
273
|
+
if (apiKey) {
|
|
274
|
+
completionOptions.apiKey = apiKey
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Add streaming support (now default)
|
|
278
|
+
if (completionOptions.stream) {
|
|
279
|
+
let assistantResponse = ''
|
|
280
|
+
|
|
281
|
+
// Stream the response in real-time
|
|
282
|
+
completionOptions.onChunk = (chunk: any) => {
|
|
283
|
+
if (chunk.choices && chunk.choices[0] && chunk.choices[0].delta && chunk.choices[0].delta.content) {
|
|
284
|
+
const content = chunk.choices[0].delta.content
|
|
285
|
+
try {
|
|
286
|
+
process.stdout.write(content)
|
|
287
|
+
} catch (error: any) {
|
|
288
|
+
// Handle EPIPE errors gracefully (when pipe is closed)
|
|
289
|
+
if (error.code === 'EPIPE') {
|
|
290
|
+
// Stop streaming if the pipe is closed
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
throw error
|
|
294
|
+
}
|
|
295
|
+
assistantResponse += content
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
await chatService.createCompletion(completionOptions)
|
|
301
|
+
} catch (streamError) {
|
|
302
|
+
console.error(chalk.red('\nStreaming error:'), streamError)
|
|
303
|
+
|
|
304
|
+
// Fallback to non-streaming if streaming fails
|
|
305
|
+
console.log(chalk.yellow('Falling back to non-streaming mode...'))
|
|
306
|
+
completionOptions.stream = false
|
|
307
|
+
delete completionOptions.onChunk
|
|
308
|
+
|
|
309
|
+
const response = await chatService.createCompletion(completionOptions)
|
|
310
|
+
|
|
311
|
+
if (response && response.choices && response.choices[0] && response.choices[0].message) {
|
|
312
|
+
assistantResponse = response.choices[0].message.content
|
|
313
|
+
console.log(assistantResponse)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
console.log() // Add newline at the end
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const response = await chatService.createCompletion(
|
|
321
|
+
completionOptions
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
// Check if response has the expected structure
|
|
325
|
+
if (
|
|
326
|
+
!response ||
|
|
327
|
+
!response.choices ||
|
|
328
|
+
!response.choices[0] ||
|
|
329
|
+
!response.choices[0].message
|
|
330
|
+
) {
|
|
331
|
+
console.error(
|
|
332
|
+
chalk.red('Error: Unexpected response format from API')
|
|
333
|
+
)
|
|
334
|
+
console.error(
|
|
335
|
+
chalk.red('Response:', JSON.stringify(response, null, 2))
|
|
336
|
+
)
|
|
337
|
+
throw new Error('Unexpected response format from API')
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Get assistant's response
|
|
341
|
+
const assistantMessage = response.choices[0].message.content
|
|
342
|
+
|
|
343
|
+
// Display the response
|
|
344
|
+
if (containsMarkdown(assistantMessage)) {
|
|
345
|
+
console.log(renderMarkdown(assistantMessage))
|
|
346
|
+
} else {
|
|
347
|
+
console.log(assistantMessage)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error(chalk.red('Error: Failed to get response'))
|
|
353
|
+
if (error instanceof Error) {
|
|
354
|
+
console.error(chalk.red(error.message))
|
|
355
|
+
}
|
|
356
|
+
process.exit(1)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Set up readline interface for user input (only for interactive mode)
|
|
361
|
+
const rl = readline.createInterface({
|
|
362
|
+
input: process.stdin,
|
|
363
|
+
output: process.stdout,
|
|
364
|
+
})
|
|
365
|
+
|
|
237
366
|
console.log(chalk.cyan('Chat with Berget AI (type "exit" to quit)'))
|
|
238
367
|
console.log(chalk.cyan('----------------------------------------'))
|
|
239
368
|
|
|
@@ -256,12 +385,12 @@ export function registerChatCommands(program: Command): void {
|
|
|
256
385
|
try {
|
|
257
386
|
// Call the API
|
|
258
387
|
const completionOptions: ChatCompletionOptions = {
|
|
259
|
-
model:
|
|
388
|
+
model: model || 'openai/gpt-oss',
|
|
260
389
|
messages: messages,
|
|
261
390
|
temperature:
|
|
262
391
|
options.temperature !== undefined ? options.temperature : 0.7,
|
|
263
392
|
max_tokens: options.maxTokens || 4096,
|
|
264
|
-
stream: options.stream
|
|
393
|
+
stream: options.stream !== false
|
|
265
394
|
}
|
|
266
395
|
|
|
267
396
|
// Only add apiKey if it actually exists
|
|
@@ -269,20 +398,46 @@ export function registerChatCommands(program: Command): void {
|
|
|
269
398
|
completionOptions.apiKey = apiKey
|
|
270
399
|
}
|
|
271
400
|
|
|
272
|
-
// Add streaming support
|
|
273
|
-
if (
|
|
401
|
+
// Add streaming support (now default)
|
|
402
|
+
if (completionOptions.stream) {
|
|
274
403
|
let assistantResponse = ''
|
|
275
|
-
|
|
404
|
+
console.log(chalk.blue('Assistant: '))
|
|
276
405
|
|
|
406
|
+
// Stream the response in real-time
|
|
277
407
|
completionOptions.onChunk = (chunk: any) => {
|
|
278
408
|
if (chunk.choices && chunk.choices[0] && chunk.choices[0].delta && chunk.choices[0].delta.content) {
|
|
279
409
|
const content = chunk.choices[0].delta.content
|
|
280
|
-
|
|
410
|
+
try {
|
|
411
|
+
process.stdout.write(content)
|
|
412
|
+
} catch (error: any) {
|
|
413
|
+
// Handle EPIPE errors gracefully (when pipe is closed)
|
|
414
|
+
if (error.code === 'EPIPE') {
|
|
415
|
+
// Stop streaming if the pipe is closed
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
throw error
|
|
419
|
+
}
|
|
281
420
|
assistantResponse += content
|
|
282
421
|
}
|
|
283
422
|
}
|
|
284
423
|
|
|
285
|
-
|
|
424
|
+
try {
|
|
425
|
+
await chatService.createCompletion(completionOptions)
|
|
426
|
+
} catch (streamError) {
|
|
427
|
+
console.error(chalk.red('\nStreaming error:'), streamError)
|
|
428
|
+
|
|
429
|
+
// Fallback to non-streaming if streaming fails
|
|
430
|
+
console.log(chalk.yellow('Falling back to non-streaming mode...'))
|
|
431
|
+
completionOptions.stream = false
|
|
432
|
+
delete completionOptions.onChunk
|
|
433
|
+
|
|
434
|
+
const response = await chatService.createCompletion(completionOptions)
|
|
435
|
+
|
|
436
|
+
if (response && response.choices && response.choices[0] && response.choices[0].message) {
|
|
437
|
+
assistantResponse = response.choices[0].message.content
|
|
438
|
+
console.log(assistantResponse)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
286
441
|
console.log('\n')
|
|
287
442
|
|
|
288
443
|
// Add assistant response to messages
|
|
@@ -332,7 +487,15 @@ export function registerChatCommands(program: Command): void {
|
|
|
332
487
|
})
|
|
333
488
|
|
|
334
489
|
// Display the response
|
|
335
|
-
console.log(chalk.blue('Assistant: ')
|
|
490
|
+
console.log(chalk.blue('Assistant: '))
|
|
491
|
+
|
|
492
|
+
// Check if the response contains markdown and render it if it does
|
|
493
|
+
if (containsMarkdown(assistantMessage)) {
|
|
494
|
+
console.log(renderMarkdown(assistantMessage))
|
|
495
|
+
} else {
|
|
496
|
+
console.log(assistantMessage)
|
|
497
|
+
}
|
|
498
|
+
|
|
336
499
|
console.log() // Empty line for better readability
|
|
337
500
|
|
|
338
501
|
// Continue the conversation
|
|
@@ -439,23 +602,27 @@ export function registerChatCommands(program: Command): void {
|
|
|
439
602
|
console.log(chalk.bold('Available Chat Models:'))
|
|
440
603
|
console.log(chalk.dim('─'.repeat(70)))
|
|
441
604
|
console.log(
|
|
442
|
-
chalk.dim('MODEL ID'.padEnd(
|
|
443
|
-
chalk.dim('OWNER'.padEnd(25)) +
|
|
605
|
+
chalk.dim('MODEL ID'.padEnd(40)) +
|
|
444
606
|
chalk.dim('CAPABILITIES')
|
|
445
607
|
)
|
|
446
608
|
console.log(chalk.dim('─'.repeat(70)))
|
|
447
609
|
|
|
448
|
-
|
|
610
|
+
// Filter to only show active models
|
|
611
|
+
const activeModels = models.data.filter((model: any) => model.active === true);
|
|
612
|
+
|
|
613
|
+
activeModels.forEach((model: any) => {
|
|
449
614
|
const capabilities = []
|
|
450
615
|
if (model.capabilities.vision) capabilities.push('vision')
|
|
451
616
|
if (model.capabilities.function_calling)
|
|
452
617
|
capabilities.push('function_calling')
|
|
453
618
|
if (model.capabilities.json_mode) capabilities.push('json_mode')
|
|
454
619
|
|
|
620
|
+
// Format model ID in Huggingface compatible format (owner/model)
|
|
621
|
+
const modelId = `${model.owned_by.toLowerCase()}/${model.id}`.padEnd(40)
|
|
622
|
+
|
|
455
623
|
console.log(
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
capabilities.join(', ')
|
|
624
|
+
modelId +
|
|
625
|
+
capabilities.join(', ')
|
|
459
626
|
)
|
|
460
627
|
})
|
|
461
628
|
} catch (error) {
|
package/src/commands/models.ts
CHANGED
|
@@ -36,10 +36,10 @@ export function registerModelCommands(program: Command): void {
|
|
|
36
36
|
|
|
37
37
|
console.log('Available Models:')
|
|
38
38
|
console.log(
|
|
39
|
-
'ID
|
|
39
|
+
'ID OWNED BY CAPABILITIES'
|
|
40
40
|
)
|
|
41
41
|
// Ensure response has the expected structure
|
|
42
|
-
const modelData = response as { data?: any[] }
|
|
42
|
+
const modelData = response as { data?: any[] }
|
|
43
43
|
if (modelData.data) {
|
|
44
44
|
modelData.data.forEach((model: any) => {
|
|
45
45
|
const capabilities = []
|
|
@@ -49,8 +49,8 @@ export function registerModelCommands(program: Command): void {
|
|
|
49
49
|
if (model.capabilities.json_mode) capabilities.push('json_mode')
|
|
50
50
|
|
|
51
51
|
console.log(
|
|
52
|
-
`${model.
|
|
53
|
-
|
|
52
|
+
`${model.root.padEnd(50)} ${model.owned_by.padEnd(
|
|
53
|
+
24
|
|
54
54
|
)} ${capabilities.join(', ')}`
|
|
55
55
|
)
|
|
56
56
|
})
|