Compare commits

...

14 Commits

Author SHA1 Message Date
github-actions[bot]
833b09081e chore(release): Update version to v1.4.349 2025-12-16 08:12:11 +00:00
Kayvan Sylvan
201d1fb791 Merge pull request #1877 from ksylvan/kayvan/modernize-part4-string-and-slice-syntax
modernize: update GitHub Actions and modernize Go code
2025-12-16 00:09:43 -08:00
Changelog Bot
6ecbd044e6 chore: incoming 1877 changelog entry 2025-12-16 00:06:39 -08:00
Kayvan Sylvan
fdadeae1e7 modernize: update GitHub Actions and modernize Go code with latest stdlib features
## CHANGES

- Upgrade GitHub Actions to latest versions (v6, v21)
- Add modernization check step in CI workflow
- Replace strings manipulation with `strings.CutPrefix` and `strings.CutSuffix`
- Replace manual loops with `slices.Contains` for validation
- Use `strings.SplitSeq` for iterator-based string splitting
- Replace `bytes.TrimPrefix` with `bytes.CutPrefix` for clarity
- Use `strings.Builder` instead of string concatenation
- Replace `fmt.Sprintf` with `fmt.Appendf` for efficiency
- Simplify padding calculation with `max` builtin
2025-12-15 23:55:37 -08:00
github-actions[bot]
57c3e36574 chore(release): Update version to v1.4.348 2025-12-16 07:34:45 +00:00
Kayvan Sylvan
1b98a8899f Merge pull request #1876 from ksylvan/kayvan/modernize-part3-typefor-and-range-loops
modernize Go code with TypeFor and range loops
2025-12-15 23:31:44 -08:00
Kayvan Sylvan
a4484d4e01 refactor: modernize Go code with TypeFor and range loops
- Replace reflect.TypeOf with TypeFor generic syntax
- Convert traditional for loops to range-based iterations
- Simplify reflection usage in CLI flag handling
- Update test loops to use range over integers
- Refactor string processing loops in template plugin
2025-12-15 23:29:41 -08:00
github-actions[bot]
005d43674f chore(release): Update version to v1.4.347 2025-12-16 06:51:40 +00:00
Kayvan Sylvan
3a69437790 Merge pull request #1875 from ksylvan/kayvan/modernize-part2-loops
modernize: update benchmarks to use b.Loop and refactor map copying
2025-12-15 22:48:59 -08:00
Changelog Bot
b057f52ca6 chore: incoming 1875 changelog entry 2025-12-15 22:46:45 -08:00
Kayvan Sylvan
dccdfbac8c test: update benchmarks to use b.Loop and refactor map copying
# CHANGES

- update benchmark loops to use cleaner `b.Loop()` syntax
- remove unnecessary `b.ResetTimer()` call in token benchmark
- use `maps.Copy` for merging variables in patterns handler
2025-12-15 22:40:55 -08:00
github-actions[bot]
98038707f1 chore(release): Update version to v1.4.346 2025-12-16 06:30:55 +00:00
Kayvan Sylvan
03b22a70f0 Merge pull request #1874 from ksylvan/kayvan/modernize-part1
refactor: replace interface{} with any across codebase
2025-12-15 22:28:15 -08:00
Kayvan Sylvan
66025d516c refactor: replace interface{} with any across codebase
- Part 1 of incorporating `modernize` tool into Fabric.
- Replace `interface{}` with `any` in slice type declarations
- Update map types from `map[string]interface{}` to `map[string]any`
- Change variadic function parameters to use `...any` instead of `...interface{}`
- Modernize JSON unmarshaling variables to `any` for consistency
- Update struct fields and method signatures to prefer `any` alias
- Ensure all type assertions and conversions use `any` throughout codebase
- Add PR guidelines in docs to encourage focused, reviewable changes
2025-12-15 22:25:18 -08:00
34 changed files with 195 additions and 164 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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: |

View File

@@ -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

View File

@@ -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,

View File

@@ -1,3 +1,3 @@
package main
var version = "v1.4.345"
var version = "v1.4.349"

Binary file not shown.

View File

@@ -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, "- "), "* ")

View File

@@ -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)

View File

@@ -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),

View File

@@ -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:

View File

@@ -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))
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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)
}
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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",
},
},

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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

View File

@@ -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",
}

View File

@@ -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
}
}

View File

@@ -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...)
}

View File

@@ -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]) {

View File

@@ -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 {

View File

@@ -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: "",

View File

@@ -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 {

View File

@@ -1 +1 @@
"1.4.345"
"1.4.349"