mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-11 07:18:03 -05:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
833b09081e | ||
|
|
201d1fb791 | ||
|
|
6ecbd044e6 | ||
|
|
fdadeae1e7 | ||
|
|
57c3e36574 | ||
|
|
1b98a8899f | ||
|
|
a4484d4e01 | ||
|
|
005d43674f | ||
|
|
3a69437790 | ||
|
|
b057f52ca6 | ||
|
|
dccdfbac8c | ||
|
|
98038707f1 | ||
|
|
03b22a70f0 | ||
|
|
66025d516c |
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -20,18 +20,22 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./go.mod
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Check for modernization opportunities
|
||||
run: |
|
||||
go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest ./...
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v21
|
||||
|
||||
- name: Check Formatting
|
||||
run: nix flake check
|
||||
|
||||
4
.github/workflows/patterns.yaml
vendored
4
.github/workflows/patterns.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
- name: Upload Patterns Artifact
|
||||
if: steps.check-changes.outputs.changes == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: patterns
|
||||
path: patterns.zip
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -15,12 +15,12 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./go.mod
|
||||
|
||||
@@ -37,11 +37,11 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./go.mod
|
||||
- name: Run GoReleaser
|
||||
|
||||
@@ -24,17 +24,17 @@ concurrency:
|
||||
jobs:
|
||||
update-version:
|
||||
if: >
|
||||
${{ github.repository_owner == 'danielmiessler' }} &&
|
||||
github.repository_owner == 'danielmiessler' &&
|
||||
github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
uses: DeterminateSystems/nix-installer-action@v21
|
||||
|
||||
- name: Set up Git
|
||||
run: |
|
||||
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,5 +1,44 @@
|
||||
# Changelog
|
||||
|
||||
## v1.4.349 (2025-12-16)
|
||||
|
||||
### PR [#1877](https://github.com/danielmiessler/Fabric/pull/1877) by [ksylvan](https://github.com/ksylvan): modernize: update GitHub Actions and modernize Go code
|
||||
|
||||
- Modernize GitHub Actions and Go code with latest stdlib features
|
||||
- Upgrade GitHub Actions to latest versions (v6, v21) and add modernization check step
|
||||
- Replace strings manipulation with `strings.CutPrefix` and `strings.CutSuffix`
|
||||
- Replace manual loops with `slices.Contains` for validation and use `strings.SplitSeq` for iterator-based splitting
|
||||
- Replace `fmt.Sprintf` with `fmt.Appendf` for efficiency and simplify padding calculation with `max` builtin
|
||||
|
||||
## v1.4.348 (2025-12-16)
|
||||
|
||||
### PR [#1876](https://github.com/danielmiessler/Fabric/pull/1876) by [ksylvan](https://github.com/ksylvan): modernize Go code with TypeFor and range loops
|
||||
|
||||
- Replace reflect.TypeOf with TypeFor generic syntax for improved type handling
|
||||
- Convert traditional for loops to range-based iterations for better code readability
|
||||
- Simplify reflection usage in CLI flag handling to reduce complexity
|
||||
- Update test loops to use range over integers for cleaner test code
|
||||
- Refactor string processing loops in template plugin to use modern Go patterns
|
||||
|
||||
## v1.4.347 (2025-12-16)
|
||||
|
||||
### PR [#1875](https://github.com/danielmiessler/Fabric/pull/1875) by [ksylvan](https://github.com/ksylvan): modernize: update benchmarks to use b.Loop and refactor map copying
|
||||
|
||||
- Updated benchmark loops to use cleaner `b.Loop()` syntax
|
||||
- Removed unnecessary `b.ResetTimer()` call in token benchmark
|
||||
- Used `maps.Copy` for merging variables in patterns handler
|
||||
|
||||
## v1.4.346 (2025-12-16)
|
||||
|
||||
### PR [#1874](https://github.com/danielmiessler/Fabric/pull/1874) by [ksylvan](https://github.com/ksylvan): refactor: replace interface{} with any across codebase
|
||||
|
||||
- Part 1 of dealing with #1873 as pointed out by @philoserf
|
||||
- Replace `interface{}` with `any` in slice type declarations throughout the codebase
|
||||
- Update map types from `map[string]interface{}` to `map[string]any` for modern Go standards
|
||||
- Change variadic function parameters to use `...any` instead of `...interface{}`
|
||||
- Modernize JSON unmarshaling variables to use `any` for consistency
|
||||
- Update struct fields and method signatures to prefer the `any` alias over legacy interface syntax
|
||||
|
||||
## v1.4.345 (2025-12-15)
|
||||
|
||||
### PR [#1870](https://github.com/danielmiessler/Fabric/pull/1870) by [ksylvan](https://github.com/ksylvan): Web UI: upgrade pdfjs and add SSR-safe dynamic PDF worker init
|
||||
|
||||
@@ -109,11 +109,11 @@ func ScanDirectory(rootDir string, maxDepth int, instructions string, ignoreList
|
||||
}
|
||||
|
||||
// Create final data structure
|
||||
var data []interface{}
|
||||
var data []any
|
||||
data = append(data, rootItem)
|
||||
|
||||
// Add report
|
||||
reportItem := map[string]interface{}{
|
||||
reportItem := map[string]any{
|
||||
"type": "report",
|
||||
"directories": dirCount,
|
||||
"files": fileCount,
|
||||
@@ -121,7 +121,7 @@ func ScanDirectory(rootDir string, maxDepth int, instructions string, ignoreList
|
||||
data = append(data, reportItem)
|
||||
|
||||
// Add instructions
|
||||
instructionsItem := map[string]interface{}{
|
||||
instructionsItem := map[string]any{
|
||||
"type": "instructions",
|
||||
"name": "code_change_instructions",
|
||||
"details": instructions,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package main
|
||||
|
||||
var version = "v1.4.345"
|
||||
var version = "v1.4.349"
|
||||
|
||||
Binary file not shown.
@@ -574,8 +574,8 @@ func (g *Generator) extractChanges(pr *github.PR) []string {
|
||||
}
|
||||
|
||||
if len(changes) == 0 && pr.Body != "" {
|
||||
lines := strings.Split(pr.Body, "\n")
|
||||
for _, line := range lines {
|
||||
lines := strings.SplitSeq(pr.Body, "\n")
|
||||
for line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "- ") || strings.HasPrefix(line, "* ") {
|
||||
change := strings.TrimPrefix(strings.TrimPrefix(line, "- "), "* ")
|
||||
|
||||
@@ -159,7 +159,7 @@ func (g *Generator) CreateNewChangelogEntry(version string) error {
|
||||
for _, file := range files {
|
||||
// Extract PR number from filename (e.g., "1640.txt" -> 1640)
|
||||
filename := filepath.Base(file)
|
||||
if prNumStr := strings.TrimSuffix(filename, ".txt"); prNumStr != filename {
|
||||
if prNumStr, ok := strings.CutSuffix(filename, ".txt"); ok {
|
||||
if prNum, err := strconv.Atoi(prNumStr); err == nil {
|
||||
processedPRs[prNum] = true
|
||||
prNumbers = append(prNumbers, prNum)
|
||||
|
||||
@@ -333,7 +333,7 @@ func (c *Client) FetchAllMergedPRsGraphQL(since time.Time) ([]*PR, error) {
|
||||
|
||||
for {
|
||||
// Prepare variables
|
||||
variables := map[string]interface{}{
|
||||
variables := map[string]any{
|
||||
"owner": graphql.String(c.owner),
|
||||
"repo": graphql.String(c.repo),
|
||||
"after": (*graphql.String)(after),
|
||||
|
||||
@@ -51,6 +51,29 @@ docs: update installation instructions
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
### Pull Request Guidelines
|
||||
|
||||
**Keep pull requests focused and minimal.**
|
||||
|
||||
PRs that touch a large number of files (50+) without clear functional justification will likely be rejected without detailed review.
|
||||
|
||||
#### Why we enforce this
|
||||
|
||||
- **Reviewability**: Large PRs are effectively un-reviewable. Studies show reviewer effectiveness drops significantly after ~200-400 lines of code. A 93-file "cleanup" PR cannot receive meaningful review.
|
||||
- **Git history**: Sweeping changes pollute `git blame`, making it harder to trace when and why functional changes were made.
|
||||
- **Merge conflicts**: Large PRs increase the likelihood of conflicts with other contributors' work.
|
||||
- **Risk**: More changed lines means more opportunities for subtle bugs, even in "safe" refactors.
|
||||
|
||||
#### What to do instead
|
||||
|
||||
If you have a large change in mind, break it into logical, independently-mergeable slices. For example:
|
||||
|
||||
- ✅ "Replace `interface{}` with `any` across codebase" (single mechanical change, easy to verify)
|
||||
- ✅ "Migrate to `strings.CutPrefix` in `internal/cli`" (scoped to one package)
|
||||
- ❌ "Modernize codebase with multiple idiom updates" (too broad, impossible to review)
|
||||
|
||||
For sweeping refactors or style changes, **open an issue first** to discuss the approach with maintainers before investing time in the work.
|
||||
|
||||
### Changelog Generation (REQUIRED)
|
||||
|
||||
After opening your PR, generate a changelog entry:
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -115,7 +116,7 @@ func Init() (ret *Flags, err error) {
|
||||
|
||||
// Create mapping from flag names (both short and long) to yaml tag names
|
||||
flagToYamlTag := make(map[string]string)
|
||||
t := reflect.TypeOf(Flags{})
|
||||
t := reflect.TypeFor[Flags]()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
yamlTag := field.Tag.Get("yaml")
|
||||
@@ -224,14 +225,14 @@ func Init() (ret *Flags, err error) {
|
||||
}
|
||||
|
||||
func parseDebugLevel(args []string) int {
|
||||
for i := 0; i < len(args); i++ {
|
||||
for i := range args {
|
||||
arg := args[i]
|
||||
if arg == "--debug" && i+1 < len(args) {
|
||||
if lvl, err := strconv.Atoi(args[i+1]); err == nil {
|
||||
return lvl
|
||||
}
|
||||
} else if strings.HasPrefix(arg, "--debug=") {
|
||||
if lvl, err := strconv.Atoi(strings.TrimPrefix(arg, "--debug=")); err == nil {
|
||||
} else if after, ok := strings.CutPrefix(arg, "--debug="); ok {
|
||||
if lvl, err := strconv.Atoi(after); err == nil {
|
||||
return lvl
|
||||
}
|
||||
}
|
||||
@@ -241,8 +242,8 @@ func parseDebugLevel(args []string) int {
|
||||
|
||||
func extractFlag(arg string) string {
|
||||
var flag string
|
||||
if strings.HasPrefix(arg, "--") {
|
||||
flag = strings.TrimPrefix(arg, "--")
|
||||
if after, ok := strings.CutPrefix(arg, "--"); ok {
|
||||
flag = after
|
||||
if i := strings.Index(flag, "="); i > 0 {
|
||||
flag = flag[:i]
|
||||
}
|
||||
@@ -348,10 +349,8 @@ func validateImageFile(imagePath string) error {
|
||||
ext := strings.ToLower(filepath.Ext(imagePath))
|
||||
validExtensions := []string{".png", ".jpeg", ".jpg", ".webp"}
|
||||
|
||||
for _, validExt := range validExtensions {
|
||||
if ext == validExt {
|
||||
return nil // Valid extension found
|
||||
}
|
||||
if slices.Contains(validExtensions, ext) {
|
||||
return nil // Valid extension found
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s", fmt.Sprintf(i18n.T("invalid_image_file_extension"), ext))
|
||||
@@ -370,13 +369,7 @@ func validateImageParameters(imagePath, size, quality, background string, compre
|
||||
// Validate size
|
||||
if size != "" {
|
||||
validSizes := []string{"1024x1024", "1536x1024", "1024x1536", "auto"}
|
||||
valid := false
|
||||
for _, validSize := range validSizes {
|
||||
if size == validSize {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
valid := slices.Contains(validSizes, size)
|
||||
if !valid {
|
||||
return fmt.Errorf("%s", fmt.Sprintf(i18n.T("invalid_image_size"), size))
|
||||
}
|
||||
@@ -385,13 +378,7 @@ func validateImageParameters(imagePath, size, quality, background string, compre
|
||||
// Validate quality
|
||||
if quality != "" {
|
||||
validQualities := []string{"low", "medium", "high", "auto"}
|
||||
valid := false
|
||||
for _, validQuality := range validQualities {
|
||||
if quality == validQuality {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
valid := slices.Contains(validQualities, quality)
|
||||
if !valid {
|
||||
return fmt.Errorf("%s", fmt.Sprintf(i18n.T("invalid_image_quality"), quality))
|
||||
}
|
||||
@@ -400,13 +387,7 @@ func validateImageParameters(imagePath, size, quality, background string, compre
|
||||
// Validate background
|
||||
if background != "" {
|
||||
validBackgrounds := []string{"opaque", "transparent"}
|
||||
valid := false
|
||||
for _, validBackground := range validBackgrounds {
|
||||
if background == validBackground {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
valid := slices.Contains(validBackgrounds, background)
|
||||
if !valid {
|
||||
return fmt.Errorf("%s", fmt.Sprintf(i18n.T("invalid_image_background"), background))
|
||||
}
|
||||
|
||||
@@ -137,8 +137,7 @@ func (h *TranslatedHelpWriter) getTranslatedDescription(flagName string) string
|
||||
|
||||
// getOriginalDescription retrieves the original description from struct tags
|
||||
func (h *TranslatedHelpWriter) getOriginalDescription(flagName string) string {
|
||||
flags := &Flags{}
|
||||
flagsType := reflect.TypeOf(flags).Elem()
|
||||
flagsType := reflect.TypeFor[Flags]()
|
||||
|
||||
for i := 0; i < flagsType.NumField(); i++ {
|
||||
field := flagsType.Field(i)
|
||||
@@ -184,10 +183,10 @@ func detectLanguageFromArgs() string {
|
||||
if i+1 < len(args) {
|
||||
return args[i+1]
|
||||
}
|
||||
} else if strings.HasPrefix(arg, "--language=") {
|
||||
return strings.TrimPrefix(arg, "--language=")
|
||||
} else if strings.HasPrefix(arg, "-g=") {
|
||||
return strings.TrimPrefix(arg, "-g=")
|
||||
} else if after, ok := strings.CutPrefix(arg, "--language="); ok {
|
||||
return after
|
||||
} else if after, ok := strings.CutPrefix(arg, "-g="); ok {
|
||||
return after
|
||||
} else if runtime.GOOS == "windows" && strings.HasPrefix(arg, "/g:") {
|
||||
return strings.TrimPrefix(arg, "/g:")
|
||||
} else if runtime.GOOS == "windows" && strings.HasPrefix(arg, "/g=") {
|
||||
@@ -218,8 +217,7 @@ func detectLanguageFromEnv() string {
|
||||
// writeAllFlags writes all flags with translated descriptions
|
||||
func (h *TranslatedHelpWriter) writeAllFlags() {
|
||||
// Use direct reflection on the Flags struct to get all flag definitions
|
||||
flags := &Flags{}
|
||||
flagsType := reflect.TypeOf(flags).Elem()
|
||||
flagsType := reflect.TypeFor[Flags]()
|
||||
|
||||
for i := 0; i < flagsType.NumField(); i++ {
|
||||
field := flagsType.Field(i)
|
||||
@@ -274,10 +272,7 @@ func (h *TranslatedHelpWriter) writeAllFlags() {
|
||||
|
||||
// Pad to align descriptions
|
||||
flagStr := flagLine.String()
|
||||
padding := 34 - len(flagStr)
|
||||
if padding < 2 {
|
||||
padding = 2
|
||||
}
|
||||
padding := max(34-len(flagStr), 2)
|
||||
|
||||
fmt.Fprintf(h.writer, "%s%s%s", flagStr, strings.Repeat(" ", padding), description)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
@@ -66,10 +67,5 @@ func CreateAudioOutputFile(audioData []byte, fileName string) (err error) {
|
||||
func IsAudioFormat(fileName string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(fileName))
|
||||
audioExts := []string{".wav", ".mp3", ".m4a", ".aac", ".ogg", ".flac"}
|
||||
for _, audioExt := range audioExts {
|
||||
if ext == audioExt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(audioExts, ext)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -146,14 +147,7 @@ func fixInvalidEscapes(jsonStr string) string {
|
||||
// Check for escape sequences only inside strings
|
||||
if inQuotes && ch == '\\' && i+1 < len(jsonStr) {
|
||||
nextChar := jsonStr[i+1]
|
||||
isValid := false
|
||||
|
||||
for _, validEscape := range validEscapes {
|
||||
if nextChar == validEscape {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
isValid := slices.Contains(validEscapes, nextChar)
|
||||
|
||||
if !isValid {
|
||||
// Invalid escape sequence - add an extra backslash
|
||||
|
||||
@@ -51,7 +51,7 @@ func LevelFromInt(i int) Level {
|
||||
}
|
||||
|
||||
// Debug writes a debug message if the global level permits.
|
||||
func Debug(l Level, format string, a ...interface{}) {
|
||||
func Debug(l Level, format string, a ...any) {
|
||||
mu.RLock()
|
||||
current := level
|
||||
w := output
|
||||
@@ -63,7 +63,7 @@ func Debug(l Level, format string, a ...interface{}) {
|
||||
|
||||
// Log writes a message unconditionally to stderr.
|
||||
// This is for important messages that should always be shown regardless of debug level.
|
||||
func Log(format string, a ...interface{}) {
|
||||
func Log(format string, a ...any) {
|
||||
mu.RLock()
|
||||
w := output
|
||||
mu.RUnlock()
|
||||
|
||||
@@ -52,7 +52,7 @@ func createExpiredToken(accessToken, refreshToken string) *util.OAuthToken {
|
||||
}
|
||||
|
||||
// mockTokenServer creates a mock OAuth token server for testing
|
||||
func mockTokenServer(_ *testing.T, responses map[string]interface{}) *httptest.Server {
|
||||
func mockTokenServer(_ *testing.T, responses map[string]any) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/oauth/token" {
|
||||
http.NotFound(w, r)
|
||||
@@ -80,7 +80,7 @@ func mockTokenServer(_ *testing.T, responses map[string]interface{}) *httptest.S
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if errorResp, ok := response.(map[string]interface{}); ok && errorResp["error"] != nil {
|
||||
if errorResp, ok := response.(map[string]any); ok && errorResp["error"] != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -114,8 +114,8 @@ func TestGeneratePKCE(t *testing.T) {
|
||||
|
||||
func TestExchangeToken_Success(t *testing.T) {
|
||||
// Create mock server
|
||||
server := mockTokenServer(t, map[string]interface{}{
|
||||
"authorization_code": map[string]interface{}{
|
||||
server := mockTokenServer(t, map[string]any{
|
||||
"authorization_code": map[string]any{
|
||||
"access_token": "test_access_token",
|
||||
"refresh_token": "test_refresh_token",
|
||||
"expires_in": 3600,
|
||||
@@ -161,8 +161,8 @@ func TestRefreshToken_Success(t *testing.T) {
|
||||
os.WriteFile(tokenPath, data, 0600)
|
||||
|
||||
// Create mock server for refresh
|
||||
server := mockTokenServer(t, map[string]interface{}{
|
||||
"refresh_token": map[string]interface{}{
|
||||
server := mockTokenServer(t, map[string]any{
|
||||
"refresh_token": map[string]any{
|
||||
"access_token": "new_access_token",
|
||||
"refresh_token": "new_refresh_token",
|
||||
"expires_in": 3600,
|
||||
@@ -416,7 +416,7 @@ func TestGetValidTokenWithValidToken(t *testing.T) {
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkGeneratePKCE(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
_, _, err := generatePKCE()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
@@ -427,8 +427,7 @@ func BenchmarkGeneratePKCE(b *testing.B) {
|
||||
func BenchmarkTokenIsExpired(b *testing.B) {
|
||||
token := createTestToken("access", "refresh", 3600)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
token.IsExpired(5)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package gemini
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GeminiVoice represents a Gemini TTS voice with its characteristics
|
||||
@@ -126,16 +127,17 @@ func ListGeminiVoices(shellCompleteMode bool) string {
|
||||
if shellCompleteMode {
|
||||
// For shell completion, just return voice names
|
||||
names := GetGeminiVoiceNames()
|
||||
result := ""
|
||||
var result strings.Builder
|
||||
for _, name := range names {
|
||||
result += name + "\n"
|
||||
result.WriteString(name + "\n")
|
||||
}
|
||||
return result
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// For human-readable output
|
||||
voices := GetGeminiVoices()
|
||||
result := "Available Gemini Text-to-Speech voices:\n\n"
|
||||
var result strings.Builder
|
||||
result.WriteString("Available Gemini Text-to-Speech voices:\n\n")
|
||||
|
||||
// Group by characteristics for better readability
|
||||
groups := map[string][]GeminiVoice{
|
||||
@@ -186,22 +188,22 @@ func ListGeminiVoices(shellCompleteMode bool) string {
|
||||
// Output grouped voices
|
||||
for groupName, groupVoices := range groups {
|
||||
if len(groupVoices) > 0 {
|
||||
result += fmt.Sprintf("%s:\n", groupName)
|
||||
result.WriteString(fmt.Sprintf("%s:\n", groupName))
|
||||
for _, voice := range groupVoices {
|
||||
defaultStr := ""
|
||||
if voice.Name == "Kore" {
|
||||
defaultStr = " (default)"
|
||||
}
|
||||
result += fmt.Sprintf(" %-15s - %s%s\n", voice.Name, voice.Description, defaultStr)
|
||||
result.WriteString(fmt.Sprintf(" %-15s - %s%s\n", voice.Name, voice.Description, defaultStr))
|
||||
}
|
||||
result += "\n"
|
||||
result.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
result += "Use --voice <voice_name> to select a specific voice.\n"
|
||||
result += "Example: fabric --voice Charon -m gemini-2.5-flash-preview-tts -o output.wav \"Hello world\"\n"
|
||||
result.WriteString("Use --voice <voice_name> to select a specific voice.\n")
|
||||
result.WriteString("Example: fabric --voice Charon -m gemini-2.5-flash-preview-tts -o output.wav \"Hello world\"\n")
|
||||
|
||||
return result
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// NOTE: This implementation maintains a curated list based on official Google documentation.
|
||||
|
||||
@@ -90,7 +90,7 @@ func (c *Client) ListModels() ([]string, error) {
|
||||
func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions, channel chan string) (err error) {
|
||||
url := fmt.Sprintf("%s/chat/completions", c.ApiUrl.Value)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
payload := map[string]any{
|
||||
"messages": msgs,
|
||||
"model": opts.Model,
|
||||
"stream": true, // Enable streaming
|
||||
@@ -140,27 +140,27 @@ func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.Cha
|
||||
continue
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(line, []byte("data: ")) {
|
||||
line = bytes.TrimPrefix(line, []byte("data: "))
|
||||
if after, ok := bytes.CutPrefix(line, []byte("data: ")); ok {
|
||||
line = after
|
||||
}
|
||||
|
||||
if string(line) == "[DONE]" {
|
||||
break
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
var result map[string]any
|
||||
if err = json.Unmarshal(line, &result); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var choices []interface{}
|
||||
var choices []any
|
||||
var ok bool
|
||||
if choices, ok = result["choices"].([]interface{}); !ok || len(choices) == 0 {
|
||||
if choices, ok = result["choices"].([]any); !ok || len(choices) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var delta map[string]interface{}
|
||||
if delta, ok = choices[0].(map[string]interface{})["delta"].(map[string]interface{}); !ok {
|
||||
var delta map[string]any
|
||||
if delta, ok = choices[0].(map[string]any)["delta"].(map[string]any); !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.Cha
|
||||
func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions) (content string, err error) {
|
||||
url := fmt.Sprintf("%s/chat/completions", c.ApiUrl.Value)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
payload := map[string]any{
|
||||
"messages": msgs,
|
||||
"model": opts.Model,
|
||||
// Add other options from opts if supported by LM Studio
|
||||
@@ -208,21 +208,21 @@ func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, o
|
||||
return
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
var result map[string]any
|
||||
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
err = fmt.Errorf("failed to decode response: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
var choices []interface{}
|
||||
var choices []any
|
||||
var ok bool
|
||||
if choices, ok = result["choices"].([]interface{}); !ok || len(choices) == 0 {
|
||||
if choices, ok = result["choices"].([]any); !ok || len(choices) == 0 {
|
||||
err = fmt.Errorf("invalid response format: missing or empty choices")
|
||||
return
|
||||
}
|
||||
|
||||
var message map[string]interface{}
|
||||
if message, ok = choices[0].(map[string]interface{})["message"].(map[string]interface{}); !ok {
|
||||
var message map[string]any
|
||||
if message, ok = choices[0].(map[string]any)["message"].(map[string]any); !ok {
|
||||
err = fmt.Errorf("invalid response format: missing message in first choice")
|
||||
return
|
||||
}
|
||||
@@ -238,7 +238,7 @@ func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, o
|
||||
func (c *Client) Complete(ctx context.Context, prompt string, opts *domain.ChatOptions) (text string, err error) {
|
||||
url := fmt.Sprintf("%s/completions", c.ApiUrl.Value)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
payload := map[string]any{
|
||||
"prompt": prompt,
|
||||
"model": opts.Model,
|
||||
// Add other options from opts if supported by LM Studio
|
||||
@@ -270,20 +270,20 @@ func (c *Client) Complete(ctx context.Context, prompt string, opts *domain.ChatO
|
||||
return
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
var result map[string]any
|
||||
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
err = fmt.Errorf("failed to decode response: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
var choices []interface{}
|
||||
var choices []any
|
||||
var ok bool
|
||||
if choices, ok = result["choices"].([]interface{}); !ok || len(choices) == 0 {
|
||||
if choices, ok = result["choices"].([]any); !ok || len(choices) == 0 {
|
||||
err = fmt.Errorf("invalid response format: missing or empty choices")
|
||||
return
|
||||
}
|
||||
|
||||
if text, ok = choices[0].(map[string]interface{})["text"].(string); !ok {
|
||||
if text, ok = choices[0].(map[string]any)["text"].(string); !ok {
|
||||
err = fmt.Errorf("invalid response format: missing or non-string text in first choice")
|
||||
return
|
||||
}
|
||||
@@ -294,7 +294,7 @@ func (c *Client) Complete(ctx context.Context, prompt string, opts *domain.ChatO
|
||||
func (c *Client) GetEmbeddings(ctx context.Context, input string, opts *domain.ChatOptions) (embeddings []float64, err error) {
|
||||
url := fmt.Sprintf("%s/embeddings", c.ApiUrl.Value)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
payload := map[string]any{
|
||||
"input": input,
|
||||
"model": opts.Model,
|
||||
// Add other options from opts if supported by LM Studio
|
||||
|
||||
@@ -155,7 +155,7 @@ func (o *Client) createChatRequest(ctx context.Context, msgs []*chat.ChatComplet
|
||||
}
|
||||
}
|
||||
|
||||
options := map[string]interface{}{
|
||||
options := map[string]any{
|
||||
"temperature": opts.Temperature,
|
||||
"presence_penalty": opts.PresencePenalty,
|
||||
"frequency_penalty": opts.FrequencyPenalty,
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/domain"
|
||||
@@ -31,12 +32,7 @@ var ImageGenerationSupportedModels = []string{
|
||||
|
||||
// supportsImageGeneration checks if the given model supports the image_generation tool
|
||||
func supportsImageGeneration(model string) bool {
|
||||
for _, supportedModel := range ImageGenerationSupportedModels {
|
||||
if model == supportedModel {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(ImageGenerationSupportedModels, model)
|
||||
}
|
||||
|
||||
// getOutputFormatFromExtension determines the API output format based on file extension
|
||||
|
||||
@@ -345,7 +345,7 @@ func TestAddImageGenerationToolWithUserParameters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *domain.ChatOptions
|
||||
expected map[string]interface{}
|
||||
expected map[string]any
|
||||
}{
|
||||
{
|
||||
name: "All parameters specified",
|
||||
@@ -356,7 +356,7 @@ func TestAddImageGenerationToolWithUserParameters(t *testing.T) {
|
||||
ImageBackground: "transparent",
|
||||
ImageCompression: 0, // Not applicable for PNG
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"size": "1536x1024",
|
||||
"quality": "high",
|
||||
"background": "transparent",
|
||||
@@ -372,7 +372,7 @@ func TestAddImageGenerationToolWithUserParameters(t *testing.T) {
|
||||
ImageBackground: "opaque",
|
||||
ImageCompression: 75,
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"size": "1024x1024",
|
||||
"quality": "medium",
|
||||
"background": "opaque",
|
||||
@@ -386,7 +386,7 @@ func TestAddImageGenerationToolWithUserParameters(t *testing.T) {
|
||||
ImageFile: "/tmp/test.webp",
|
||||
ImageQuality: "low",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"quality": "low",
|
||||
"output_format": "webp",
|
||||
},
|
||||
@@ -396,7 +396,7 @@ func TestAddImageGenerationToolWithUserParameters(t *testing.T) {
|
||||
opts: &domain.ChatOptions{
|
||||
ImageFile: "/tmp/test.png",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"output_format": "png",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestBuildResponseRequestWithMaxTokens(t *testing.T) {
|
||||
|
||||
var msgs []*chat.ChatCompletionMessage
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
for range 2 {
|
||||
msgs = append(msgs, &chat.ChatCompletionMessage{
|
||||
Role: "User",
|
||||
Content: "My msg",
|
||||
@@ -42,7 +42,7 @@ func TestBuildResponseRequestNoMaxTokens(t *testing.T) {
|
||||
|
||||
var msgs []*chat.ChatCompletionMessage
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
for range 2 {
|
||||
msgs = append(msgs, &chat.ChatCompletionMessage{
|
||||
Role: "User",
|
||||
Content: "My msg",
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/domain"
|
||||
@@ -107,18 +108,19 @@ func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, o
|
||||
return "", fmt.Errorf("perplexity API request failed: %w", err) // Corrected capitalization
|
||||
}
|
||||
|
||||
content := resp.GetLastContent()
|
||||
var content strings.Builder
|
||||
content.WriteString(resp.GetLastContent())
|
||||
|
||||
// Append citations if available
|
||||
citations := resp.GetCitations()
|
||||
if len(citations) > 0 {
|
||||
content += "\n\n# CITATIONS\n\n"
|
||||
content.WriteString("\n\n# CITATIONS\n\n")
|
||||
for i, citation := range citations {
|
||||
content += fmt.Sprintf("- [%d] %s\n", i+1, citation)
|
||||
content.WriteString(fmt.Sprintf("- [%d] %s\n", i+1, citation))
|
||||
}
|
||||
}
|
||||
|
||||
return content, nil
|
||||
return content.String(), nil
|
||||
}
|
||||
|
||||
func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions, channel chan string) error {
|
||||
|
||||
@@ -134,7 +134,7 @@ func (o *StorageEntity) buildFileName(name string) string {
|
||||
return fmt.Sprintf("%s%v", name, o.FileExtension)
|
||||
}
|
||||
|
||||
func (o *StorageEntity) SaveAsJson(name string, item interface{}) (err error) {
|
||||
func (o *StorageEntity) SaveAsJson(name string, item any) (err error) {
|
||||
var jsonString []byte
|
||||
if jsonString, err = json.Marshal(item); err == nil {
|
||||
err = o.Save(name, jsonString)
|
||||
@@ -145,7 +145,7 @@ func (o *StorageEntity) SaveAsJson(name string, item interface{}) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *StorageEntity) LoadAsJson(name string, item interface{}) (err error) {
|
||||
func (o *StorageEntity) LoadAsJson(name string, item any) (err error) {
|
||||
var content []byte
|
||||
if content, err = o.Load(name); err != nil {
|
||||
return
|
||||
|
||||
@@ -187,9 +187,10 @@ esac`
|
||||
executor := NewExtensionExecutor(registry)
|
||||
|
||||
// Helper function to create and register extension
|
||||
createExtension := func(name, opName, cmdTemplate string, config map[string]interface{}) error {
|
||||
createExtension := func(name, opName, cmdTemplate string, config map[string]any) error {
|
||||
configPath := filepath.Join(tmpDir, name+".yaml")
|
||||
configContent := `name: ` + name + `
|
||||
var configContent strings.Builder
|
||||
configContent.WriteString(`name: ` + name + `
|
||||
executable: ` + testScript + `
|
||||
type: executable
|
||||
timeout: 30s
|
||||
@@ -199,14 +200,14 @@ operations:
|
||||
config:
|
||||
output:
|
||||
method: file
|
||||
file_config:`
|
||||
file_config:`)
|
||||
|
||||
// Add config options
|
||||
for k, v := range config {
|
||||
configContent += "\n " + k + ": " + strings.TrimSpace(v.(string))
|
||||
configContent.WriteString("\n " + k + ": " + strings.TrimSpace(v.(string)))
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
if err := os.WriteFile(configPath, []byte(configContent.String()), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -216,7 +217,7 @@ config:
|
||||
// Test basic fixed file output
|
||||
t.Run("BasicFixedFile", func(t *testing.T) {
|
||||
outputFile := filepath.Join(tmpDir, "output.txt")
|
||||
config := map[string]interface{}{
|
||||
config := map[string]any{
|
||||
"output_file": `"output.txt"`,
|
||||
"work_dir": `"` + tmpDir + `"`,
|
||||
"cleanup": "true",
|
||||
@@ -241,7 +242,7 @@ config:
|
||||
|
||||
// Test no work_dir specified
|
||||
t.Run("NoWorkDir", func(t *testing.T) {
|
||||
config := map[string]interface{}{
|
||||
config := map[string]any{
|
||||
"output_file": `"direct-output.txt"`,
|
||||
"cleanup": "true",
|
||||
}
|
||||
@@ -263,7 +264,7 @@ config:
|
||||
outputFile := filepath.Join(tmpDir, "cleanup-test.txt")
|
||||
|
||||
// Test with cleanup enabled
|
||||
config := map[string]interface{}{
|
||||
config := map[string]any{
|
||||
"output_file": `"cleanup-test.txt"`,
|
||||
"work_dir": `"` + tmpDir + `"`,
|
||||
"cleanup": "true",
|
||||
@@ -307,7 +308,7 @@ config:
|
||||
// Test error cases
|
||||
t.Run("ErrorCases", func(t *testing.T) {
|
||||
outputFile := filepath.Join(tmpDir, "error-test.txt")
|
||||
config := map[string]interface{}{
|
||||
config := map[string]any{
|
||||
"output_file": `"error-test.txt"`,
|
||||
"work_dir": `"` + tmpDir + `"`,
|
||||
"cleanup": "true",
|
||||
@@ -341,7 +342,7 @@ config:
|
||||
|
||||
// Test with missing output_file
|
||||
t.Run("MissingOutputFile", func(t *testing.T) {
|
||||
config := map[string]interface{}{
|
||||
config := map[string]any{
|
||||
"work_dir": `"` + tmpDir + `"`,
|
||||
"cleanup": "true",
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ type ExtensionDefinition struct {
|
||||
Operations map[string]OperationConfig `yaml:"operations"`
|
||||
|
||||
// Additional config
|
||||
Config map[string]interface{} `yaml:"config"`
|
||||
Config map[string]any `yaml:"config"`
|
||||
}
|
||||
|
||||
type OperationConfig struct {
|
||||
@@ -53,7 +53,7 @@ type ExtensionRegistry struct {
|
||||
|
||||
// Helper methods for Config access
|
||||
func (e *ExtensionDefinition) GetOutputMethod() string {
|
||||
if output, ok := e.Config["output"].(map[string]interface{}); ok {
|
||||
if output, ok := e.Config["output"].(map[string]any); ok {
|
||||
if method, ok := output["method"].(string); ok {
|
||||
return method
|
||||
}
|
||||
@@ -61,9 +61,9 @@ func (e *ExtensionDefinition) GetOutputMethod() string {
|
||||
return "stdout" // default to stdout if not specified
|
||||
}
|
||||
|
||||
func (e *ExtensionDefinition) GetFileConfig() map[string]interface{} {
|
||||
if output, ok := e.Config["output"].(map[string]interface{}); ok {
|
||||
if fileConfig, ok := output["file_config"].(map[string]interface{}); ok {
|
||||
func (e *ExtensionDefinition) GetFileConfig() map[string]any {
|
||||
if output, ok := e.Config["output"].(map[string]any); ok {
|
||||
if fileConfig, ok := output["file_config"].(map[string]any); ok {
|
||||
return fileConfig
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func init() {
|
||||
var pluginPattern = regexp.MustCompile(`\{\{plugin:([^:]+):([^:]+)(?::([^}]+))?\}\}`)
|
||||
var extensionPattern = regexp.MustCompile(`\{\{ext:([^:]+):([^:]+)(?::([^}]+))?\}\}`)
|
||||
|
||||
func debugf(format string, a ...interface{}) {
|
||||
func debugf(format string, a ...any) {
|
||||
debuglog.Debug(debuglog.Trace, format, a...)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ func toTitle(s string) string {
|
||||
lower := strings.ToLower(s)
|
||||
runes := []rune(lower)
|
||||
|
||||
for i := 0; i < len(runes); i++ {
|
||||
for i := range runes {
|
||||
// Capitalize if previous char is non-letter AND
|
||||
// (we're at the end OR next char is not space)
|
||||
if i == 0 || !unicode.IsLetter(runes[i-1]) {
|
||||
|
||||
@@ -24,7 +24,7 @@ func (h *ModelsHandler) GetModelNames(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response := make(map[string]interface{})
|
||||
response := make(map[string]any)
|
||||
vendors := make(map[string][]string)
|
||||
|
||||
for _, groupItems := range vendorsModels.GroupsItems {
|
||||
|
||||
@@ -102,7 +102,7 @@ func ServeOllama(registry *core.PluginRegistry, address string, version string)
|
||||
// Ollama Endpoints
|
||||
r.GET("/api/tags", typeConversion.ollamaTags)
|
||||
r.GET("/api/version", func(c *gin.Context) {
|
||||
c.Data(200, "application/json", []byte(fmt.Sprintf("{\"%s\"}", version)))
|
||||
c.Data(200, "application/json", fmt.Appendf(nil, "{\"%s\"}", version))
|
||||
})
|
||||
r.POST("/api/chat", typeConversion.ollamaChat)
|
||||
|
||||
@@ -224,7 +224,7 @@ func (f APIConvert) ollamaChat(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "testing endpoint"})
|
||||
return
|
||||
}
|
||||
for _, word := range strings.Split(fabricResponse.Content, " ") {
|
||||
for word := range strings.SplitSeq(fabricResponse.Content, " ") {
|
||||
forwardedResponse = OllamaResponse{
|
||||
Model: "",
|
||||
CreatedAt: "",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package restapi
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"net/http"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
|
||||
@@ -74,9 +75,7 @@ func (h *PatternsHandler) ApplyPattern(c *gin.Context) {
|
||||
variables[key] = values[0]
|
||||
}
|
||||
}
|
||||
for key, value := range request.Variables {
|
||||
variables[key] = value
|
||||
}
|
||||
maps.Copy(variables, request.Variables)
|
||||
|
||||
pattern, err := h.patterns.GetApplyVariables(name, variables, request.Input)
|
||||
if err != nil {
|
||||
|
||||
@@ -1 +1 @@
|
||||
"1.4.345"
|
||||
"1.4.349"
|
||||
|
||||
Reference in New Issue
Block a user