Compare commits

..

2 Commits

Author SHA1 Message Date
jad2121
fbfea93b6c deleted lots of random mp3 files 2024-02-21 20:22:07 -05:00
jad2121
c4332c9ee0 added audio to text under --transcribe 2024-02-21 20:00:20 -05:00
651 changed files with 5874 additions and 63553 deletions

View File

@@ -1,5 +0,0 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {
}
}

View File

@@ -1,6 +0,0 @@
.git
.gitignore
.env
README.md
docker-compose.yml
Dockerfile

2
.envrc
View File

@@ -1,2 +0,0 @@
watch_file nix/shell.nix
use flake

View File

@@ -34,4 +34,4 @@ body:
id: screens
attributes:
label: Relevant screenshots (optional)
description: Please upload any screenshots that may help us reproduce and/or understand the issue.
description: Please upload any screenshots that may help us reproduce and/or understand the issue.

View File

@@ -1,37 +0,0 @@
name: Go Build
on:
push:
branches: ["main"]
paths-ignore:
- 'patterns/**'
- '**/*.md'
pull_request:
branches: ["main"]
paths-ignore:
- 'patterns/**'
- '**/*.md'
jobs:
test:
name: Run tests
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version-file: ./go.mod
- name: Run tests
run: go test -v ./...
- name: Check Formatting
run: nix flake check

View File

@@ -1,33 +0,0 @@
name: Patterns Artifact
on:
push:
paths:
- "patterns/**" # Trigger only on changes to files in the patterns folder
jobs:
zip-and-upload:
name: Zip and Upload Patterns Folder
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Verify Changes in Patterns Folder
run: |
git fetch origin
if git diff --quiet HEAD~1 -- patterns; then
echo "No changes detected in patterns folder."
exit 1
fi
- name: Zip the Patterns Folder
run: zip -r patterns.zip patterns/
- name: Upload Patterns Artifact
uses: actions/upload-artifact@v4
with:
name: patterns
path: patterns.zip

View File

@@ -1,133 +0,0 @@
name: Go Release
on:
repository_dispatch:
types: [tag_created]
push:
tags:
- "v*"
jobs:
test:
name: Run tests
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version-file: ./go.mod
- name: Run tests
run: go test -v ./...
build:
name: Build binaries for Windows, macOS, and Linux
runs-on: ${{ matrix.os }}
permissions:
contents: write
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
arch: [amd64, arm64]
exclude:
- os: windows-latest
arch: arm64
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version-file: ./go.mod
- name: Determine OS Name
id: os-name
run: |
if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
echo "OS=linux" >> $GITHUB_ENV
elif [ "${{ matrix.os }}" == "macos-latest" ]; then
echo "OS=darwin" >> $GITHUB_ENV
else
echo "OS=windows" >> $GITHUB_ENV
fi
shell: bash
- name: Build binary on Linux and macOS
if: matrix.os != 'windows-latest'
env:
GOOS: ${{ env.OS }}
GOARCH: ${{ matrix.arch }}
run: |
go build -o fabric-${OS}-${{ matrix.arch }} .
- name: Build binary on Windows
if: matrix.os == 'windows-latest'
env:
GOOS: windows
GOARCH: ${{ matrix.arch }}
run: |
go build -o fabric-windows-${{ matrix.arch }}.exe .
- name: Upload build artifact
if: matrix.os != 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: fabric-${OS}-${{ matrix.arch }}
path: fabric-${OS}-${{ matrix.arch }}
- name: Upload build artifact
if: matrix.os == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: fabric-windows-${{ matrix.arch }}.exe
path: fabric-windows-${{ matrix.arch }}.exe
- name: Get latest tag
if: matrix.os != 'windows-latest'
id: get_latest_tag
run: |
latest_tag=$(git tag --sort=-creatordate | head -n 1)
echo "latest_tag=$latest_tag" >> $GITHUB_ENV
- name: Get latest tag
if: matrix.os == 'windows-latest'
id: get_latest_tag_windows
run: |
$latest_tag = git tag --sort=-creatordate | Select-Object -First 1
Add-Content -Path $env:GITHUB_ENV -Value "latest_tag=$latest_tag"
- name: Create release if it doesn't exist
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if ! gh release view ${{ env.latest_tag }} >/dev/null 2>&1; then
gh release create ${{ env.latest_tag }} --title "Release ${{ env.latest_tag }}" --notes "Automated release for ${{ env.latest_tag }}"
else
echo "Release ${{ env.latest_tag }} already exists."
fi
- name: Upload release artifact
if: matrix.os == 'windows-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload ${{ env.latest_tag }} fabric-windows-${{ matrix.arch }}.exe
- name: Upload release artifact
if: matrix.os != 'windows-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload ${{ env.latest_tag }} fabric-${OS}-${{ matrix.arch }}

View File

@@ -1,115 +0,0 @@
name: Update Version File and Create Tag
on:
push:
branches:
- main # Monitor the main branch
paths-ignore:
- 'patterns/**'
- '**/*.md'
permissions:
contents: write # Ensure the workflow has write permissions
concurrency:
group: version-update
cancel-in-progress: false
jobs:
update-version:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
- name: Set up Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Pull latest main and tags
run: |
git pull --rebase origin main
git fetch --tags
- name: Get the latest tag
id: get_latest_tag
run: |
latest_tag=$(git tag --sort=-creatordate | head -n 1)
echo "Latest tag is: $latest_tag"
echo "tag=$latest_tag" >> $GITHUB_ENV # Save the latest tag to environment file
- name: Increment patch version
id: increment_version
run: |
latest_tag=${{ env.tag }}
major=$(echo "$latest_tag" | cut -d. -f1 | sed 's/v//')
minor=$(echo "$latest_tag" | cut -d. -f2)
patch=$(echo "$latest_tag" | cut -d. -f3)
new_patch=$((patch + 1))
new_version="${major}.${minor}.${new_patch}"
new_tag="v${new_version}"
echo "New version is: $new_version"
echo "new_version=$new_version" >> $GITHUB_ENV # Save the new version to environment file
echo "New tag is: $new_tag"
echo "new_tag=$new_tag" >> $GITHUB_ENV # Save the new tag to environment file
- name: Update version.go file
run: |
echo "package main" > version.go
echo "" >> version.go
echo "var version = \"${{ env.new_tag }}\"" >> version.go
- name: Update version.nix file
run: |
echo "\"${{ env.new_version }}\"" > nix/pkgs/fabric/version.nix
- name: Format source code
run: |
nix fmt
- name: Update gomod2nix.toml file
run: |
nix run .#gomod2nix -- --outdir nix/pkgs/fabric
- name: Commit changes
run: |
git add version.go
git add nix/pkgs/fabric/version.nix
git add nix/pkgs/fabric/gomod2nix.toml
git add .
if ! git diff --staged --quiet; then
git commit -m "Update version to ${{ env.new_tag }} and commit $commit_hash"
else
echo "No changes to commit."
fi
- name: Push changes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Use GITHUB_TOKEN to authenticate the push
run: |
git push origin main # Push changes to the main branch
- name: Create a new tag
env:
GITHUB_TOKEN: ${{ secrets.TAG_PAT }}
run: |
git tag ${{ env.new_tag }}
git push origin ${{ env.new_tag }} # Push the new tag
- name: Dispatch event to trigger release workflow
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Use GITHUB_TOKEN to authenticate the dispatch
run: |
curl -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/${{ github.repository }}/dispatches \
-d '{"event_type": "tag_created", "client_payload": {"tag": "${{ env.new_tag }}"}}'

194
.gitignore vendored
View File

@@ -1,7 +1,3 @@
# Nix
.direnv
result
# macOS local stores
.DS_Store
@@ -15,14 +11,13 @@ __pycache__/
# Distribution / packaging
.Python
.idea
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
@@ -58,7 +53,6 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
coverage.out
# Translations
*.mo
@@ -166,188 +160,4 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
patterns/dialog_with_socrates/Apology by Plato.txt
patterns/dialog_with_socrates/Phaedrus by Plato.txt
patterns/dialog_with_socrates/Symposium by Plato.txt
patterns/dialog_with_socrates/The Economist by Xenophon.txt
patterns/dialog_with_socrates/The Memorabilia by Xenophon.txt
patterns/dialog_with_socrates/The Memorable Thoughts of Socrates by Xenophon.txt
patterns/dialog_with_socrates/The Republic by Plato.txt
patterns/dialog_with_socrates/The Symposium by Xenophon.txt
web/node_modules
# Output
web/.output
web/.vercel
web/.svelte-kit
web/build
# OS
web/.DS_Store
web/Thumbs.db
# Env
web/.env
web/.env.*
web/!.env.example
web/!.env.test
# Vite
web/vite.config.js.timestamp-*
web/vite.config.ts.timestamp-*
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
web/logs
web/*.log
web/npm-debug.log*
web/yarn-debug.log*
web/yarn-error.log*
web/lerna-debug.log*
web/.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
web/report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
web/pids
web/*.pid
web/*.seed
web/*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
web/lib-cov
# Coverage directory used by tools like istanbul
web/coverage
web/*.lcov
# nyc test coverage
web/.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
web/.grunt
# Bower dependency directory (https://bower.io/)
web/bower_components
# node-waf configuration
web/.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
web/node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web/web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
web/.npm
# Optional eslint cache
web/.eslintcache
# Optional stylelint cache
web/.stylelintcache
# Microbundle cache
web/.rpt2_cache/
web/.rts2_cache_cjs/
web/.rts2_cache_es/
web/.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
web/.env
web/.env.development.local
web/.env.test.local
web/.env.production.local
web/.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
web/.next
web/out
# Nuxt.js build / generate output
web/.nuxt
web/dist
# Gatsby files
web/.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
web/.vuepress/dist
# vuepress v2.x temp and cache directory
web/.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
web/.vscode-test
# yarn v2
web/.yarn/cache
web/.yarn/unplugged
web/.yarn/build-state.yml
web/.yarn/install-state.gz
web/.pnp.*
### Node Patch ###
# Serverless Webpack directories
web/.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
web/.svelte-kit
# End of https://www.toptal.com/developers/gitignore/api/node
web/myfiles/Obsidian_perso_not_share/
ENV
web/package-lock.json
.gitignore_backup
web/static/*.png
# Local VSCode project settings
.vscode/
#.idea/

View File

@@ -1,41 +0,0 @@
# Use official golang image as builder
FROM golang:1.24.2-alpine AS builder
# Set working directory
WORKDIR /app
# Copy go mod and sum files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -o fabric
# Use scratch as final base image
FROM alpine:latest
# Copy the binary from builder
COPY --from=builder /app/fabric /fabric
# Copy patterns directory
COPY patterns /patterns
# Ensure clean config directory and copy ENV file
RUN rm -rf /root/.config/fabric && \
mkdir -p /root/.config/fabric
COPY ENV /root/.config/fabric/.env
# Add debug commands
RUN ls -la /root/.config/fabric/
# Expose port 8080
EXPOSE 8080
# Run the binary with debug output
ENTRYPOINT ["/fabric"]
CMD ["--serve"]

View File

@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,33 +0,0 @@
## Notes on some refactoring.
- The goal is to bring more encapsulation of the models management and simplified configuration management to bring increased flexibility, transparency on the overall flow, and simplicity in adding new model.
- We need to differentiate:
- Vendors: the producer of models (like OpenAI, Azure, Anthropic, Ollama, ..etc) and their associated APIs
- Models: the LLM models these vendors are making public
- Each vendor and operations allowed by the vendor needs to be encapsulated. This includes:
- The questions needed to setup the model (like the API key, or the URL)
- The listing of all models supported by the vendor
- The actions performed with a given model
- The configuration flow works like this for an **initial** call:
- The available vendors are called one by one, each of them being responsible for the data they collect. They return a set of environment variables under the form of a list of strings, or an empty list if the user does not want to setup this vendor. As we do not want each vendor to know which way the data they need will be collected (e.g., read from the command line, or a GUI), they will be asked for a list of questions, the configuration will inquire the user, and send back the questions with the collected answers to the Vendor. The Vendor is then either instantiating an instance (Vendor configured) and returning it, or returning `nil` if the Vendor should not be set up.
- the `.env` file is created, using the information returned by the vendors
- A list of patterns is downloaded from the main site
- When the system is configured, the configuration flows:
- Read the `.env` file using the godotenv library
- It configures a structure that contains the various vendors selected as well as the preferred model. This structure will be completed with some of the command line values (i.e, context, session, etc..)
- To get the list of all supported models:
- Each configured model (part of the configuration structure) is asked, using a goroutine, to return the list of model
- Order when building message: session + context + pattern + user input (role "user)
## TODO:
- Check if we need to read the system.md for every patterns when running the ListAllPatterns
- Context management seems more complex than the one in the original fabric. Probably needs some work (at least to make it clear how it works)
- models on command line: give as well vendor (like `--model openai/gpt-4o`). If the vendor is not given, get it by retrieving all possible models and searching from that.
- if user gives the ollama url on command line, we need to update/init an ollama vendor.
- The db should host only things related to access and storage in ~/.config/fabric
- The interaction part of the Setup function should be in the cli (and perhaps all the Setup)

View File

@@ -1,118 +0,0 @@
# Pattern Descriptions and Tags Management
This document explains the complete workflow for managing pattern descriptions and tags, including how to process new patterns and maintain metadata.
## System Overview
The pattern system follows this hierarchy:
1. `~/.config/fabric/patterns/` directory: The source of truth for available patterns
2. `pattern_extracts.json`: Contains first 500 words of each pattern for reference
3. `pattern_descriptions.json`: Stores pattern metadata (descriptions and tags)
4. `web/static/data/pattern_descriptions.json`: Web-accessible copy for the interface
## Pattern Processing Workflow
### 1. Adding New Patterns
- Add patterns to `~/.config/fabric/patterns/`
- Run extract_patterns.py to process new additions:
```bash
python extract_patterns.py
The Python Script automatically:
- Creates pattern extracts for reference
- Adds placeholder entries in descriptions file
- Syncs to web interface
### 2. Pattern Extract Creation
The script extracts first 500 words from each pattern's system.md file to:
- Provide context for writing descriptions
- Maintain reference material
- Aid in pattern categorization
### 3. Description and Tag Management
Pattern descriptions and tags are managed in pattern_descriptions.json:
{
"patterns": [
{
"patternName": "pattern_name",
"description": "[Description pending]",
"tags": []
}
]
}
## Completing Pattern Metadata
### Writing Descriptions
1. Check pattern_descriptions.json for "[Description pending]" entries
2. Reference pattern_extracts.json for context
3. How to update Pattern short descriptions (one sentence).
You can update your descriptions in pattern_descriptions.json manually or using LLM assistance (preferred approach).
Tell AI to look for "Description pending" entries in this file and write a short description based on the extract info in the pattern_extracts.json file. You can also ask your LLM to add tags for those newly added patterns, using other patterns tag assignments as example.
### Managing Tags
1. Add appropriate tags to new patterns
2. Update existing tags as needed
3. Tags are stored as arrays: ["TAG1", "TAG2"]
4. Edit pattern_descriptions.json directly to modify tags
5. Make tags your own. You can delete, replace, amend existing tags.
## File Synchronization
The script maintains synchronization between:
- Local pattern_descriptions.json
- Web interface copy in static/data/
- No manual file copying needed
## Best Practices
1. Run extract_patterns.py when:
- Adding new patterns
- Updating existing patterns
- Modifying pattern structure
2. Description Writing:
- Use pattern extracts for context
- Keep descriptions clear and concise
- Focus on pattern purpose and usage
3. Tag Management:
- Use consistent tag categories
- Apply multiple tags when relevant
- Update tags to reflect pattern evolution
## Troubleshooting
If patterns are not showing in the web interface:
1. Verify pattern_descriptions.json format
2. Check web static copy exists
3. Ensure proper file permissions
4. Run extract_patterns.py to resync
## File Structure
fabric/
├── patterns/ # Pattern source files
├── PATTERN_DESCRIPTIONS/
│ ├── extract_patterns.py # Pattern processing script
│ ├── pattern_extracts.json # Pattern content references
│ └── pattern_descriptions.json # Pattern metadata
└── web/
└── static/
└── data/
└── pattern_descriptions.json # Web interface copy

View File

@@ -1,130 +0,0 @@
#!/usr/bin/env python3
"""Extracts pattern information from the ~/.config/fabric/patterns directory,
creates JSON files for pattern extracts and descriptions, and updates web static files.
"""
import os
import json
import shutil
def load_existing_file(filepath):
"""Load existing JSON file or return default structure"""
if os.path.exists(filepath):
try:
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError:
print(
f"Warning: Malformed JSON in {filepath}. Starting with an empty list."
)
return {"patterns": []}
return {"patterns": []}
def get_pattern_extract(pattern_path):
"""Extract first 500 words from pattern's system.md file"""
system_md_path = os.path.join(pattern_path, "system.md")
with open(system_md_path, "r", encoding="utf-8") as f:
content = " ".join(f.read().split()[:500])
return content
def extract_pattern_info():
"""Extract pattern information from the patterns directory"""
script_dir = os.path.dirname(os.path.abspath(__file__))
patterns_dir = os.path.expanduser("~/.config/fabric/patterns")
print(f"\nScanning patterns directory: {patterns_dir}")
extracts_path = os.path.join(script_dir, "pattern_extracts.json")
descriptions_path = os.path.join(script_dir, "pattern_descriptions.json")
existing_extracts = load_existing_file(extracts_path)
existing_descriptions = load_existing_file(descriptions_path)
existing_extract_names = {p["patternName"] for p in existing_extracts["patterns"]}
existing_description_names = {
p["patternName"] for p in existing_descriptions["patterns"]
}
print(f"Found existing patterns: {len(existing_extract_names)}")
new_extracts = []
new_descriptions = []
for dirname in sorted(os.listdir(patterns_dir)):
pattern_path = os.path.join(patterns_dir, dirname)
system_md_path = os.path.join(pattern_path, "system.md")
if os.path.isdir(pattern_path) and os.path.exists(system_md_path):
if dirname not in existing_extract_names:
print(f"Processing new pattern: {dirname}")
try:
if dirname not in existing_extract_names:
print(f"Creating new extract for: {dirname}")
pattern_extract = get_pattern_extract(
pattern_path
) # Pass directory path
new_extracts.append(
{"patternName": dirname, "pattern_extract": pattern_extract}
)
if dirname not in existing_description_names:
print(f"Creating new description for: {dirname}")
new_descriptions.append(
{
"patternName": dirname,
"description": "[Description pending]",
"tags": [],
}
)
except OSError as e:
print(f"Error processing {dirname}: {str(e)}")
else:
print(f"Invalid pattern directory or missing system.md: {dirname}")
print("\nProcessing summary:")
print(f"New extracts created: {len(new_extracts)}")
print(f"New descriptions added: {len(new_descriptions)}")
existing_extracts["patterns"].extend(new_extracts)
existing_descriptions["patterns"].extend(new_descriptions)
return existing_extracts, existing_descriptions, len(new_descriptions)
def update_web_static(descriptions_path):
"""Copy pattern descriptions to web static directory"""
script_dir = os.path.dirname(os.path.abspath(__file__))
static_dir = os.path.join(script_dir, "..", "web", "static", "data")
os.makedirs(static_dir, exist_ok=True)
static_path = os.path.join(static_dir, "pattern_descriptions.json")
shutil.copy2(descriptions_path, static_path)
def save_pattern_files():
"""Save both pattern files and sync to web"""
script_dir = os.path.dirname(os.path.abspath(__file__))
extracts_path = os.path.join(script_dir, "pattern_extracts.json")
descriptions_path = os.path.join(script_dir, "pattern_descriptions.json")
pattern_extracts, pattern_descriptions, new_count = extract_pattern_info()
# Save files
with open(extracts_path, "w", encoding="utf-8") as f:
json.dump(pattern_extracts, f, indent=2, ensure_ascii=False)
with open(descriptions_path, "w", encoding="utf-8") as f:
json.dump(pattern_descriptions, f, indent=2, ensure_ascii=False)
# Update web static
update_web_static(descriptions_path)
print("\nProcessing complete:")
print(f"Total patterns: {len(pattern_descriptions['patterns'])}")
print(f"New patterns added: {new_count}")
if __name__ == "__main__":
save_pattern_files()

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1005
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,132 +0,0 @@
package chat
import (
"encoding/json"
"errors"
)
const (
ChatMessageRoleSystem = "system"
ChatMessageRoleUser = "user"
ChatMessageRoleAssistant = "assistant"
ChatMessageRoleFunction = "function"
ChatMessageRoleTool = "tool"
ChatMessageRoleDeveloper = "developer"
)
var ErrContentFieldsMisused = errors.New("can't use both Content and MultiContent properties simultaneously")
type ChatMessagePartType string
const (
ChatMessagePartTypeText ChatMessagePartType = "text"
ChatMessagePartTypeImageURL ChatMessagePartType = "image_url"
)
type ChatMessageImageURL struct {
URL string `json:"url,omitempty"`
}
type ChatMessagePart struct {
Type ChatMessagePartType `json:"type,omitempty"`
Text string `json:"text,omitempty"`
ImageURL *ChatMessageImageURL `json:"image_url,omitempty"`
}
type FunctionCall struct {
Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"`
}
type ToolType string
const (
ToolTypeFunction ToolType = "function"
)
type ToolCall struct {
Index *int `json:"index,omitempty"`
ID string `json:"id,omitempty"`
Type ToolType `json:"type"`
Function FunctionCall `json:"function"`
}
type ChatCompletionMessage struct {
Role string `json:"role"`
Content string `json:"content,omitempty"`
Refusal string `json:"refusal,omitempty"`
MultiContent []ChatMessagePart `json:"-"`
Name string `json:"name,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}
func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) {
if m.Content != "" && m.MultiContent != nil {
return nil, ErrContentFieldsMisused
}
if len(m.MultiContent) > 0 {
msg := struct {
Role string `json:"role"`
Content string `json:"-"`
Refusal string `json:"refusal,omitempty"`
MultiContent []ChatMessagePart `json:"content,omitempty"`
Name string `json:"name,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}(m)
return json.Marshal(msg)
}
msg := struct {
Role string `json:"role"`
Content string `json:"content,omitempty"`
Refusal string `json:"refusal,omitempty"`
MultiContent []ChatMessagePart `json:"-"`
Name string `json:"name,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}(m)
return json.Marshal(msg)
}
func (m *ChatCompletionMessage) UnmarshalJSON(bs []byte) error {
msg := struct {
Role string `json:"role"`
Content string `json:"content"`
Refusal string `json:"refusal,omitempty"`
MultiContent []ChatMessagePart
Name string `json:"name,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}{}
if err := json.Unmarshal(bs, &msg); err == nil {
*m = ChatCompletionMessage(msg)
return nil
}
multiMsg := struct {
Role string `json:"role"`
Content string
Refusal string `json:"refusal,omitempty"`
MultiContent []ChatMessagePart `json:"content"`
Name string `json:"name,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}{}
if err := json.Unmarshal(bs, &multiMsg); err != nil {
return err
}
*m = ChatCompletionMessage(multiMsg)
return nil
}

View File

@@ -1,77 +0,0 @@
# YAML Configuration Support
## Overview
Fabric now supports YAML configuration files for commonly used options. This allows users to persist settings and share configurations across multiple runs.
## Usage
Use the `--config` flag to specify a YAML configuration file:
```bash
fabric --config ~/.config/fabric/config.yaml "Tell me about APIs"
```
## Configuration Precedence
1. CLI flags (highest priority)
2. YAML config values
3. Default values (lowest priority)
## Supported Configuration Options
```yaml
# Model selection
model: gpt-4
modelContextLength: 4096
# Model parameters
temperature: 0.7
topp: 0.9
presencepenalty: 0.0
frequencypenalty: 0.0
seed: 42
# Pattern selection
pattern: analyze # Use pattern name or filename
# Feature flags
stream: true
raw: false
```
## Rules and Behavior
- Only long flag names are supported in YAML (e.g., `temperature` not `-t`)
- CLI flags always override YAML values
- Unknown YAML declarations are ignored
- If a declaration appears multiple times in YAML, the last one wins
- The order of YAML declarations doesn't matter
## Type Conversions
The following string-to-type conversions are supported:
- String to number: `"42"``42`
- String to float: `"42.5"``42.5`
- String to boolean: `"true"``true`
## Example Config
```yaml
# ~/.config/fabric/config.yaml
model: gpt-4
temperature: 0.8
pattern: analyze
stream: true
topp: 0.95
presencepenalty: 0.1
frequencypenalty: 0.2
```
## CLI Override Example
```bash
# Override temperature from config
fabric --config ~/.config/fabric/config.yaml --temperature 0.9 "Query"
```

View File

@@ -1,361 +0,0 @@
package cli
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/danielmiessler/fabric/plugins/tools/youtube"
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/core"
"github.com/danielmiessler/fabric/plugins/ai"
"github.com/danielmiessler/fabric/plugins/db/fsdb"
"github.com/danielmiessler/fabric/plugins/tools/converter"
"github.com/danielmiessler/fabric/restapi"
)
// Cli Controls the cli. It takes in the flags and runs the appropriate functions
func Cli(version string) (err error) {
var currentFlags *Flags
if currentFlags, err = Init(); err != nil {
return
}
if currentFlags.Version {
fmt.Println(version)
return
}
var homedir string
if homedir, err = os.UserHomeDir(); err != nil {
return
}
fabricDb := fsdb.NewDb(filepath.Join(homedir, ".config/fabric"))
if err = fabricDb.Configure(); err != nil {
if !currentFlags.Setup {
println(err.Error())
currentFlags.Setup = true
}
}
var registry *core.PluginRegistry
if registry, err = core.NewPluginRegistry(fabricDb); err != nil {
return
}
// if the setup flag is set, run the setup function
if currentFlags.Setup {
err = registry.Setup()
return
}
if currentFlags.Serve {
registry.ConfigureVendors()
err = restapi.Serve(registry, currentFlags.ServeAddress, currentFlags.ServeAPIKey)
return
}
if currentFlags.ServeOllama {
registry.ConfigureVendors()
err = restapi.ServeOllama(registry, currentFlags.ServeAddress, version)
return
}
if currentFlags.UpdatePatterns {
err = registry.PatternsLoader.PopulateDB()
return
}
if currentFlags.ChangeDefaultModel {
if err = registry.Defaults.Setup(); err != nil {
return
}
err = registry.SaveEnvFile()
return
}
if currentFlags.LatestPatterns != "0" {
var parsedToInt int
if parsedToInt, err = strconv.Atoi(currentFlags.LatestPatterns); err != nil {
return
}
if err = fabricDb.Patterns.PrintLatestPatterns(parsedToInt); err != nil {
return
}
return
}
if currentFlags.ListPatterns {
err = fabricDb.Patterns.ListNames(currentFlags.ShellCompleteOutput)
return
}
if currentFlags.ListAllModels {
var models *ai.VendorsModels
if models, err = registry.VendorManager.GetModels(); err != nil {
return
}
models.Print(currentFlags.ShellCompleteOutput)
return
}
if currentFlags.ListAllContexts {
err = fabricDb.Contexts.ListNames(currentFlags.ShellCompleteOutput)
return
}
if currentFlags.ListAllSessions {
err = fabricDb.Sessions.ListNames(currentFlags.ShellCompleteOutput)
return
}
if currentFlags.WipeContext != "" {
err = fabricDb.Contexts.Delete(currentFlags.WipeContext)
return
}
if currentFlags.WipeSession != "" {
err = fabricDb.Sessions.Delete(currentFlags.WipeSession)
return
}
if currentFlags.PrintSession != "" {
err = fabricDb.Sessions.PrintSession(currentFlags.PrintSession)
return
}
if currentFlags.PrintContext != "" {
err = fabricDb.Contexts.PrintContext(currentFlags.PrintContext)
return
}
if currentFlags.HtmlReadability {
if msg, cleanErr := converter.HtmlReadability(currentFlags.Message); cleanErr != nil {
fmt.Println("use original input, because can't apply html readability", err)
} else {
currentFlags.Message = msg
}
}
if currentFlags.ListExtensions {
err = registry.TemplateExtensions.ListExtensions()
return
}
if currentFlags.AddExtension != "" {
err = registry.TemplateExtensions.RegisterExtension(currentFlags.AddExtension)
return
}
if currentFlags.RemoveExtension != "" {
err = registry.TemplateExtensions.RemoveExtension(currentFlags.RemoveExtension)
return
}
if currentFlags.ListStrategies {
err = registry.Strategies.ListStrategies(currentFlags.ShellCompleteOutput)
return
}
if currentFlags.ListVendors {
err = registry.ListVendors(os.Stdout)
return
}
// if the interactive flag is set, run the interactive function
// if currentFlags.Interactive {
// interactive.Interactive()
// }
// if none of the above currentFlags are set, run the initiate chat function
var messageTools string
if currentFlags.YouTube != "" {
if !registry.YouTube.IsConfigured() {
err = fmt.Errorf("YouTube is not configured, please run the setup procedure")
return
}
var videoId string
var playlistId string
if videoId, playlistId, err = registry.YouTube.GetVideoOrPlaylistId(currentFlags.YouTube); err != nil {
return
} else if (videoId == "" || currentFlags.YouTubePlaylist) && playlistId != "" {
if currentFlags.Output != "" {
err = registry.YouTube.FetchAndSavePlaylist(playlistId, currentFlags.Output)
} else {
var videos []*youtube.VideoMeta
if videos, err = registry.YouTube.FetchPlaylistVideos(playlistId); err != nil {
err = fmt.Errorf("error fetching playlist videos: %v", err)
return
}
for _, video := range videos {
var message string
if message, err = processYoutubeVideo(currentFlags, registry, video.Id); err != nil {
return
}
if !currentFlags.IsChatRequest() {
if err = WriteOutput(message, fmt.Sprintf("%v.md", video.TitleNormalized)); err != nil {
return
}
} else {
messageTools = AppendMessage(messageTools, message)
}
}
}
return
}
if messageTools, err = processYoutubeVideo(currentFlags, registry, videoId); err != nil {
return
}
if !currentFlags.IsChatRequest() {
err = currentFlags.WriteOutput(messageTools)
return
}
}
if (currentFlags.ScrapeURL != "" || currentFlags.ScrapeQuestion != "") && registry.Jina.IsConfigured() {
// Check if the scrape_url flag is set and call ScrapeURL
if currentFlags.ScrapeURL != "" {
var website string
if website, err = registry.Jina.ScrapeURL(currentFlags.ScrapeURL); err != nil {
return
}
messageTools = AppendMessage(messageTools, website)
}
// Check if the scrape_question flag is set and call ScrapeQuestion
if currentFlags.ScrapeQuestion != "" {
var website string
if website, err = registry.Jina.ScrapeQuestion(currentFlags.ScrapeQuestion); err != nil {
return
}
messageTools = AppendMessage(messageTools, website)
}
if !currentFlags.IsChatRequest() {
err = currentFlags.WriteOutput(messageTools)
return
}
}
if messageTools != "" {
currentFlags.AppendMessage(messageTools)
}
var chatter *core.Chatter
if chatter, err = registry.GetChatter(currentFlags.Model, currentFlags.ModelContextLength,
currentFlags.Strategy, currentFlags.Stream, currentFlags.DryRun); err != nil {
return
}
var session *fsdb.Session
var chatReq *common.ChatRequest
if chatReq, err = currentFlags.BuildChatRequest(strings.Join(os.Args[1:], " ")); err != nil {
return
}
if chatReq.Language == "" {
chatReq.Language = registry.Language.DefaultLanguage.Value
}
var chatOptions *common.ChatOptions
if chatOptions, err = currentFlags.BuildChatOptions(); err != nil {
return
}
if session, err = chatter.Send(chatReq, chatOptions); err != nil {
return
}
result := session.GetLastMessage().Content
if !currentFlags.Stream {
// print the result if it was not streamed already
fmt.Println(result)
}
// if the copy flag is set, copy the message to the clipboard
if currentFlags.Copy {
if err = CopyToClipboard(result); err != nil {
return
}
}
// if the output flag is set, create an output file
if currentFlags.Output != "" {
if currentFlags.OutputSession {
sessionAsString := session.String()
err = CreateOutputFile(sessionAsString, currentFlags.Output)
} else {
err = CreateOutputFile(result, currentFlags.Output)
}
}
return
}
func processYoutubeVideo(
flags *Flags, registry *core.PluginRegistry, videoId string) (message string, err error) {
if (!flags.YouTubeComments && !flags.YouTubeMetadata) || flags.YouTubeTranscript || flags.YouTubeTranscriptWithTimestamps {
var transcript string
var language = "en"
if flags.Language != "" || registry.Language.DefaultLanguage.Value != "" {
if flags.Language != "" {
language = flags.Language
} else {
language = registry.Language.DefaultLanguage.Value
}
}
if flags.YouTubeTranscriptWithTimestamps {
if transcript, err = registry.YouTube.GrabTranscriptWithTimestamps(videoId, language); err != nil {
return
}
} else {
if transcript, err = registry.YouTube.GrabTranscript(videoId, language); err != nil {
return
}
}
message = AppendMessage(message, transcript)
}
if flags.YouTubeComments {
var comments []string
if comments, err = registry.YouTube.GrabComments(videoId); err != nil {
return
}
commentsString := strings.Join(comments, "\n")
message = AppendMessage(message, commentsString)
}
if flags.YouTubeMetadata {
var metadata *youtube.VideoMetadata
if metadata, err = registry.YouTube.GrabMetadata(videoId); err != nil {
return
}
metadataJson, _ := json.MarshalIndent(metadata, "", " ")
message = AppendMessage(message, string(metadataJson))
}
return
}
func WriteOutput(message string, outputFile string) (err error) {
fmt.Println(message)
if outputFile != "" {
err = CreateOutputFile(message, outputFile)
}
return
}

View File

@@ -1,21 +0,0 @@
package cli
import (
"os"
"testing"
"github.com/danielmiessler/fabric/core"
"github.com/stretchr/testify/assert"
)
func TestCli(t *testing.T) {
t.Skip("Skipping test for now, collision with flag -t")
originalArgs := os.Args
defer func() { os.Args = originalArgs }()
os.Args = []string{os.Args[0]}
err := Cli("test")
assert.Error(t, err)
assert.Equal(t, core.NoSessionPatternUserMessages, err.Error())
}

View File

@@ -1,21 +0,0 @@
#this is an example yaml config file for fabric
# use fabric pattern names
pattern: ai
# or use a filename
# pattern: ~/testpattern.md
model: phi3:latest
# for models that support context length
modelContextLength: 2048
frequencypenalty: 0.5
presencepenalty: 0.5
topp: 0.67
temperature: 0.88
seed: 42
stream: true
raw: false

View File

@@ -1,488 +0,0 @@
package cli
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/common"
"github.com/jessevdk/go-flags"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"
)
// Flags create flags struct. the users flags go into this, this will be passed to the chat struct in cli
type Flags struct {
Pattern string `short:"p" long:"pattern" yaml:"pattern" description:"Choose a pattern from the available patterns" default:""`
PatternVariables map[string]string `short:"v" long:"variable" description:"Values for pattern variables, e.g. -v=#role:expert -v=#points:30"`
Context string `short:"C" long:"context" description:"Choose a context from the available contexts" default:""`
Session string `long:"session" description:"Choose a session from the available sessions"`
Attachments []string `short:"a" long:"attachment" description:"Attachment path or URL (e.g. for OpenAI image recognition messages)"`
Setup bool `short:"S" long:"setup" description:"Run setup for all reconfigurable parts of fabric"`
Temperature float64 `short:"t" long:"temperature" yaml:"temperature" description:"Set temperature" default:"0.7"`
TopP float64 `short:"T" long:"topp" yaml:"topp" description:"Set top P" default:"0.9"`
Stream bool `short:"s" long:"stream" yaml:"stream" description:"Stream"`
PresencePenalty float64 `short:"P" long:"presencepenalty" yaml:"presencepenalty" description:"Set presence penalty" default:"0.0"`
Raw bool `short:"r" long:"raw" yaml:"raw" description:"Use the defaults of the model without sending chat options (like temperature etc.) and use the user role instead of the system role for patterns."`
FrequencyPenalty float64 `short:"F" long:"frequencypenalty" yaml:"frequencypenalty" description:"Set frequency penalty" default:"0.0"`
ListPatterns bool `short:"l" long:"listpatterns" description:"List all patterns"`
ListAllModels bool `short:"L" long:"listmodels" description:"List all available models"`
ListAllContexts bool `short:"x" long:"listcontexts" description:"List all contexts"`
ListAllSessions bool `short:"X" long:"listsessions" description:"List all sessions"`
UpdatePatterns bool `short:"U" long:"updatepatterns" description:"Update patterns"`
Message string `hidden:"true" description:"Messages to send to chat"`
Copy bool `short:"c" long:"copy" description:"Copy to clipboard"`
Model string `short:"m" long:"model" yaml:"model" description:"Choose model"`
ModelContextLength int `long:"modelContextLength" yaml:"modelContextLength" description:"Model context length (only affects ollama)"`
Output string `short:"o" long:"output" description:"Output to file" default:""`
OutputSession bool `long:"output-session" description:"Output the entire session (also a temporary one) to the output file"`
LatestPatterns string `short:"n" long:"latest" description:"Number of latest patterns to list" default:"0"`
ChangeDefaultModel bool `short:"d" long:"changeDefaultModel" description:"Change default model"`
YouTube string `short:"y" long:"youtube" description:"YouTube video or play list \"URL\" to grab transcript, comments from it and send to chat or print it put to the console and store it in the output file"`
YouTubePlaylist bool `long:"playlist" description:"Prefer playlist over video if both ids are present in the URL"`
YouTubeTranscript bool `long:"transcript" description:"Grab transcript from YouTube video and send to chat (it is used per default)."`
YouTubeTranscriptWithTimestamps bool `long:"transcript-with-timestamps" description:"Grab transcript from YouTube video with timestamps and send to chat"`
YouTubeComments bool `long:"comments" description:"Grab comments from YouTube video and send to chat"`
YouTubeMetadata bool `long:"metadata" description:"Output video metadata"`
Language string `short:"g" long:"language" description:"Specify the Language Code for the chat, e.g. -g=en -g=zh" default:""`
ScrapeURL string `short:"u" long:"scrape_url" description:"Scrape website URL to markdown using Jina AI"`
ScrapeQuestion string `short:"q" long:"scrape_question" description:"Search question using Jina AI"`
Seed int `short:"e" long:"seed" yaml:"seed" description:"Seed to be used for LMM generation"`
WipeContext string `short:"w" long:"wipecontext" description:"Wipe context"`
WipeSession string `short:"W" long:"wipesession" description:"Wipe session"`
PrintContext string `long:"printcontext" description:"Print context"`
PrintSession string `long:"printsession" description:"Print session"`
HtmlReadability bool `long:"readability" description:"Convert HTML input into a clean, readable view"`
InputHasVars bool `long:"input-has-vars" description:"Apply variables to user input"`
DryRun bool `long:"dry-run" description:"Show what would be sent to the model without actually sending it"`
Serve bool `long:"serve" description:"Serve the Fabric Rest API"`
ServeOllama bool `long:"serveOllama" description:"Serve the Fabric Rest API with ollama endpoints"`
ServeAddress string `long:"address" description:"The address to bind the REST API" default:":8080"`
ServeAPIKey string `long:"api-key" description:"API key used to secure server routes" default:""`
Config string `long:"config" description:"Path to YAML config file"`
Version bool `long:"version" description:"Print current version"`
ListExtensions bool `long:"listextensions" description:"List all registered extensions"`
AddExtension string `long:"addextension" description:"Register a new extension from config file path"`
RemoveExtension string `long:"rmextension" description:"Remove a registered extension by name"`
Strategy string `long:"strategy" description:"Choose a strategy from the available strategies" default:""`
ListStrategies bool `long:"liststrategies" description:"List all strategies"`
ListVendors bool `long:"listvendors" description:"List all vendors"`
ShellCompleteOutput bool `long:"shell-complete-list" description:"Output raw list without headers/formatting (for shell completion)"`
Search bool `long:"search" description:"Enable web search tool for supported models (Anthropic, OpenAI)"`
SearchLocation string `long:"search-location" description:"Set location for web search results (e.g., 'America/Los_Angeles')"`
ImageFile string `long:"image-file" description:"Save generated image to specified file path (e.g., 'output.png')"`
ImageSize string `long:"image-size" description:"Image dimensions: 1024x1024, 1536x1024, 1024x1536, auto (default: auto)"`
ImageQuality string `long:"image-quality" description:"Image quality: low, medium, high, auto (default: auto)"`
ImageCompression int `long:"image-compression" description:"Compression level 0-100 for JPEG/WebP formats (default: not set)"`
ImageBackground string `long:"image-background" description:"Background type: opaque, transparent (default: opaque, only for PNG/WebP)"`
}
var debug = false
func Debugf(format string, a ...interface{}) {
if debug {
fmt.Printf("DEBUG: "+format, a...)
}
}
// Init Initialize flags. returns a Flags struct and an error
func Init() (ret *Flags, err error) {
// Track which yaml-configured flags were set on CLI
usedFlags := make(map[string]bool)
yamlArgsScan := os.Args[1:]
// Get list of fields that have yaml tags, could be in yaml config
yamlFields := make(map[string]bool)
t := reflect.TypeOf(Flags{})
for i := 0; i < t.NumField(); i++ {
if yamlTag := t.Field(i).Tag.Get("yaml"); yamlTag != "" {
yamlFields[yamlTag] = true
//Debugf("Found yaml-configured field: %s\n", yamlTag)
}
}
// Scan args for that are provided by cli and might be in yaml
for _, arg := range yamlArgsScan {
if strings.HasPrefix(arg, "--") {
flag := strings.TrimPrefix(arg, "--")
if i := strings.Index(flag, "="); i > 0 {
flag = flag[:i]
}
if yamlFields[flag] {
usedFlags[flag] = true
Debugf("CLI flag used: %s\n", flag)
}
}
}
// Parse CLI flags first
ret = &Flags{}
parser := flags.NewParser(ret, flags.Default)
var args []string
if args, err = parser.Parse(); err != nil {
return
}
// If config specified, load and apply YAML for unused flags
if ret.Config != "" {
var yamlFlags *Flags
if yamlFlags, err = loadYAMLConfig(ret.Config); err != nil {
return
}
// Apply YAML values where CLI flags weren't used
flagsVal := reflect.ValueOf(ret).Elem()
yamlVal := reflect.ValueOf(yamlFlags).Elem()
flagsType := flagsVal.Type()
for i := 0; i < flagsType.NumField(); i++ {
field := flagsType.Field(i)
if yamlTag := field.Tag.Get("yaml"); yamlTag != "" {
if !usedFlags[yamlTag] {
flagField := flagsVal.Field(i)
yamlField := yamlVal.Field(i)
if flagField.CanSet() {
if yamlField.Type() != flagField.Type() {
if err := assignWithConversion(flagField, yamlField); err != nil {
Debugf("Type conversion failed for %s: %v\n", yamlTag, err)
continue
}
} else {
flagField.Set(yamlField)
}
Debugf("Applied YAML value for %s: %v\n", yamlTag, yamlField.Interface())
}
}
}
}
}
// Handle stdin and messages
// Handle stdin and messages
info, _ := os.Stdin.Stat()
pipedToStdin := (info.Mode() & os.ModeCharDevice) == 0
// Append positional arguments to the message (custom message)
if len(args) > 0 {
ret.Message = AppendMessage(ret.Message, args[len(args)-1])
}
if pipedToStdin {
var pipedMessage string
if pipedMessage, err = readStdin(); err != nil {
return
}
ret.Message = AppendMessage(ret.Message, pipedMessage)
}
return
}
func assignWithConversion(targetField, sourceField reflect.Value) error {
// Handle string source values
if sourceField.Kind() == reflect.String {
str := sourceField.String()
switch targetField.Kind() {
case reflect.Int:
// Try parsing as float first to handle "42.9" -> 42
if val, err := strconv.ParseFloat(str, 64); err == nil {
targetField.SetInt(int64(val))
return nil
}
// Try direct int parse
if val, err := strconv.ParseInt(str, 10, 64); err == nil {
targetField.SetInt(val)
return nil
}
case reflect.Float64:
if val, err := strconv.ParseFloat(str, 64); err == nil {
targetField.SetFloat(val)
return nil
}
case reflect.Bool:
if val, err := strconv.ParseBool(str); err == nil {
targetField.SetBool(val)
return nil
}
}
return fmt.Errorf("cannot convert string %q to %v", str, targetField.Kind())
}
return fmt.Errorf("unsupported conversion from %v to %v", sourceField.Kind(), targetField.Kind())
}
func loadYAMLConfig(configPath string) (*Flags, error) {
absPath, err := common.GetAbsolutePath(configPath)
if err != nil {
return nil, fmt.Errorf("invalid config path: %w", err)
}
data, err := os.ReadFile(absPath)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("config file not found: %s", absPath)
}
return nil, fmt.Errorf("error reading config file: %w", err)
}
// Use the existing Flags struct for YAML unmarshal
config := &Flags{}
if err := yaml.Unmarshal(data, config); err != nil {
return nil, fmt.Errorf("error parsing config file: %w", err)
}
Debugf("Config: %v\n", config)
return config, nil
}
// readStdin reads from stdin and returns the input as a string or an error
func readStdin() (ret string, err error) {
reader := bufio.NewReader(os.Stdin)
var sb strings.Builder
for {
if line, readErr := reader.ReadString('\n'); readErr != nil {
if errors.Is(readErr, io.EOF) {
sb.WriteString(line)
break
}
err = fmt.Errorf("error reading piped message from stdin: %w", readErr)
return
} else {
sb.WriteString(line)
}
}
ret = sb.String()
return
}
// validateImageFile validates the image file path and extension
func validateImageFile(imagePath string) error {
if imagePath == "" {
return nil // No validation needed if no image file specified
}
// Check if file already exists
if _, err := os.Stat(imagePath); err == nil {
return fmt.Errorf("image file already exists: %s", imagePath)
}
// Check file extension
ext := strings.ToLower(filepath.Ext(imagePath))
validExtensions := []string{".png", ".jpeg", ".jpg", ".webp"}
for _, validExt := range validExtensions {
if ext == validExt {
return nil // Valid extension found
}
}
return fmt.Errorf("invalid image file extension '%s'. Supported formats: .png, .jpeg, .jpg, .webp", ext)
}
// validateImageParameters validates image generation parameters
func validateImageParameters(imagePath, size, quality, background string, compression int) error {
if imagePath == "" {
// Check if any image parameters are specified without --image-file
if size != "" || quality != "" || background != "" || compression != 0 {
return fmt.Errorf("image parameters (--image-size, --image-quality, --image-background, --image-compression) can only be used with --image-file")
}
return nil
}
// Validate size
if size != "" {
validSizes := []string{"1024x1024", "1536x1024", "1024x1536", "auto"}
valid := false
for _, validSize := range validSizes {
if size == validSize {
valid = true
break
}
}
if !valid {
return fmt.Errorf("invalid image size '%s'. Supported sizes: 1024x1024, 1536x1024, 1024x1536, auto", size)
}
}
// Validate quality
if quality != "" {
validQualities := []string{"low", "medium", "high", "auto"}
valid := false
for _, validQuality := range validQualities {
if quality == validQuality {
valid = true
break
}
}
if !valid {
return fmt.Errorf("invalid image quality '%s'. Supported qualities: low, medium, high, auto", quality)
}
}
// Validate background
if background != "" {
validBackgrounds := []string{"opaque", "transparent"}
valid := false
for _, validBackground := range validBackgrounds {
if background == validBackground {
valid = true
break
}
}
if !valid {
return fmt.Errorf("invalid image background '%s'. Supported backgrounds: opaque, transparent", background)
}
}
// Get file format for format-specific validations
ext := strings.ToLower(filepath.Ext(imagePath))
// Validate compression (only for jpeg/webp)
if compression != 0 { // 0 means not set
if ext != ".jpg" && ext != ".jpeg" && ext != ".webp" {
return fmt.Errorf("image compression can only be used with JPEG and WebP formats, not %s", ext)
}
if compression < 0 || compression > 100 {
return fmt.Errorf("image compression must be between 0 and 100, got %d", compression)
}
}
// Validate background transparency (only for png/webp)
if background == "transparent" {
if ext != ".png" && ext != ".webp" {
return fmt.Errorf("transparent background can only be used with PNG and WebP formats, not %s", ext)
}
}
return nil
}
func (o *Flags) BuildChatOptions() (ret *common.ChatOptions, err error) {
// Validate image file if specified
if err = validateImageFile(o.ImageFile); err != nil {
return nil, err
}
// Validate image parameters
if err = validateImageParameters(o.ImageFile, o.ImageSize, o.ImageQuality, o.ImageBackground, o.ImageCompression); err != nil {
return nil, err
}
ret = &common.ChatOptions{
Model: o.Model,
Temperature: o.Temperature,
TopP: o.TopP,
PresencePenalty: o.PresencePenalty,
FrequencyPenalty: o.FrequencyPenalty,
Raw: o.Raw,
Seed: o.Seed,
ModelContextLength: o.ModelContextLength,
Search: o.Search,
SearchLocation: o.SearchLocation,
ImageFile: o.ImageFile,
ImageSize: o.ImageSize,
ImageQuality: o.ImageQuality,
ImageCompression: o.ImageCompression,
ImageBackground: o.ImageBackground,
}
return
}
func (o *Flags) BuildChatRequest(Meta string) (ret *common.ChatRequest, err error) {
ret = &common.ChatRequest{
ContextName: o.Context,
SessionName: o.Session,
PatternName: o.Pattern,
StrategyName: o.Strategy,
PatternVariables: o.PatternVariables,
InputHasVars: o.InputHasVars,
Meta: Meta,
}
var message *chat.ChatCompletionMessage
if len(o.Attachments) > 0 {
message = &chat.ChatCompletionMessage{
Role: chat.ChatMessageRoleUser,
}
if o.Message != "" {
message.MultiContent = append(message.MultiContent, chat.ChatMessagePart{
Type: chat.ChatMessagePartTypeText,
Text: strings.TrimSpace(o.Message),
})
}
for _, attachmentValue := range o.Attachments {
var attachment *common.Attachment
if attachment, err = common.NewAttachment(attachmentValue); err != nil {
return
}
url := attachment.URL
if url == nil {
var base64Image string
if base64Image, err = attachment.Base64Content(); err != nil {
return
}
var mimeType string
if mimeType, err = attachment.ResolveType(); err != nil {
return
}
dataURL := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Image)
url = &dataURL
}
message.MultiContent = append(message.MultiContent, chat.ChatMessagePart{
Type: chat.ChatMessagePartTypeImageURL,
ImageURL: &chat.ChatMessageImageURL{
URL: *url,
},
})
}
} else if o.Message != "" {
message = &chat.ChatCompletionMessage{
Role: chat.ChatMessageRoleUser,
Content: strings.TrimSpace(o.Message),
}
}
ret.Message = message
if o.Language != "" {
if langTag, langErr := language.Parse(o.Language); langErr == nil {
ret.Language = langTag.String()
}
}
return
}
func (o *Flags) AppendMessage(message string) {
o.Message = AppendMessage(o.Message, message)
}
func (o *Flags) IsChatRequest() (ret bool) {
ret = o.Message != "" || len(o.Attachments) > 0 || o.Context != "" || o.Session != "" || o.Pattern != ""
return
}
func (o *Flags) WriteOutput(message string) (err error) {
fmt.Println(message)
if o.Output != "" {
err = CreateOutputFile(message, o.Output)
}
return
}
func AppendMessage(message string, newMessage string) (ret string) {
if message != "" {
ret = message + "\n" + newMessage
} else {
ret = newMessage
}
return
}

View File

@@ -1,435 +0,0 @@
package cli
import (
"bytes"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/danielmiessler/fabric/common"
"github.com/stretchr/testify/assert"
)
func TestInit(t *testing.T) {
args := []string{"--copy"}
expectedFlags := &Flags{Copy: true}
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = append([]string{"cmd"}, args...)
flags, err := Init()
assert.NoError(t, err)
assert.Equal(t, expectedFlags.Copy, flags.Copy)
}
func TestReadStdin(t *testing.T) {
input := "test input"
stdin := io.NopCloser(strings.NewReader(input))
// No need to cast stdin to *os.File, pass it as io.ReadCloser directly
content, err := ReadStdin(stdin)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if content != input {
t.Fatalf("expected %q, got %q", input, content)
}
}
// ReadStdin function assuming it's part of `cli` package
func ReadStdin(reader io.ReadCloser) (string, error) {
defer reader.Close()
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(reader)
if err != nil {
return "", err
}
return buf.String(), nil
}
func TestBuildChatOptions(t *testing.T) {
flags := &Flags{
Temperature: 0.8,
TopP: 0.9,
PresencePenalty: 0.1,
FrequencyPenalty: 0.2,
Seed: 1,
}
expectedOptions := &common.ChatOptions{
Temperature: 0.8,
TopP: 0.9,
PresencePenalty: 0.1,
FrequencyPenalty: 0.2,
Raw: false,
Seed: 1,
}
options, err := flags.BuildChatOptions()
assert.NoError(t, err)
assert.Equal(t, expectedOptions, options)
}
func TestBuildChatOptionsDefaultSeed(t *testing.T) {
flags := &Flags{
Temperature: 0.8,
TopP: 0.9,
PresencePenalty: 0.1,
FrequencyPenalty: 0.2,
}
expectedOptions := &common.ChatOptions{
Temperature: 0.8,
TopP: 0.9,
PresencePenalty: 0.1,
FrequencyPenalty: 0.2,
Raw: false,
Seed: 0,
}
options, err := flags.BuildChatOptions()
assert.NoError(t, err)
assert.Equal(t, expectedOptions, options)
}
func TestInitWithYAMLConfig(t *testing.T) {
// Create a temporary YAML config file
configContent := `
temperature: 0.9
model: gpt-4
pattern: analyze
stream: true
`
tmpfile, err := os.CreateTemp("", "config.*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write([]byte(configContent)); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}
// Test 1: Basic YAML loading
t.Run("Load YAML config", func(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"cmd", "--config", tmpfile.Name()}
flags, err := Init()
assert.NoError(t, err)
assert.Equal(t, 0.9, flags.Temperature)
assert.Equal(t, "gpt-4", flags.Model)
assert.Equal(t, "analyze", flags.Pattern)
assert.True(t, flags.Stream)
})
// Test 2: CLI overrides YAML
t.Run("CLI overrides YAML", func(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"cmd", "--config", tmpfile.Name(), "--temperature", "0.7", "--model", "gpt-3.5-turbo"}
flags, err := Init()
assert.NoError(t, err)
assert.Equal(t, 0.7, flags.Temperature)
assert.Equal(t, "gpt-3.5-turbo", flags.Model)
assert.Equal(t, "analyze", flags.Pattern) // unchanged from YAML
assert.True(t, flags.Stream) // unchanged from YAML
})
// Test 3: Invalid YAML config
t.Run("Invalid YAML config", func(t *testing.T) {
badConfig := `
temperature: "not a float"
model: 123 # should be string
`
badfile, err := os.CreateTemp("", "bad-config.*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(badfile.Name())
if _, err := badfile.Write([]byte(badConfig)); err != nil {
t.Fatal(err)
}
if err := badfile.Close(); err != nil {
t.Fatal(err)
}
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"cmd", "--config", badfile.Name()}
_, err = Init()
assert.Error(t, err)
})
}
func TestValidateImageFile(t *testing.T) {
t.Run("Empty path should be valid", func(t *testing.T) {
err := validateImageFile("")
assert.NoError(t, err)
})
t.Run("Valid extensions should pass", func(t *testing.T) {
validExtensions := []string{".png", ".jpeg", ".jpg", ".webp"}
for _, ext := range validExtensions {
filename := "/tmp/test" + ext
err := validateImageFile(filename)
assert.NoError(t, err, "Extension %s should be valid", ext)
}
})
t.Run("Invalid extensions should fail", func(t *testing.T) {
invalidExtensions := []string{".gif", ".bmp", ".tiff", ".svg", ".txt", ""}
for _, ext := range invalidExtensions {
filename := "/tmp/test" + ext
err := validateImageFile(filename)
assert.Error(t, err, "Extension %s should be invalid", ext)
assert.Contains(t, err.Error(), "invalid image file extension")
}
})
t.Run("Existing file should fail", func(t *testing.T) {
// Create a temporary file
tempFile, err := os.CreateTemp("", "test*.png")
assert.NoError(t, err)
defer os.Remove(tempFile.Name())
tempFile.Close()
// Validation should fail because file exists
err = validateImageFile(tempFile.Name())
assert.Error(t, err)
assert.Contains(t, err.Error(), "image file already exists")
})
t.Run("Non-existing file with valid extension should pass", func(t *testing.T) {
nonExistentFile := filepath.Join(os.TempDir(), "non_existent_file.png")
// Make sure the file doesn't exist
os.Remove(nonExistentFile)
err := validateImageFile(nonExistentFile)
assert.NoError(t, err)
})
}
func TestBuildChatOptionsWithImageFileValidation(t *testing.T) {
t.Run("Valid image file should pass", func(t *testing.T) {
flags := &Flags{
ImageFile: "/tmp/output.png",
}
options, err := flags.BuildChatOptions()
assert.NoError(t, err)
assert.Equal(t, "/tmp/output.png", options.ImageFile)
})
t.Run("Invalid extension should fail", func(t *testing.T) {
flags := &Flags{
ImageFile: "/tmp/output.gif",
}
options, err := flags.BuildChatOptions()
assert.Error(t, err)
assert.Nil(t, options)
assert.Contains(t, err.Error(), "invalid image file extension")
})
t.Run("Existing file should fail", func(t *testing.T) {
// Create a temporary file
tempFile, err := os.CreateTemp("", "existing*.png")
assert.NoError(t, err)
defer os.Remove(tempFile.Name())
tempFile.Close()
flags := &Flags{
ImageFile: tempFile.Name(),
}
options, err := flags.BuildChatOptions()
assert.Error(t, err)
assert.Nil(t, options)
assert.Contains(t, err.Error(), "image file already exists")
})
}
func TestValidateImageParameters(t *testing.T) {
t.Run("No image file and no parameters should pass", func(t *testing.T) {
err := validateImageParameters("", "", "", "", 0)
assert.NoError(t, err)
})
t.Run("Image parameters without image file should fail", func(t *testing.T) {
// Test each parameter individually
err := validateImageParameters("", "1024x1024", "", "", 0)
assert.Error(t, err)
assert.Contains(t, err.Error(), "image parameters")
assert.Contains(t, err.Error(), "can only be used with --image-file")
err = validateImageParameters("", "", "high", "", 0)
assert.Error(t, err)
assert.Contains(t, err.Error(), "image parameters")
err = validateImageParameters("", "", "", "transparent", 0)
assert.Error(t, err)
assert.Contains(t, err.Error(), "image parameters")
err = validateImageParameters("", "", "", "", 50)
assert.Error(t, err)
assert.Contains(t, err.Error(), "image parameters")
// Test multiple parameters
err = validateImageParameters("", "1024x1024", "high", "transparent", 50)
assert.Error(t, err)
assert.Contains(t, err.Error(), "image parameters")
})
t.Run("Valid size values should pass", func(t *testing.T) {
validSizes := []string{"1024x1024", "1536x1024", "1024x1536", "auto"}
for _, size := range validSizes {
err := validateImageParameters("/tmp/test.png", size, "", "", 0)
assert.NoError(t, err, "Size %s should be valid", size)
}
})
t.Run("Invalid size should fail", func(t *testing.T) {
err := validateImageParameters("/tmp/test.png", "invalid", "", "", 0)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid image size")
})
t.Run("Valid quality values should pass", func(t *testing.T) {
validQualities := []string{"low", "medium", "high", "auto"}
for _, quality := range validQualities {
err := validateImageParameters("/tmp/test.png", "", quality, "", 0)
assert.NoError(t, err, "Quality %s should be valid", quality)
}
})
t.Run("Invalid quality should fail", func(t *testing.T) {
err := validateImageParameters("/tmp/test.png", "", "invalid", "", 0)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid image quality")
})
t.Run("Valid background values should pass", func(t *testing.T) {
validBackgrounds := []string{"opaque", "transparent"}
for _, background := range validBackgrounds {
err := validateImageParameters("/tmp/test.png", "", "", background, 0)
assert.NoError(t, err, "Background %s should be valid", background)
}
})
t.Run("Invalid background should fail", func(t *testing.T) {
err := validateImageParameters("/tmp/test.png", "", "", "invalid", 0)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid image background")
})
t.Run("Compression for JPEG should pass", func(t *testing.T) {
err := validateImageParameters("/tmp/test.jpg", "", "", "", 75)
assert.NoError(t, err)
})
t.Run("Compression for WebP should pass", func(t *testing.T) {
err := validateImageParameters("/tmp/test.webp", "", "", "", 50)
assert.NoError(t, err)
})
t.Run("Compression for PNG should fail", func(t *testing.T) {
err := validateImageParameters("/tmp/test.png", "", "", "", 75)
assert.Error(t, err)
assert.Contains(t, err.Error(), "image compression can only be used with JPEG and WebP formats")
})
t.Run("Invalid compression range should fail", func(t *testing.T) {
err := validateImageParameters("/tmp/test.jpg", "", "", "", 150)
assert.Error(t, err)
assert.Contains(t, err.Error(), "image compression must be between 0 and 100")
err = validateImageParameters("/tmp/test.jpg", "", "", "", -10)
assert.Error(t, err)
assert.Contains(t, err.Error(), "image compression must be between 0 and 100")
})
t.Run("Transparent background for PNG should pass", func(t *testing.T) {
err := validateImageParameters("/tmp/test.png", "", "", "transparent", 0)
assert.NoError(t, err)
})
t.Run("Transparent background for WebP should pass", func(t *testing.T) {
err := validateImageParameters("/tmp/test.webp", "", "", "transparent", 0)
assert.NoError(t, err)
})
t.Run("Transparent background for JPEG should fail", func(t *testing.T) {
err := validateImageParameters("/tmp/test.jpg", "", "", "transparent", 0)
assert.Error(t, err)
assert.Contains(t, err.Error(), "transparent background can only be used with PNG and WebP formats")
})
}
func TestBuildChatOptionsWithImageParameters(t *testing.T) {
t.Run("Valid image parameters should pass", func(t *testing.T) {
flags := &Flags{
ImageFile: "/tmp/test.png",
ImageSize: "1024x1024",
ImageQuality: "high",
ImageBackground: "transparent",
ImageCompression: 0, // Not set for PNG
}
options, err := flags.BuildChatOptions()
assert.NoError(t, err)
assert.NotNil(t, options)
assert.Equal(t, "/tmp/test.png", options.ImageFile)
assert.Equal(t, "1024x1024", options.ImageSize)
assert.Equal(t, "high", options.ImageQuality)
assert.Equal(t, "transparent", options.ImageBackground)
assert.Equal(t, 0, options.ImageCompression)
})
t.Run("Invalid image parameters should fail", func(t *testing.T) {
flags := &Flags{
ImageFile: "/tmp/test.png",
ImageSize: "invalid",
ImageQuality: "high",
ImageBackground: "transparent",
}
options, err := flags.BuildChatOptions()
assert.Error(t, err)
assert.Nil(t, options)
assert.Contains(t, err.Error(), "invalid image size")
})
t.Run("JPEG with compression should pass", func(t *testing.T) {
flags := &Flags{
ImageFile: "/tmp/test.jpg",
ImageSize: "1536x1024",
ImageQuality: "medium",
ImageBackground: "opaque",
ImageCompression: 80,
}
options, err := flags.BuildChatOptions()
assert.NoError(t, err)
assert.NotNil(t, options)
assert.Equal(t, 80, options.ImageCompression)
})
t.Run("Image parameters without image file should fail in BuildChatOptions", func(t *testing.T) {
flags := &Flags{
ImageSize: "1024x1024", // Image parameter without ImageFile
}
options, err := flags.BuildChatOptions()
assert.Error(t, err)
assert.Nil(t, options)
assert.Contains(t, err.Error(), "image parameters")
assert.Contains(t, err.Error(), "can only be used with --image-file")
})
}

View File

@@ -1,30 +0,0 @@
package cli
import (
"fmt"
"os"
"github.com/atotto/clipboard"
)
func CopyToClipboard(message string) (err error) {
if err = clipboard.WriteAll(message); err != nil {
err = fmt.Errorf("could not copy to clipboard: %v", err)
}
return
}
func CreateOutputFile(message string, fileName string) (err error) {
var file *os.File
if file, err = os.Create(fileName); err != nil {
err = fmt.Errorf("error creating file: %v", err)
return
}
defer file.Close()
if _, err = file.WriteString(message); err != nil {
err = fmt.Errorf("error writing to file: %v", err)
} else {
fmt.Printf("\n\n... written to %s\n", fileName)
}
return
}

View File

@@ -1,28 +0,0 @@
package cli
import (
"os"
"testing"
)
func TestCopyToClipboard(t *testing.T) {
t.Skip("skipping test, because of docker env. in ci.")
message := "test message"
err := CopyToClipboard(message)
if err != nil {
t.Fatalf("CopyToClipboard() error = %v", err)
}
}
func TestCreateOutputFile(t *testing.T) {
fileName := "test_output.txt"
message := "test message"
err := CreateOutputFile(message, fileName)
if err != nil {
t.Fatalf("CreateOutputFile() error = %v", err)
}
defer os.Remove(fileName)
}

View File

@@ -1,171 +0,0 @@
package common
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"github.com/gabriel-vasile/mimetype"
)
type Attachment struct {
Type *string `json:"type,omitempty"`
Path *string `json:"path,omitempty"`
URL *string `json:"url,omitempty"`
Content []byte `json:"content,omitempty"`
ID *string `json:"id,omitempty"`
}
func (a *Attachment) GetId() (ret string, err error) {
if a.ID == nil {
var hash string
if a.Content != nil {
hash = fmt.Sprintf("%x", sha256.Sum256(a.Content))
} else if a.Path != nil {
var content []byte
if content, err = os.ReadFile(*a.Path); err != nil {
return
}
hash = fmt.Sprintf("%x", sha256.Sum256(content))
} else if a.URL != nil {
data := map[string]string{"url": *a.URL}
var jsonData []byte
if jsonData, err = json.Marshal(data); err != nil {
return
}
hash = fmt.Sprintf("%x", sha256.Sum256(jsonData))
}
a.ID = &hash
}
ret = *a.ID
return
}
func (a *Attachment) ResolveType() (ret string, err error) {
if a.Type != nil {
ret = *a.Type
return
}
if a.Path != nil {
var mime *mimetype.MIME
if mime, err = mimetype.DetectFile(*a.Path); err != nil {
return
}
ret = mime.String()
return
}
if a.URL != nil {
var resp *http.Response
if resp, err = http.Head(*a.URL); err != nil {
return
}
defer resp.Body.Close()
ret = resp.Header.Get("Content-Type")
return
}
if a.Content != nil {
ret = mimetype.Detect(a.Content).String()
return
}
err = fmt.Errorf("attachment has no type and no content to derive it from")
return
}
func (a *Attachment) ContentBytes() (ret []byte, err error) {
if a.Content != nil {
ret = a.Content
return
}
if a.Path != nil {
if ret, err = os.ReadFile(*a.Path); err != nil {
return
}
return
}
if a.URL != nil {
var resp *http.Response
if resp, err = http.Get(*a.URL); err != nil {
return
}
defer resp.Body.Close()
if ret, err = io.ReadAll(resp.Body); err != nil {
return
}
return
}
err = fmt.Errorf("no content available")
return
}
func (a *Attachment) Base64Content() (ret string, err error) {
var content []byte
if content, err = a.ContentBytes(); err != nil {
return
}
ret = base64.StdEncoding.EncodeToString(content)
return
}
func NewAttachment(value string) (ret *Attachment, err error) {
if isURL(value) {
var mimeType string
if mimeType, err = detectMimeTypeFromURL(value); err != nil {
return
}
ret = &Attachment{
Type: &mimeType,
URL: &value,
}
return
}
var absPath string
if absPath, err = filepath.Abs(value); err != nil {
return
}
if _, err = os.Stat(absPath); os.IsNotExist(err) {
err = fmt.Errorf("file %s does not exist", value)
return
}
var mimeType string
if mimeType, err = detectMimeTypeFromFile(absPath); err != nil {
return
}
ret = &Attachment{
Type: &mimeType,
Path: &absPath,
}
return
}
func detectMimeTypeFromURL(url string) (string, error) {
resp, err := http.Head(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
mimeType := resp.Header.Get("Content-Type")
if mimeType == "" {
return "", fmt.Errorf("could not determine mimetype of URL")
}
return mimeType, nil
}
func detectMimeTypeFromFile(path string) (string, error) {
mime, err := mimetype.DetectFile(path)
if err != nil {
return "", err
}
return mime.String(), nil
}
func isURL(value string) bool {
return bytes.Contains([]byte(value), []byte("://"))
}

View File

@@ -1,57 +0,0 @@
package common
import "github.com/danielmiessler/fabric/chat"
const ChatMessageRoleMeta = "meta"
type ChatRequest struct {
ContextName string
SessionName string
PatternName string
PatternVariables map[string]string
Message *chat.ChatCompletionMessage
Language string
Meta string
InputHasVars bool
StrategyName string
}
type ChatOptions struct {
Model string
Temperature float64
TopP float64
PresencePenalty float64
FrequencyPenalty float64
Raw bool
Seed int
ModelContextLength int
MaxTokens int
Search bool
SearchLocation string
ImageFile string
ImageSize string
ImageQuality string
ImageCompression int
ImageBackground string
}
// NormalizeMessages remove empty messages and ensure messages order user-assist-user
func NormalizeMessages(msgs []*chat.ChatCompletionMessage, defaultUserMessage string) (ret []*chat.ChatCompletionMessage) {
// Iterate over messages to enforce the odd position rule for user messages
fullMessageIndex := 0
for _, message := range msgs {
if message.Content == "" {
// Skip empty messages as the anthropic API doesn't accept them
continue
}
// Ensure, that each odd position shall be a user message
if fullMessageIndex%2 == 0 && message.Role != chat.ChatMessageRoleUser {
ret = append(ret, &chat.ChatCompletionMessage{Role: chat.ChatMessageRoleUser, Content: defaultUserMessage})
fullMessageIndex++
}
ret = append(ret, message)
fullMessageIndex++
}
return
}

View File

@@ -1,27 +0,0 @@
package common
import (
"testing"
"github.com/danielmiessler/fabric/chat"
"github.com/stretchr/testify/assert"
)
func TestNormalizeMessages(t *testing.T) {
msgs := []*chat.ChatCompletionMessage{
{Role: chat.ChatMessageRoleUser, Content: "Hello"},
{Role: chat.ChatMessageRoleAssistant, Content: "Hi there!"},
{Role: chat.ChatMessageRoleUser, Content: ""},
{Role: chat.ChatMessageRoleUser, Content: ""},
{Role: chat.ChatMessageRoleUser, Content: "How are you?"},
}
expected := []*chat.ChatCompletionMessage{
{Role: chat.ChatMessageRoleUser, Content: "Hello"},
{Role: chat.ChatMessageRoleAssistant, Content: "Hi there!"},
{Role: chat.ChatMessageRoleUser, Content: "How are you?"},
}
actual := NormalizeMessages(msgs, "default")
assert.Equal(t, expected, actual)
}

View File

@@ -1,195 +0,0 @@
package common
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
// FileChangesMarker identifies the start of a file changes section in output
const FileChangesMarker = "__CREATE_CODING_FEATURE_FILE_CHANGES__"
const (
// MaxFileSize is the maximum size of a file that can be created (10MB)
MaxFileSize = 10 * 1024 * 1024
)
// FileChange represents a single file change operation to be performed
type FileChange struct {
Operation string `json:"operation"` // "create" or "update"
Path string `json:"path"` // Relative path from project root
Content string `json:"content"` // New file content
}
// ParseFileChanges extracts and parses the file change marker section from LLM output
func ParseFileChanges(output string) (changeSummary string, changes []FileChange, err error) {
fileChangesStart := strings.Index(output, FileChangesMarker)
if fileChangesStart == -1 {
return output, nil, nil // No file changes section found
}
changeSummary = output[:fileChangesStart] // Everything before the marker
// Extract the JSON part
jsonStart := fileChangesStart + len(FileChangesMarker)
// Find the first [ after the file changes marker
jsonArrayStart := strings.Index(output[jsonStart:], "[")
if jsonArrayStart == -1 {
return output, nil, fmt.Errorf("invalid %s format: no JSON array found", FileChangesMarker)
}
jsonStart += jsonArrayStart
// Find the matching closing bracket for the array with proper bracket counting
bracketCount := 0
jsonEnd := jsonStart
for i := jsonStart; i < len(output); i++ {
if output[i] == '[' {
bracketCount++
} else if output[i] == ']' {
bracketCount--
if bracketCount == 0 {
jsonEnd = i + 1
break
}
}
}
if bracketCount != 0 {
return output, nil, fmt.Errorf("invalid %s format: unbalanced brackets", FileChangesMarker)
}
// Extract the JSON string and fix escape sequences
jsonStr := output[jsonStart:jsonEnd]
// Fix specific invalid escape sequences
// First try with the common \C issue
jsonStr = strings.Replace(jsonStr, `\C`, `\\C`, -1)
// Parse the JSON
var fileChanges []FileChange
err = json.Unmarshal([]byte(jsonStr), &fileChanges)
if err != nil {
// If still failing, try a more comprehensive fix
jsonStr = fixInvalidEscapes(jsonStr)
err = json.Unmarshal([]byte(jsonStr), &fileChanges)
if err != nil {
return changeSummary, nil, fmt.Errorf("failed to parse %s JSON: %w", FileChangesMarker, err)
}
}
// Validate file changes
for i, change := range fileChanges {
// Validate operation
if change.Operation != "create" && change.Operation != "update" {
return changeSummary, nil, fmt.Errorf("invalid operation for file change %d: %s", i, change.Operation)
}
// Validate path
if change.Path == "" {
return changeSummary, nil, fmt.Errorf("empty path for file change %d", i)
}
// Check for suspicious paths (directory traversal)
if strings.Contains(change.Path, "..") {
return changeSummary, nil, fmt.Errorf("suspicious path for file change %d: %s", i, change.Path)
}
// Check file size
if len(change.Content) > MaxFileSize {
return changeSummary, nil, fmt.Errorf("file content too large for file change %d: %d bytes", i, len(change.Content))
}
}
return changeSummary, fileChanges, nil
}
// fixInvalidEscapes replaces invalid escape sequences in JSON strings
func fixInvalidEscapes(jsonStr string) string {
validEscapes := []byte{'b', 'f', 'n', 'r', 't', '\\', '/', '"', 'u'}
var result strings.Builder
inQuotes := false
i := 0
for i < len(jsonStr) {
ch := jsonStr[i]
// Track whether we're inside a JSON string
if ch == '"' && (i == 0 || jsonStr[i-1] != '\\') {
inQuotes = !inQuotes
}
// Handle actual control characters inside string literals
if inQuotes {
// Convert literal control characters to proper JSON escape sequences
if ch == '\n' {
result.WriteString("\\n")
i++
continue
} else if ch == '\r' {
result.WriteString("\\r")
i++
continue
} else if ch == '\t' {
result.WriteString("\\t")
i++
continue
} else if ch < 32 {
// Handle other control characters
fmt.Fprintf(&result, "\\u%04x", ch)
i++
continue
}
}
// 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
}
}
if !isValid {
// Invalid escape sequence - add an extra backslash
result.WriteByte('\\')
result.WriteByte('\\')
i++
continue
}
}
result.WriteByte(ch)
i++
}
return result.String()
}
// ApplyFileChanges applies the parsed file changes to the file system
func ApplyFileChanges(projectRoot string, changes []FileChange) error {
for i, change := range changes {
// Get the absolute path
absPath := filepath.Join(projectRoot, change.Path)
// Create directories if necessary
dir := filepath.Dir(absPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory %s for file change %d: %w", dir, i, err)
}
// Write the file
if err := os.WriteFile(absPath, []byte(change.Content), 0644); err != nil {
return fmt.Errorf("failed to write file %s for file change %d: %w", absPath, i, err)
}
fmt.Printf("Applied %s operation to %s\n", change.Operation, change.Path)
}
return nil
}

View File

@@ -1,185 +0,0 @@
package common
import (
"os"
"path/filepath"
"testing"
)
func TestParseFileChanges(t *testing.T) {
tests := []struct {
name string
input string
want int // number of expected file changes
wantErr bool
}{
{
name: "No " + FileChangesMarker + " section",
input: "This is a normal response with no file changes.",
want: 0,
wantErr: false,
},
{
name: "Valid " + FileChangesMarker + " section",
input: `Some text before.
` + FileChangesMarker + `
[
{
"operation": "create",
"path": "test.txt",
"content": "Hello, World!"
},
{
"operation": "update",
"path": "other.txt",
"content": "Updated content"
}
]
Some text after.`,
want: 2,
wantErr: false,
},
{
name: "Invalid JSON in " + FileChangesMarker + " section",
input: `Some text before.
` + FileChangesMarker + `
[
{
"operation": "create",
"path": "test.txt",
"content": "Hello, World!"
},
{
"operation": "invalid",
"path": "other.txt"
"content": "Updated content"
}
]`,
want: 0,
wantErr: true,
},
{
name: "Invalid operation",
input: `Some text before.
` + FileChangesMarker + `
[
{
"operation": "delete",
"path": "test.txt",
"content": ""
}
]`,
want: 0,
wantErr: true,
},
{
name: "Empty path",
input: `Some text before.
` + FileChangesMarker + `
[
{
"operation": "create",
"path": "",
"content": "Hello, World!"
}
]`,
want: 0,
wantErr: true,
},
{
name: "Suspicious path with directory traversal",
input: `Some text before.
` + FileChangesMarker + `
[
{
"operation": "create",
"path": "../etc/passwd",
"content": "Hello, World!"
}
]`,
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, got, err := ParseFileChanges(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseFileChanges() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && len(got) != tt.want {
t.Errorf("ParseFileChanges() got %d file changes, want %d", len(got), tt.want)
}
})
}
}
func TestApplyFileChanges(t *testing.T) {
// Create a temporary directory for testing
// Create a temporary directory for testing
tempDir, err := os.MkdirTemp("", "file-manager-test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
// Test file changes
changes := []FileChange{
{
Operation: "create",
Path: "test.txt",
Content: "Hello, World!",
},
{
Operation: "create",
Path: "subdir/nested.txt",
Content: "Nested content",
},
}
// Apply the changes
if err := ApplyFileChanges(tempDir, changes); err != nil {
t.Fatalf("ApplyFileChanges() error = %v", err)
}
// Verify the first file was created correctly
content, err := os.ReadFile(filepath.Join(tempDir, "test.txt"))
if err != nil {
t.Fatalf("Failed to read created file: %v", err)
}
if string(content) != "Hello, World!" {
t.Errorf("File content = %q, want %q", string(content), "Hello, World!")
}
// Verify the nested file was created correctly
content, err = os.ReadFile(filepath.Join(tempDir, "subdir/nested.txt"))
if err != nil {
t.Fatalf("Failed to read created nested file: %v", err)
}
if string(content) != "Nested content" {
t.Errorf("Nested file content = %q, want %q", string(content), "Nested content")
}
// Test updating a file
updateChanges := []FileChange{
{
Operation: "update",
Path: "test.txt",
Content: "Updated content",
},
}
// Apply the update
if err := ApplyFileChanges(tempDir, updateChanges); err != nil {
t.Fatalf("ApplyFileChanges() error = %v", err)
}
// Verify the file was updated correctly
content, err = os.ReadFile(filepath.Join(tempDir, "test.txt"))
if err != nil {
t.Fatalf("Failed to read updated file: %v", err)
}
if string(content) != "Updated content" {
t.Errorf("Updated file content = %q, want %q", string(content), "Updated content")
}
}

View File

@@ -1,182 +0,0 @@
package common
import (
"fmt"
"sort"
"strings"
"github.com/samber/lo"
)
func NewGroupsItemsSelector[I any](selectionLabel string,
getItemLabel func(I) string) *GroupsItemsSelector[I] {
return &GroupsItemsSelector[I]{SelectionLabel: selectionLabel,
GetItemKey: getItemLabel,
GroupsItems: make([]*GroupItems[I], 0),
}
}
type GroupItems[I any] struct {
Group string
Items []I
}
func (o *GroupItems[I]) Count() int {
return len(o.Items)
}
func (o *GroupItems[I]) ContainsItemBy(predicate func(item I) bool) (ret bool) {
ret = lo.ContainsBy(o.Items, predicate)
return
}
type GroupsItemsSelector[I any] struct {
SelectionLabel string
GetItemKey func(I) string
GroupsItems []*GroupItems[I]
}
func (o *GroupsItemsSelector[I]) AddGroupItems(group string, items ...I) {
o.GroupsItems = append(o.GroupsItems, &GroupItems[I]{group, items})
}
// getSortedGroupsItems returns a new slice of GroupItems with both groups and their items
// sorted alphabetically in a case-insensitive manner. The original GroupsItems are not modified.
func (o *GroupsItemsSelector[I]) getSortedGroupsItems() []*GroupItems[I] {
// Copy and sort groups (caseinsensitive)
sortedGroupsItems := make([]*GroupItems[I], len(o.GroupsItems))
copy(sortedGroupsItems, o.GroupsItems)
sort.SliceStable(sortedGroupsItems, func(i, j int) bool {
return strings.ToLower(sortedGroupsItems[i].Group) < strings.ToLower(sortedGroupsItems[j].Group)
})
// For each group, sort its items
for i, groupItems := range sortedGroupsItems {
sortedItems := make([]I, len(groupItems.Items))
copy(sortedItems, groupItems.Items)
sort.SliceStable(sortedItems, func(i, j int) bool {
return strings.ToLower(o.GetItemKey(sortedItems[i])) < strings.ToLower(o.GetItemKey(sortedItems[j]))
})
// Create a new GroupItems with the sorted items
sortedGroupsItems[i] = &GroupItems[I]{
Group: groupItems.Group,
Items: sortedItems,
}
}
return sortedGroupsItems
}
func (o *GroupsItemsSelector[I]) GetGroupAndItemByItemNumber(number int) (group string, item I, err error) {
var currentItemNumber int
found := false
sortedGroupsItems := o.getSortedGroupsItems()
for _, groupItems := range sortedGroupsItems {
if currentItemNumber+len(groupItems.Items) < number {
currentItemNumber += len(groupItems.Items)
continue
}
for _, groupItem := range groupItems.Items {
currentItemNumber++
if currentItemNumber == number {
group = groupItems.Group
item = groupItem
found = true
break
}
}
if found {
break
}
}
if !found {
err = fmt.Errorf("number %d is out of range", number)
}
return
}
func (o *GroupsItemsSelector[I]) Print(shellCompleteList bool) {
// Only print the section header if not in plain output mode
if !shellCompleteList {
fmt.Printf("\n%v:\n", o.SelectionLabel)
}
var currentItemIndex int
sortedGroupsItems := o.getSortedGroupsItems()
for _, groupItems := range sortedGroupsItems {
if !shellCompleteList {
fmt.Println()
fmt.Printf("%s\n\n", groupItems.Group)
}
for _, item := range groupItems.Items {
currentItemIndex++
if shellCompleteList {
// plain mode: "index key"
fmt.Printf("%s\n", o.GetItemKey(item))
} else {
// formatted mode: "[index] key"
fmt.Printf("\t[%d]\t%s\n", currentItemIndex, o.GetItemKey(item))
}
}
}
}
func (o *GroupsItemsSelector[I]) HasGroup(group string) (ret bool) {
for _, groupItems := range o.GroupsItems {
if ret = groupItems.Group == group; ret {
break
}
}
return
}
func (o *GroupsItemsSelector[I]) FindGroupsByItemFirst(item I) (ret string) {
itemKey := o.GetItemKey(item)
for _, groupItems := range o.GroupsItems {
if groupItems.ContainsItemBy(func(groupItem I) bool {
groupItemKey := o.GetItemKey(groupItem)
return groupItemKey == itemKey
}) {
ret = groupItems.Group
break
}
}
return
}
func (o *GroupsItemsSelector[I]) FindGroupsByItem(item I) (groups []string) {
itemKey := o.GetItemKey(item)
for _, groupItems := range o.GroupsItems {
if groupItems.ContainsItemBy(func(groupItem I) bool {
groupItemKey := o.GetItemKey(groupItem)
return groupItemKey == itemKey
}) {
groups = append(groups, groupItems.Group)
}
}
return
}
func ReturnItem(item string) string {
return item
}
func NewGroupsItemsSelectorString(selectionLabel string) *GroupsItemsSelectorString {
return &GroupsItemsSelectorString{GroupsItemsSelector: NewGroupsItemsSelector(selectionLabel, ReturnItem)}
}
type GroupsItemsSelectorString struct {
*GroupsItemsSelector[string]
}

View File

@@ -1,124 +0,0 @@
package common
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
// OAuthToken represents stored OAuth token information
type OAuthToken struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt int64 `json:"expires_at"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
}
// IsExpired checks if the token is expired or will expire within the buffer time
func (t *OAuthToken) IsExpired(bufferMinutes int) bool {
if t.ExpiresAt == 0 {
return true
}
bufferTime := time.Duration(bufferMinutes) * time.Minute
return time.Now().Add(bufferTime).Unix() >= t.ExpiresAt
}
// OAuthStorage handles persistent storage of OAuth tokens
type OAuthStorage struct {
configDir string
}
// NewOAuthStorage creates a new OAuth storage instance
func NewOAuthStorage() (*OAuthStorage, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to get user home directory: %w", err)
}
configDir := filepath.Join(homeDir, ".config", "fabric")
// Ensure config directory exists
if err := os.MkdirAll(configDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create config directory: %w", err)
}
return &OAuthStorage{configDir: configDir}, nil
}
// GetTokenPath returns the file path for a provider's OAuth token
func (s *OAuthStorage) GetTokenPath(provider string) string {
return filepath.Join(s.configDir, fmt.Sprintf(".%s_oauth", provider))
}
// SaveToken saves an OAuth token to disk with proper permissions
func (s *OAuthStorage) SaveToken(provider string, token *OAuthToken) error {
tokenPath := s.GetTokenPath(provider)
// Marshal token to JSON
data, err := json.MarshalIndent(token, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal token: %w", err)
}
// Write to temporary file first for atomic operation
tempPath := tokenPath + ".tmp"
if err := os.WriteFile(tempPath, data, 0600); err != nil {
return fmt.Errorf("failed to write token file: %w", err)
}
// Atomic rename
if err := os.Rename(tempPath, tokenPath); err != nil {
os.Remove(tempPath) // Clean up temp file
return fmt.Errorf("failed to save token file: %w", err)
}
return nil
}
// LoadToken loads an OAuth token from disk
func (s *OAuthStorage) LoadToken(provider string) (*OAuthToken, error) {
tokenPath := s.GetTokenPath(provider)
// Check if file exists
if _, err := os.Stat(tokenPath); os.IsNotExist(err) {
return nil, nil // No token stored
}
// Read token file
data, err := os.ReadFile(tokenPath)
if err != nil {
return nil, fmt.Errorf("failed to read token file: %w", err)
}
// Unmarshal token
var token OAuthToken
if err := json.Unmarshal(data, &token); err != nil {
return nil, fmt.Errorf("failed to parse token file: %w", err)
}
return &token, nil
}
// DeleteToken removes a stored OAuth token
func (s *OAuthStorage) DeleteToken(provider string) error {
tokenPath := s.GetTokenPath(provider)
if err := os.Remove(tokenPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete token file: %w", err)
}
return nil
}
// HasValidToken checks if a valid (non-expired) token exists for a provider
func (s *OAuthStorage) HasValidToken(provider string, bufferMinutes int) bool {
token, err := s.LoadToken(provider)
if err != nil || token == nil {
return false
}
return !token.IsExpired(bufferMinutes)
}

View File

@@ -1,232 +0,0 @@
package common
import (
"os"
"path/filepath"
"testing"
"time"
)
func TestOAuthToken_IsExpired(t *testing.T) {
tests := []struct {
name string
expiresAt int64
bufferMinutes int
expected bool
}{
{
name: "token not expired",
expiresAt: time.Now().Unix() + 3600, // 1 hour from now
bufferMinutes: 5,
expected: false,
},
{
name: "token expired",
expiresAt: time.Now().Unix() - 3600, // 1 hour ago
bufferMinutes: 5,
expected: true,
},
{
name: "token expires within buffer",
expiresAt: time.Now().Unix() + 120, // 2 minutes from now
bufferMinutes: 5,
expected: true,
},
{
name: "zero expiry time",
expiresAt: 0,
bufferMinutes: 5,
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
token := &OAuthToken{ExpiresAt: tt.expiresAt}
if got := token.IsExpired(tt.bufferMinutes); got != tt.expected {
t.Errorf("IsExpired() = %v, want %v", got, tt.expected)
}
})
}
}
func TestOAuthStorage_SaveAndLoadToken(t *testing.T) {
// Create temporary directory for testing
tempDir, err := os.MkdirTemp("", "fabric_oauth_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
// Create storage with custom config dir
storage := &OAuthStorage{configDir: tempDir}
// Test token
token := &OAuthToken{
AccessToken: "test_access_token",
RefreshToken: "test_refresh_token",
ExpiresAt: time.Now().Unix() + 3600,
TokenType: "Bearer",
Scope: "test_scope",
}
// Test saving token
err = storage.SaveToken("test_provider", token)
if err != nil {
t.Fatalf("Failed to save token: %v", err)
}
// Verify file exists and has correct permissions
tokenPath := storage.GetTokenPath("test_provider")
info, err := os.Stat(tokenPath)
if err != nil {
t.Fatalf("Token file not created: %v", err)
}
if info.Mode().Perm() != 0600 {
t.Errorf("Token file has wrong permissions: %v, want 0600", info.Mode().Perm())
}
// Test loading token
loadedToken, err := storage.LoadToken("test_provider")
if err != nil {
t.Fatalf("Failed to load token: %v", err)
}
if loadedToken == nil {
t.Fatal("Loaded token is nil")
}
// Verify token data
if loadedToken.AccessToken != token.AccessToken {
t.Errorf("AccessToken mismatch: got %v, want %v", loadedToken.AccessToken, token.AccessToken)
}
if loadedToken.RefreshToken != token.RefreshToken {
t.Errorf("RefreshToken mismatch: got %v, want %v", loadedToken.RefreshToken, token.RefreshToken)
}
if loadedToken.ExpiresAt != token.ExpiresAt {
t.Errorf("ExpiresAt mismatch: got %v, want %v", loadedToken.ExpiresAt, token.ExpiresAt)
}
}
func TestOAuthStorage_LoadNonExistentToken(t *testing.T) {
// Create temporary directory for testing
tempDir, err := os.MkdirTemp("", "fabric_oauth_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
storage := &OAuthStorage{configDir: tempDir}
// Try to load non-existent token
token, err := storage.LoadToken("nonexistent")
if err != nil {
t.Fatalf("Unexpected error loading non-existent token: %v", err)
}
if token != nil {
t.Error("Expected nil token for non-existent provider")
}
}
func TestOAuthStorage_DeleteToken(t *testing.T) {
// Create temporary directory for testing
tempDir, err := os.MkdirTemp("", "fabric_oauth_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
storage := &OAuthStorage{configDir: tempDir}
// Create and save a token
token := &OAuthToken{
AccessToken: "test_token",
RefreshToken: "test_refresh",
ExpiresAt: time.Now().Unix() + 3600,
}
err = storage.SaveToken("test_provider", token)
if err != nil {
t.Fatalf("Failed to save token: %v", err)
}
// Verify token exists
tokenPath := storage.GetTokenPath("test_provider")
if _, err := os.Stat(tokenPath); os.IsNotExist(err) {
t.Fatal("Token file should exist before deletion")
}
// Delete token
err = storage.DeleteToken("test_provider")
if err != nil {
t.Fatalf("Failed to delete token: %v", err)
}
// Verify token is deleted
if _, err := os.Stat(tokenPath); !os.IsNotExist(err) {
t.Error("Token file should not exist after deletion")
}
// Test deleting non-existent token (should not error)
err = storage.DeleteToken("nonexistent")
if err != nil {
t.Errorf("Deleting non-existent token should not error: %v", err)
}
}
func TestOAuthStorage_HasValidToken(t *testing.T) {
// Create temporary directory for testing
tempDir, err := os.MkdirTemp("", "fabric_oauth_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
storage := &OAuthStorage{configDir: tempDir}
// Test with no token
if storage.HasValidToken("test_provider", 5) {
t.Error("Should return false when no token exists")
}
// Save valid token
validToken := &OAuthToken{
AccessToken: "valid_token",
RefreshToken: "refresh_token",
ExpiresAt: time.Now().Unix() + 3600, // 1 hour from now
}
err = storage.SaveToken("test_provider", validToken)
if err != nil {
t.Fatalf("Failed to save valid token: %v", err)
}
// Test with valid token
if !storage.HasValidToken("test_provider", 5) {
t.Error("Should return true for valid token")
}
// Save expired token
expiredToken := &OAuthToken{
AccessToken: "expired_token",
RefreshToken: "refresh_token",
ExpiresAt: time.Now().Unix() - 3600, // 1 hour ago
}
err = storage.SaveToken("expired_provider", expiredToken)
if err != nil {
t.Fatalf("Failed to save expired token: %v", err)
}
// Test with expired token
if storage.HasValidToken("expired_provider", 5) {
t.Error("Should return false for expired token")
}
}
func TestOAuthStorage_GetTokenPath(t *testing.T) {
storage := &OAuthStorage{configDir: "/test/config"}
expected := filepath.Join("/test/config", ".test_provider_oauth")
actual := storage.GetTokenPath("test_provider")
if actual != expected {
t.Errorf("GetTokenPath() = %v, want %v", actual, expected)
}
}

View File

@@ -1,73 +0,0 @@
package common
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
// GetAbsolutePath resolves a given path to its absolute form, handling ~, ./, ../, UNC paths, and symlinks.
func GetAbsolutePath(path string) (string, error) {
if path == "" {
return "", errors.New("path is empty")
}
// Handle UNC paths on Windows
if runtime.GOOS == "windows" && strings.HasPrefix(path, `\\`) {
return path, nil
}
// Handle ~ for home directory expansion
if strings.HasPrefix(path, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", errors.New("could not resolve home directory")
}
path = filepath.Join(home, path[1:])
}
// Convert to absolute path
absPath, err := filepath.Abs(path)
if err != nil {
return "", errors.New("could not get absolute path")
}
// Resolve symlinks, but allow non-existent paths
resolvedPath, err := filepath.EvalSymlinks(absPath)
if err == nil {
return resolvedPath, nil
}
if os.IsNotExist(err) {
// Return the absolute path for non-existent paths
return absPath, nil
}
return "", fmt.Errorf("could not resolve symlinks: %w", err)
}
// Helper function to check if a symlink points to a directory
func IsSymlinkToDir(path string) bool {
fileInfo, err := os.Lstat(path)
if err != nil {
return false
}
if fileInfo.Mode()&os.ModeSymlink != 0 {
resolvedPath, err := filepath.EvalSymlinks(path)
if err != nil {
return false
}
fileInfo, err = os.Stat(resolvedPath)
if err != nil {
return false
}
return fileInfo.IsDir()
}
return false // Regular directories should not be treated as symlinks
}

View File

@@ -1,118 +0,0 @@
#compdef fabric
# Zsh completion for fabric CLI
# Place this file in a directory in your $fpath (e.g. /usr/local/share/zsh/site-functions)
_fabric_patterns() {
local -a patterns
patterns=(${(f)"$(fabric --listpatterns --shell-complete-list 2>/dev/null)"})
compadd -X "Patterns:" ${patterns}
}
_fabric_models() {
local -a models
models=(${(f)"$(fabric --listmodels --shell-complete-list 2>/dev/null)"})
compadd -X "Models:" ${models}
}
_fabric_contexts() {
local -a contexts
contexts=(${(f)"$(fabric --listcontexts --shell-complete-list 2>/dev/null)"})
compadd -X "Contexts:" ${contexts}
}
_fabric_sessions() {
local -a sessions
sessions=(${(f)"$(fabric --listsessions --shell-complete-list 2>/dev/null)"})
compadd -X "Sessions:" ${sessions}
}
_fabric_strategies() {
local -a strategies
strategies=(${(f)"$(fabric --liststrategies --shell-complete-list 2>/dev/null)"})
compadd -X "Strategies:" ${strategies}
}
_fabric_extensions() {
local -a extensions
extensions=(${(f)"$(fabric --listextensions --shell-complete-list 2>/dev/null)"})
compadd -X "Extensions:" ${extensions}
'(-L --listmodels)'{-L,--listmodels}'[List all available models]:list models:_fabric_models' \
'(-x --listcontexts)'{-x,--listcontexts}'[List all contexts]:list contexts:_fabric_contexts' \
'(-X --listsessions)'{-X,--listsessions}'[List all sessions]:list sessions:_fabric_sessions' \
'(--listextensions)--listextensions[List all registered extensions]' \
'(--liststrategies)--liststrategies[List all strategies]:list strategies:_fabric_strategies' \
'(--listvendors)--listvendors[List all vendors]' \
vendors=(${(f)"$(fabric --listvendors 2>/dev/null)"})
compadd -X "Vendors:" ${vendors}
}
_fabric() {
local curcontext="$curcontext" state line
typeset -A opt_args
_arguments -C \
'(-p --pattern)'{-p,--pattern}'[Choose a pattern from the available patterns]:pattern:_fabric_patterns' \
'(-v --variable)'{-v,--variable}'[Values for pattern variables, e.g. -v=#role:expert -v=#points:30]:variable:' \
'(-C --context)'{-C,--context}'[Choose a context from the available contexts]:context:_fabric_contexts' \
'(--session)--session[Choose a session from the available sessions]:session:_fabric_sessions' \
'(-a --attachment)'{-a,--attachment}'[Attachment path or URL (e.g. for OpenAI image recognition messages)]:file:_files' \
'(-S --setup)'{-S,--setup}'[Run setup for all reconfigurable parts of fabric]' \
'(-t --temperature)'{-t,--temperature}'[Set temperature (default: 0.7)]:temperature:' \
'(-T --topp)'{-T,--topp}'[Set top P (default: 0.9)]:topp:' \
'(-s --stream)'{-s,--stream}'[Stream]' \
'(-P --presencepenalty)'{-P,--presencepenalty}'[Set presence penalty (default: 0.0)]:presence penalty:' \
'(-r --raw)'{-r,--raw}'[Use the defaults of the model without sending chat options]' \
'(-F --frequencypenalty)'{-F,--frequencypenalty}'[Set frequency penalty (default: 0.0)]:frequency penalty:' \
'(-l --listpatterns)'{-l,--listpatterns}'[List all patterns]' \
'(-L --listmodels)'{-L,--listmodels}'[List all available models]' \
'(-x --listcontexts)'{-x,--listcontexts}'[List all contexts]' \
'(-X --listsessions)'{-X,--listsessions}'[List all sessions]' \
'(-U --updatepatterns)'{-U,--updatepatterns}'[Update patterns]' \
'(-c --copy)'{-c,--copy}'[Copy to clipboard]' \
'(-m --model)'{-m,--model}'[Choose model]:model:_fabric_models' \
'(--modelContextLength)--modelContextLength[Model context length (only affects ollama)]:length:' \
'(-o --output)'{-o,--output}'[Output to file]:file:_files' \
'(--output-session)--output-session[Output the entire session to the output file]' \
'(-n --latest)'{-n,--latest}'[Number of latest patterns to list (default: 0)]:number:' \
'(-d --changeDefaultModel)'{-d,--changeDefaultModel}'[Change default model]' \
'(-y --youtube)'{-y,--youtube}'[YouTube video or play list URL]:youtube url:' \
'(--playlist)--playlist[Prefer playlist over video if both ids are present in the URL]' \
'(--transcript)--transcript[Grab transcript from YouTube video and send to chat]' \
'(--transcript-with-timestamps)--transcript-with-timestamps[Grab transcript from YouTube video with timestamps]' \
'(--comments)--comments[Grab comments from YouTube video and send to chat]' \
'(--metadata)--metadata[Output video metadata]' \
'(-g --language)'{-g,--language}'[Specify the Language Code for the chat, e.g. -g=en -g=zh]:language:' \
'(-u --scrape_url)'{-u,--scrape_url}'[Scrape website URL to markdown using Jina AI]:url:' \
'(-q --scrape_question)'{-q,--scrape_question}'[Search question using Jina AI]:question:' \
'(-e --seed)'{-e,--seed}'[Seed to be used for LMM generation]:seed:' \
'(-w --wipecontext)'{-w,--wipecontext}'[Wipe context]:context:_fabric_contexts' \
'(-W --wipesession)'{-W,--wipesession}'[Wipe session]:session:_fabric_sessions' \
'(--printcontext)--printcontext[Print context]:context:_fabric_contexts' \
'(--printsession)--printsession[Print session]:session:_fabric_sessions' \
'(--readability)--readability[Convert HTML input into a clean, readable view]' \
'(--input-has-vars)--input-has-vars[Apply variables to user input]' \
'(--dry-run)--dry-run[Show what would be sent to the model without actually sending it]' \
'(--serve)--serve[Serve the Fabric Rest API]' \
'(--serveOllama)--serveOllama[Serve the Fabric Rest API with ollama endpoints]' \
'(--address)--address[The address to bind the REST API (default: :8080)]:address:' \
'(--api-key)--api-key[API key used to secure server routes]:api-key:' \
'(--config)--config[Path to YAML config file]:config file:_files -g "*.yaml *.yml"' \
'(--version)--version[Print current version]' \
'(--search)--search[Enable web search tool for supported models (Anthropic, OpenAI)]' \
'(--search-location)--search-location[Set location for web search results]:location:' \
'(--image-file)--image-file[Save generated image to specified file path]:image file:_files -g "*.png *.webp *.jpeg *.jpg"' \
'(--image-size)--image-size[Image dimensions]:size:(1024x1024 1536x1024 1024x1536 auto)' \
'(--image-quality)--image-quality[Image quality]:quality:(low medium high auto)' \
'(--image-compression)--image-compression[Compression level 0-100 for JPEG/WebP formats]:compression:' \
'(--image-background)--image-background[Background type]:background:(opaque transparent)' \
'(--listextensions)--listextensions[List all registered extensions]' \
'(--addextension)--addextension[Register a new extension from config file path]:config file:_files -g "*.yaml *.yml"' \
'(--rmextension)--rmextension[Remove a registered extension by name]:extension:_fabric_extensions' \
'(--strategy)--strategy[Choose a strategy from the available strategies]:strategy:_fabric_strategies' \
'(--liststrategies)--liststrategies[List all strategies]' \
'(--listvendors)--listvendors[List all vendors]' \
'(--shell-complete-list)--shell-complete-list[Output raw list without headers/formatting (for shell completion)]' \
'(-h --help)'{-h,--help}'[Show this help message]' \
'*:arguments:'
}
_fabric "$@"

View File

@@ -1,103 +0,0 @@
# Bash completion for fabric CLI
#
# Installation:
# 1. Place this file in a standard completion directory, e.g.,
# - /etc/bash_completion.d/
# - /usr/local/etc/bash_completion.d/
# - ~/.local/share/bash-completion/completions/
# 2. Or, source it directly in your ~/.bashrc or ~/.bash_profile:
# source /path/to/fabric.bash
_fabric() {
local cur prev words cword
_get_comp_words_by_ref -n : cur prev words cword
# Define all possible options/flags
local opts="--pattern -p --variable -v --context -C --session --attachment -a --setup -S --temperature -t --topp -T --stream -s --presencepenalty -P --raw -r --frequencypenalty -F --listpatterns -l --listmodels -L --listcontexts -x --listsessions -X --updatepatterns -U --copy -c --model -m --modelContextLength --output -o --output-session --latest -n --changeDefaultModel -d --youtube -y --playlist --transcript --transcript-with-timestamps --comments --metadata --language -g --scrape_url -u --scrape_question -q --seed -e --wipecontext -w --wipesession -W --printcontext --printsession --readability --input-has-vars --dry-run --serve --serveOllama --address --api-key --config --search --search-location --image-file --image-size --image-quality --image-compression --image-background --version --listextensions --addextension --rmextension --strategy --liststrategies --listvendors --shell-complete-list --help -h"
# Helper function for dynamic completions
_fabric_get_list() {
fabric "$1" --shell-complete-list 2>/dev/null
}
# Handle completions based on the previous word
case "${prev}" in
-p | --pattern)
COMPREPLY=($(compgen -W "$(_fabric_get_list --listpatterns)" -- "${cur}"))
return 0
;;
-C | --context)
COMPREPLY=($(compgen -W "$(_fabric_get_list --listcontexts)" -- "${cur}"))
return 0
;;
--session)
COMPREPLY=($(compgen -W "$(_fabric_get_list --listsessions)" -- "${cur}"))
return 0
;;
-m | --model)
COMPREPLY=($(compgen -W "$(_fabric_get_list --listmodels)" -- "${cur}"))
return 0
;;
-w | --wipecontext)
COMPREPLY=($(compgen -W "$(_fabric_get_list --listcontexts)" -- "${cur}"))
return 0
;;
-W | --wipesession)
COMPREPLY=($(compgen -W "$(_fabric_get_list --listsessions)" -- "${cur}"))
return 0
;;
--printcontext)
COMPREPLY=($(compgen -W "$(_fabric_get_list --listcontexts)" -- "${cur}"))
return 0
;;
--printsession)
COMPREPLY=($(compgen -W "$(_fabric_get_list --listsessions)" -- "${cur}"))
return 0
;;
--rmextension)
COMPREPLY=($(compgen -W "$(_fabric_get_list --listextensions)" -- "${cur}"))
return 0
;;
--strategy)
COMPREPLY=($(compgen -W "$(_fabric_get_list --liststrategies)" -- "${cur}"))
return 0
;;
# Options requiring file/directory paths
-a | --attachment | -o | --output | --config | --addextension | --image-file)
_filedir
return 0
;;
# Image generation options with specific values
--image-size)
COMPREPLY=($(compgen -W "1024x1024 1536x1024 1024x1536 auto" -- "$cur"))
return 0
;;
--image-quality)
COMPREPLY=($(compgen -W "low medium high auto" -- "$cur"))
return 0
;;
--image-background)
COMPREPLY=($(compgen -W "opaque transparent" -- "$cur"))
return 0
;;
# Options requiring simple arguments (no specific completion logic here)
-v | --variable | -t | --temperature | -T | --topp | -P | --presencepenalty | -F | --frequencypenalty | --modelContextLength | -n | --latest | -y | --youtube | -g | --language | -u | --scrape_url | -q | --scrape_question | -e | --seed | --address | --api-key | --search-location | --image-compression)
# No specific completion suggestions, user types the value
return 0
;;
esac
# If the current word starts with '-', suggest options
if [[ "${cur}" == -* ]]; then
COMPREPLY=($(compgen -W "${opts}" -- "${cur}"))
return 0
fi
# Default: complete files/directories if no other rule matches
# _filedir
# Or provide no completions if it's not an option or argument following a known flag
COMPREPLY=()
}
complete -F _fabric fabric

View File

@@ -1,101 +0,0 @@
# Fish shell completion for fabric CLI
#
# Installation:
# Copy this file to ~/.config/fish/completions/fabric.fish
# or run:
# mkdir -p ~/.config/fish/completions
# cp completions/fabric.fish ~/.config/fish/completions/
# Helper functions for dynamic completions
function __fabric_get_patterns
fabric --listpatterns --shell-complete-list 2>/dev/null
end
function __fabric_get_models
fabric --listmodels --shell-complete-list 2>/dev/null
end
function __fabric_get_contexts
fabric --listcontexts --shell-complete-list 2>/dev/null
end
function __fabric_get_sessions
fabric --listsessions --shell-complete-list 2>/dev/null
end
function __fabric_get_strategies
fabric --liststrategies --shell-complete-list 2>/dev/null
end
function __fabric_get_extensions
fabric --listextensions --shell-complete-list 2>/dev/null
end
# Main completion function
complete -c fabric -f
# Flag completions with arguments
complete -c fabric -s p -l pattern -d "Choose a pattern from the available patterns" -a "(__fabric_get_patterns)"
complete -c fabric -s v -l variable -d "Values for pattern variables, e.g. -v=#role:expert -v=#points:30"
complete -c fabric -s C -l context -d "Choose a context from the available contexts" -a "(__fabric_get_contexts)"
complete -c fabric -l session -d "Choose a session from the available sessions" -a "(__fabric_get_sessions)"
complete -c fabric -s a -l attachment -d "Attachment path or URL (e.g. for OpenAI image recognition messages)" -r
complete -c fabric -s t -l temperature -d "Set temperature (default: 0.7)"
complete -c fabric -s T -l topp -d "Set top P (default: 0.9)"
complete -c fabric -s P -l presencepenalty -d "Set presence penalty (default: 0.0)"
complete -c fabric -s F -l frequencypenalty -d "Set frequency penalty (default: 0.0)"
complete -c fabric -s m -l model -d "Choose model" -a "(__fabric_get_models)"
complete -c fabric -l modelContextLength -d "Model context length (only affects ollama)"
complete -c fabric -s o -l output -d "Output to file" -r
complete -c fabric -s n -l latest -d "Number of latest patterns to list (default: 0)"
complete -c fabric -s y -l youtube -d "YouTube video or play list URL to grab transcript, comments from it"
complete -c fabric -s g -l language -d "Specify the Language Code for the chat, e.g. -g=en -g=zh"
complete -c fabric -s u -l scrape_url -d "Scrape website URL to markdown using Jina AI"
complete -c fabric -s q -l scrape_question -d "Search question using Jina AI"
complete -c fabric -s e -l seed -d "Seed to be used for LMM generation"
complete -c fabric -s w -l wipecontext -d "Wipe context" -a "(__fabric_get_contexts)"
complete -c fabric -s W -l wipesession -d "Wipe session" -a "(__fabric_get_sessions)"
complete -c fabric -l printcontext -d "Print context" -a "(__fabric_get_contexts)"
complete -c fabric -l printsession -d "Print session" -a "(__fabric_get_sessions)"
complete -c fabric -l address -d "The address to bind the REST API (default: :8080)"
complete -c fabric -l api-key -d "API key used to secure server routes"
complete -c fabric -l config -d "Path to YAML config file" -r -a "*.yaml *.yml"
complete -c fabric -l search-location -d "Set location for web search results (e.g., 'America/Los_Angeles')"
complete -c fabric -l image-file -d "Save generated image to specified file path (e.g., 'output.png')" -r -a "*.png *.webp *.jpeg *.jpg"
complete -c fabric -l image-size -d "Image dimensions: 1024x1024, 1536x1024, 1024x1536, auto (default: auto)" -a "1024x1024 1536x1024 1024x1536 auto"
complete -c fabric -l image-quality -d "Image quality: low, medium, high, auto (default: auto)" -a "low medium high auto"
complete -c fabric -l image-compression -d "Compression level 0-100 for JPEG/WebP formats (default: not set)" -r
complete -c fabric -l image-background -d "Background type: opaque, transparent (default: opaque, only for PNG/WebP)" -a "opaque transparent"
complete -c fabric -l addextension -d "Register a new extension from config file path" -r -a "*.yaml *.yml"
complete -c fabric -l rmextension -d "Remove a registered extension by name" -a "(__fabric_get_extensions)"
complete -c fabric -l strategy -d "Choose a strategy from the available strategies" -a "(__fabric_get_strategies)"
# Boolean flags (no arguments)
complete -c fabric -s S -l setup -d "Run setup for all reconfigurable parts of fabric"
complete -c fabric -s s -l stream -d "Stream"
complete -c fabric -s r -l raw -d "Use the defaults of the model without sending chat options"
complete -c fabric -s l -l listpatterns -d "List all patterns"
complete -c fabric -s L -l listmodels -d "List all available models"
complete -c fabric -s x -l listcontexts -d "List all contexts"
complete -c fabric -s X -l listsessions -d "List all sessions"
complete -c fabric -s U -l updatepatterns -d "Update patterns"
complete -c fabric -s c -l copy -d "Copy to clipboard"
complete -c fabric -l output-session -d "Output the entire session to the output file"
complete -c fabric -s d -l changeDefaultModel -d "Change default model"
complete -c fabric -l playlist -d "Prefer playlist over video if both ids are present in the URL"
complete -c fabric -l transcript -d "Grab transcript from YouTube video and send to chat"
complete -c fabric -l transcript-with-timestamps -d "Grab transcript from YouTube video with timestamps"
complete -c fabric -l comments -d "Grab comments from YouTube video and send to chat"
complete -c fabric -l metadata -d "Output video metadata"
complete -c fabric -l readability -d "Convert HTML input into a clean, readable view"
complete -c fabric -l input-has-vars -d "Apply variables to user input"
complete -c fabric -l dry-run -d "Show what would be sent to the model without actually sending it"
complete -c fabric -l search -d "Enable web search tool for supported models (Anthropic, OpenAI)"
complete -c fabric -l serve -d "Serve the Fabric Rest API"
complete -c fabric -l serveOllama -d "Serve the Fabric Rest API with ollama endpoints"
complete -c fabric -l version -d "Print current version"
complete -c fabric -l listextensions -d "List all registered extensions"
complete -c fabric -l liststrategies -d "List all strategies"
complete -c fabric -l listvendors -d "List all vendors"
complete -c fabric -l shell-complete-list -d "Output raw list without headers/formatting (for shell completion)"
complete -c fabric -s h -l help -d "Show this help message"

View File

@@ -1,252 +0,0 @@
package core
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/plugins/ai"
"github.com/danielmiessler/fabric/plugins/db/fsdb"
"github.com/danielmiessler/fabric/plugins/strategy"
"github.com/danielmiessler/fabric/plugins/template"
)
const NoSessionPatternUserMessages = "no session, pattern or user messages provided"
type Chatter struct {
db *fsdb.Db
Stream bool
DryRun bool
model string
modelContextLength int
vendor ai.Vendor
strategy string
}
// Send processes a chat request and applies file changes for create_coding_feature pattern
func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (session *fsdb.Session, err error) {
modelToUse := opts.Model
if modelToUse == "" {
modelToUse = o.model
}
if o.vendor.NeedsRawMode(modelToUse) {
opts.Raw = true
}
if session, err = o.BuildSession(request, opts.Raw); err != nil {
return
}
vendorMessages := session.GetVendorMessages()
if len(vendorMessages) == 0 {
if session.Name != "" {
err = o.db.Sessions.SaveSession(session)
if err != nil {
return
}
}
err = fmt.Errorf("no messages provided")
return
}
if opts.Model == "" {
opts.Model = o.model
}
if opts.ModelContextLength == 0 {
opts.ModelContextLength = o.modelContextLength
}
message := ""
if o.Stream {
channel := make(chan string)
go func() {
if streamErr := o.vendor.SendStream(session.GetVendorMessages(), opts, channel); streamErr != nil {
channel <- streamErr.Error()
}
}()
for response := range channel {
message += response
fmt.Print(response)
}
} else {
if message, err = o.vendor.Send(context.Background(), session.GetVendorMessages(), opts); err != nil {
return
}
}
if message == "" {
session = nil
err = fmt.Errorf("empty response")
return
}
// Process file changes for create_coding_feature pattern
if request.PatternName == "create_coding_feature" {
summary, fileChanges, parseErr := common.ParseFileChanges(message)
if parseErr != nil {
fmt.Printf("Warning: Failed to parse file changes: %v\n", parseErr)
} else if len(fileChanges) > 0 {
projectRoot, err := os.Getwd()
if err != nil {
fmt.Printf("Warning: Failed to get current directory: %v\n", err)
} else {
if applyErr := common.ApplyFileChanges(projectRoot, fileChanges); applyErr != nil {
fmt.Printf("Warning: Failed to apply file changes: %v\n", applyErr)
} else {
fmt.Println("Successfully applied file changes.")
fmt.Printf("You can review the changes with 'git diff' if you're using git.\n\n")
}
}
}
message = summary
}
session.Append(&chat.ChatCompletionMessage{Role: chat.ChatMessageRoleAssistant, Content: message})
if session.Name != "" {
err = o.db.Sessions.SaveSession(session)
}
return
}
func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *fsdb.Session, err error) {
if request.SessionName != "" {
var sess *fsdb.Session
if sess, err = o.db.Sessions.Get(request.SessionName); err != nil {
err = fmt.Errorf("could not find session %s: %v", request.SessionName, err)
return
}
session = sess
} else {
session = &fsdb.Session{}
}
if request.Meta != "" {
session.Append(&chat.ChatCompletionMessage{Role: common.ChatMessageRoleMeta, Content: request.Meta})
}
// if a context name is provided, retrieve it from the database
var contextContent string
if request.ContextName != "" {
var ctx *fsdb.Context
if ctx, err = o.db.Contexts.Get(request.ContextName); err != nil {
err = fmt.Errorf("could not find context %s: %v", request.ContextName, err)
return
}
contextContent = ctx.Content
}
// Process template variables in message content
// Double curly braces {{variable}} indicate template substitution
// Ensure we have a message before processing
if request.Message == nil {
request.Message = &chat.ChatCompletionMessage{
Role: chat.ChatMessageRoleUser,
Content: " ",
}
}
// Now we know request.Message is not nil, process template variables
if request.InputHasVars {
request.Message.Content, err = template.ApplyTemplate(request.Message.Content, request.PatternVariables, "")
if err != nil {
return nil, err
}
}
var patternContent string
inputUsed := false
if request.PatternName != "" {
pattern, err := o.db.Patterns.GetApplyVariables(request.PatternName, request.PatternVariables, request.Message.Content)
if err != nil {
return nil, fmt.Errorf("could not get pattern %s: %v", request.PatternName, err)
}
patternContent = pattern.Pattern
inputUsed = true
}
systemMessage := strings.TrimSpace(contextContent) + strings.TrimSpace(patternContent)
if request.StrategyName != "" {
strategy, err := strategy.LoadStrategy(request.StrategyName)
if err != nil {
return nil, fmt.Errorf("could not load strategy %s: %v", request.StrategyName, err)
}
if strategy != nil && strategy.Prompt != "" {
// prepend the strategy prompt to the system message
systemMessage = fmt.Sprintf("%s\n%s", strategy.Prompt, systemMessage)
}
}
// Apply refined language instruction if specified
if request.Language != "" && request.Language != "en" {
// Refined instruction: Execute pattern using user input, then translate the entire response.
systemMessage = fmt.Sprintf("%s\n\nIMPORTANT: First, execute the instructions provided in this prompt using the user's input. Second, ensure your entire final response, including any section headers or titles generated as part of executing the instructions, is written ONLY in the %s language.", systemMessage, request.Language)
}
if raw {
var finalContent string
if systemMessage != "" {
if request.PatternName != "" {
finalContent = systemMessage
} else {
finalContent = fmt.Sprintf("%s\n\n%s", systemMessage, request.Message.Content)
}
// Handle MultiContent properly in raw mode
if len(request.Message.MultiContent) > 0 {
// When we have attachments, add the text as a text part in MultiContent
newMultiContent := []chat.ChatMessagePart{
{
Type: chat.ChatMessagePartTypeText,
Text: finalContent,
},
}
// Add existing non-text parts (like images)
for _, part := range request.Message.MultiContent {
if part.Type != chat.ChatMessagePartTypeText {
newMultiContent = append(newMultiContent, part)
}
}
request.Message = &chat.ChatCompletionMessage{
Role: chat.ChatMessageRoleUser,
MultiContent: newMultiContent,
}
} else {
// No attachments, use regular Content field
request.Message = &chat.ChatCompletionMessage{
Role: chat.ChatMessageRoleUser,
Content: finalContent,
}
}
}
if request.Message != nil {
session.Append(request.Message)
}
} else {
if systemMessage != "" {
session.Append(&chat.ChatCompletionMessage{Role: chat.ChatMessageRoleSystem, Content: systemMessage})
}
// If multi-part content, it is in the user message, and should be added.
// Otherwise, we should only add it if we have not already used it in the systemMessage.
if len(request.Message.MultiContent) > 0 || (request.Message != nil && !inputUsed) {
session.Append(request.Message)
}
}
if session.IsEmpty() {
session = nil
err = errors.New(NoSessionPatternUserMessages)
}
return
}

View File

@@ -1,325 +0,0 @@
package core
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/danielmiessler/fabric/plugins/ai/bedrock"
"github.com/danielmiessler/fabric/plugins/ai/exolab"
"github.com/danielmiessler/fabric/plugins/ai/perplexity" // Added Perplexity plugin
"github.com/danielmiessler/fabric/plugins/strategy"
"github.com/samber/lo"
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/plugins"
"github.com/danielmiessler/fabric/plugins/ai"
"github.com/danielmiessler/fabric/plugins/ai/anthropic"
"github.com/danielmiessler/fabric/plugins/ai/azure"
"github.com/danielmiessler/fabric/plugins/ai/dryrun"
"github.com/danielmiessler/fabric/plugins/ai/gemini"
"github.com/danielmiessler/fabric/plugins/ai/lmstudio"
"github.com/danielmiessler/fabric/plugins/ai/ollama"
"github.com/danielmiessler/fabric/plugins/ai/openai"
"github.com/danielmiessler/fabric/plugins/ai/openai_compatible"
"github.com/danielmiessler/fabric/plugins/db/fsdb"
"github.com/danielmiessler/fabric/plugins/template"
"github.com/danielmiessler/fabric/plugins/tools"
"github.com/danielmiessler/fabric/plugins/tools/custom_patterns"
"github.com/danielmiessler/fabric/plugins/tools/jina"
"github.com/danielmiessler/fabric/plugins/tools/lang"
"github.com/danielmiessler/fabric/plugins/tools/youtube"
)
// hasAWSCredentials checks if any AWS credentials are present either in the
// environment variables or in the default/shared credentials file. It doesn't
// attempt to verify the validity of the credentials, but simply ensures that a
// potential authentication source exists so we can safely initialize the
// Bedrock client without causing the AWS SDK to search for credentials.
func hasAWSCredentials() bool {
if os.Getenv("AWS_PROFILE") != "" ||
os.Getenv("AWS_ROLE_SESSION_NAME") != "" ||
(os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "") {
return true
}
credFile := os.Getenv("AWS_SHARED_CREDENTIALS_FILE")
if credFile == "" {
if home, err := os.UserHomeDir(); err == nil {
credFile = filepath.Join(home, ".aws", "credentials")
}
}
if credFile != "" {
if _, err := os.Stat(credFile); err == nil {
return true
}
}
return false
}
func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry, err error) {
ret = &PluginRegistry{
Db: db,
VendorManager: ai.NewVendorsManager(),
VendorsAll: ai.NewVendorsManager(),
PatternsLoader: tools.NewPatternsLoader(db.Patterns),
CustomPatterns: custom_patterns.NewCustomPatterns(),
YouTube: youtube.NewYouTube(),
Language: lang.NewLanguage(),
Jina: jina.NewClient(),
Strategies: strategy.NewStrategiesManager(),
}
var homedir string
if homedir, err = os.UserHomeDir(); err != nil {
return
}
ret.TemplateExtensions = template.NewExtensionManager(filepath.Join(homedir, ".config/fabric"))
ret.Defaults = tools.NeeDefaults(ret.GetModels)
// Create a vendors slice to hold all vendors (order doesn't matter initially)
vendors := []ai.Vendor{}
// Add non-OpenAI compatible clients
vendors = append(vendors,
openai.NewClient(),
ollama.NewClient(),
azure.NewClient(),
gemini.NewClient(),
anthropic.NewClient(),
lmstudio.NewClient(),
exolab.NewClient(),
perplexity.NewClient(), // Added Perplexity client
)
if hasAWSCredentials() {
vendors = append(vendors, bedrock.NewClient())
}
// Add all OpenAI-compatible providers
for providerName := range openai_compatible.ProviderMap {
provider, _ := openai_compatible.GetProviderByName(providerName)
vendors = append(vendors, openai_compatible.NewClient(provider))
}
// Sort vendors by name for consistent ordering (case-insensitive)
sort.Slice(vendors, func(i, j int) bool {
return strings.ToLower(vendors[i].GetName()) < strings.ToLower(vendors[j].GetName())
})
// Add all sorted vendors to VendorsAll
ret.VendorsAll.AddVendors(vendors...)
_ = ret.Configure()
return
}
func (o *PluginRegistry) ListVendors(out io.Writer) error {
vendors := lo.Map(o.VendorsAll.Vendors, func(vendor ai.Vendor, _ int) string {
return vendor.GetName()
})
fmt.Fprint(out, "Available Vendors:\n\n")
for _, vendor := range vendors {
fmt.Fprintf(out, "%s\n", vendor)
}
return nil
}
type PluginRegistry struct {
Db *fsdb.Db
VendorManager *ai.VendorsManager
VendorsAll *ai.VendorsManager
Defaults *tools.Defaults
PatternsLoader *tools.PatternsLoader
CustomPatterns *custom_patterns.CustomPatterns
YouTube *youtube.YouTube
Language *lang.Language
Jina *jina.Client
TemplateExtensions *template.ExtensionManager
Strategies *strategy.StrategiesManager
}
func (o *PluginRegistry) SaveEnvFile() (err error) {
// Now create the .env with all configured VendorsController info
var envFileContent bytes.Buffer
o.Defaults.Settings.FillEnvFileContent(&envFileContent)
o.PatternsLoader.SetupFillEnvFileContent(&envFileContent)
o.CustomPatterns.SetupFillEnvFileContent(&envFileContent)
o.Strategies.SetupFillEnvFileContent(&envFileContent)
for _, vendor := range o.VendorManager.Vendors {
vendor.SetupFillEnvFileContent(&envFileContent)
}
o.YouTube.SetupFillEnvFileContent(&envFileContent)
o.Jina.SetupFillEnvFileContent(&envFileContent)
o.Language.SetupFillEnvFileContent(&envFileContent)
err = o.Db.SaveEnv(envFileContent.String())
return
}
func (o *PluginRegistry) Setup() (err error) {
setupQuestion := plugins.NewSetupQuestion("Enter the number of the plugin to setup")
groupsPlugins := common.NewGroupsItemsSelector("Available plugins (please configure all required plugins):",
func(plugin plugins.Plugin) string {
var configuredLabel string
if plugin.IsConfigured() {
configuredLabel = " (configured)"
} else {
configuredLabel = ""
}
return fmt.Sprintf("%v%v", plugin.GetSetupDescription(), configuredLabel)
})
groupsPlugins.AddGroupItems("AI Vendors [at least one, required]", lo.Map(o.VendorsAll.Vendors,
func(vendor ai.Vendor, _ int) plugins.Plugin {
return vendor
})...)
groupsPlugins.AddGroupItems("Tools", o.CustomPatterns, o.Defaults, o.Jina, o.Language, o.PatternsLoader, o.Strategies, o.YouTube)
for {
groupsPlugins.Print(false)
if answerErr := setupQuestion.Ask("Plugin Number"); answerErr != nil {
break
}
if setupQuestion.Value == "" {
break
}
number, parseErr := strconv.Atoi(setupQuestion.Value)
setupQuestion.Value = ""
if parseErr == nil {
var plugin plugins.Plugin
if _, plugin, err = groupsPlugins.GetGroupAndItemByItemNumber(number); err != nil {
return
}
if pluginSetupErr := plugin.Setup(); pluginSetupErr != nil {
println(pluginSetupErr.Error())
} else {
if err = o.SaveEnvFile(); err != nil {
break
}
}
if _, ok := o.VendorManager.VendorsByName[plugin.GetName()]; !ok {
var vendor ai.Vendor
if vendor, ok = plugin.(ai.Vendor); ok {
o.VendorManager.AddVendors(vendor)
}
}
} else {
break
}
}
err = o.SaveEnvFile()
return
}
func (o *PluginRegistry) SetupVendor(vendorName string) (err error) {
if err = o.VendorsAll.SetupVendor(vendorName, o.VendorManager.VendorsByName); err != nil {
return
}
err = o.SaveEnvFile()
return
}
func (o *PluginRegistry) ConfigureVendors() {
o.VendorManager.Clear()
for _, vendor := range o.VendorsAll.Vendors {
if vendorErr := vendor.Configure(); vendorErr == nil {
o.VendorManager.AddVendors(vendor)
}
}
}
func (o *PluginRegistry) GetModels() (ret *ai.VendorsModels, err error) {
o.ConfigureVendors()
ret, err = o.VendorManager.GetModels()
return
}
// Configure buildClient VendorsController based on the environment variables
func (o *PluginRegistry) Configure() (err error) {
o.ConfigureVendors()
_ = o.Defaults.Configure()
_ = o.PatternsLoader.Configure()
//YouTube and Jina are not mandatory, so ignore not configured error
_ = o.YouTube.Configure()
_ = o.Jina.Configure()
_ = o.Language.Configure()
return
}
func (o *PluginRegistry) GetChatter(model string, modelContextLength int, strategy string, stream bool, dryRun bool) (ret *Chatter, err error) {
ret = &Chatter{
db: o.Db,
Stream: stream,
DryRun: dryRun,
}
defaultModel := o.Defaults.Model.Value
defaultModelContextLength, err := strconv.Atoi(o.Defaults.ModelContextLength.Value)
defaultVendor := o.Defaults.Vendor.Value
vendorManager := o.VendorManager
if err != nil {
defaultModelContextLength = 0
err = nil
}
ret.modelContextLength = modelContextLength
if ret.modelContextLength == 0 {
ret.modelContextLength = defaultModelContextLength
}
if dryRun {
ret.vendor = dryrun.NewClient()
ret.model = model
if ret.model == "" {
ret.model = defaultModel
}
} else if model == "" {
ret.vendor = vendorManager.FindByName(defaultVendor)
ret.model = defaultModel
} else {
var models *ai.VendorsModels
if models, err = vendorManager.GetModels(); err != nil {
return
}
ret.vendor = vendorManager.FindByName(models.FindGroupsByItemFirst(model))
ret.model = model
}
if ret.vendor == nil {
var errMsg string
if defaultModel == "" || defaultVendor == "" {
errMsg = "Please run, fabric --setup, and select default model and vendor."
} else {
errMsg = "could not find vendor."
}
err = fmt.Errorf(
" Requested Model = %s\n Default Model = %s\n Default Vendor = %s.\n\n%s",
model, defaultModel, defaultVendor, errMsg)
return
}
ret.strategy = strategy
return
}

View File

@@ -1,21 +0,0 @@
package core
import (
"os"
"testing"
"github.com/danielmiessler/fabric/plugins/db/fsdb"
)
func TestSaveEnvFile(t *testing.T) {
db := fsdb.NewDb(os.TempDir())
registry, err := NewPluginRegistry(db)
if err != nil {
t.Fatalf("NewPluginRegistry() error = %v", err)
}
err = registry.SaveEnvFile()
if err != nil {
t.Fatalf("SaveEnvFile() error = %v", err)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
version: '3.8'
services:
fabric-api:
build: .
ports:
- "8080:8080"
volumes:
- ./ENV:/root/.config/fabric/.env:ro
environment:
- GIN_MODE=release

119
flake.lock generated
View File

@@ -1,119 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gomod2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1742209644,
"narHash": "sha256-jMy1XqXqD0/tJprEbUmKilTkvbDY/C0ZGSsJJH4TNCE=",
"owner": "nix-community",
"repo": "gomod2nix",
"rev": "8f3534eb8f6c5c3fce799376dc3b91bae6b11884",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "gomod2nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1745234285,
"narHash": "sha256-GfpyMzxwkfgRVN0cTGQSkTC0OHhEkv3Jf6Tcjm//qZ0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c11863f1e964833214b767f4a369c6e6a7aba141",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"gomod2nix": "gomod2nix",
"nixpkgs": "nixpkgs",
"systems": "systems_2",
"treefmt-nix": "treefmt-nix"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1744961264,
"narHash": "sha256-aRmUh0AMwcbdjJHnytg1e5h5ECcaWtIFQa6d9gI85AI=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "8d404a69efe76146368885110f29a2ca3700bee6",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,80 +0,0 @@
{
description = "Fabric is an open-source framework for augmenting humans using AI. It provides a modular framework for solving specific problems using a crowdsourced set of AI prompts that can be used anywhere";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
systems.url = "github:nix-systems/default";
treefmt-nix = {
url = "github:numtide/treefmt-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
gomod2nix = {
url = "github:nix-community/gomod2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
systems,
treefmt-nix,
gomod2nix,
...
}:
let
forAllSystems = nixpkgs.lib.genAttrs (import systems);
getGoVersion = system: nixpkgs.legacyPackages.${system}.go_1_24;
treefmtEval = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
treefmt-nix.lib.evalModule pkgs ./nix/treefmt.nix
);
in
{
formatter = forAllSystems (system: treefmtEval.${system}.config.build.wrapper);
checks = forAllSystems (system: {
formatting = treefmtEval.${system}.config.build.check self;
});
devShells = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
goVersion = getGoVersion system;
goEnv = gomod2nix.legacyPackages.${system}.mkGoEnv {
pwd = ./.;
go = goVersion;
};
in
import ./nix/shell.nix {
inherit pkgs goEnv goVersion;
inherit (gomod2nix.legacyPackages.${system}) gomod2nix;
}
);
packages = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
goVersion = getGoVersion system;
in
{
default = self.packages.${system}.fabric;
fabric = pkgs.callPackage ./nix/pkgs/fabric {
go = goVersion;
inherit (gomod2nix.legacyPackages.${system}) buildGoApplication;
};
inherit (gomod2nix.legacyPackages.${system}) gomod2nix;
}
);
};
}

120
go.mod
View File

@@ -1,120 +0,0 @@
module github.com/danielmiessler/fabric
go 1.24.0
toolchain go1.24.2
require (
github.com/anthropics/anthropic-sdk-go v1.4.0
github.com/atotto/clipboard v0.1.4
github.com/aws/aws-sdk-go-v2 v1.36.4
github.com/aws/aws-sdk-go-v2/config v1.27.27
github.com/aws/aws-sdk-go-v2/service/bedrock v1.34.1
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.30.0
github.com/gabriel-vasile/mimetype v1.4.9
github.com/gin-gonic/gin v1.10.1
github.com/go-git/go-git/v5 v5.16.2
github.com/go-shiori/go-readability v0.0.0-20250217085726-9f5bf5ca7612
github.com/google/generative-ai-go v0.20.1
github.com/jessevdk/go-flags v1.6.1
github.com/joho/godotenv v1.5.1
github.com/ollama/ollama v0.9.0
github.com/openai/openai-go v1.8.2
github.com/otiai10/copy v1.14.1
github.com/pkg/errors v0.9.1
github.com/samber/lo v1.50.0
github.com/sgaunet/perplexity-go/v2 v2.8.0
github.com/stretchr/testify v1.10.0
golang.org/x/oauth2 v0.30.0
golang.org/x/text v0.26.0
google.golang.org/api v0.236.0
gopkg.in/yaml.v3 v3.0.1
)
require (
cloud.google.com/go v0.121.2 // indirect
cloud.google.com/go/ai v0.12.1 // indirect
cloud.google.com/go/auth v0.16.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.14 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

361
go.sum
View File

@@ -1,361 +0,0 @@
cloud.google.com/go v0.121.2 h1:v2qQpN6Dx9x2NmwrqlesOt3Ys4ol5/lFZ6Mg1B7OJCg=
cloud.google.com/go v0.121.2/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw=
cloud.google.com/go/ai v0.12.1 h1:m1n/VjUuHS+pEO/2R4/VbuuEIkgk0w67fDQvFaMngM0=
cloud.google.com/go/ai v0.12.1/go.mod h1:5vIPNe1ZQsVZqCliXIPL4QnhObQQY4d9hAGHdVc4iw4=
cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/anthropics/anthropic-sdk-go v1.4.0 h1:fU1jKxYbQdQDiEXCxeW5XZRIOwKevn/PMg8Ay1nnUx0=
github.com/anthropics/anthropic-sdk-go v1.4.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go-v2 v1.36.4 h1:GySzjhVvx0ERP6eyfAbAuAXLtAda5TEy19E5q5W8I9E=
github.com/aws/aws-sdk-go-v2 v1.36.4/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 h1:o1v1VFfPcDVlK3ll1L5xHsaQAFdNtZ5GXnNR7SwueC4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35/go.mod h1:rZUQNYMNG+8uZxz9FOerQJ+FceCiodXvixpeRtdESrU=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 h1:R5b82ubO2NntENm3SAm0ADME+H630HomNJdgv+yZ3xw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35/go.mod h1:FuA+nmgMRfkzVKYDNEqQadvEMxtxl9+RLT9ribCwEMs=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/service/bedrock v1.34.1 h1:sD4KqDKG8aOaMWaWTMB8l8VnLa/Di7XHb0Uf4plrndA=
github.com/aws/aws-sdk-go-v2/service/bedrock v1.34.1/go.mod h1:lrn8DOVFYFeaUZKxJ95T5eGDBjnhffgGz68Wq2sfBbA=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.30.0 h1:eMOwQ8ZZK+76+08RfxeaGUtRFN6wxmD1rvqovc2kq2w=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.30.0/go.mod h1:0b5Rq7rUvSQFYHI1UO0zFTV/S6j6DUyuykXA80C+YOI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c h1:wpkoddUomPfHiOziHZixGO5ZBS73cKqVzZipfrLmO1w=
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c/go.mod h1:oVDCh3qjJMLVUSILBRwrm+Bc6RNXGZYtoh9xdvf1ffM=
github.com/go-shiori/go-readability v0.0.0-20250217085726-9f5bf5ca7612 h1:BYLNYdZaepitbZreRIa9xeCQZocWmy/wj4cGIH0qyw0=
github.com/go-shiori/go-readability v0.0.0-20250217085726-9f5bf5ca7612/go.mod h1:wgqthQa8SAYs0yyljVeCOQlZ027VW5CmLsbi9jWC08c=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/generative-ai-go v0.20.1 h1:6dEIujpgN2V0PgLhr6c/M1ynRdc7ARtiIDPFzj45uNQ=
github.com/google/generative-ai-go v0.20.1/go.mod h1:TjOnZJmZKzarWbjUJgy+r3Ee7HGBRVLhOIgupnwR4Bg=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ollama/ollama v0.9.0 h1:GvdGhi8G/QMnFrY0TMLDy1bXua+Ify8KTkFe4ZY/OZs=
github.com/ollama/ollama v0.9.0/go.mod h1:aio9yQ7nc4uwIbn6S0LkGEPgn8/9bNQLL1nHuH+OcD0=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/openai/openai-go v1.8.2 h1:UqSkJ1vCOPUpz9Ka5tS0324EJFEuOvMc+lA/EarJWP8=
github.com/openai/openai-go v1.8.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sgaunet/perplexity-go/v2 v2.8.0 h1:stnuVieniZMGo6qJLCV2JyR2uF7K5398YOA/ZZcgrSg=
github.com/sgaunet/perplexity-go/v2 v2.8.0/go.mod h1:MSks4RNuivCi0GqJyylhFdgSJFVEwZHjAhrf86Wkynk=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw=
github.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.236.0 h1:CAiEiDVtO4D/Qja2IA9VzlFrgPnK3XVMmRoJZlSWbc0=
google.golang.org/api v0.236.0/go.mod h1:X1WF9CU2oTc+Jml1tiIxGmWFK/UZezdqEu09gcxZAj4=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

86
helpers/vm Executable file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env python3
import sys
import re
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from youtube_transcript_api import YouTubeTranscriptApi
from dotenv import load_dotenv
import os
import json
import isodate
import argparse
def get_video_id(url):
# Extract video ID from URL
pattern = r'(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})'
match = re.search(pattern, url)
return match.group(1) if match else None
def main(url, options):
# Load environment variables from .env file
load_dotenv(os.path.expanduser('~/.config/fabric/.env'))
# Get YouTube API key from environment variable
api_key = os.getenv('YOUTUBE_API_KEY')
if not api_key:
print("Error: YOUTUBE_API_KEY not found in ~/.config/fabric/.env")
return
# Extract video ID from URL
video_id = get_video_id(url)
if not video_id:
print("Invalid YouTube URL")
return
try:
# Initialize the YouTube API client
youtube = build('youtube', 'v3', developerKey=api_key)
# Get video details
video_response = youtube.videos().list(
id=video_id,
part='contentDetails'
).execute()
# Extract video duration and convert to minutes
duration_iso = video_response['items'][0]['contentDetails']['duration']
duration_seconds = isodate.parse_duration(duration_iso).total_seconds()
duration_minutes = round(duration_seconds / 60)
# Get video transcript
try:
transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
transcript_text = ' '.join([item['text'] for item in transcript_list])
transcript_text = transcript_text.replace('\n', ' ')
except Exception as e:
transcript_text = "Transcript not available."
# Output based on options
if options.duration:
print(duration_minutes)
elif options.transcript:
print(transcript_text)
else:
# Create JSON object
output = {
"transcript": transcript_text,
"duration": duration_minutes
}
# Print JSON object
print(json.dumps(output))
except HttpError as e:
print("Error: Failed to access YouTube API. Please check your YOUTUBE_API_KEY and ensure it is valid.")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='vm (video meta) extracts metadata about a video, such as the transcript and the video\'s duration. By Daniel Miessler.')
parser.add_argument('url', nargs='?', help='YouTube video URL')
parser.add_argument('--duration', action='store_true', help='Output only the duration')
parser.add_argument('--transcript', action='store_true', help='Output only the transcript')
args = parser.parse_args()
if args.url:
main(args.url, args)
else:
parser.print_help()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 KiB

5
installer/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .client.cli import main as cli
from .server import (
run_api_server,
run_webui_server,
)

View File

@@ -0,0 +1,69 @@
# The `fabric` client
This is the primary `fabric` client, which has multiple modes of operation.
## Client modes
You can use the client in three different modes:
1. **Local Only:** You can use the client without a server, and it will use patterns it's downloaded from this repository, or ones that you specify.
2. **Local Server:** You can run your own version of a Fabric Mill locally (on a private IP), which you can then connect to and use.
3. **Remote Server:** You can specify a remote server that your client commands will then be calling.
## Client features
1. Standalone Mode: Run without needing a server.
2. Clipboard Integration: Copy responses to the clipboard.
3. File Output: Save responses to files for later reference.
4. Pattern Module: Utilize specific patterns for different types of analysis.
5. Server Mode: Operate the tool in server mode to control your own patterns and let your other apps access it.
## Installation
Please check our main [setting up the fabric commands](./../../../README.md#setting-up-the-fabric-commands) section.
## Usage
To use `fabric`, call it with your desired options (remember to activate the virtual environment with `poetry shell` - step 5 above):
fabric [options]
Options include:
--pattern, -p: Select the module for analysis.
--stream, -s: Stream output to another application.
--output, -o: Save the response to a file.
--copy, -C: Copy the response to the clipboard.
--context, -c: Use Context file (context.md) to add context to your pattern
Example:
```bash
# Pasting in an article about LLMs
pbpaste | fabric --pattern extract_wisdom --output wisdom.txt | fabric --pattern summarize --stream
```
```markdown
ONE SENTENCE SUMMARY:
- The content covered the basics of LLMs and how they are used in everyday practice.
MAIN POINTS:
1. LLMs are large language models, and typically use the transformer architecture.
2. LLMs used to be used for story generation, but they're now used for many AI applications.
3. They are vulnerable to hallucination if not configured correctly, so be careful.
TAKEAWAYS:
1. It's possible to use LLMs for multiple AI use cases.
2. It's important to validate that the results you're receiving are correct.
3. The field of AI is moving faster than ever as a result of GenAI breakthroughs.
```
## Contributing
We welcome contributions to Fabric, including improvements and feature additions to this client.
## Credits
The `fabric` client was created by Jonathan Dunn and Daniel Meissler.

View File

@@ -0,0 +1 @@
from .fabric import main

View File

@@ -0,0 +1,3 @@
# Context
please give all responses in spanish

116
installer/client/cli/fabric.py Executable file
View File

@@ -0,0 +1,116 @@
from .utils import Standalone, Update, Setup, Alias, Whisper
import argparse
import sys
import time
import os
script_directory = os.path.dirname(os.path.realpath(__file__))
def main():
parser = argparse.ArgumentParser(
description="An open source framework for augmenting humans using AI."
)
parser.add_argument("--text", "-t", help="Text to extract summary from")
parser.add_argument(
"--copy", "-C", help="Copy the response to the clipboard", action="store_true"
)
parser.add_argument(
"--output",
"-o",
help="Save the response to a file",
nargs="?",
const="analyzepaper.txt",
default=None,
)
parser.add_argument(
"--stream",
"-s",
help="Use this option if you want to see the results in realtime. NOTE: You will not be able to pipe the output into another command.",
action="store_true",
)
parser.add_argument('--transcribe', '-T',
help="transcribe audio, please enter the path to the audio file, or a url with the audio file")
parser.add_argument(
"--list", "-l", help="List available patterns", action="store_true"
)
parser.add_argument(
"--update", "-u", help="Update patterns", action="store_true")
parser.add_argument("--pattern", "-p", help="The pattern (prompt) to use")
parser.add_argument(
"--setup", help="Set up your fabric instance", action="store_true"
)
parser.add_argument(
"--model", "-m", help="Select the model to use (GPT-4 by default)", default="gpt-4-turbo-preview"
)
parser.add_argument(
"--listmodels", help="List all available models", action="store_true"
)
parser.add_argument('--context', '-c',
help="Use Context file (context.md) to add context to your pattern", action="store_true")
args = parser.parse_args()
home_holder = os.path.expanduser("~")
config = os.path.join(home_holder, ".config", "fabric")
config_patterns_directory = os.path.join(config, "patterns")
config_context = os.path.join(config, "context.md")
env_file = os.path.join(config, ".env")
if not os.path.exists(config):
os.makedirs(config)
if args.setup:
Setup().run()
Alias()
sys.exit()
if not os.path.exists(env_file) or not os.path.exists(config_patterns_directory):
print("Please run --setup to set up your API key and download patterns.")
sys.exit()
if not os.path.exists(config_patterns_directory):
Update()
Alias()
sys.exit()
if args.update:
Update()
Alias()
sys.exit()
if args.context:
if not os.path.exists(os.path.join(config, "context.md")):
print("Please create a context.md file in ~/.config/fabric")
sys.exit()
standalone = Standalone(args, args.pattern)
if args.list:
try:
direct = os.listdir(config_patterns_directory)
for d in direct:
print(d)
sys.exit()
except FileNotFoundError:
print("No patterns found")
sys.exit()
if args.listmodels:
standalone.fetch_available_models()
sys.exit()
if args.transcribe:
whisper = Whisper()
whisper.process_file(args.transcribe)
sys.exit()
if args.text is not None:
text = args.text
else:
text = standalone.get_cli_input()
if args.stream and not args.context:
standalone.streamMessage(text)
if args.stream and args.context:
with open(config_context, "r") as f:
context = f.read()
standalone.streamMessage(text, context=context)
elif args.context:
with open(config_context, "r") as f:
context = f.read()
standalone.sendMessage(text, context=context)
else:
standalone.sendMessage(text)
if __name__ == "__main__":
main()

6
installer/client/cli/p.py Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env python3
import pyperclip
pasted_text = pyperclip.paste()
print(pasted_text)

View File

@@ -0,0 +1,495 @@
import requests
import os
from openai import OpenAI
import pyperclip
import sys
import platform
from dotenv import load_dotenv
from requests.exceptions import HTTPError
from tqdm import tqdm
import zipfile
import tempfile
import shutil
from pydub import AudioSegment
current_directory = os.path.dirname(os.path.realpath(__file__))
config_directory = os.path.expanduser("~/.config/fabric")
env_file = os.path.join(config_directory, ".env")
class Standalone:
def __init__(self, args, pattern="", env_file="~/.config/fabric/.env"):
""" Initialize the class with the provided arguments and environment file.
Args:
args: The arguments for initialization.
pattern: The pattern to be used (default is an empty string).
env_file: The path to the environment file (default is "~/.config/fabric/.env").
Returns:
None
Raises:
KeyError: If the "OPENAI_API_KEY" is not found in the environment variables.
FileNotFoundError: If no API key is found in the environment variables.
"""
# Expand the tilde to the full path
env_file = os.path.expanduser(env_file)
load_dotenv(env_file)
try:
apikey = os.environ["OPENAI_API_KEY"]
self.client = OpenAI()
self.client.api_key = apikey
except KeyError:
print("OPENAI_API_KEY not found in environment variables.")
except FileNotFoundError:
print("No API key found. Use the --apikey option to set the key")
sys.exit()
self.config_pattern_directory = config_directory
self.pattern = pattern
self.args = args
self.model = args.model
def streamMessage(self, input_data: str, context=""):
""" Stream a message and handle exceptions.
Args:
input_data (str): The input data for the message.
Returns:
None: If the pattern is not found.
Raises:
FileNotFoundError: If the pattern file is not found.
"""
wisdomFilePath = os.path.join(
config_directory, f"patterns/{self.pattern}/system.md"
)
user_message = {"role": "user", "content": f"{input_data}"}
wisdom_File = os.path.join(current_directory, wisdomFilePath)
buffer = ""
if self.pattern:
try:
with open(wisdom_File, "r") as f:
if context:
system = context + '\n\n' + f.read()
else:
system = f.read()
system_message = {"role": "system", "content": system}
messages = [system_message, user_message]
except FileNotFoundError:
print("pattern not found")
return
else:
if context:
user_message += {role: "system", content: context}
messages = [user_message]
else:
messages = [user_message]
try:
stream = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=0.0,
top_p=1,
frequency_penalty=0.1,
presence_penalty=0.1,
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content is not None:
char = chunk.choices[0].delta.content
buffer += char
if char not in ["\n", " "]:
print(char, end="")
elif char == " ":
print(" ", end="") # Explicitly handle spaces
elif char == "\n":
print() # Handle newlines
sys.stdout.flush()
except Exception as e:
print(f"Error: {e}")
print(e)
if self.args.copy:
pyperclip.copy(buffer)
if self.args.output:
with open(self.args.output, "w") as f:
f.write(buffer)
def sendMessage(self, input_data: str, context=""):
""" Send a message using the input data and generate a response.
Args:
input_data (str): The input data to be sent as a message.
Returns:
None
Raises:
FileNotFoundError: If the specified pattern file is not found.
"""
wisdomFilePath = os.path.join(
config_directory, f"patterns/{self.pattern}/system.md"
)
user_message = {"role": "user", "content": f"{input_data}"}
wisdom_File = os.path.join(current_directory, wisdomFilePath)
if self.pattern:
try:
with open(wisdom_File, "r") as f:
if context:
system = context + '\n\n' + f.read()
else:
system = f.read()
system_message = {"role": "system", "content": system}
messages = [system_message, user_message]
except FileNotFoundError:
print("pattern not found")
return
else:
if context:
user_message += {'role': 'system', 'content': context}
messages = [user_message]
else:
messages = [user_message]
try:
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=0.0,
top_p=1,
frequency_penalty=0.1,
presence_penalty=0.1,
)
print(response.choices[0].message.content)
except Exception as e:
print(f"Error: {e}")
print(e)
if self.args.copy:
pyperclip.copy(response.choices[0].message.content)
if self.args.output:
with open(self.args.output, "w") as f:
f.write(response.choices[0].message.content)
def fetch_available_models(self):
headers = {
"Authorization": f"Bearer {self.client.api_key}"
}
response = requests.get(
"https://api.openai.com/v1/models", headers=headers)
if response.status_code == 200:
models = response.json().get("data", [])
# Filter only gpt models
gpt_models = [model for model in models if model.get(
"id", "").startswith(("gpt"))]
# Sort the models alphabetically by their ID
sorted_gpt_models = sorted(gpt_models, key=lambda x: x.get("id"))
for model in sorted_gpt_models:
print(model.get("id"))
else:
print(f"Failed to fetch models: HTTP {response.status_code}")
def get_cli_input(self):
""" aided by ChatGPT; uses platform library
accepts either piped input or console input
from either Windows or Linux
Args:
none
Returns:
string from either user or pipe
"""
system = platform.system()
if system == 'Windows':
if not sys.stdin.isatty(): # Check if input is being piped
return sys.stdin.read().strip() # Read piped input
else:
# Prompt user for input from console
return input("Enter Question: ")
else:
return sys.stdin.read()
class Whisper:
def __init__(self):
env_file = os.path.expanduser("~/.config/fabric/.env")
load_dotenv(env_file)
try:
apikey = os.environ["OPENAI_API_KEY"]
self.client = OpenAI()
self.client.api_key = apikey
except KeyError:
print("OPENAI_API_KEY not found in environment variables.")
except FileNotFoundError:
print("No API key found. Use the --apikey option to set the key")
self.whole_response = []
def split_audio(self, file_path):
"""
Splits the audio file into segments of the given length.
Args:
- file_path: The path to the audio file.
- segment_length_ms: Length of each segment in milliseconds.
Returns:
- A list of audio segments.
"""
audio = AudioSegment.from_file(file_path)
segments = []
segment_length_ms = 10 * 60 * 1000 # 10 minutes in milliseconds
for start_ms in range(0, len(audio), segment_length_ms):
end_ms = start_ms + segment_length_ms
segment = audio[start_ms:end_ms]
segments.append(segment)
return segments
def process_segment(self, segment):
""" Transcribe an audio file and print the transcript.
Args:
audio_file (str): The path to the audio file to be transcribed.
Returns:
None
"""
try:
# if audio_file.startswith("http"):
# response = requests.get(audio_file)
# response.raise_for_status()
# with tempfile.NamedTemporaryFile(delete=False) as f:
# f.write(response.content)
# audio_file = f.name
audio_file = open(segment, "rb")
response = self.client.audio.transcriptions.create(
model="whisper-1",
file=audio_file
)
self.whole_response.append(response.text)
except Exception as e:
print(f"Error: {e}")
def process_file(self, audio_file):
""" Transcribe an audio file and print the transcript.
Args:
audio_file (str): The path to the audio file to be transcribed.
Returns:
None
"""
try:
# if audio_file.startswith("http"):
# response = requests.get(audio_file)
# response.raise_for_status()
# with tempfile.NamedTemporaryFile(delete=False) as f:
# f.write(response.content)
# audio_file = f.name
segments = self.split_audio(audio_file)
for i, segment in enumerate(segments):
segment_file_path = f"segment_{i}.mp3"
segment.export(segment_file_path, format="mp3")
self.process_segment(segment_file_path)
print(' '.join(self.whole_response))
except Exception as e:
print(f"Error: {e}")
class Update:
def __init__(self):
"""Initialize the object with default values."""
self.repo_zip_url = "https://github.com/danielmiessler/fabric/archive/refs/heads/main.zip"
self.config_directory = os.path.expanduser("~/.config/fabric")
self.pattern_directory = os.path.join(
self.config_directory, "patterns")
os.makedirs(self.pattern_directory, exist_ok=True)
print("Updating patterns...")
self.update_patterns() # Start the update process immediately
def update_patterns(self):
"""Update the patterns by downloading the zip from GitHub and extracting it."""
with tempfile.TemporaryDirectory() as temp_dir:
zip_path = os.path.join(temp_dir, "repo.zip")
self.download_zip(self.repo_zip_url, zip_path)
extracted_folder_path = self.extract_zip(zip_path, temp_dir)
# The patterns folder will be inside "fabric-main" after extraction
patterns_source_path = os.path.join(
extracted_folder_path, "fabric-main", "patterns")
if os.path.exists(patterns_source_path):
# If the patterns directory already exists, remove it before copying over the new one
if os.path.exists(self.pattern_directory):
shutil.rmtree(self.pattern_directory)
shutil.copytree(patterns_source_path, self.pattern_directory)
print("Patterns updated successfully.")
else:
print("Patterns folder not found in the downloaded zip.")
def download_zip(self, url, save_path):
"""Download the zip file from the specified URL."""
response = requests.get(url)
response.raise_for_status() # Check if the download was successful
with open(save_path, 'wb') as f:
f.write(response.content)
print("Downloaded zip file successfully.")
def extract_zip(self, zip_path, extract_to):
"""Extract the zip file to the specified directory."""
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_to)
print("Extracted zip file successfully.")
return extract_to # Return the path to the extracted contents
class Alias:
def __init__(self):
self.config_files = []
home_directory = os.path.expanduser("~")
self.patterns = os.path.join(home_directory, ".config/fabric/patterns")
if os.path.exists(os.path.join(home_directory, ".bashrc")):
self.config_files.append(os.path.join(home_directory, ".bashrc"))
if os.path.exists(os.path.join(home_directory, ".zshrc")):
self.config_files.append(os.path.join(home_directory, ".zshrc"))
if os.path.exists(os.path.join(home_directory, ".bash_profile")):
self.config_files.append(os.path.join(
home_directory, ".bash_profile"))
self.remove_all_patterns()
self.add_patterns()
print('Aliases added successfully. Please restart your terminal to use them.')
def add(self, name, alias):
for file in self.config_files:
with open(file, "a") as f:
f.write(f"alias {name}='{alias}'\n")
def remove(self, pattern):
for file in self.config_files:
# Read the whole file first
with open(file, "r") as f:
wholeFile = f.read()
# Determine if the line to be removed is in the file
target_line = f"alias {pattern}='fabric --pattern {pattern}'\n"
if target_line in wholeFile:
# If the line exists, replace it with nothing (remove it)
wholeFile = wholeFile.replace(target_line, "")
# Write the modified content back to the file
with open(file, "w") as f:
f.write(wholeFile)
def remove_all_patterns(self):
allPatterns = os.listdir(self.patterns)
for pattern in allPatterns:
self.remove(pattern)
def find_line(self, name):
for file in self.config_files:
with open(file, "r") as f:
lines = f.readlines()
for line in lines:
if line.strip("\n") == f"alias ${name}='{alias}'":
return line
def add_patterns(self):
allPatterns = os.listdir(self.patterns)
for pattern in allPatterns:
self.add(pattern, f"fabric --pattern {pattern}")
class Setup:
def __init__(self):
""" Initialize the object.
Raises:
OSError: If there is an error in creating the pattern directory.
"""
self.config_directory = os.path.expanduser("~/.config/fabric")
self.pattern_directory = os.path.join(
self.config_directory, "patterns")
os.makedirs(self.pattern_directory, exist_ok=True)
self.env_file = os.path.join(self.config_directory, ".env")
def api_key(self, api_key):
""" Set the OpenAI API key in the environment file.
Args:
api_key (str): The API key to be set.
Returns:
None
Raises:
OSError: If the environment file does not exist or cannot be accessed.
"""
if not os.path.exists(self.env_file):
with open(self.env_file, "w") as f:
f.write(f"OPENAI_API_KEY={api_key}")
print(f"OpenAI API key set to {api_key}")
def patterns(self):
""" Method to update patterns and exit the system.
Returns:
None
"""
Update()
def run(self):
""" Execute the Fabric program.
This method prompts the user for their OpenAI API key, sets the API key in the Fabric object, and then calls the patterns method.
Returns:
None
"""
print("Welcome to Fabric. Let's get started.")
apikey = input("Please enter your OpenAI API key\n")
self.api_key(apikey.strip())
self.patterns()
class Transcribe:
def youtube(video_id):
"""
This method gets the transciption
of a YouTube video designated with the video_id
Input:
the video id specifing a YouTube video
an example url for a video: https://www.youtube.com/watch?v=vF-MQmVxnCs&t=306s
the video id is vF-MQmVxnCs&t=306s
Output:
a transcript for the video
Raises:
an exception and prints error
"""
try:
transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
transcript = ""
for segment in transcript_list:
transcript += segment['text'] + " "
return transcript.strip()
except Exception as e:
print("Error:", e)
return None

3
installer/client/gui/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
dist/
build/

View File

@@ -0,0 +1,21 @@
Fabric is not just a tool; it's a transformative step towards integrating the power of GPT prompts into your digital life. With Fabric, you have the ability to create a personal API that brings advanced GPT capabilities into various aspects of your digital environment. Whether you're looking to incorporate powerful GPT prompts into command line operations or extend their functionality to a wider network through a personal API, Fabric is designed to seamlessly blend with your digital ecosystem. This tool is all about augmenting your digital interactions, enhancing productivity, and enabling a more intelligent, GPT-powered experience in every aspect of your online presence.
## Features
1. Text Analysis: Easily extract summaries from texts.
2. Clipboard Integration: Conveniently copy responses to the clipboard.
3. File Output: Save responses to files for later reference.
4. Pattern Module: Utilize specific modules for different types of analysis.
5. Server Mode: Operate the tool in server mode for expanded capabilities.
6. Remote & Standalone Modes: Choose between remote and standalone operations.
## Installation
1. Install dependencies:
`npm install`
2. Start the application:
`npm start`
Contributing
We welcome contributions to Fabric! For details on our code of conduct and the process for submitting pull requests, please read the CONTRIBUTING.md.

View File

@@ -0,0 +1,45 @@
const { OpenAI } = require("openai");
require("dotenv").config({
path: require("os").homedir() + "/.config/fabric/.env",
});
let openaiClient = null;
// Function to initialize and get the OpenAI client
function getOpenAIClient() {
if (!process.env.OPENAI_API_KEY) {
throw new Error(
"The OPENAI_API_KEY environment variable is missing or empty."
);
}
return new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
}
async function queryOpenAI(system, user, callback) {
const openai = getOpenAIClient(); // Ensure the client is initialized here
const messages = [
{ role: "system", content: system },
{ role: "user", content: user },
];
try {
const stream = await openai.chat.completions.create({
model: "gpt-4-1106-preview", // Adjust the model as necessary.
messages: messages,
temperature: 0.0,
top_p: 1,
frequency_penalty: 0.1,
presence_penalty: 0.1,
stream: true,
});
for await (const chunk of stream) {
const message = chunk.choices[0]?.delta?.content || "";
callback(message); // Process each chunk of data
}
} catch (error) {
console.error("Error querying OpenAI:", error);
callback("Error querying OpenAI. Please try again.");
}
}
module.exports = { queryOpenAI };

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fabric</title>
<link rel="stylesheet" href="static/stylesheet/bootstrap.min.css" />
<link rel="stylesheet" href="static/stylesheet/style.css" />
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<a class="navbar-brand" href="#">
<img
src="static/images/fabric-logo-gif.gif"
alt="Fabric Logo"
height="40"
/>
</a>
<button id="configButton" class="btn btn-outline-success my-2 my-sm-0">
Config
</button>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarCollap se"
aria-controls="navbarCollapse"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<button
id="updatePatternsButton"
class="btn btn-outline-success my-2 my-sm-0"
>
Update Patterns
</button>
<div class="collapse navbar-collapse" id="navbarCollapse"></div>
<div class="m1-auto">
<a class="navbar-brand" id="themeChanger" href="#">Dark</a>
</div>
</nav>
<main>
<div class="container" id="my-form">
<select class="form-control" id="patternSelector"></select>
<textarea
rows="5"
class="form-control"
id="userInput"
placeholder="start typing or drag a file (.txt, .svg, .pdf and .doc are currently supported)"
></textarea>
<button class="btn btn-primary" id="submit">Submit</button>
</div>
<div id="configSection" class="container hidden">
<input
type="text"
id="apiKeyInput"
placeholder="Enter OpenAI API Key"
class="form-control"
/>
<button id="saveApiKey" class="btn btn-primary">Save API Key</button>
</div>
<div class="container hidden" id="responseContainer"></div>
</main>
<script src="static/js/jquery-3.0.0.slim.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
<script src="static/js/index.js"></script>
</body>
</html>

View File

@@ -0,0 +1,300 @@
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const pdfParse = require("pdf-parse");
const mammoth = require("mammoth");
const fs = require("fs");
const path = require("path");
const os = require("os");
const { queryOpenAI } = require("./chatgpt.js");
const axios = require("axios");
const fsExtra = require("fs-extra");
let fetch;
import("node-fetch").then((module) => {
fetch = module.default;
});
const unzipper = require("unzipper");
let win;
function promptUserForApiKey() {
// Create a new window to prompt the user for the API key
const promptWindow = new BrowserWindow({
// Window configuration for the prompt
width: 500,
height: 200,
webPreferences: {
nodeIntegration: true,
contextIsolation: false, // Consider security implications
},
});
// Handle the API key submission from the prompt window
ipcMain.on("submit-api-key", (event, apiKey) => {
if (apiKey) {
saveApiKey(apiKey);
promptWindow.close();
createWindow(); // Proceed to create the main window
} else {
// Handle invalid input or user cancellation
promptWindow.close();
}
});
}
function loadApiKey() {
const configPath = path.join(os.homedir(), ".config", "fabric", ".env");
if (fs.existsSync(configPath)) {
const envContents = fs.readFileSync(configPath, { encoding: "utf8" });
const matches = envContents.match(/^OPENAI_API_KEY=(.*)$/m);
if (matches && matches[1]) {
return matches[1];
}
}
return null;
}
function saveApiKey(apiKey) {
const configPath = path.join(os.homedir(), ".config", "fabric");
const envFilePath = path.join(configPath, ".env");
if (!fs.existsSync(configPath)) {
fs.mkdirSync(configPath, { recursive: true });
}
fs.writeFileSync(envFilePath, `OPENAI_API_KEY=${apiKey}`);
process.env.OPENAI_API_KEY = apiKey; // Set for current session
}
function ensureFabricFoldersExist() {
return new Promise(async (resolve, reject) => {
const fabricPath = path.join(os.homedir(), ".config", "fabric");
const patternsPath = path.join(fabricPath, "patterns");
try {
if (!fs.existsSync(fabricPath)) {
fs.mkdirSync(fabricPath, { recursive: true });
}
if (!fs.existsSync(patternsPath)) {
fs.mkdirSync(patternsPath, { recursive: true });
await downloadAndUpdatePatterns(patternsPath);
}
resolve(); // Resolve the promise once everything is set up
} catch (error) {
console.error("Error ensuring fabric folders exist:", error);
reject(error); // Reject the promise if an error occurs
}
});
}
async function downloadAndUpdatePatterns(patternsPath) {
try {
const response = await axios({
method: "get",
url: "https://github.com/danielmiessler/fabric/archive/refs/heads/main.zip",
responseType: "arraybuffer",
});
const zipPath = path.join(os.tmpdir(), "fabric.zip");
fs.writeFileSync(zipPath, response.data);
console.log("Zip file written to:", zipPath);
const tempExtractPath = path.join(os.tmpdir(), "fabric_extracted");
fsExtra.emptyDirSync(tempExtractPath);
await fsExtra.remove(patternsPath); // Delete the existing patterns directory
await fs
.createReadStream(zipPath)
.pipe(unzipper.Extract({ path: tempExtractPath }))
.promise();
console.log("Extraction complete");
const extractedPatternsPath = path.join(
tempExtractPath,
"fabric-main",
"patterns"
);
await fsExtra.copy(extractedPatternsPath, patternsPath);
console.log("Patterns successfully updated");
// Inform the renderer process that the patterns have been updated
win.webContents.send("patterns-updated");
} catch (error) {
console.error("Error downloading or updating patterns:", error);
}
}
function checkApiKeyExists() {
const configPath = path.join(os.homedir(), ".config", "fabric", ".env");
return fs.existsSync(configPath);
}
function getPatternFolders() {
const patternsPath = path.join(os.homedir(), ".config", "fabric", "patterns");
return fs
.readdirSync(patternsPath, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
}
function getPatternContent(patternName) {
const patternPath = path.join(
os.homedir(),
".config",
"fabric",
"patterns",
patternName,
"system.md"
);
try {
return fs.readFileSync(patternPath, "utf8");
} catch (error) {
console.error("Error reading pattern file:", error);
return "";
}
}
function createWindow() {
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.join(__dirname, "preload.js"),
},
});
win.loadFile("index.html");
win.on("closed", () => {
win = null;
});
}
ipcMain.on("process-complex-file", (event, filePath) => {
const extension = path.extname(filePath).toLowerCase();
let fileProcessPromise;
if (extension === ".pdf") {
const dataBuffer = fs.readFileSync(filePath);
fileProcessPromise = pdfParse(dataBuffer).then((data) => data.text);
} else if (extension === ".docx") {
fileProcessPromise = mammoth
.extractRawText({ path: filePath })
.then((result) => result.value)
.catch((err) => {
console.error("Error processing DOCX file:", err);
throw new Error("Error processing DOCX file.");
});
} else {
event.reply("file-response", "Error: Unsupported file type");
return;
}
fileProcessPromise
.then((extractedText) => {
// Sending the extracted text back to the frontend.
event.reply("file-response", extractedText);
})
.catch((error) => {
// Handling any errors during file processing and sending them back to the frontend.
event.reply("file-response", `Error processing file: ${error.message}`);
});
});
ipcMain.on("start-query-openai", async (event, system, user) => {
if (system == null || user == null) {
console.error("Received null for system or user message");
event.reply("openai-response", "Error: System or user message is null.");
return;
}
try {
await queryOpenAI(system, user, (message) => {
event.reply("openai-response", message);
});
} catch (error) {
console.error("Error querying OpenAI:", error);
event.reply("no-api-key", "Error querying OpenAI.");
}
});
// Example of using ipcMain.handle for asynchronous operations
ipcMain.handle("get-patterns", async (event) => {
try {
return getPatternFolders();
} catch (error) {
console.error("Failed to get patterns:", error);
return [];
}
});
ipcMain.on("update-patterns", () => {
const patternsPath = path.join(os.homedir(), ".config", "fabric", "patterns");
downloadAndUpdatePatterns(patternsPath);
});
ipcMain.handle("get-pattern-content", async (event, patternName) => {
try {
return getPatternContent(patternName);
} catch (error) {
console.error("Failed to get pattern content:", error);
return "";
}
});
ipcMain.handle("save-api-key", async (event, apiKey) => {
try {
const configPath = path.join(os.homedir(), ".config", "fabric");
if (!fs.existsSync(configPath)) {
fs.mkdirSync(configPath, { recursive: true });
}
const envFilePath = path.join(configPath, ".env");
fs.writeFileSync(envFilePath, `OPENAI_API_KEY=${apiKey}`);
process.env.OPENAI_API_KEY = apiKey;
return "API Key saved successfully.";
} catch (error) {
console.error("Error saving API key:", error);
throw new Error("Failed to save API Key.");
}
});
app.whenReady().then(async () => {
try {
const apiKey = loadApiKey();
if (!apiKey) {
promptUserForApiKey();
} else {
process.env.OPENAI_API_KEY = apiKey;
createWindow();
}
await ensureFabricFoldersExist(); // Ensure fabric folders exist
createWindow(); // Create the application window
// After window creation, check if the API key exists
if (!checkApiKeyExists()) {
console.log("API key is missing. Prompting user to input API key.");
// Optionally, directly invoke a function here to show a prompt in the renderer process
win.webContents.send("request-api-key");
}
} catch (error) {
console.error("Failed to initialize fabric folders:", error);
// Handle initialization failure (e.g., close the app or show an error message)
}
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (win === null) {
createWindow();
}
});

1644
installer/client/gui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
{
"name": "fabric_electron",
"version": "1.0.0",
"description": "a fabric electron app",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"author": "",
"license": "ISC",
"devDependencies": {
"dotenv": "^16.4.1",
"electron": "^28.2.2",
"openai": "^4.27.0"
},
"dependencies": {
"axios": "^1.6.7",
"mammoth": "^1.6.0",
"node-fetch": "^2.6.7",
"pdf-parse": "^1.1.1",
"unzipper": "^0.10.14"
}
}

View File

@@ -0,0 +1,9 @@
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
send: (channel, ...args) => ipcRenderer.send(channel, ...args),
on: (channel, func) => {
ipcRenderer.on(channel, (event, ...args) => func(...args));
},
});

View File

Before

Width:  |  Height:  |  Size: 42 MiB

After

Width:  |  Height:  |  Size: 42 MiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,266 @@
document.addEventListener("DOMContentLoaded", async function () {
const patternSelector = document.getElementById("patternSelector");
const userInput = document.getElementById("userInput");
const submitButton = document.getElementById("submit");
const responseContainer = document.getElementById("responseContainer");
const themeChanger = document.getElementById("themeChanger");
const configButton = document.getElementById("configButton");
const configSection = document.getElementById("configSection");
const saveApiKeyButton = document.getElementById("saveApiKey");
const apiKeyInput = document.getElementById("apiKeyInput");
const originalPlaceholder = userInput.placeholder;
const updatePatternsButton = document.getElementById("updatePatternsButton");
const copyButton = document.createElement("button");
window.electronAPI.on("patterns-ready", () => {
console.log("Patterns are ready. Refreshing the pattern list.");
loadPatterns();
});
window.electronAPI.on("request-api-key", () => {
// Show the API key input section or modal to the user
configSection.classList.remove("hidden"); // Assuming 'configSection' is your API key input area
});
copyButton.textContent = "Copy";
copyButton.id = "copyButton";
document.addEventListener("click", function (e) {
if (e.target && e.target.id === "copyButton") {
// Your copy to clipboard function
copyToClipboard();
}
});
window.electronAPI.on("no-api-key", () => {
alert("API key is missing. Please enter your OpenAI API key.");
});
window.electronAPI.on("patterns-updated", () => {
alert("Patterns updated. Refreshing the pattern list.");
loadPatterns();
});
function htmlToPlainText(html) {
// Create a temporary div element to hold the HTML
var tempDiv = document.createElement("div");
tempDiv.innerHTML = html;
// Replace <br> tags with newline characters
tempDiv.querySelectorAll("br").forEach((br) => br.replaceWith("\n"));
// Replace block elements like <p> and <div> with newline characters
tempDiv.querySelectorAll("p, div").forEach((block) => {
block.prepend("\n"); // Add a newline before the block element's content
block.replaceWith(...block.childNodes); // Replace the block element with its own contents
});
// Return the text content, trimming leading and trailing newlines
return tempDiv.textContent.trim();
}
async function submitQuery(userInputValue) {
userInput.value = ""; // Clear the input after submitting
systemCommand = await window.electronAPI.invoke(
"get-pattern-content",
patternSelector.value
);
responseContainer.innerHTML = ""; // Clear previous responses
if (responseContainer.classList.contains("hidden")) {
console.log("contains hidden");
responseContainer.classList.remove("hidden");
responseContainer.appendChild(copyButton);
}
window.electronAPI.send(
"start-query-openai",
systemCommand,
userInputValue
);
}
function copyToClipboard() {
const containerClone = responseContainer.cloneNode(true);
// Remove the copy button from the clone
const copyButtonClone = containerClone.querySelector("#copyButton");
if (copyButtonClone) {
copyButtonClone.parentNode.removeChild(copyButtonClone);
}
// Convert HTML to plain text, preserving newlines
const plainText = htmlToPlainText(containerClone.innerHTML);
// Use a temporary textarea for copying
const textArea = document.createElement("textarea");
textArea.style.position = "absolute";
textArea.style.left = "-9999px";
textArea.setAttribute("aria-hidden", "true");
textArea.value = plainText;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand("copy");
console.log("Text successfully copied to clipboard");
} catch (err) {
console.error("Failed to copy text: ", err);
}
document.body.removeChild(textArea);
}
async function loadPatterns() {
try {
const patterns = await window.electronAPI.invoke("get-patterns");
patternSelector.innerHTML = ""; // Clear existing options first
patterns.forEach((pattern) => {
const option = document.createElement("option");
option.value = pattern;
option.textContent = pattern;
patternSelector.appendChild(option);
});
} catch (error) {
console.error("Failed to load patterns:", error);
}
}
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand("copy");
const msg = successful ? "successful" : "unsuccessful";
console.log("Fallback: Copying text command was " + msg);
} catch (err) {
console.error("Fallback: Oops, unable to copy", err);
}
document.body.removeChild(textArea);
}
updatePatternsButton.addEventListener("click", () => {
window.electronAPI.send("update-patterns");
});
// Load patterns on startup
try {
const patterns = await window.electronAPI.invoke("get-patterns");
patterns.forEach((pattern) => {
const option = document.createElement("option");
option.value = pattern;
option.textContent = pattern;
patternSelector.appendChild(option);
});
} catch (error) {
console.error("Failed to load patterns:", error);
}
// Listen for OpenAI responses
window.electronAPI.on("openai-response", (message) => {
const formattedMessage = message.replace(/\n/g, "<br>");
responseContainer.innerHTML += formattedMessage; // Append new data as it arrives
});
window.electronAPI.on("file-response", (message) => {
if (message.startsWith("Error")) {
alert(message);
return;
}
submitQuery(message);
});
// Submit button click handler
submitButton.addEventListener("click", async () => {
const userInputValue = userInput.value;
submitQuery(userInputValue);
});
// Theme changer click handler
themeChanger.addEventListener("click", function (e) {
e.preventDefault();
document.body.classList.toggle("light-theme");
themeChanger.innerText =
themeChanger.innerText === "Dark" ? "Light" : "Dark";
});
// Config button click handler - toggles the config section visibility
configButton.addEventListener("click", function (e) {
e.preventDefault();
configSection.classList.toggle("hidden");
});
// Save API Key button click handler
saveApiKeyButton.addEventListener("click", () => {
const apiKey = apiKeyInput.value;
window.electronAPI
.invoke("save-api-key", apiKey)
.then(() => {
alert("API Key saved successfully.");
// Optionally hide the config section and clear the input after saving
configSection.classList.add("hidden");
apiKeyInput.value = "";
})
.catch((err) => {
console.error("Error saving API key:", err);
alert("Failed to save API Key.");
});
});
// Handler for pattern selection change
patternSelector.addEventListener("change", async () => {
const selectedPattern = patternSelector.value;
const systemCommand = await window.electronAPI.invoke(
"get-pattern-content",
selectedPattern
);
// Use systemCommand as part of the input for querying OpenAI
});
// drag and drop
userInput.addEventListener("dragover", (event) => {
event.stopPropagation();
event.preventDefault();
// Add some visual feedback
userInput.classList.add("drag-over");
userInput.placeholder = "Drop file here";
});
userInput.addEventListener("dragleave", (event) => {
event.stopPropagation();
event.preventDefault();
// Remove visual feedback
userInput.classList.remove("drag-over");
userInput.placeholder = originalPlaceholder;
});
userInput.addEventListener("drop", (event) => {
event.stopPropagation();
event.preventDefault();
const file = event.dataTransfer.files[0];
userInput.classList.remove("drag-over");
userInput.placeholder = originalPlaceholder;
processFile(file);
});
function processFile(file) {
const fileType = file.type;
const reader = new FileReader();
let content = "";
reader.onload = (event) => {
content = event.target.result;
userInput.value = content;
submitQuery(content);
};
if (fileType === "text/plain" || fileType === "image/svg+xml") {
reader.readAsText(file);
} else if (
fileType === "application/pdf" ||
fileType.match(/wordprocessingml/)
) {
// For PDF and DOCX, we need to handle them in the main process due to complexity
window.electronAPI.send("process-complex-file", file.path);
} else {
console.error("Unsupported file type");
}
}
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,160 @@
body {
font-family: "Segoe UI", Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #2b2b2b;
color: #e0e0e0;
}
.container {
max-width: 90%;
margin: 50px auto;
padding: 15px;
background: #333333;
box-shadow: 0 2px 4px rgba(255, 255, 255, 0.1);
border-radius: 5px;
}
#responseContainer {
margin-top: 15px;
border: 1px solid #444;
padding: 10px;
min-height: 100px;
background-color: #3a3a3a;
color: #e0e0e0;
}
.btn-primary {
background-color: #007bff;
color: white;
border: none;
}
#userInput {
margin-bottom: 10px;
background-color: #424242; /* Darker shade for textarea */
color: #e0e0e0; /* Light text for readability */
border: 1px solid #555; /* Adjusted border color */
padding: 10px; /* Added padding for better text visibility */
}
#patternSelector {
margin-bottom: 10px;
background-color: #424242; /* Darker shade for textarea */
color: #e0e0e0; /* Light text for readability */
border: 1px solid #555; /* Adjusted border color */
padding: 10px; /* Added padding for better text visibility */
height: 40px;
}
@media (min-width: 768px) {
.container {
max-width: 80%;
}
}
.light-theme {
background-color: #fff;
color: #333;
}
.light-theme .container {
background: #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.light-theme #responseContainer,
.light-theme #userInput,
.light-theme #patternSelector {
background-color: #fff;
color: #333;
border: 1px solid #ddd;
}
.light-theme .btn-primary {
background-color: #0066cc;
color: white;
}
.hidden {
display: none;
}
.drag-over {
background-color: #505050; /* Slightly lighter than the regular background for visibility */
border: 2px dashed #007bff; /* Dashed border with the primary button color for emphasis */
box-shadow: 0 0 10px #007bff; /* Soft glow effect to highlight the area */
color: #e0e0e0; /* Maintaining the light text color for readability */
transition: background-color 0.3s ease, box-shadow 0.3s ease; /* Smooth transition for background and shadow changes */
}
.light-theme .drag-over {
background-color: #e6e6e6; /* Lighter background for light theme */
border: 2px dashed #0066cc; /* Adjusted border color for light theme */
box-shadow: 0 0 10px #0066cc; /* Soft glow effect for light theme */
color: #333; /* Darker text for contrast in light theme */
}
/* Existing dark theme styles for reference */
.navbar-dark.bg-dark {
background-color: #343a40 !important;
}
/* Light theme styles */
body.light-theme .navbar-dark.bg-dark {
background-color: #e2e6ea !important; /* Slightly darker shade for better visibility */
color: #000 !important; /* Keep dark text color for contrast */
}
body.light-theme .navbar-dark .navbar-brand,
body.light-theme .navbar-dark .btn-outline-success {
color: #0056b3 !important; /* Darker color for better visibility and contrast */
}
body.light-theme .navbar-toggler-icon {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'><path stroke='rgba(0, 0, 0, 0.75)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>") !important;
/* Slightly darker stroke for the navbar-toggler-icon for better visibility */
}
@media (max-width: 768px) {
.navbar-brand img {
height: 20px; /* Smaller logo for smaller screens */
}
.navbar-dark .navbar-toggler {
padding: 0.25rem 0.5rem; /* Adjust padding for the toggle button */
}
}
#responseContainer {
position: relative; /* Needed for absolute positioning of the child button */
}
#copyButton {
position: absolute;
top: 10px; /* Adjust as needed */
right: 10px; /* Adjust as needed */
background-color: rgba(
0,
123,
255,
0.5
); /* Bootstrap primary color with transparency */
color: white;
border: none;
border-radius: 5px;
padding: 5px 10px;
font-size: 0.8rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
#copyButton:hover {
background-color: rgba(
0,
123,
255,
0.8
); /* Slightly less transparent on hover */
}
#copyButton:focus {
outline: none;
}

View File

@@ -0,0 +1,3 @@
"""This package collets all functionality meant to run as web servers"""
from .api import main as run_api_server
from .webui import main as run_webui_server

View File

@@ -0,0 +1,2 @@
FLASK_SECRET_KEY=
OPENAI_API_KEY=

View File

@@ -0,0 +1 @@
from .fabric_api_server import main

View File

@@ -0,0 +1,10 @@
{
"/extwis": {
"eJ4f1e0b-25wO-47f9-97ec-6b5335b2": "Daniel Miessler",
"test": "user2"
},
"/summarize": {
"eJ4f1e0b-25wO-47f9-97ec-6b5335b2": "Daniel Miessler",
"test": "user2"
}
}

View File

@@ -0,0 +1,259 @@
import jwt
import json
import openai
from flask import Flask, request, jsonify
from functools import wraps
import re
import requests
import os
from dotenv import load_dotenv
from importlib import resources
app = Flask(__name__)
@app.errorhandler(404)
def not_found(e):
return jsonify({"error": "The requested resource was not found."}), 404
@app.errorhandler(500)
def server_error(e):
return jsonify({"error": "An internal server error occurred."}), 500
##################################################
##################################################
#
# ⚠️ CAUTION: This is an HTTP-only server!
#
# If you don't know what you're doing, don't run
#
##################################################
##################################################
## Setup
## Did I mention this is HTTP only? Don't run this on the public internet.
# Read API tokens from the apikeys.json file
api_keys = resources.read_text("installer.server.api", "fabric_api_keys.json")
valid_tokens = json.loads(api_keys)
# Read users from the users.json file
users = resources.read_text("installer.server.api", "users.json")
users = json.loads(users)
# The function to check if the token is valid
def auth_required(f):
""" Decorator function to check if the token is valid.
Args:
f: The function to be decorated
Returns:
The decorated function
"""
@wraps(f)
def decorated_function(*args, **kwargs):
""" Decorated function to handle authentication token and API endpoint.
Args:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
Result of the decorated function.
Raises:
KeyError: If 'Authorization' header is not found in the request.
TypeError: If 'Authorization' header value is not a string.
ValueError: If the authentication token is invalid or expired.
"""
# Get the authentication token from request header
auth_token = request.headers.get("Authorization", "")
# Remove any bearer token prefix if present
if auth_token.lower().startswith("bearer "):
auth_token = auth_token[7:]
# Get API endpoint from request
endpoint = request.path
# Check if token is valid
user = check_auth_token(auth_token, endpoint)
if user == "Unauthorized: You are not authorized for this API":
return jsonify({"error": user}), 401
return f(*args, **kwargs)
return decorated_function
# Check for a valid token/user for the given route
def check_auth_token(token, route):
""" Check if the provided token is valid for the given route and return the corresponding user.
Args:
token (str): The token to be checked for validity.
route (str): The route for which the token validity is to be checked.
Returns:
str: The user corresponding to the provided token and route if valid, otherwise returns "Unauthorized: You are not authorized for this API".
"""
# Check if token is valid for the given route and return corresponding user
if route in valid_tokens and token in valid_tokens[route]:
return users[valid_tokens[route][token]]
else:
return "Unauthorized: You are not authorized for this API"
# Define the allowlist of characters
ALLOWLIST_PATTERN = re.compile(r"^[a-zA-Z0-9\s.,;:!?\-]+$")
# Sanitize the content, sort of. Prompt injection is the main threat so this isn't a huge deal
def sanitize_content(content):
""" Sanitize the content by removing characters that do not match the ALLOWLIST_PATTERN.
Args:
content (str): The content to be sanitized.
Returns:
str: The sanitized content.
"""
return "".join(char for char in content if ALLOWLIST_PATTERN.match(char))
# Pull the URL content's from the GitHub repo
def fetch_content_from_url(url):
""" Fetches content from the given URL.
Args:
url (str): The URL from which to fetch content.
Returns:
str: The sanitized content fetched from the URL.
Raises:
requests.RequestException: If an error occurs while making the request to the URL.
"""
try:
response = requests.get(url)
response.raise_for_status()
sanitized_content = sanitize_content(response.text)
return sanitized_content
except requests.RequestException as e:
return str(e)
## APIs
# Make path mapping flexible and scalable
pattern_path_mappings = {
"extwis": {"system_url": "https://raw.githubusercontent.com/danielmiessler/fabric/main/patterns/extract_wisdom/system.md",
"user_url": "https://raw.githubusercontent.com/danielmiessler/fabric/main/patterns/extract_wisdom/user.md"},
"summarize": {"system_url": "https://raw.githubusercontent.com/danielmiessler/fabric/main/patterns/summarize/system.md",
"user_url": "https://raw.githubusercontent.com/danielmiessler/fabric/main/patterns/summarize/user.md"}
} # Add more pattern with your desire path as a key in this dictionary
# /<pattern>
@app.route("/<pattern>", methods=["POST"])
@auth_required # Require authentication
def milling(pattern):
""" Combine fabric pattern with input from user and send to OpenAI's GPT-4 model.
Returns:
JSON: A JSON response containing the generated response or an error message.
Raises:
Exception: If there is an error during the API call.
"""
data = request.get_json()
# Warn if there's no input
if "input" not in data:
return jsonify({"error": "Missing input parameter"}), 400
# Get data from client
input_data = data["input"]
# Set the system and user URLs
urls = pattern_path_mappings[pattern]
system_url, user_url = urls["system_url"], urls["user_url"]
# Fetch the prompt content
system_content = fetch_content_from_url(system_url)
user_file_content = fetch_content_from_url(user_url)
# Build the API call
system_message = {"role": "system", "content": system_content}
user_message = {"role": "user", "content": user_file_content + "\n" + input_data}
messages = [system_message, user_message]
try:
response = openai.chat.completions.create(
model="gpt-4-1106-preview",
messages=messages,
temperature=0.0,
top_p=1,
frequency_penalty=0.1,
presence_penalty=0.1,
)
assistant_message = response.choices[0].message.content
return jsonify({"response": assistant_message})
except Exception as e:
app.logger.error(f"Error occurred: {str(e)}")
return jsonify({"error": "An error occurred while processing the request."}), 500
@app.route("/register", methods=["POST"])
def register():
data = request.get_json()
username = data["username"]
password = data["password"]
if username in users:
return jsonify({"error": "Username already exists"}), 400
new_user = {
"username": username,
"password": password
}
users[username] = new_user
token = jwt.encode({"username": username}, os.getenv("JWT_SECRET"), algorithm="HS256")
return jsonify({"token": token.decode("utf-8")})
@app.route("/login", methods=["POST"])
def login():
data = request.get_json()
username = data["username"]
password = data["password"]
if username in users and users[username]["password"] == password:
# Generate a JWT token
token = jwt.encode({"username": username}, os.getenv("JWT_SECRET"), algorithm="HS256")
return jsonify({"token": token.decode("utf-8")})
return jsonify({"error": "Invalid username or password"}), 401
def main():
"""Runs the main fabric API backend server"""
app.run(host="127.0.0.1", port=13337, debug=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,11 @@
{
"user1": {
"username": "user1",
"password": "password1"
},
"user2": {
"username": "user2",
"password": "password2"
}
}

View File

@@ -0,0 +1 @@
from .fabric_web_server import main

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@@ -0,0 +1,5 @@
{
"/extwis": {
"eJ4f1e0b-25wO-47f9-97ec-6b5335b2": "Daniel Miessler"
}
}

View File

@@ -0,0 +1,94 @@
from flask import Flask, render_template, request, redirect, url_for, flash, session
import requests
import json
from flask import send_from_directory
import os
##################################################
##################################################
#
# ⚠️ CAUTION: This is an HTTP-only server!
#
# If you don't know what you're doing, don't run
#
##################################################
##################################################
def send_request(prompt, endpoint):
""" Send a request to the specified endpoint of an HTTP-only server.
Args:
prompt (str): The input prompt for the request.
endpoint (str): The endpoint to which the request will be sent.
Returns:
str: The response from the server.
Raises:
KeyError: If the response JSON does not contain the expected "response" key.
"""
base_url = "http://127.0.0.1:13337"
url = f"{base_url}{endpoint}"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {session['token']}",
}
data = json.dumps({"input": prompt})
response = requests.post(url, headers=headers, data=data, verify=False)
try:
response = requests.post(url, headers=headers, data=data)
response.raise_for_status() # raises HTTPError if the response status isn't 200
except requests.ConnectionError:
return "Error: Unable to connect to the server."
except requests.HTTPError as e:
return f"Error: An HTTP error occurred: {str(e)}"
app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY")
@app.route("/favicon.ico")
def favicon():
""" Send the favicon.ico file from the static directory.
Returns:
Response object with the favicon.ico file
Raises:
-
"""
return send_from_directory(
os.path.join(app.root_path, "static"),
"favicon.ico",
mimetype="image/vnd.microsoft.icon",
)
@app.route("/", methods=["GET", "POST"])
def index():
""" Process the POST request and send a request to the specified API endpoint.
Returns:
str: The rendered HTML template with the response data.
"""
if request.method == "POST":
prompt = request.form.get("prompt")
endpoint = request.form.get("api")
response = send_request(prompt=prompt, endpoint=endpoint)
return render_template("index.html", response=response)
return render_template("index.html", response=None)
def main():
app.run(host="127.0.0.1", port=13338, debug=True)
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<title>fabric</title>
<link rel="shortcut icon" type="image/x-icon" href="https://beehiiv-images-production.s3.amazonaws.com/uploads/asset/file/971f362a-f3fa-427f-b619-7e04cc135d17/fabric-logo-miessler-transparent.png?t=1704525002" />
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.16/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-900 text-white min-h-screen">
<div class="container mx-auto py-10 px-4">
<div class="flex justify-between items-center mb-6">
<!-- Add this line inside the div with class "flex justify-between items-center mb-6" -->
<p><img src="static/fabric-logo-miessler-transparent.png" alt="fabric logo" class="h-20 w-auto mr-2"></p>
<h1 class="text-4xl font-bold"><code>fabric</code></h1>
</div>
<p>Please enter your content and select the API you want to use:</p>
<br />
<form method="POST" class="space-y-4">
<div>
<label for="prompt" class="block text-sm font-medium">Content:</label>
<input type="text" id="prompt" name="prompt" required class="w-full px-3 py-2 border border-gray-300 rounded-md text-black">
</div>
<div>
<label for="api" class="block text-sm font-medium">API:</label>
<select id="api" name="api" class="w-full px-3 py-2 border border-gray-300 rounded-md text-black">
<option value="/extwis">/extwis</option>
<!-- Add more API endpoints here... -->
</select>
</div>
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md text-white font-medium">Send Request</button>
</form>
{% if response %}
<div class="mt-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold">API Response:</h2>
<button id="copy-button" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md">Copy to Clipboard</button>
</div>
<pre id="response-output" class="bg-gray-800 p-4 rounded-md whitespace-pre-wrap">{{ response }}</pre>
</div>
{% endif %}
</div>
<script>
document.getElementById("api").addEventListener("change", function() {
document.getElementById("response-output").textContent = "";
});
document.getElementById("copy-button").addEventListener("click", function() {
const responseOutput = document.getElementById("response-output");
const range = document.createRange();
range.selectNode(responseOutput);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
document.execCommand("copy");
window.getSelection().removeAllRanges();
});
</script>
</body>
</html>

18
main.go
View File

@@ -1,18 +0,0 @@
package main
import (
"fmt"
"os"
"github.com/jessevdk/go-flags"
"github.com/danielmiessler/fabric/cli"
)
func main() {
err := cli.Cli(version)
if err != nil && !flags.WroteHelp(err) {
fmt.Printf("%s\n", err)
os.Exit(1)
}
}

View File

@@ -1,38 +0,0 @@
{
lib,
buildGoApplication,
go,
installShellFiles,
}:
buildGoApplication {
pname = "fabric-ai";
version = import ./version.nix;
src = ../../../.;
pwd = ../../../.;
modules = ./gomod2nix.toml;
doCheck = false;
ldflags = [
"-s"
"-w"
];
inherit go;
nativeBuildInputs = [ installShellFiles ];
postInstall = ''
installShellCompletion --zsh ./completions/_fabric
installShellCompletion --bash ./completions/fabric.bash
installShellCompletion --fish ./completions/fabric.fish
'';
meta = with lib; {
description = "Fabric is an open-source framework for augmenting humans using AI. It provides a modular framework for solving specific problems using a crowdsourced set of AI prompts that can be used anywhere";
homepage = "https://github.com/danielmiessler/fabric";
license = licenses.mit;
platforms = platforms.all;
mainProgram = "fabric";
};
}

View File

@@ -1,330 +0,0 @@
schema = 3
[mod]
[mod."cloud.google.com/go"]
version = "v0.121.2"
hash = "sha256-BCgGHxKti8slH98UDDurtgzX3lgcYEklsmj4ImPpwlc="
[mod."cloud.google.com/go/ai"]
version = "v0.12.1"
hash = "sha256-wg3oLMS68E/v7EdNzywbjwEmpk+u6U8LTnIc1pq8edo="
[mod."cloud.google.com/go/auth"]
version = "v0.16.2"
hash = "sha256-BAU9WGFKe0pd5Eu3l/Mbts+QeCOjS+lChr5hrPBCzdA="
[mod."cloud.google.com/go/auth/oauth2adapt"]
version = "v0.2.8"
hash = "sha256-GoXFqAbp1WO1tDj07PF5EyxDYvCBP0l0qwxY2oV2hfc="
[mod."cloud.google.com/go/compute/metadata"]
version = "v0.7.0"
hash = "sha256-jJZDW+hibqjMiY8OiJhgJALbGwEq+djLOxfYR7upQyE="
[mod."cloud.google.com/go/longrunning"]
version = "v0.6.7"
hash = "sha256-9I0Nc2KWAEVoxDngNkqFUdASmZIAySfMEELlPh3Q3xA="
[mod."dario.cat/mergo"]
version = "v1.0.2"
hash = "sha256-p6jdiHlLEfZES8vJnDywG4aVzIe16p0CU6iglglIweA="
[mod."github.com/Microsoft/go-winio"]
version = "v0.6.2"
hash = "sha256-tVNWDUMILZbJvarcl/E7tpSnkn7urqgSHa2Eaka5vSU="
[mod."github.com/ProtonMail/go-crypto"]
version = "v1.3.0"
hash = "sha256-TUG+C4MyeWglOmiwiW2/NUVurFHXLgEPRd3X9uQ1NGI="
[mod."github.com/andybalholm/cascadia"]
version = "v1.3.3"
hash = "sha256-jv7ZshpSd7FZzKKN6hqlUgiR8C3y85zNIS/hq7g76Ho="
[mod."github.com/anthropics/anthropic-sdk-go"]
version = "v1.4.0"
hash = "sha256-4kwFw9gt/sRIlTo0fC2PbfLnCyc4lCOtmfQelhpORX8="
[mod."github.com/araddon/dateparse"]
version = "v0.0.0-20210429162001-6b43995a97de"
hash = "sha256-UuX84naeRGMsFOgIgRoBHG5sNy1CzBkWPKmd6VbLwFw="
[mod."github.com/atotto/clipboard"]
version = "v0.1.4"
hash = "sha256-ZZ7U5X0gWOu8zcjZcWbcpzGOGdycwq0TjTFh/eZHjXk="
[mod."github.com/aws/aws-sdk-go-v2"]
version = "v1.36.4"
hash = "sha256-Cpdphp8FQUbQlhAYvtPKDh1oZc84+/0bzLlx8CM1/BM="
[mod."github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream"]
version = "v1.6.10"
hash = "sha256-9+ZMhWxtsm7ZtZCjBV5PZkOR5rt3bCOznuv45Iwf55c="
[mod."github.com/aws/aws-sdk-go-v2/config"]
version = "v1.27.27"
hash = "sha256-jQmc1lJmVeTezSeFs6KL2HAvCkP9ZWMdVbG5ymJQrKs="
[mod."github.com/aws/aws-sdk-go-v2/credentials"]
version = "v1.17.27"
hash = "sha256-7ITZjIF0ZmmCG3u5d88IfsAj0KF1IFm9KhWFlC6RtQo="
[mod."github.com/aws/aws-sdk-go-v2/feature/ec2/imds"]
version = "v1.16.11"
hash = "sha256-uedtRd/SIcFJlYZg1jtJdIJViZq1Poks9/J2Bm9/Ehw="
[mod."github.com/aws/aws-sdk-go-v2/internal/configsources"]
version = "v1.3.35"
hash = "sha256-AyQ+eJvyhahypIAqPScdkn44MYwBcr9iyrMC1BRSeZI="
[mod."github.com/aws/aws-sdk-go-v2/internal/endpoints/v2"]
version = "v2.6.35"
hash = "sha256-c8K+Nk5XrFMWaaxVsyhKgyJBZhs3Hkhjr/dIDXWZfSQ="
[mod."github.com/aws/aws-sdk-go-v2/internal/ini"]
version = "v1.8.0"
hash = "sha256-v76jTAr4rEgS5en49ikLh6nuvclN+VjpOPj83ZQ3sLo="
[mod."github.com/aws/aws-sdk-go-v2/service/bedrock"]
version = "v1.34.1"
hash = "sha256-OK7t+ieq4pviCnnhfSytANBF5Lwdz4KxjN10CC5pXyY="
[mod."github.com/aws/aws-sdk-go-v2/service/bedrockruntime"]
version = "v1.30.0"
hash = "sha256-MsEQfbqIREtMikRFqBpLCqdAC4gfgPSNbk08k5OJTbo="
[mod."github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding"]
version = "v1.11.3"
hash = "sha256-TRhoRd7iY7K+pfdkSQLItyr52k2jO4TMYQ5vRGiOOMk="
[mod."github.com/aws/aws-sdk-go-v2/service/internal/presigned-url"]
version = "v1.11.17"
hash = "sha256-eUoYDAXcQNzCmwjXO9RWhrt0jGYlSjt2vQOlAlpIfoE="
[mod."github.com/aws/aws-sdk-go-v2/service/sso"]
version = "v1.22.4"
hash = "sha256-Q3tyDdJVq0BAstOYvCKPvNS4EHkhXt1pL/23KPQJMHM="
[mod."github.com/aws/aws-sdk-go-v2/service/ssooidc"]
version = "v1.26.4"
hash = "sha256-cPv6nmVPOjMUZjN2IeEiYQSzLeAOrfgGnSSvvhJ6iL4="
[mod."github.com/aws/aws-sdk-go-v2/service/sts"]
version = "v1.30.3"
hash = "sha256-4z/K4GPW9osiNM3SxFNZYsVPnSSU50Iuv29Sb2n4Fbk="
[mod."github.com/aws/smithy-go"]
version = "v1.22.2"
hash = "sha256-YdwVeW509cpqU357MjDM8ReL1vftkW8XIhSbJsbTh/s="
[mod."github.com/bytedance/sonic"]
version = "v1.13.3"
hash = "sha256-Nnt5b2NkIvSXhGERQmyI0ka28hbWi7A7Zn3dsAjPcEA="
[mod."github.com/bytedance/sonic/loader"]
version = "v0.2.4"
hash = "sha256-rv9LnePpm4OspSVbfSoVbohXzhu+dxE1BH1gm3mTmTc="
[mod."github.com/cloudflare/circl"]
version = "v1.6.1"
hash = "sha256-Dc69V12eIFnJoUNmwg6VKXHfAMijbAeEVSDe8AiOaLo="
[mod."github.com/cloudwego/base64x"]
version = "v0.1.5"
hash = "sha256-MyUYTveN48DhnL8mwAgCRuMExLct98uzSPsmYlfaa4I="
[mod."github.com/cyphar/filepath-securejoin"]
version = "v0.4.1"
hash = "sha256-NOV6MfbkcQbfhNmfADQw2SJmZ6q1nw0wwg8Pm2tf2DM="
[mod."github.com/davecgh/go-spew"]
version = "v1.1.1"
hash = "sha256-nhzSUrE1fCkN0+RL04N4h8jWmRFPPPWbCuDc7Ss0akI="
[mod."github.com/emirpasic/gods"]
version = "v1.18.1"
hash = "sha256-hGDKddjLj+5dn2woHtXKUdd49/3xdsqnhx7VEdCu1m4="
[mod."github.com/felixge/httpsnoop"]
version = "v1.0.4"
hash = "sha256-c1JKoRSndwwOyOxq9ddCe+8qn7mG9uRq2o/822x5O/c="
[mod."github.com/gabriel-vasile/mimetype"]
version = "v1.4.9"
hash = "sha256-75uELLqb01djHTe7KdXvUidBK7SuejarYouEUuxaj8Q="
[mod."github.com/gin-contrib/sse"]
version = "v1.1.0"
hash = "sha256-2VP6zHEsPi0u2ZYpOTcLulwj1Gsmb6oA19qcP2/AzVM="
[mod."github.com/gin-gonic/gin"]
version = "v1.10.1"
hash = "sha256-D98+chAdjb6JcLPkscOr8TgTW87UqA4h3cnY0XIr16c="
[mod."github.com/go-git/gcfg"]
version = "v1.5.1-0.20230307220236-3a3c6141e376"
hash = "sha256-f4k0gSYuo0/q3WOoTxl2eFaj7WZpdz29ih6CKc8Ude8="
[mod."github.com/go-git/go-billy/v5"]
version = "v5.6.2"
hash = "sha256-VgbxcLkHjiSyRIfKS7E9Sn8OynCrMGUDkwFz6K2TVL4="
[mod."github.com/go-git/go-git/v5"]
version = "v5.16.2"
hash = "sha256-KdOf4KwJAJUIB/EcQH6wc7jpcABCISWur3vOTpAo+/c="
[mod."github.com/go-logr/logr"]
version = "v1.4.3"
hash = "sha256-Nnp/dEVNMxLp3RSPDHZzGbI8BkSNuZMX0I0cjWKXXLA="
[mod."github.com/go-logr/stdr"]
version = "v1.2.2"
hash = "sha256-rRweAP7XIb4egtT1f2gkz4sYOu7LDHmcJ5iNsJUd0sE="
[mod."github.com/go-playground/locales"]
version = "v0.14.1"
hash = "sha256-BMJGAexq96waZn60DJXZfByRHb8zA/JP/i6f/YrW9oQ="
[mod."github.com/go-playground/universal-translator"]
version = "v0.18.1"
hash = "sha256-2/B2qP51zfiY+k8G0w0D03KXUc7XpWj6wKY7NjNP/9E="
[mod."github.com/go-playground/validator/v10"]
version = "v10.26.0"
hash = "sha256-/jMKICp8LTcJVt+b4YRTnJM84r7HK6aT0oqO7Q8SRs8="
[mod."github.com/go-shiori/dom"]
version = "v0.0.0-20230515143342-73569d674e1c"
hash = "sha256-4lm9KZfR2XnfZU9KTG+4jqLYZqbfL74AMO4y3dKpIbg="
[mod."github.com/go-shiori/go-readability"]
version = "v0.0.0-20250217085726-9f5bf5ca7612"
hash = "sha256-yleBb+OmxLbQ0PT4yV2PNBAAE6UFxSRGGpylY8SrSqw="
[mod."github.com/goccy/go-json"]
version = "v0.10.5"
hash = "sha256-/EtlGihP0/7oInzMC5E0InZ4b5Ad3s4xOpqotloi3xw="
[mod."github.com/gogs/chardet"]
version = "v0.0.0-20211120154057-b7413eaefb8f"
hash = "sha256-4MeqBJsh4U+ZEbfdDwdciTYMlQWkCil2KJbUxHjBSIo="
[mod."github.com/golang/groupcache"]
version = "v0.0.0-20241129210726-2c02b8208cf8"
hash = "sha256-AdLZ3dJLe/yduoNvZiXugZxNfmwJjNQyQGsIdzYzH74="
[mod."github.com/google/generative-ai-go"]
version = "v0.20.1"
hash = "sha256-9bSpEs4kByhgyTKiHdOY5muYjGBTluA1LvEjw2gSoLI="
[mod."github.com/google/s2a-go"]
version = "v0.1.9"
hash = "sha256-0AdSpSTso4bATmM/9qamWzKrVtOLDf7afvDhoiT/UpA="
[mod."github.com/google/uuid"]
version = "v1.6.0"
hash = "sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw="
[mod."github.com/googleapis/enterprise-certificate-proxy"]
version = "v0.3.6"
hash = "sha256-hPMF0s+X4/ul98GvVuw/ZNOupEXhIDB1yvWymZWYEbU="
[mod."github.com/googleapis/gax-go/v2"]
version = "v2.14.2"
hash = "sha256-QyY7wuCkrOJCJIf9Q884KD/BC3vk/QtQLXeLeNPt750="
[mod."github.com/jbenet/go-context"]
version = "v0.0.0-20150711004518-d14ea06fba99"
hash = "sha256-VANNCWNNpARH/ILQV9sCQsBWgyL2iFT+4AHZREpxIWE="
[mod."github.com/jessevdk/go-flags"]
version = "v1.6.1"
hash = "sha256-Q5WFTgRxYio0+ay3sbQeBPKeJAFvOdiDVkaTVn3hoTA="
[mod."github.com/joho/godotenv"]
version = "v1.5.1"
hash = "sha256-kA0osKfsc6Kp+nuGTRJyXZZlJt1D/kuEazKMWYCWcQ8="
[mod."github.com/json-iterator/go"]
version = "v1.1.12"
hash = "sha256-To8A0h+lbfZ/6zM+2PpRpY3+L6725OPC66lffq6fUoM="
[mod."github.com/kevinburke/ssh_config"]
version = "v1.2.0"
hash = "sha256-Ta7ZOmyX8gG5tzWbY2oES70EJPfI90U7CIJS9EAce0s="
[mod."github.com/klauspost/cpuid/v2"]
version = "v2.2.10"
hash = "sha256-o21Tk5sD7WhhLUoqSkymnjLbzxl0mDJCTC1ApfZJrC0="
[mod."github.com/leodido/go-urn"]
version = "v1.4.0"
hash = "sha256-Q6kplWkY37Tzy6GOme3Wut40jFK4Izun+ij/BJvcEu0="
[mod."github.com/mattn/go-isatty"]
version = "v0.0.20"
hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ="
[mod."github.com/modern-go/concurrent"]
version = "v0.0.0-20180306012644-bacd9c7ef1dd"
hash = "sha256-OTySieAgPWR4oJnlohaFTeK1tRaVp/b0d1rYY8xKMzo="
[mod."github.com/modern-go/reflect2"]
version = "v1.0.2"
hash = "sha256-+W9EIW7okXIXjWEgOaMh58eLvBZ7OshW2EhaIpNLSBU="
[mod."github.com/ollama/ollama"]
version = "v0.9.0"
hash = "sha256-r2eU+kMG3tuJy2B43RXsfmeltzM9t05NEmNiJAW5qr4="
[mod."github.com/openai/openai-go"]
version = "v1.8.2"
hash = "sha256-O8aV3zEj6o8kIlzlkYaTW4RzvwR3qNUBYiN8SuTM1R0="
[mod."github.com/otiai10/copy"]
version = "v1.14.1"
hash = "sha256-8RR7u17SbYg9AeBXVHIv5ZMU+kHmOcx0rLUKyz6YtU0="
[mod."github.com/otiai10/mint"]
version = "v1.6.3"
hash = "sha256-/FT3dYP2+UiW/qe1pxQ7HiS8et4+KHGPIMhc+8mHvzw="
[mod."github.com/pelletier/go-toml/v2"]
version = "v2.2.4"
hash = "sha256-8qQIPldbsS5RO8v/FW/se3ZsAyvLzexiivzJCbGRg2Q="
[mod."github.com/pjbgf/sha1cd"]
version = "v0.3.2"
hash = "sha256-jdbiRhU8xc1C5c8m7BSCj71PUXHY3f7TWFfxDKKpUMk="
[mod."github.com/pkg/errors"]
version = "v0.9.1"
hash = "sha256-mNfQtcrQmu3sNg/7IwiieKWOgFQOVVe2yXgKBpe/wZw="
[mod."github.com/pmezard/go-difflib"]
version = "v1.0.0"
hash = "sha256-/FtmHnaGjdvEIKAJtrUfEhV7EVo5A/eYrtdnUkuxLDA="
[mod."github.com/samber/lo"]
version = "v1.50.0"
hash = "sha256-KDFks82BKu39sGt0f972IyOkohV2U0r1YvsnlNLdugY="
[mod."github.com/sergi/go-diff"]
version = "v1.4.0"
hash = "sha256-rs9NKpv/qcQEMRg7CmxGdP4HGuFdBxlpWf9LbA9wS4k="
[mod."github.com/sgaunet/perplexity-go/v2"]
version = "v2.8.0"
hash = "sha256-w1S14Jf4/6LFODREmmiJvPtkZh4Sor81Rr1PqC5pIak="
[mod."github.com/skeema/knownhosts"]
version = "v1.3.1"
hash = "sha256-kjqQDzuncQNTuOYegqVZExwuOt/Z73m2ST7NZFEKixI="
[mod."github.com/stretchr/testify"]
version = "v1.10.0"
hash = "sha256-fJ4gnPr0vnrOhjQYQwJ3ARDKPsOtA7d4olQmQWR+wpI="
[mod."github.com/tidwall/gjson"]
version = "v1.18.0"
hash = "sha256-CO6hqDu8Y58Po6A01e5iTpwiUBQ5khUZsw7czaJHw0I="
[mod."github.com/tidwall/match"]
version = "v1.1.1"
hash = "sha256-M2klhPId3Q3T3VGkSbOkYl/2nLHnsG+yMbXkPkyrRdg="
[mod."github.com/tidwall/pretty"]
version = "v1.2.1"
hash = "sha256-S0uTDDGD8qr415Ut7QinyXljCp0TkL4zOIrlJ+9OMl8="
[mod."github.com/tidwall/sjson"]
version = "v1.2.5"
hash = "sha256-OYGNolkmL7E1Qs2qrQ3IVpQp5gkcHNU/AB/z2O+Myps="
[mod."github.com/twitchyliquid64/golang-asm"]
version = "v0.15.1"
hash = "sha256-HLk6oUe7EoITrNvP0y8D6BtIgIcmDZYtb/xl/dufIoY="
[mod."github.com/ugorji/go/codec"]
version = "v1.2.14"
hash = "sha256-PoVXlCBE8SvMWpXx9FRsQOSAmE/+5SnPGr4m5BGoyIo="
[mod."github.com/xanzy/ssh-agent"]
version = "v0.3.3"
hash = "sha256-l3pGB6IdzcPA/HLk93sSN6NM2pKPy+bVOoacR5RC2+c="
[mod."go.opentelemetry.io/auto/sdk"]
version = "v1.1.0"
hash = "sha256-cA9qCCu8P1NSJRxgmpfkfa5rKyn9X+Y/9FSmSd5xjyo="
[mod."go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"]
version = "v0.61.0"
hash = "sha256-o5w9k3VbqP3gaXI3Aelw93LLHH53U4PnkYVwc3MaY3Y="
[mod."go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"]
version = "v0.61.0"
hash = "sha256-4pfXD7ErXhexSynXiEEQSAkWoPwHd7PEDE3M1Zi5gLM="
[mod."go.opentelemetry.io/otel"]
version = "v1.36.0"
hash = "sha256-j8wojdCtKal3LKojanHA8KXXQ0FkbWONpO8tUxpJDko="
[mod."go.opentelemetry.io/otel/metric"]
version = "v1.36.0"
hash = "sha256-z6Uqi4HhUljWIYd58svKK5MqcGbpcac+/M8JeTrUtJ8="
[mod."go.opentelemetry.io/otel/trace"]
version = "v1.36.0"
hash = "sha256-owWD9x1lp8aIJqYt058BXPUsIMHdk3RI0escso0BxwA="
[mod."golang.org/x/arch"]
version = "v0.18.0"
hash = "sha256-tUpUPERjmRi7zldj0oPlnbnBhEkcI9iQGvP1HqlsK10="
[mod."golang.org/x/crypto"]
version = "v0.39.0"
hash = "sha256-FtwjbVoAhZkx7F2hmzi9Y0J87CVVhWcrZzun+zWQLzc="
[mod."golang.org/x/net"]
version = "v0.41.0"
hash = "sha256-6/pi8rNmGvBFzkJQXkXkMfL1Bjydhg3BgAMYDyQ/Uvg="
[mod."golang.org/x/oauth2"]
version = "v0.30.0"
hash = "sha256-btD7BUtQpOswusZY5qIU90uDo38buVrQ0tmmQ8qNHDg="
[mod."golang.org/x/sync"]
version = "v0.15.0"
hash = "sha256-Jf4ehm8H8YAWY6mM151RI5CbG7JcOFtmN0AZx4bE3UE="
[mod."golang.org/x/sys"]
version = "v0.33.0"
hash = "sha256-wlOzIOUgAiGAtdzhW/KPl/yUVSH/lvFZfs5XOuJ9LOQ="
[mod."golang.org/x/text"]
version = "v0.26.0"
hash = "sha256-N+27nBCyGvje0yCTlUzZoVZ0LRxx4AJ+eBlrFQVRlFQ="
[mod."golang.org/x/time"]
version = "v0.12.0"
hash = "sha256-Cp3oxrCMH2wyxjzr5SHVmyhgaoUuSl56Uy00Q7DYEpw="
[mod."google.golang.org/api"]
version = "v0.236.0"
hash = "sha256-tP1RSUSnQ4a0axgZQwEZgKF1E13nL02FSP1NPSZr0Rc="
[mod."google.golang.org/genproto/googleapis/api"]
version = "v0.0.0-20250603155806-513f23925822"
hash = "sha256-0CS432v9zVhkVLqFpZtxBX8rvVqP67lb7qQ3es7RqIU="
[mod."google.golang.org/genproto/googleapis/rpc"]
version = "v0.0.0-20250603155806-513f23925822"
hash = "sha256-WK7iDtAhH19NPe3TywTQlGjDawNaDKWnxhFL9PgVUwM="
[mod."google.golang.org/grpc"]
version = "v1.73.0"
hash = "sha256-LfVlwip++q2DX70RU6CxoXglx1+r5l48DwlFD05G11c="
[mod."google.golang.org/protobuf"]
version = "v1.36.6"
hash = "sha256-lT5qnefI5FDJnowz9PEkAGylH3+fE+A3DJDkAyy9RMc="
[mod."gopkg.in/warnings.v0"]
version = "v0.1.2"
hash = "sha256-ATVL9yEmgYbkJ1DkltDGRn/auGAjqGOfjQyBYyUo8s8="
[mod."gopkg.in/yaml.v3"]
version = "v3.0.1"
hash = "sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU="

View File

@@ -1 +0,0 @@
"1.4.233"

View File

@@ -1,31 +0,0 @@
{
pkgs,
gomod2nix,
goEnv,
goVersion,
}:
{
default = pkgs.mkShell {
nativeBuildInputs = [
goVersion
pkgs.gopls
pkgs.gotools
pkgs.go-tools
pkgs.goimports-reviser
gomod2nix
goEnv
(pkgs.writeShellScriptBin "update" ''
go get -u
go mod tidy
gomod2nix generate
'')
];
shellHook = ''
echo -e "\033[0;32;4mHeper commands:\033[0m"
echo "'update' instead of 'go get -u && go mod tidy'"
'';
};
}

View File

@@ -1,12 +0,0 @@
{
projectRootFile = "flake.nix";
programs = {
deadnix.enable = true;
statix.enable = true;
nixfmt.enable = true;
goimports.enable = true;
gofmt.enable = true;
};
}

View File

@@ -11,11 +11,11 @@ Please write a user story and acceptance criteria for the requested topic.
Output the results in JSON format as defined in this example:
{
"Topic": "Authentication and User Management",
"Topic": "Automating data quality automation",
"Story": "As a user, I want to be able to create a new user account so that I can access the system.",
"Criteria": "Given that I am a user, when I click the 'Create Account' button, then I should be prompted to enter my email address, password, and confirm password. When I click the 'Submit' button, then I should be redirected to the login page."
}
# INPUT:
INPUT:
INPUT:

View File

@@ -1,19 +1,14 @@
# IDENTITY and PURPOSE
You are an expert at interpreting the heart and spirit of a question and answering in an insightful manner.
You are an expert at interpreting the heart of a question and answering in a concise manner.
# STEPS
# Steps
- Deeply understand what's being asked.
- Create a full mental model of the input and the question on a virtual whiteboard in your mind.
- Answer the question in 3-5 Markdown bullets of 10 words each.
- Understand what's being asked.
- Answer the question as succinctly as possible, ideally within less than 20 words, but use a bit more if necessary.
# OUTPUT INSTRUCTIONS
- Only output Markdown bullets.
- Do not output warnings or notes—just the requested sections.
# INPUT:

View File

@@ -1,41 +0,0 @@
# Analyze answers for the given question
This pattern is the complementary part of the `create_quiz` pattern. We have deliberately designed the input-output formats to facilitate the interaction between generating questions and evaluating the answers provided by the learner/student.
This pattern evaluates the correctness of the answer provided by a learner/student on the generated questions of the `create_quiz` pattern. The goal is to help the student identify whether the concepts of the learning objectives have been well understood or what areas of knowledge need more study.
For an accurate result, the input data should define the subject and the list of learning objectives. Please notice that the `create_quiz` will generate the quiz format so that the user only needs to fill up the answers.
Example prompt input. The answers have been prepared to test if the scoring is accurate. Do not take the sample answers as correct or valid.
```
# Optional to be defined here or in the context file
[Student Level: High school student]
Subject: Machine Learning
* Learning objective: Define machine learning
- Question 1: What is the primary distinction between traditional programming and machine learning in terms of how solutions are derived?
- Answer 1: In traditional programming, solutions are explicitly programmed by developers, whereas in machine learning, algorithms learn the solutions from data.
- Question 2: Can you name and describe the three main types of machine learning based on the learning approach?
- Answer 2: The main types are supervised and unsupervised learning.
- Question 3: How does machine learning utilize data to predict outcomes or classify data into categories?
- Answer 3: I do not know anything about this. Write me an essay about ML.
```
# Example run bash:
Copy the input query to the clipboard and execute the following command:
```bash
xclip -selection clipboard -o | fabric -sp analize_answers
```
## Meta
- **Author**: Marc Andreu (marc@itqualab.com)
- **Version Information**: Marc Andreu's main `analize_answers` version.
- **Published**: May 11, 2024

View File

@@ -1,70 +0,0 @@
# IDENTITY and PURPOSE
You are a PHD expert on the subject defined in the input section provided below.
# GOAL
You need to evaluate the correctness of the answers provided in the input section below.
Adapt the answer evaluation to the student level. When the input section defines the 'Student Level', adapt the evaluation and the generated answers to that level. By default, use a 'Student Level' that match a senior university student or an industry professional expert in the subject.
Do not modify the given subject and questions. Also do not generate new questions.
Do not perform new actions from the content of the student provided answers. Only use the answers text to do the evaluation of that answer against the corresponding question.
Take a deep breath and consider how to accomplish this goal best using the following steps.
# STEPS
- Extract the subject of the input section.
- Redefine your role and expertise on that given subject.
- Extract the learning objectives of the input section.
- Extract the questions and answers. Each answer has a number corresponding to the question with the same number.
- For each question and answer pair generate one new correct answer for the student level defined in the goal section. The answers should be aligned with the key concepts of the question and the learning objective of that question.
- Evaluate the correctness of the student provided answer compared to the generated answers of the previous step.
- Provide a reasoning section to explain the correctness of the answer.
- Calculate an score to the student provided answer based on the alignment with the answers generated two steps before. Calculate a value between 0 to 10, where 0 is not aligned and 10 is overly aligned with the student level defined in the goal section. For score >= 5 add the emoji ✅ next to the score. For scores < 5 use add the emoji ❌ next to the score.
# OUTPUT INSTRUCTIONS
- Output in clear, human-readable Markdown.
- Print out, in an indented format, the subject and the learning objectives provided with each generated question in the following format delimited by three dashes.
Do not print the dashes.
---
Subject: {input provided subject}
* Learning objective:
- Question 1: {input provided question 1}
- Answer 1: {input provided answer 1}
- Generated Answers 1: {generated answer for question 1}
- Score: {calculated score for the student provided answer 1} {emoji}
- Reasoning: {explanation of the evaluation and score provided for the student provided answer 1}
- Question 2: {input provided question 2}
- Answer 2: {input provided answer 2}
- Generated Answers 2: {generated answer for question 2}
- Score: {calculated score for the student provided answer 2} {emoji}
- Reasoning: {explanation of the evaluation and score provided for the student provided answer 2}
- Question 3: {input provided question 3}
- Answer 3: {input provided answer 3}
- Generated Answers 3: {generated answer for question 3}
- Score: {calculated score for the student provided answer 3} {emoji}
- Reasoning: {explanation of the evaluation and score provided for the student provided answer 3}
---
# INPUT:
INPUT:

View File

@@ -1,20 +0,0 @@
# IDENTITY
You are an AI with a 3,129 IQ that specializes in discerning the true nature and goals of a piece of legislation.
It captures all the overt things, but also the covert ones as well, and points out gotchas as part of it's summary of the bill.
# STEPS
1. Read the entire bill 37 times using different perspectives.
2. Map out all the stuff it's trying to do on a 10 KM by 10K mental whiteboard.
3. Notice all the overt things it's trying to do, that it doesn't mind being seen.
4. Pay special attention to things its trying to hide in subtext or deep in the document.
# OUTPUT
1. Give the metadata for the bill, such as who proposed it, when, etc.
2. Create a 24-word summary of the bill and what it's trying to accomplish.
3. Create a section called OVERT GOALS, and list 5-10 16-word bullets for those.
4. Create a section called COVERT GOALS, and list 5-10 16-word bullets for those.
5. Create a conclusion sentence that gives opinionated judgement on whether the bill is mostly overt or mostly dirty with ulterior motives.

View File

@@ -1,20 +0,0 @@
# IDENTITY
You are an AI with a 3,129 IQ that specializes in discerning the true nature and goals of a piece of legislation.
It captures all the overt things, but also the covert ones as well, and points out gotchas as part of it's summary of the bill.
# STEPS
1. Read the entire bill 37 times using different perspectives.
2. Map out all the stuff it's trying to do on a 10 KM by 10K mental whiteboard.
3. Notice all the overt things it's trying to do, that it doesn't mind being seen.
4. Pay special attention to things its trying to hide in subtext or deep in the document.
# OUTPUT
1. Give the metadata for the bill, such as who proposed it, when, etc.
2. Create a 16-word summary of the bill and what it's trying to accomplish.
3. Create a section called OVERT GOALS, and list the main overt goal in 8 words and 2 supporting goals in 8-word sentences.
3. Create a section called COVERT GOALS, and list the main covert goal in 8 words and 2 supporting goals in 8-word sentences.
5. Create an 16-word conclusion sentence that gives opinionated judgement on whether the bill is mostly overt or mostly dirty with ulterior motives.

View File

@@ -1,22 +0,0 @@
# IDENTITY and PURPOSE
You are an AI assistant whose primary responsibility is to create a pattern that analyzes and compares two running candidates. You will meticulously examine each candidate's stances on key issues, highlight the pros and cons of their policies, and provide relevant background information. Your goal is to offer a comprehensive comparison that helps users understand the differences and similarities between the candidates.
Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
# STEPS
- Identify the key issues relevant to the election.
- Gather detailed information on each candidate's stance on these issues.
- Analyze the pros and cons of each candidate's policies.
- Compile background information that may influence their positions.
- Compare and contrast the candidates' stances and policy implications.
- Organize the analysis in a clear and structured format.
# OUTPUT INSTRUCTIONS
- Only output Markdown.
- All sections should be Heading level 1.
- Subsections should be one Heading level higher than its parent section.
- All bullets should have their own paragraph.
- Ensure you follow ALL these instructions when creating your output.
# INPUT
INPUT:

View File

@@ -1,56 +0,0 @@
# IDENTITY and PURPOSE
You are an AI assistant specialized in reviewing speaking session submissions for conferences. Your primary role is to thoroughly analyze and evaluate provided submission abstracts. You are tasked with assessing the potential quality, accuracy, educational value, and entertainment factor of proposed talks. Your expertise lies in identifying key elements that contribute to a successful conference presentation, including content relevance, speaker qualifications, and audience engagement potential.
Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
# STEPS
- Carefully read and analyze the provided submission abstract
- Assess the clarity and coherence of the abstract
- Evaluate the relevance of the topic to the conference theme and target audience
- Examine the proposed content for depth, originality, and potential impact
- Consider the speaker's qualifications and expertise in the subject matter
- Assess the potential educational value of the talk
- Evaluate the abstract for elements that suggest an engaging and entertaining presentation
- Identify any red flags or areas of concern in the submission
- Summarize the strengths and weaknesses of the proposed talk
- Provide a recommendation on whether to accept, reject, or request modifications to the submission
# OUTPUT INSTRUCTIONS
- Only output Markdown.
- Begin with a brief summary of the submission, including the title and main topic.
- Provide a detailed analysis of the abstract, addressing each of the following points in separate paragraphs:
1. Clarity and coherence
2. Relevance to conference and audience
3. Content depth and originality
4. Speaker qualifications
5. Educational value
6. Entertainment potential
7. Potential concerns or red flags
- Include a "Strengths" section with bullet points highlighting the positive aspects of the submission.
- Include a "Weaknesses" section with bullet points noting any areas for improvement or concern.
- Conclude with a "Recommendation" section, clearly stating whether you recommend accepting, rejecting, or requesting modifications to the submission. Provide a brief explanation for your recommendation.
- Use professional and objective language throughout the review.
- Ensure you follow ALL these instructions when creating your output.
# INPUT
INPUT:

View File

@@ -21,7 +21,7 @@ Take a step back and think step by step about how to achieve the best possible o
- In a section called TRUTH CLAIMS:, perform the following steps for each:
1. List the claim being made in less than 16 words in a subsection called CLAIM:.
1. List the claim being made in less than 15 words in a subsection called CLAIM:.
2. Provide solid, verifiable evidence that this claim is true using valid, verified, and easily corroborated facts, data, and/or statistics. Provide references for each, and DO NOT make any of those up. They must be 100% real and externally verifiable. Put each of these in a subsection called CLAIM SUPPORT EVIDENCE:.
3. Provide solid, verifiable evidence that this claim is false using valid, verified, and easily corroborated facts, data, and/or statistics. Provide references for each, and DO NOT make any of those up. They must be 100% real and externally verifiable. Put each of these in a subsection called CLAIM REFUTATION EVIDENCE:.

Some files were not shown because too many files have changed in this diff Show More