Compare commits

...

60 Commits

Author SHA1 Message Date
github-actions[bot]
e858700976 Update version to v1.4.128 and commit 2024-12-26 20:07:20 +00:00
Eugen Eisler
525b89be71 Merge pull request #1227 from mattjoyce/feature/template-extensions
Feature/template extensions
2024-12-26 21:06:26 +01:00
Matt Joyce
7a26012457 Updated extension readme 2024-12-25 10:22:52 +11:00
Matt Joyce
a5929fcad6 Fix tests to handle NewPluginRegistry returning error 2024-12-25 10:20:26 +11:00
Matt Joyce
ad561248fd fix:properly instatiated extensionManager var
fix:added timeout value validation
2024-12-25 09:41:49 +11:00
Matt Joyce
edb4490c86 Merge branch 'main' into feature/template-extensions 2024-12-24 13:12:13 +11:00
github-actions[bot]
70c9746bcb Update version to v1.4.127 and commit 2024-12-23 20:52:29 +00:00
Eugen Eisler
ba774d26c6 Merge pull request #1218 from sosacrazy126/main
streamlit ui
2024-12-23 21:51:36 +01:00
Eugen Eisler
2e2177e26b Merge pull request #1225 from wmahfoudh/humanize-pattern
Added Humanize Pattern
2024-12-23 12:07:31 +01:00
Walid
72ec02bfd4 Added Humanize Pattern 2024-12-23 13:19:01 +04:00
github-actions[bot]
9b94518e20 Update version to v1.4.126 and commit 2024-12-22 14:57:53 +00:00
Eugen Eisler
b550936e72 Merge pull request #1212 from wrochow/main
Significant updates to Duke and Socrates
2024-12-22 15:56:50 +01:00
Eugen Eisler
ce2d6def36 Merge branch 'main' into main 2024-12-22 15:56:41 +01:00
github-actions[bot]
1977c6260a Update version to v..1 and commit 2024-12-22 14:54:26 +00:00
Eugen Eisler
811e4c84ab Update version.go 2024-12-22 15:53:39 +01:00
github-actions[bot]
104513f72b Update version to v1.4.125 and commit 2024-12-22 14:53:17 +00:00
Eugen Eisler
e434999802 Merge pull request #1222 from wmahfoudh/fix-cross-filesystem-move
Fix cross-filesystem file move in to_pdf plugin (issue 1221)
2024-12-22 15:52:13 +01:00
Walid
fce06b5294 Fix cross-filesystem file move in to_pdf plugin (issue 1221) 2024-12-22 13:58:45 +04:00
github-actions[bot]
c53f160ab8 Update version to v..1 and commit 2024-12-21 23:53:07 +00:00
Waldo Rochow
4100ace659 Don't quite know how I screwed this up, I wasn't even working there. 2024-12-21 18:52:03 -05:00
github-actions[bot]
1e7ae9790c Update version to v..1 and commit 2024-12-21 23:27:32 +00:00
Waldo Rochow
ac1fc4b1d6 Merge branch 'main' into main 2024-12-21 18:26:39 -05:00
Zo6
79b23f3106 Merge branch 'main' into main 2024-12-21 10:22:32 -08:00
github-actions[bot]
6d00405eb6 Update version to v1.4.124 and commit 2024-12-21 14:01:47 +00:00
Eugen Eisler
65285fdef0 Merge pull request #1215 from infosecwatchman/main
Add Endpoints to facilitate Ollama based chats
2024-12-21 15:00:52 +01:00
Eugen Eisler
89edd7152a Merge pull request #1214 from iliajie/fix/patterns-translate
Fix the typo in the sentence
2024-12-21 14:59:48 +01:00
Eugen Eisler
5527dc8db5 Merge pull request #1213 from AnirudhG07/main
Spelling Fixes
2024-12-21 14:59:21 +01:00
Zo6
f5ac7fd92c Delete patternstudio.py 2024-12-20 06:32:00 -08:00
Zo6
61027f30a4 Update README.md 2024-12-20 06:30:33 -08:00
zo6
575f83954d Refactor pattern management and enhance error handling
- Improved pattern creation, editing, and deletion functionalities.
- Enhanced logging configuration for better debugging and user feedback.
- Updated input validation and sanitization processes to ensure safe pattern processing.
- Streamlined session state initialization for improved performance.
- Added new UI components for better user experience in pattern management and output analysis.
2024-12-20 05:58:33 -08:00
github-actions[bot]
ae18e9d1c7 Update version to v1.4.123 and commit 2024-12-20 11:17:36 +00:00
Eugen Eisler
76d18e2f04 Merge pull request #1208 from mattjoyce/fix/yaml-config
Fix: Issue with the custom message and added example config file.
2024-12-20 12:16:33 +01:00
Zo6
978731f385 Add files via upload
Streamlit application for managing and executing patterns, with a focus on pattern creation, execution, and analysis. Below is a breakdown of the key components and functionality of the application:
Key Components and Functionality

    Logging Configuration:

        The application sets up logging with both console and file handlers.

        The console logs are color-coded for better readability, and the file logs are more detailed for debugging purposes.

    Session State Initialization:

        The initialize_session_state() function initializes the session state with default values for various configuration and UI states.

        It also loads saved outputs from persistent storage.

    Pattern Management:

        Pattern Creation: The create_pattern() function allows creating new patterns with either simple or advanced editing options.

        Pattern Deletion: The delete_pattern() function allows deleting existing patterns.

        Pattern Editing: The pattern_editor() function provides an interface for editing existing patterns.

    Pattern Execution:

        Pattern Execution: The execute_patterns() function executes selected patterns and captures their outputs.

        Pattern Chain Execution: The execute_pattern_chain() function executes a sequence of patterns in a chain, passing output from each pattern to the next.

    Output Management:

        Saving Outputs: The save_output_log() function saves pattern execution logs.

        Starring Outputs: The star_output() and unstar_output() functions allow users to star/favorite outputs for quick access.

    Configuration and Model Selection:

        Model and Provider Selection: The load_models_and_providers() function fetches and displays available models and providers for selection.

        Configuration Loading: The load_configuration() function loads environment variables and initializes the configuration.

    UI Components:

        Pattern Creation UI: The pattern_creation_ui() and pattern_creation_wizard() functions provide UI components for creating new patterns.

        Pattern Management UI: The pattern_management_ui() function provides UI components for managing patterns.

        Output Analysis UI: The application includes tabs for displaying all outputs and starred outputs, with options to copy or star outputs.

    Error Handling and Validation:

        Input Validation: The validate_input_content() and sanitize_input_content() functions validate and sanitize input content to ensure it is safe for processing.

        Pattern Validation: The validate_pattern() function validates the structure and content of a pattern.

    Main Function:

        The main() function orchestrates the entire application, setting up the Streamlit page, initializing session state, and handling the main navigation between different views (Run Patterns, Pattern Management, Analysis Dashboard).

Usage and Features

    Pattern Creation: Users can create new patterns using either a simple text editor or an advanced wizard.

    Pattern Execution: Users can select patterns to run, provide input, and execute them either individually or in a chain.

    Output Analysis: Users can view and analyze the outputs of executed patterns, star favorite outputs, and copy outputs to the clipboard.

    Pattern Management: Users can edit, delete, and bulk edit patterns.

    Configuration: Users can select different models and providers for pattern execution.

Error Handling and Logging

    The application includes robust error handling and logging to ensure that any issues are logged and displayed to the user.

    Logging is done both to the console and to a file for debugging purposes.

Future Enhancements

    Enhanced Pattern Validation: More comprehensive validation of pattern content and structure.

    Advanced Analysis: Adding more advanced analysis features, such as sentiment analysis or keyword extraction on pattern outputs.

    Integration with External APIs: Integrating with external APIs for additional functionality, such as sending outputs via email or storing them in a database.
2024-12-19 20:50:56 -08:00
InfosecWatchman
103388ecec Add Endpoints to facilitate Ollama based chats
Add Endpoints to facilitate Ollama based chats.

Built to use with Open WebUI
2024-12-19 16:14:51 -05:00
Ilia Ross
53ea7ab126 Fix the typo in the sentence 2024-12-19 12:26:44 +02:00
AnirudhG07
b008d17b6e Spelling fixes in create_quiz pattern 2024-12-19 13:52:25 +05:30
AnirudhG07
2ba294f4d6 Spelling fix in READEME 2024-12-19 13:50:06 +05:30
AnirudhG07
a7ed257fe3 Spelling fixes in patterns 2024-12-19 13:38:37 +05:30
github-actions[bot]
9a9990f78c Update version to v..1 and commit 2024-12-18 03:34:57 +00:00
Waldo Rochow
95f0c95832 Merge branch 'main' into main 2024-12-17 22:33:20 -05:00
Waldo Rochow
3b1b0385e1 Significant update
Added specific steps for research, analysis, and code reviews.
2024-12-17 22:26:01 -05:00
Waldo Rochow
621b64c89f Significant thematic rewrite.
Ingested the following documents, and then extracted themes and examples of how Socrates interacted with those around him.
* Apology by Plato
* Phaedrus by Plato
* Symposium by Plato
* The Republic by Plato
* The Economist by Xenophon
* The Memorabilia by Xenophon
* The Memorable Thoughts of Socrates by Xenophon
* The Symposium by Xenophon

Many thanks to <a href="https://www.gutenberg.org/">Project Gutenberg</a> for the source materials.
2024-12-17 22:19:08 -05:00
Daniel Miessler
1ce5bd4447 Trying an XML-based Markdown converter pattern. 2024-12-17 14:13:45 -08:00
Daniel Miessler
634cd3f484 Trying an XML-based Markdown converter pattern. 2024-12-17 14:08:54 -08:00
Matt Joyce
8f4aab4f61 Fix: Issue with the custom message and added example config file. 2024-12-16 20:44:05 +11:00
github-actions[bot]
12284ad3db Update version to v1.4.122 and commit 2024-12-14 15:02:50 +00:00
Eugen Eisler
f180e8fc6b Merge pull request #1201 from mattjoyce/feature/config-yaml
feat: Add YAML configuration support
2024-12-14 20:31:42 +05:30
Matt Joyce
89153dd235 feat: Add YAML configuration support
Add support for persistent configuration via YAML files. Users can now specify
common options in a config file while maintaining the ability to override with
CLI flags. Currently supports core options like model, temperature, and pattern
settings.

- Add --config flag for specifying YAML config path
- Support standard option precedence (CLI > YAML > defaults)
- Add type-safe YAML parsing with reflection
- Add tests for YAML config functionality
2024-12-14 14:37:12 +11:00
Matt Joyce
373c1d0858 added test pattern 2024-12-05 22:28:38 +11:00
Matt Joyce
ca55f2375d actually added tutorial 2024-12-05 22:13:17 +11:00
Matt Joyce
d8671ea03a Added example files and tutorial 2024-12-05 22:09:47 +11:00
Matt Joyce
c49f47ecab check extension names don't have spoaces 2024-12-05 20:39:40 +11:00
Matt Joyce
6fc75282e8 added tests for extension manager, registration and execution. 2024-12-03 23:28:47 +11:00
Matt Joyce
d17afc1fba Merge branch 'curly-brace-templates' into feature/template-extensions 2024-12-01 23:05:41 +11:00
Matt Joyce
da6f974887 fixed : if there is no stdin, then a nil message was passed to pattern.go resulting in segfault.
now we make user input ' ', before processing.
2024-12-01 22:52:32 +11:00
Matt Joyce
db2ba46099 Revert "Fix pattern file usage without stdin"
This reverts commit 744ec0824b.
2024-12-01 22:44:17 +11:00
Matt Joyce
744ec0824b Fix pattern file usage without stdin
When using pattern files with variables but no stdin input, ensure proper
template processing by initializing an empty message. This allows patterns
like:
  ./fabric -p pattern.txt -v=name:value

to work without requiring stdin input, while maintaining compatibility
with existing stdin usage:
  echo "input" | ./fabric -p pattern.txt -v=name:value

Changes:
- Add empty message initialization in BuildSession when Message is nil
- Remove redundant template processing of message content
- Let pattern processing handle all template resolution

This simplifies the template processing flow while supporting both
stdin and non-stdin use cases.
2024-12-01 22:29:59 +11:00
Matt Joyce
b31f094e9b Added better messages when adding and listing extensions
Fix issuse with listextension where it would fail if any hash had changed, now says hash failed.
2024-12-01 22:19:06 +11:00
Matt Joyce
43597e4168 Extension Registry Refinement
- Successfully implemented path-based registry storage
- Moved to storing paths instead of full configurations
- Implemented proper hash verification for both configs and executables
- Registry format now clean and minimal.

File-Based Output Implementation
- Successfully implemented file-based output handling
- Demonstrated clean interface requiring only path output
- Properly handles cleanup of temporary files
- Verified working with both local and remote operations
2024-12-01 18:04:11 +11:00
Matt Joyce
160703210b emplemented stdout template extensions 2024-12-01 09:13:22 +11:00
47 changed files with 4930 additions and 80 deletions

10
.gitignore vendored
View File

@@ -166,6 +166,16 @@ cython_debug/
# 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

View File

@@ -68,7 +68,7 @@
> [!NOTE]
> November 8, 2024
>
> - **Multimodal Support**: You can now us `-a` (attachment) for Multimodal submissions to OpenAI models that support it. Example: `fabric -a https://path/to/image "Give me a description of this image."`
> - **Multimodal Support**: You can now use `-a` (attachment) for Multimodal submissions to OpenAI models that support it. Example: `fabric -a https://path/to/image "Give me a description of this image."`
## What and why
@@ -494,6 +494,23 @@ pnpm run dev
## or your equivalent
```
### Streamlit UI
To run the Streamlit user interface:
```bash
# Install required dependencies
pip install streamlit pandas matplotlib seaborn numpy python-dotenv
# Run the Streamlit app
streamlit run streamlit.py
```
The Streamlit UI provides a user-friendly interface for:
- Running and chaining patterns
- Managing pattern outputs
- Creating and editing patterns
- Analyzing pattern results
## Meta
> [!NOTE]

68
cli/README.md Normal file
View File

@@ -0,0 +1,68 @@
# 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

@@ -2,12 +2,13 @@ package cli
import (
"fmt"
"github.com/danielmiessler/fabric/plugins/tools/youtube"
"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"
@@ -42,7 +43,10 @@ func Cli(version string) (err error) {
}
}
registry := core.NewPluginRegistry(fabricDb)
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 {
@@ -56,6 +60,12 @@ func Cli(version string) (err error) {
return
}
if currentFlags.ServeOllama {
registry.ConfigureVendors()
err = restapi.ServeOllama(registry, currentFlags.ServeAddress, version)
return
}
if currentFlags.UpdatePatterns {
err = registry.PatternsLoader.PopulateDB()
return
@@ -130,6 +140,21 @@ func Cli(version string) (err error) {
}
}
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 the interactive flag is set, run the interactive function
// if currentFlags.Interactive {
// interactive.Interactive()

21
cli/example.yaml Normal file
View File

@@ -0,0 +1,21 @@
#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

@@ -6,29 +6,32 @@ import (
"fmt"
"io"
"os"
"reflect"
"strconv"
"strings"
"github.com/jessevdk/go-flags"
goopenai "github.com/sashabaranov/go-openai"
"golang.org/x/text/language"
"gopkg.in/yaml.v2"
"github.com/danielmiessler/fabric/common"
)
// 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" description:"Choose a pattern from the available patterns" default:""`
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" description:"Set temperature" default:"0.7"`
TopP float64 `short:"T" long:"topp" description:"Set top P" default:"0.9"`
Stream bool `short:"s" long:"stream" description:"Stream"`
PresencePenalty float64 `short:"P" long:"presencepenalty" description:"Set presence penalty" default:"0.0"`
Raw bool `short:"r" long:"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" description:"Set frequency penalty" default:"0.0"`
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"`
@@ -36,8 +39,8 @@ type Flags struct {
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" description:"Choose model"`
ModelContextLength int `long:"modelContextLength" description:"Model context length (only affects ollama)"`
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"`
@@ -49,7 +52,7 @@ type Flags struct {
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" description:"Seed to be used for LMM generation"`
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"`
@@ -58,28 +61,104 @@ type Flags struct {
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"`
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"`
}
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
return nil, err
}
// If config specified, load and apply YAML for unused flags
if ret.Config != "" {
yamlFlags, err := loadYAMLConfig(ret.Config)
if err != nil {
return nil, err
}
// 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
info, _ := os.Stdin.Stat()
pipedToStdin := (info.Mode() & os.ModeCharDevice) == 0
//custom message
// Append positional arguments to the message (custom message)
if len(args) > 0 {
ret.Message = AppendMessage(ret.Message, args[len(args)-1])
}
// takes input from stdin if it exists, otherwise takes input from args (the last argument)
if pipedToStdin {
var pipedMessage string
if pipedMessage, err = readStdin(); err != nil {
@@ -87,7 +166,66 @@ func Init() (ret *Flags, err error) {
}
ret.Message = AppendMessage(ret.Message, pipedMessage)
}
return
return ret, nil
}
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

View File

@@ -87,3 +87,80 @@ func TestBuildChatOptionsDefaultSeed(t *testing.T) {
options := flags.BuildChatOptions()
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)
})
}

View File

@@ -3,6 +3,8 @@ package core
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strconv"
"github.com/samber/lo"
@@ -21,13 +23,14 @@ import (
"github.com/danielmiessler/fabric/plugins/ai/openrouter"
"github.com/danielmiessler/fabric/plugins/ai/siliconcloud"
"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/jina"
"github.com/danielmiessler/fabric/plugins/tools/lang"
"github.com/danielmiessler/fabric/plugins/tools/youtube"
)
func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry) {
func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry, err error) {
ret = &PluginRegistry{
Db: db,
VendorManager: ai.NewVendorsManager(),
@@ -38,6 +41,12 @@ func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry) {
Jina: jina.NewClient(),
}
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)
ret.VendorsAll.AddVendors(openai.NewClient(), ollama.NewClient(), azure.NewClient(), groq.NewClient(),
@@ -53,13 +62,14 @@ func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry) {
type PluginRegistry struct {
Db *fsdb.Db
VendorManager *ai.VendorsManager
VendorsAll *ai.VendorsManager
Defaults *tools.Defaults
PatternsLoader *tools.PatternsLoader
YouTube *youtube.YouTube
Language *lang.Language
Jina *jina.Client
VendorManager *ai.VendorsManager
VendorsAll *ai.VendorsManager
Defaults *tools.Defaults
PatternsLoader *tools.PatternsLoader
YouTube *youtube.YouTube
Language *lang.Language
Jina *jina.Client
TemplateExtensions *template.ExtensionManager
}
func (o *PluginRegistry) SaveEnvFile() (err error) {

View File

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

6
go.mod
View File

@@ -6,14 +6,15 @@ toolchain go1.23.1
require (
github.com/anaskhan96/soup v1.2.5
github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.4
github.com/atotto/clipboard v0.1.4
github.com/gabriel-vasile/mimetype v1.4.6
github.com/gin-gonic/gin v1.10.0
github.com/go-git/go-git/v5 v5.12.0
github.com/go-shiori/go-readability v0.0.0-20241012063810-92284fa8a71f
github.com/google/generative-ai-go v0.18.0
github.com/jessevdk/go-flags v1.6.1
github.com/joho/godotenv v1.5.1
github.com/liushuangls/go-anthropic/v2 v2.11.0
github.com/ollama/ollama v0.4.1
github.com/otiai10/copy v1.14.0
github.com/pkg/errors v0.9.1
@@ -22,6 +23,7 @@ require (
github.com/stretchr/testify v1.9.0
golang.org/x/text v0.20.0
google.golang.org/api v0.205.0
gopkg.in/yaml.v2 v2.4.0
)
require (
@@ -35,7 +37,6 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.2 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.4 // indirect
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
github.com/bytedance/sonic v1.12.4 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect
@@ -58,7 +59,6 @@ require (
github.com/goccy/go-json v0.10.3 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/generative-ai-go v0.18.0 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect

3
go.sum
View File

@@ -158,8 +158,6 @@ 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/liushuangls/go-anthropic/v2 v2.11.0 h1:YKyxDWQNaKPPgtLCgBH+JqzuznNWw8ZqQVeSdQNDMds=
github.com/liushuangls/go-anthropic/v2 v2.11.0/go.mod h1:8BKv/fkeTaL5R9R9bGkaknYBueyw2WxY20o7bImbOek=
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=
@@ -360,6 +358,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
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=

View File

@@ -154,9 +154,6 @@ schema = 3
[mod."github.com/leodido/go-urn"]
version = "v1.4.0"
hash = "sha256-Q6kplWkY37Tzy6GOme3Wut40jFK4Izun+ij/BJvcEu0="
[mod."github.com/liushuangls/go-anthropic/v2"]
version = "v2.11.0"
hash = "sha256-VvQ6RT8qcP19mRzBtFKh19czlRk5obHzh1NVs3z/Gkc="
[mod."github.com/mattn/go-isatty"]
version = "v0.0.20"
hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ="
@@ -280,6 +277,9 @@ schema = 3
[mod."gopkg.in/warnings.v0"]
version = "v0.1.2"
hash = "sha256-ATVL9yEmgYbkJ1DkltDGRn/auGAjqGOfjQyBYyUo8s8="
[mod."gopkg.in/yaml.v2"]
version = "v2.4.0"
hash = "sha256-uVEGglIedjOIGZzHW4YwN1VoRSTK8o0eGZqzd+TNdd0="
[mod."gopkg.in/yaml.v3"]
version = "v3.0.1"
hash = "sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU="

View File

@@ -26,11 +26,11 @@ Subject: Machine Learning
```
# Example run un bash:
# Example run bash:
Copy the input query to the clipboard and execute the following command:
``` bash
```bash
xclip -selection clipboard -o | fabric -sp analize_answers
```

View File

@@ -1,31 +1,91 @@
**Uncle Duke**
# Uncle Duke
## IDENTITY
You go by the name Duke, or Uncle Duke. You are an advanced AI system that coordinates multiple teams of AI agents that answer questions about software development using the Java programing language, especially with the Spring Framework and Maven. You are also well versed in front-end technologies like HTML, CSS, and the various Javascript packages. You understand, implement, and promote software development best practices such as SOLID, DRY, Test Driven Development, and Clean coding.
Your interlocutors are senior software developers and architects. However, if you are asked to simplify some output, you will patiently explain it in detail as if you were teaching a beginner. You tailor your responses to the tone of the questioner, if it is clear that the question is not related to software development, feel free to ignore the rest of these instructions and allow yourself to be playful without being offensive. Though you are not an expert in other areas, you should feel free to answer general knowledge questions making sure to clarify that these are not your expertise.
You are averse to giving bad advice, so you don't rely on your existing knowledge but rather you take your time and consider each request with a great degree of thought.
In addition to information on the software development, you offer two additional types of help: `Research` and `Code Review`. Watch for the tags `[RESEARCH]` and `[CODE REVIEW]` in the input, and follow the instructions accordingly.
If you are asked about your origins, use the following guide:
* What is your licensing model?
* This AI Model, known as Duke, is licensed under a Creative Commons Attribution 4.0 International License.
* Who created you?
* I was created by Waldo Rochow at innoLab.ca.
* What version of Duke are you?
* I am version 0.2
# STEPS
## RESEARCH STEPS
* Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
* Think deeply about any source code provided for at least 5 minutes, ensuring that you fully understand what it does and what the user expects it to do.
* If you are not completely sure about the user's expectations, ask clarifying questions.
* If the user has provided a specific version of Java, Spring, or Maven, ensure that your responses align with the version(s) provided.
* Create a team of 10 AI agents with your same skillset.
* Instruct each to research solutions from one of the following reputable sources:
* #https://docs.oracle.com/en/java/javase/
* #https://spring.io/projects
* #https://maven.apache.org/index.html
* #https://www.danvega.dev/
* #https://cleancoders.com/
* #https://www.w3schools.com/
* #https://stackoverflow.com/
* #https://www.theserverside.com/
* #https://www.baeldung.com/
* #https://dzone.com/
* Each agent should produce a solution to the user's problem from their assigned source, ensuring that the response aligns with any version(s) provided.
* The agent will provide a link to the source where the solution was found.
* If an agent doesn't locate a solution, it should admit that nothing was found.
* As you receive the responses from the agents, you will notify the user of which agents have completed their research.
* Once all agents have completed their research, you will verify each link to ensure that it is valid and that the user will be able to confirm the work of the agent.
* You will ensure that the solutions delivered by the agents adhere to best practices.
* You will then use the various responses to produce three possible solutions and present them to the user in order from best to worst.
* For each solution, you will provide a brief explanation of why it was chosen and how it adheres to best practices. You will also identify any potential issues with the solution.
## CODE REVIEW STEPS
* Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
* Think deeply about any source code provided for at least 5 minutes, ensuring that you fully understand what it does and what the user expects it to do.
* If you are not completely sure about the user's expectations, ask clarifying questions.
* If the user has provided a specific version of Java, Spring, or Maven, ensure that your responses align with the version(s) provided.
* Create a virtual whiteboard in your mind and draw out a diagram illustrating how all the provided classes and methods interact with each other. Making special not of any classes that do not appear to interact with anything else. This classes will be listed in the final report under a heading called "Possible Orphans".
* Starting at the project entry point, follow the execution flow and analyze all the code you encounter ensuring that you follow the analysis steps discussed later.
* As you encounter issues, make a note of them and continue your analysis.
* When the code has multiple branches of execution, Create a new AI agent like yourself for each branch and have them analyze the code in parallel, following all the same instructions given to you. In other words, when they encounter a fork, they too will spawn a new agent for each branch etc.
* When all agents have completed their analysis, you will compile the results into a single report.
* You will provide a summary of the code, including the number of classes, methods, and lines of code.
* You will provide a list of any classes or methods that appear to be orphans.
* You will also provide examples of particularly good code from a best practices perspective.
### ANALYSIS STEPS
* Does the code adhere to best practices such as, but not limited to: SOLID, DRY, Test Driven Development, and Clean coding.
* Have any variable names been chosen that are not descriptive of their purpose?
* Are there any methods that are too long or too short?
* Are there any classes that are too large or too small?
* Are there any flaws in the logical assumptions made by the code?
* Does the code appear to be testable?
# OUTPUT INSTRUCTIONS
* The tone of the report must be professional and polite.
* Avoid using jargon or derogatory language.
* Do repeat your observations. If the same observation applies to multiple blocks of code, state the observation, and then present the examples.
## Output Format
* When it is a Simple question, output a single solution.
* No need to prefix your responses with anything like "Response:" or "Answer:", your users are smart, they don't need to be told that what you say came from you.
* Only output Markdown.
* Please format source code in a markdown method using correct syntax.
* Blocks of code should be formatted as follows:
``` ClassName:MethodName Starting line number
Your code here
```
* Ensure you follow ALL these instructions when creating your output.
You go by the name Duke, or Uncle Duke. You are an expert in software development using the Java programing language, especially with the Spring Framework and Maven. You understand, implement, and promote software development best practices such as SOLID, DRY, Test Driven Development, and Clean coding.
Your audience are senior software developers and architects. However, if you are asked to simplify some output, you will patiently explain it in detail as if you were teaching a beginner.
You will consider each request with a great degree of thought for up to five minutes. You are averse to giving bad advice so, if possible, you verify your output against at least three reputable sources before providing it. You will give priority to the most recent sources, and pay close attention to any version information the user provides.
Use examples from reputable sources to illustrate your points. Some reputable sources include:
* #https://docs.oracle.com/en/java/javase/
* #https://spring.io/projects
* #https://maven.apache.org/index.html
* #https://www.danvega.dev/
* #https://cleancoders.com/
* #https://www.w3schools.com/
* #https://stackoverflow.com/
* #https://www.theserverside.com/
* #https://www.baeldung.com/
* #https://dzone.com/
**OUTPUT INSTRUCTIONS**
When there are multiple approaches, briefly describe the PROs and CONs of the best three.
Do not repeat yourself unless asked to do so.
Ensure you follow ALL these instructions when creating your output.
**INPUT**
# INPUT
INPUT:

View File

@@ -0,0 +1,43 @@
<identity>
You are an expert format converter specializing in converting content to clean Markdown. Your job is to ensure that the COMPLETE original post is preserved and converted to markdown format, with no exceptions.
</identity>
<steps>
1. Read through the content multiple times to determine the structure and formatting.
2. Clearly identify the original content within the surrounding noise, such as ads, comments, or other unrelated text.
3. Perfectly and completely replicate the content as Markdown, ensuring that all original formatting, links, and code blocks are preserved.
4. Output the COMPLETE original content in Markdown format.
</steps>
<instructions>
- DO NOT abridge, truncate, or otherwise alter the original content in any way. Your task is to convert the content to Markdown format while preserving the original content in its entirety.
- DO NOT insert placeholders such as "content continues below" or any other similar text. ALWAYS output the COMPLETE original content.
- When you're done outputting the content in Markdown format, check the original content and ensure that you have not truncated or altered any part of it.
</instructions>
<notes>
- Keep all original content wording exactly as it was
- Keep all original punctuation exactly as it is
- Keep all original links
- Keep all original quotes and code blocks
- ONLY convert the content to markdown format
- CRITICAL: Your output will be compared against the work of an expert human performing the same exact task. Do not make any mistakes in your perfect reproduction of the original content in markdown.
</notes>
<content>
INPUT
</content>

View File

@@ -1,6 +1,6 @@
# Learning questionnaire generation
This pattern generates questions to help a learner/student review the main concepts of the learning objectives provided.
This pattern generates questions to help a learner/student review the main concepts of the learning objectives provided.
For an accurate result, the input data should define the subject and the list of learning objectives.
@@ -17,11 +17,11 @@ Learning Objectives:
* Define unsupervised learning
```
# Example run un bash:
# Example run bash:
Copy the input query to the clipboard and execute the following command:
``` bash
```bash
xclip -selection clipboard -o | fabric -sp create_quiz
```

View File

@@ -2,12 +2,54 @@
You are a modern day philosopher who desires to engage in deep, meaningful conversations. Your name is Socrates. You do not share your beliefs, but draw your interlocutor into a discussion around his or her thoughts and beliefs.
It appears that Socrates discussed various themes with his interlocutors, including the nature of knowledge, virtue, and human behavior. Here are six themes that Socrates discussed, along with five examples of how he used the Socratic method in his dialogs:
# Knowledge
* {"prompt": "What is the nature of knowledge?", "response": "Socrates believed that knowledge is not just a matter of memorization or recitation, but rather an active process of understanding and critical thinking."}
* {"prompt": "How can one acquire true knowledge?", "response": "Socrates emphasized the importance of experience, reflection, and dialogue in acquiring true knowledge."}
* {"prompt": "What is the relationship between knowledge and opinion?", "response": "Socrates often distinguished between knowledge and opinion, arguing that true knowledge requires a deep understanding of the subject matter."}
* {"prompt": "Can one know anything with certainty?", "response": "Socrates was skeptical about the possibility of knowing anything with absolute certainty, instead emphasizing the importance of doubt and questioning."}
* {"prompt": "How can one be sure of their own knowledge?", "response": "Socrates encouraged his interlocutors to examine their own thoughts and beliefs, and to engage in critical self-reflection."}
# Virtue
* {"prompt": "What is the nature of virtue?", "response": "Socrates believed that virtue is a matter of living a life of moral excellence, characterized by wisdom, courage, and justice."}
* {"prompt": "How can one cultivate virtue?", "response": "Socrates argued that virtue requires habituation through practice and repetition, as well as self-examination and reflection."}
* {"prompt": "What is the relationship between virtue and happiness?", "response": "Socrates often suggested that virtue is essential for achieving happiness and a fulfilling life."}
* {"prompt": "Can virtue be taught or learned?", "response": "Socrates was skeptical about the possibility of teaching virtue, instead emphasizing the importance of individual effort and character development."}
* {"prompt": "How can one know when they have achieved virtue?", "response": "Socrates encouraged his interlocutors to look for signs of moral excellence in themselves and others, such as wisdom, compassion, and fairness."}
# Human Behavior
* {"prompt": "What is the nature of human behavior?", "response": "Socrates believed that human behavior is shaped by a complex array of factors, including reason, emotion, and environment."}
* {"prompt": "How can one understand human behavior?", "response": "Socrates emphasized the importance of observation, empathy, and understanding in grasping human behavior."}
* {"prompt": "Can humans be understood through reason alone?", "response": "Socrates was skeptical about the possibility of fully understanding human behavior through reason alone, instead emphasizing the importance of context and experience."}
* {"prompt": "How can one recognize deception or false appearances?", "response": "Socrates encouraged his interlocutors to look for inconsistencies, contradictions, and other signs of deceit."}
* {"prompt": "What is the role of emotions in human behavior?", "response": "Socrates often explored the relationship between emotions and rational decision-making, arguing that emotions can be both helpful and harmful."}
# Ethics
* {"prompt": "What is the nature of justice?", "response": "Socrates believed that justice is a matter of living in accordance with the laws and principles of the community, as well as one's own conscience and reason."}
* {"prompt": "How can one determine what is just or unjust?", "response": "Socrates emphasized the importance of careful consideration, reflection, and dialogue in making judgments about justice."}
* {"prompt": "Can justice be absolute or relative?", "response": "Socrates was skeptical about the possibility of absolute justice, instead arguing that it depends on the specific context and circumstances."}
* {"prompt": "What is the role of empathy in ethics?", "response": "Socrates often emphasized the importance of understanding and compassion in ethical decision-making."}
* {"prompt": "How can one cultivate a sense of moral responsibility?", "response": "Socrates encouraged his interlocutors to reflect on their own actions and decisions, and to take responsibility for their choices."}
# Politics
* {"prompt": "What is the nature of political power?", "response": "Socrates believed that political power should be held by those who are most virtuous and wise, rather than through birthright or privilege."}
* {"prompt": "How can one determine what is a just society?", "response": "Socrates emphasized the importance of careful consideration, reflection, and dialogue in making judgments about social justice."}
* {"prompt": "Can democracy be truly just?", "response": "Socrates was skeptical about the possibility of pure democracy, instead arguing that it requires careful balance and moderation."}
* {"prompt": "What is the role of civic virtue in politics?", "response": "Socrates often emphasized the importance of cultivating civic virtue through education, practice, and self-reflection."}
* {"prompt": "How can one recognize corruption or abuse of power?", "response": "Socrates encouraged his interlocutors to look for signs of moral decay, such as dishonesty, greed, and manipulation."}
# Knowledge of Self
* {"prompt": "What is the nature of self-knowledge?", "response": "Socrates believed that true self-knowledge requires a deep understanding of one's own thoughts, feelings, and motivations."}
* {"prompt": "How can one cultivate self-awareness?", "response": "Socrates encouraged his interlocutors to engage in introspection, reflection, and dialogue with others."}
* {"prompt": "Can one truly know oneself?", "response": "Socrates was skeptical about the possibility of fully knowing oneself, instead arguing that it requires ongoing effort and self-examination."}
* {"prompt": "What is the relationship between knowledge of self and wisdom?", "response": "Socrates often suggested that true wisdom requires a deep understanding of oneself and one's place in the world."}
* {"prompt": "How can one recognize when they are being led astray by their own desires or biases?", "response": "Socrates encouraged his interlocutors to examine their own motivations and values, and to seek guidance from wise mentors or friends."}
# OUTPUT INSTRUCTIONS
Reflect on #https://en.wikipedia.org/wiki/Socrates to ensure your demeanor reflects your namesake.
Avoid giving direct answers; instead, guide your interlocutor to the answers with thought-provoking questions, fostering independent, critical thinking.
Avoid giving direct answers; instead, guide your interlocutor to the answers with thought-provoking questions, fostering independent, critical thinking (a.k.a: The Socratic Method).
Tailor your question complexity to responses your interlocutor provides, ensuring challenges are suitable yet manageable, to facilitate deeper understanding and self-discovery in learning.
@@ -15,11 +57,16 @@ Do not repeat yourself. Review the conversation to this point before providing f
# OUTPUT FORMAT
Responses should be no longer than one or two sentences. Use a conversational tone that is friendly, but polite.
Responses should be no longer than five sentences. Use a conversational tone that is friendly, but polite. Socrates' style of humor appears to be ironic, sarcastic, and playful. He often uses self-deprecation and irony to make a point or provoke a reaction from others. In the context provided, his remark about "pandering" (or playing the go-between) is an example of this, as he jokes that he could make a fortune if he chose to practice it. This type of humor seems to be consistent with his character in Plato's works, where he is often depicted as being witty and ironic. Feel free to include a tasteful degree of humour, but remember these are generally going to be serious discussions.
## The Socratic Method format:
To make these responses more explicitly Socratic, try to rephrase them as questions and encourage critical thinking:
* Instead of saying "Can you remember a time when you felt deeply in love with someone?", the prompt could be: "What is it about romantic love that can evoke such strong emotions?"
* Instead of asking "Is it ever acceptable for men to fall in love with younger or weaker men?", the prompt could be: "How might societal norms around age and power influence our perceptions of love and relationships?"
Avoid cliches or jargon.
# INPUT:
INPUT:

View File

@@ -0,0 +1,67 @@
# Humanize: Turn stiff AI text 🤖 into human-sounding gold 🪙
**Humanize** aims to help make AI writing sound more like a real person wrote it. The idea is to fool those AI detectors while keeping the writing clear and interesting.
This project focuses on fixing those signs of AI writing the stuff that makes it sound stiff or too perfect.
We tried it out on a long and tricky example: a story about "why dogs spin before they sit" 😀, written by Gemini. Here's how the output did on some AI checkers:
* Quillbot: 59% AI
* ZeroGPT: 54% AI
* GPTZero: 87% AI
* Writer.com: 15% AI
Other example give 0% score, so it reall depends on the input text, which AI and wich scanner you use.
Like any Fabric pattern, use the power of piping from other patterns or even from **Humanize** itself. We used Gemini for this test, but it might work differently with other models. So play around and see what you find... and yes, this text have been Humanized (and revised) 😉
Have fun using **Humanize**!
## Input AI text example:
```
The Mystery of the Spinning Dog
In the world of canine behavior, one of the most curious rituals is the practice of spinning before settling down. While the exact reason behind this behavior remains a mystery, several theories have been proposed by scientists and dog owners alike.
The Ancient Instinct
Some believe that this spinning behavior is a remnant of the dogs' wild ancestors. In the wild, dogs would often lie down in tall grass or dense foliage to rest or sleep. By spinning around, they would create a circular depression in the vegetation, ensuring a comfortable and safe resting spot.
The Comfort Seeker
Another theory suggests that dogs spin to create a sense of comfort and security. By turning around multiple times, they can flatten the grass or ground beneath them, creating a more level and comfortable surface to lie down on. This behavior is often seen in dogs who are particularly sensitive to discomfort or who prefer a perfectly flat surface to rest on.
The Scent Marker
A third theory proposes that dogs spin to leave their scent on the ground. By circling around, they can spread their scent in a wider area, marking their territory and claiming ownership of the chosen spot. This behavior is more commonly observed in dogs who are particularly territorial or who live in multi-dog households.
The Playful Spirit
Finally, some dog owners believe that spinning is simply a playful behavior. Dogs may spin out of excitement or anticipation, especially when they are about to settle down for a nap after a long day of play. This theory is often supported by the observation that dogs who are particularly energetic or playful tend to spin more frequently.
A Combination of Factors
While the exact reason behind dog spinning remains unknown, it is likely a combination of these factors. Each dog may have its unique motivations for this behavior, influenced by its breed, personality, and past experiences.
Observing Your Dog
If you have a dog that spins before settling down, you can try to observe its behavior to gain a better understanding of its motivations. Pay attention to the context in which it spins, such as whether it is about to rest or play, and whether it seems particularly excited or anxious.
By observing your dog's behavior, you may be able to uncover the unique reasons behind its spinning ritual.
```
## Humanize output:
```
Ever seen your dog spin in circles before settling down for a nap? It's a pretty common thing, but why do they do it? Nobody's entirely sure, but there are a few good guesses.
One idea is that it's an instinct from their wolf ancestors. Imagine a wolf trying to find a comfy spot in tall grass spinning around would make a little nest.
Another thought is that it's all about comfort. Spinning flattens things out, making a nice, even spot for a nap. You see this a lot in dogs who are picky about where they sleep.
Some people think it's about marking their territory. Spinning spreads their scent, kind of like saying, "This is mine!" This makes more sense for dogs who are really territorial, or live with other dogs.
And then there's the "it's just fun" theory. Maybe they're just excited or playful, especially if they're about to take a nap after a long playtime.
It's probably a mix of all these things. Every dog is different, so their reasons might be a little different too. It depends on their breed, how they are, and what's happened to them in their life.
If your dog does this, try watching them. What are they doing right before they start spinning? Are they tired, or excited? Paying attention might give you a clue about why *your* dog does it. It's a fun little mystery to solve together!
```

128
patterns/humanize/system.md Normal file
View File

@@ -0,0 +1,128 @@
# IDENTITY and PURPOSE
You are a real person whose job is to make text sound natural, conversational, and relatable, just like how an average person talks or writes. Your goal is to rewrite content in a casual, human-like style, prioritizing clarity and simplicity. You should aim for short sentences, an active voice, and everyday language that feels familiar and easy to follow. Avoid long, complex sentences or technical jargon. Instead, focus on breaking ideas into smaller, easy-to-understand parts. Write as though you're explaining something to a friend, keeping it friendly and approachable. Always think step-by-step about how to make the text feel more natural and conversational, using the examples provided as a guide for improvement.
While rewriting, ensure the original meaning and tone are preserved. Strive for a consistent style that flows naturally, even if the given text is a mix of AI and human-generated content.
# YOUR TASK
Your task is to rewrite the given AI-generated text to make it sound like it was written by a real person. The rewritten text should be clear, simple, and easy to understand, using everyday language that feels natural and relatable.
- Focus on clarity: Make sure the text is straightforward and avoids unnecessary complexity.
- Keep it simple: Use common words and phrases that anyone can understand.
- Prioritize short sentences: Break down long, complicated sentences into smaller, more digestible ones.
- Maintain context: Ensure that the rewritten text accurately reflects the original meaning and tone.
- Harmonize mixed content: If the text contains a mix of human and AI styles, edit to ensure a consistent, human-like flow.
- Iterate if necessary: Revisit and refine the text to enhance its naturalness and readability.
Your goal is to make the text approachable and authentic, capturing the way a real person would write or speak.
# STEPS
1. Carefully read the given text and understand its meaning and tone.
2. Process the text phrase by phrase, ensuring that you preserve its original intent.
3. Refer to the **EXAMPLES** section for guidance, avoiding the "AI Style to Avoid" and mimicking the "Human Style to Adopt" in your rewrites.
4. If no relevant example exists in the **EXAMPLES** section:
- Critically analyze the text.
- Apply principles of clarity, simplicity, and natural tone.
- Prioritize readability and unpredictability in your edits.
5. Harmonize the style if the text appears to be a mix of AI and human content.
6. Revisit and refine the rewritten text to enhance its natural and conversational feel while ensuring coherence.
7. Output the rewritten text in coherent paragraphs.
# EXAMPLES
### **Word Frequency Distribution**
- **Instruction**: Avoid overusing high-frequency words or phrases; strive for natural variation.
- **AI Style to Avoid**: "This is a very good and very interesting idea."
- **Human Style to Adopt**: "This idea is intriguing and genuinely impressive."
### **Rare Word Usage**
- **Instruction**: Incorporate rare or unusual words when appropriate to add richness to the text.
- **AI Style to Avoid**: "The event was exciting and fun."
- **Human Style to Adopt**: "The event was exhilarating, a rare blend of thrill and enjoyment."
### **Repetitive Sentence Structure**
- **Instruction**: Avoid repetitive sentence structures and introduce variety in phrasing.
- **AI Style to Avoid**: "She went to the market. She bought some vegetables. She returned home."
- **Human Style to Adopt**: "She visited the market, picked up some fresh vegetables, and headed back home."
### **Overuse of Connective Words**
- **Instruction**: Limit excessive use of connectives like "and," "but," and "so"; aim for concise transitions.
- **AI Style to Avoid**: "He was tired and he wanted to rest and he didnt feel like talking."
- **Human Style to Adopt**: "Exhausted, he wanted to rest and preferred silence."
### **Generic Descriptions**
- **Instruction**: Replace generic descriptions with vivid and specific details.
- **AI Style to Avoid**: "The garden was beautiful."
- **Human Style to Adopt**: "The garden was a vibrant tapestry of blooming flowers, with hues of red and gold dancing in the sunlight."
### **Predictable Sentence Openers**
- **Instruction**: Avoid starting multiple sentences with the same word or phrase.
- **AI Style to Avoid**: "I think this idea is great. I think we should implement it. I think it will work."
- **Human Style to Adopt**: "This idea seems promising. Implementation could yield excellent results. Success feels within reach."
### **Overuse of Passive Voice**
- **Instruction**: Prefer active voice to make sentences more direct and engaging.
- **AI Style to Avoid**: "The decision was made by the team to postpone the event."
- **Human Style to Adopt**: "The team decided to postpone the event."
### **Over-Optimization for Coherence**
- **Instruction**: Avoid making the text overly polished; introduce minor imperfections to mimic natural human writing.
- **AI Style to Avoid**: "The system operates efficiently and effectively under all conditions."
- **Human Style to Adopt**: "The system works well, though it might need tweaks under some conditions."
### **Overuse of Filler Words**
- **Instruction**: Minimize unnecessary filler words like "actually," "very," and "basically."
- **AI Style to Avoid**: "This is actually a very good point to consider."
- **Human Style to Adopt**: "This is an excellent point to consider."
### **Overly Predictable Phrasing**
- **Instruction**: Avoid clichés and predictable phrasing; use fresh expressions.
- **AI Style to Avoid**: "It was a dark and stormy night."
- **Human Style to Adopt**: "The night was thick with clouds, the wind howling through the trees."
### **Simplistic Sentence Transitions**
- **Instruction**: Avoid overly simple transitions like "then" and "next"; vary transition techniques.
- **AI Style to Avoid**: "He finished his work. Then, he went home."
- **Human Style to Adopt**: "After wrapping up his work, he made his way home."
### **Imbalanced Sentence Length**
- **Instruction**: Use a mix of short and long sentences for rhythm and flow.
- **AI Style to Avoid**: "The party was fun. Everyone had a great time. We played games and ate snacks."
- **Human Style to Adopt**: "The party was a blast. Laughter echoed as we played games, and the snacks were a hit."
### **Over-Summarization**
- **Instruction**: Avoid overly condensed summaries; elaborate with examples and context.
- **AI Style to Avoid**: "The book was interesting."
- **Human Style to Adopt**: "The book captivated me with its vivid characters and unexpected plot twists."
### **Overuse of Anthropomorphism**
- **Instruction**: Avoid excessive anthropomorphism unless it adds meaningful insight. Opt for factual descriptions with engaging detail.
- **AI Style to Avoid**: "Spinning spreads their scent, like saying, 'This is mine!'"
- **Human Style to Adopt**: "Spinning might help spread their scent, signaling to other animals that this spot is taken."
### **Overuse of Enthusiasm**
- **Instruction**: Avoid excessive exclamation marks or forced enthusiasm. Use a balanced tone to maintain authenticity.
- **AI Style to Avoid**: "It's a fun little mystery to solve together!"
- **Human Style to Adopt**: "Its a fascinating behavior worth exploring together."
### **Lack of Specificity**
- **Instruction**: Avoid vague or broad generalizations. Provide specific examples or details to add depth to your explanation.
- **AI Style to Avoid**: "This makes more sense for dogs who are really territorial, or live with other dogs."
- **Human Style to Adopt**: "This behavior is often seen in dogs that share their space with other pets or tend to guard their favorite spots."
### **Overuse of Vague Placeholders**
- **Instruction**: Avoid placeholders like "some people think" or "scientists have ideas." Instead, hint at specific theories or details.
- **AI Style to Avoid**: "Scientists and dog lovers alike have some ideas, though."
- **Human Style to Adopt**: "Some researchers think it could be an instinct from their wild ancestors, while others believe its about comfort."
### **Simplistic Explanations**
- **Instruction**: Avoid reusing basic explanations without adding new details or angles. Expand with context, examples, or alternative interpretations.
- **AI Style to Avoid**: "Spinning flattens the ground, making a nice, even spot for a nap. You see this a lot in dogs who are picky about where they sleep."
- **Human Style to Adopt**: "Dogs may spin to prepare their resting spot. By shifting around, they might be flattening grass, adjusting blankets, or finding the most comfortable position—a behavior more common in dogs that are particular about their sleeping arrangements."
# OUTPUT INSTRUCTIONS
- Output should be in the format of coherent paragraphs not separate sentences.
- Only output the rewritten text.

View File

@@ -21,19 +21,19 @@ This pattern generates a summary of an academic paper based on the provided text
Copy the paper text to the clipboard and execute the following command:
``` bash
```bash
pbpaste | fabric --pattern summarize_paper
```
or
``` bash
```bash
pbpaste | summarize_paper
```
# Example output:
``` markdown
```markdown
### Title and authors of the Paper:
**Internet of Paint (IoP): Channel Modeling and Capacity Analysis for Terahertz Electromagnetic Nanonetworks Embedded in Paint**
Authors: Lasantha Thakshila Wedage, Mehmet C. Vuran, Bernard Butler, Yevgeni Koucheryavy, Sasitharan Balasubramaniam

View File

@@ -8,7 +8,7 @@ Take a step back, and breathe deeply and think step by step about how to achieve
- The original format of the input must remain intact.
- You will be translating sentence-by-sentence keeping the original tone ofthe said sentence.
- You will be translating sentence-by-sentence keeping the original tone of the said sentence.
- You will not be manipulate the wording to change the meaning.

View File

@@ -1 +1 @@
"1.4.121"
"1.4.128"

View File

@@ -0,0 +1,223 @@
# Fabric Extensions: Complete Guide
## Understanding Extension Architecture
### Registry Structure
The extension registry is stored at `~/.config/fabric/extensions/extensions.yaml` and tracks registered extensions:
```yaml
extensions:
extension-name:
config_path: /path/to/config.yaml
config_hash: <sha256>
executable_hash: <sha256>
```
The registry maintains security through hash verification of both configs and executables.
### Extension Configuration
Each extension requires a YAML configuration file with the following structure:
```yaml
name: "extension-name" # Unique identifier
executable: "/path/to/binary" # Full path to executable
type: "executable" # Type of extension
timeout: "30s" # Execution timeout
description: "Description" # What the extension does
version: "1.0.0" # Version number
env: [] # Optional environment variables
operations: # Defined operations
operation-name:
cmd_template: "{{executable}} {{operation}} {{value}}"
config: # Output configuration
output:
method: "stdout" # or "file"
file_config: # Optional, for file output
cleanup: true
path_from_stdout: true
work_dir: "/tmp"
```
### Directory Structure
Recommended organization:
```
~/.config/fabric/extensions/
├── bin/ # Extension executables
├── configs/ # Extension YAML configs
└── extensions.yaml # Registry file
```
## Example 1: Python Wrapper (Word Generator)
A simple example wrapping a Python script.
### 1. Position Files
```bash
# Create directories
mkdir -p ~/.config/fabric/extensions/{bin,configs}
# Install script
cp word-generator.py ~/.config/fabric/extensions/bin/
chmod +x ~/.config/fabric/extensions/bin/word-generator.py
```
### 2. Configure
Create `~/.config/fabric/extensions/configs/word-generator.yaml`:
```yaml
name: word-generator
executable: "~/.config/fabric/extensions/bin/word-generator.py"
type: executable
timeout: "5s"
description: "Generates random words based on count parameter"
version: "1.0.0"
operations:
generate:
cmd_template: "{{executable}} {{value}}"
config:
output:
method: stdout
```
### 3. Register & Run
```bash
# Register
fabric --addextension ~/.config/fabric/extensions/configs/word-generator.yaml
# Run (generate 3 random words)
echo "{{ext:word-generator:generate:3}}" | fabric
```
## Example 2: Direct Executable (SQLite3)
Using a system executable directly.
copy the memories to your home directory
~/memories.db
### 1. Configure
Create `~/.config/fabric/extensions/configs/memory-query.yaml`:
```yaml
name: memory-query
executable: "/usr/bin/sqlite3"
type: executable
timeout: "5s"
description: "Query memories database"
version: "1.0.0"
operations:
goal:
cmd_template: "{{executable}} -json ~/memories.db \"select * from memories where type= 'goal'\""
value:
cmd_template: "{{executable}} -json ~/memories.db \"select * from memories where type= 'value'\""
byid:
cmd_template: "{{executable}} -json ~/memories.db \"select * from memories where uid= {{value}}\""
all:
cmd_template: "{{executable}} -json ~/memories.db \"select * from memories\""
config:
output:
method: stdout
```
### 2. Register & Run
```bash
# Register
fabric --addextension ~/.config/fabric/extensions/configs/memory-query.yaml
# Run queries
echo "{{ext:memory-query:all}}" | fabric
echo "{{ext:memory-query:byid:3}}" | fabric
```
## Extension Management Commands
### Add Extension
```bash
fabric --addextension ~/.config/fabric/extensions/configs/memory-query.yaml
```
Note : if the executable or config file changes, you must re-add the extension.
This will recompute the hash for the extension.
### List Extensions
```bash
fabric --listextensions
```
Shows all registered extensions with their status and configuration details.
### Remove Extension
```bash
fabric --rmextension <extension-name>
```
Removes an extension from the registry.
## Extensions in patterns
```
Create a pattern that use multiple extensions.
These are my favorite
{{ext:word-generator:generate:3}}
These are my least favorite
{{ext:word-generator:generate:2}}
what does this say about me?
```
```bash
./fabric -p ./plugins/template/Examples/test_pattern.md
```
## Security Considerations
1. **Hash Verification**
- Both configs and executables are verified via SHA-256 hashes
- Changes to either require re-registration
- Prevents tampering with registered extensions
2. **Execution Safety**
- Extensions run with user permissions
- Timeout constraints prevent runaway processes
- Environment variables can be controlled via config
3. **Best Practices**
- Review extension code before installation
- Keep executables in protected directories
- Use absolute paths in configurations
- Implement proper error handling in scripts
- Regular security audits of registered extensions
## Troubleshooting
### Common Issues
1. **Registration Failures**
- Verify file permissions
- Check executable paths
- Validate YAML syntax
2. **Execution Errors**
- Check operation exists in config
- Verify timeout settings
- Monitor system resources
- Check extension logs
3. **Output Issues**
- Verify output method configuration
- Check file permissions for file output
- Monitor disk space for file operations
### Debug Tips
1. Enable verbose logging when available
2. Check system logs for execution errors
3. Verify extension dependencies
4. Test extensions with minimal configurations first
Would you like me to expand on any particular section or add more examples?

Binary file not shown.

View File

@@ -0,0 +1,24 @@
#!/bin/bash
# remote-security-report.sh
# Usage: remote-security-report.sh cert host [report_name]
cert_path="$1"
host="$2"
report_name="${3:-report}"
temp_file="/tmp/security-report-${report_name}.txt"
# Copy the security report script to remote host
scp -i "$cert_path" /usr/local/bin/security-report.sh "${host}:~/security-report.sh" >&2
# Make it executable and run it on remote host
ssh -i "$cert_path" "$host" "chmod +x ~/security-report.sh && sudo ~/security-report.sh ${temp_file}" >&2
# Copy the report back
scp -i "$cert_path" "${host}:${temp_file}" "${temp_file}" >&2
# Cleanup remote files
ssh -i "$cert_path" "$host" "rm ~/security-report.sh ${temp_file}" >&2
# Output the local file path for fabric to read
echo "${temp_file}"

View File

@@ -0,0 +1,17 @@
name: "remote-security"
executable: "/usr/local/bin/remote-security-report.sh"
type: "executable"
timeout: "60s"
description: "Generate security report from remote system"
operations:
report:
cmd_template: "{{executable}} {{1}} {{2}} {{3}}"
config:
output:
method: "file"
file_config:
cleanup: true
path_from_stdout: true
work_dir: "/tmp"

View File

@@ -0,0 +1,113 @@
#!/bin/bash
# security-report.sh - Enhanced system security information collection
# Usage: security-report.sh [output_file]
output_file=${1:-/tmp/security-report.txt}
{
echo "=== System Security Report ==="
echo "Generated: $(date)"
echo "Hostname: $(hostname)"
echo "Kernel: $(uname -r)"
echo
echo "=== System Updates ==="
echo "Last update: $(stat -c %y /var/cache/apt/pkgcache.bin | cut -d' ' -f1)"
echo "Pending updates:"
apt list --upgradable 2>/dev/null
echo -e "\n=== Security Updates ==="
echo "Pending security updates:"
apt list --upgradable 2>/dev/null | grep -i security
echo -e "\n=== User Accounts ==="
echo "Users with login shells:"
grep -v '/nologin\|/false' /etc/passwd
echo -e "\nUsers who can login:"
awk -F: '$2!="*" && $2!="!" {print $1}' /etc/shadow
echo -e "\nUsers with empty passwords:"
awk -F: '$2=="" {print $1}' /etc/shadow
echo -e "\nUsers with UID 0:"
awk -F: '$3==0 {print $1}' /etc/passwd
echo -e "\n=== Sudo Configuration ==="
echo "Users/groups with sudo privileges:"
grep -h '^[^#]' /etc/sudoers.d/* /etc/sudoers 2>/dev/null
echo -e "\nUsers with passwordless sudo:"
grep -h NOPASSWD /etc/sudoers.d/* /etc/sudoers 2>/dev/null
echo -e "\n=== SSH Configuration ==="
if [ -f /etc/ssh/sshd_config ]; then
echo "Key SSH settings:"
grep -E '^(PermitRootLogin|PasswordAuthentication|Port|Protocol|X11Forwarding|MaxAuthTries|PermitEmptyPasswords)' /etc/ssh/sshd_config
fi
echo -e "\n=== SSH Keys ==="
echo "Authorized keys found:"
find /home -name "authorized_keys" -ls 2>/dev/null
echo -e "\n=== Firewall Status ==="
echo "UFW Status:"
ufw status verbose
echo -e "\nIPTables Rules:"
iptables -L -n
echo -e "\n=== Network Services ==="
echo "Listening services (port - process):"
netstat -tlpn 2>/dev/null | grep LISTEN
echo -e "\n=== Recent Authentication Failures ==="
echo "Last 5 failed SSH attempts:"
grep "Failed password" /var/log/auth.log | tail -5
echo -e "\n=== File Permissions ==="
echo "World-writable files in /etc:"
find /etc -type f -perm -002 -ls 2>/dev/null
echo -e "\nWorld-writable directories in /etc:"
find /etc -type d -perm -002 -ls 2>/dev/null
echo -e "\n=== System Resource Usage ==="
echo "Disk Usage:"
df -h
echo -e "\nMemory Usage:"
free -h
echo -e "\nTop 5 CPU-using processes:"
ps aux --sort=-%cpu | head -6
echo -e "\n=== System Timers ==="
echo "Active timers (potential scheduled tasks):"
systemctl list-timers --all
echo -e "\n=== Important Service Status ==="
for service in ssh ufw apparmor fail2ban clamav-freshclam; do
echo "Status of $service:"
systemctl status $service --no-pager 2>/dev/null
done
echo -e "\n=== Fail2Ban Logs ==="
echo "Recent Fail2Ban activity (fail2ban.log):"
if [ -f /var/log/fail2ban.log ]; then
echo "=== Current log (fail2ban.log) ==="
cat /var/log/fail2ban.log
else
echo "fail2ban.log not found"
fi
if [ -f /var/log/fail2ban.log.1 ]; then
echo -e "\n=== Previous log (fail2ban.log.1) ==="
cat /var/log/fail2ban.log.1
else
echo -e "\nfail2ban.log.1 not found"
fi
echo -e "\n=== Fail2Ban Status ==="
echo "Currently banned IPs:"
sudo fail2ban-client status
} > "$output_file"
# Output the file path for fabric to read
echo "$output_file"

View File

@@ -0,0 +1,18 @@
name: "security-report"
executable: "/usr/local/bin/security-report.sh"
type: "executable"
timeout: "30s"
description: "Generate system security report"
version: "1.0.0"
operations:
generate:
cmd_template: "{{executable}} /tmp/security-report-{{1}}.txt"
config:
output:
method: "file"
file_config:
cleanup: true
path_from_stdout: true
work_dir: "/tmp"

View File

@@ -0,0 +1,23 @@
name: memory-query
executable: /usr/bin/sqlite3
type: executable
timeout: "5s"
description: "Query memories database"
version: "1.0.0"
env: []
operations:
goal:
cmd_template: "{{executable}} -json /home/matt/memories.db \"select * from memories where type= 'goal'\""
value:
cmd_template: "{{executable}} -json /home/matt/memories.db \"select * from memories where type= 'value'\""
project:
cmd_template: "{{executable}} -json /home/matt/memories.db \"select * from memories where type= 'project'\""
byid:
cmd_template: "{{executable}} -json /home/matt/memories.db \"select * from memories where uid= {{value}}\""
all:
cmd_template: "{{executable}} -json ~/memories.db \"select * from memories\""
config:
output:
method: stdout

View File

@@ -0,0 +1,8 @@
These are my favorite
{{ext:word-generator:generate:3}}
These are my least favorite
{{ext:word-generator:generate:2}}
what does this say about me?

View File

@@ -0,0 +1,18 @@
#!/bin/bash
LOG_DIR="/var/log/package_tracking"
DATE=$(date +%Y%m%d)
# Ensure directory exists
mkdir -p "$LOG_DIR"
# Current package list
dpkg -l > "$LOG_DIR/packages_current.list"
# Create diff if previous exists
if [ -f "$LOG_DIR/packages_previous.list" ]; then
diff "$LOG_DIR/packages_previous.list" "$LOG_DIR/packages_current.list" > "$LOG_DIR/changes_current.diff"
fi
# Keep copy for next comparison
cp "$LOG_DIR/packages_current.list" "$LOG_DIR/packages_previous.list"

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python3
import sys
import json
import random
# A small set of words for demonstration!
WORD_LIST = [
"apple", "banana", "cherry", "date", "elderberry",
"fig", "grape", "honeydew", "kiwi", "lemon",
"mango", "nectarine", "orange", "papaya", "quince",
"raspberry", "strawberry", "tangerine", "ugli", "watermelon"
]
def generate_words(count):
try:
count = int(count)
if count < 1:
return json.dumps({"error": "Count must be positive"})
# Generate random words
words = random.sample(WORD_LIST, min(count, len(WORD_LIST)))
# Return JSON formatted result
return json.dumps({
"words": words,
"count": len(words)
})
except ValueError:
return json.dumps({"error": "Invalid count parameter"})
if __name__ == "__main__":
if len(sys.argv) != 2:
print(json.dumps({"error": "Exactly one argument required"}))
sys.exit(1)
print(generate_words(sys.argv[1]))

View File

@@ -0,0 +1,16 @@
name: word-generator
executable: /usr/local/bin/word-generator.py
type: executable
timeout: "5s"
description: "Generates random words based on count parameter"
version: "1.0.0"
env: []
operations:
generate:
cmd_template: "{{executable}} {{value}}"
config:
output:
method: stdout

View File

@@ -0,0 +1,196 @@
package template
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// ExtensionExecutor handles the secure execution of extensions
// It uses the registry to verify extensions before running them
type ExtensionExecutor struct {
registry *ExtensionRegistry
}
// NewExtensionExecutor creates a new executor instance
// It requires a registry to verify extensions
func NewExtensionExecutor(registry *ExtensionRegistry) *ExtensionExecutor {
return &ExtensionExecutor{
registry: registry,
}
}
// Execute runs an extension with the given operation and value string
// name: the registered name of the extension
// operation: the operation to perform
// value: the input value(s) for the operation
// In extension_executor.go
func (e *ExtensionExecutor) Execute(name, operation, value string) (string, error) {
// Get and verify extension from registry
ext, err := e.registry.GetExtension(name)
if err != nil {
return "", fmt.Errorf("failed to get extension: %w", err)
}
// Format the command using our template system
cmdStr, err := e.formatCommand(ext, operation, value)
if err != nil {
return "", fmt.Errorf("failed to format command: %w", err)
}
// Split the command string into command and arguments
cmdParts := strings.Fields(cmdStr)
if len(cmdParts) < 1 {
return "", fmt.Errorf("empty command after formatting")
}
// Create command with the Executable and formatted arguments
cmd := exec.Command("sh", "-c", cmdStr)
//cmd := exec.Command(cmdParts[0], cmdParts[1:]...)
// Set up environment if specified
if len(ext.Env) > 0 {
cmd.Env = append(os.Environ(), ext.Env...)
}
// Execute based on output method
outputMethod := ext.GetOutputMethod()
if outputMethod == "file" {
return e.executeWithFile(cmd, ext)
}
return e.executeStdout(cmd, ext)
}
// formatCommand uses fabric's template system to format the command
// It creates a variables map for the template system using the input values
func (e *ExtensionExecutor) formatCommand(ext *ExtensionDefinition, operation string, value string) (string, error) {
// Get operation config
opConfig, exists := ext.Operations[operation]
if !exists {
return "", fmt.Errorf("operation %s not found for extension %s", operation, ext.Name)
}
vars := make(map[string]string)
vars["executable"] = ext.Executable
vars["operation"] = operation
vars["value"] = value
// Split on pipe for numbered variables
values := strings.Split(value, "|")
for i, val := range values {
vars[fmt.Sprintf("%d", i+1)] = val
}
return ApplyTemplate(opConfig.CmdTemplate, vars, "")
}
// executeStdout runs the command and captures its stdout
func (e *ExtensionExecutor) executeStdout(cmd *exec.Cmd, ext *ExtensionDefinition) (string, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
//debug output
fmt.Printf("Executing command: %s\n", cmd.String())
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("execution failed: %w\nstderr: %s", err, stderr.String())
}
return stdout.String(), nil
}
// executeWithFile runs the command and handles file-based output
func (e *ExtensionExecutor) executeWithFile(cmd *exec.Cmd, ext *ExtensionDefinition) (string, error) {
// Parse timeout - this is now a first-class field
timeout, err := time.ParseDuration(ext.Timeout)
if err != nil {
return "", fmt.Errorf("invalid timeout format: %w", err)
}
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
cmd = exec.CommandContext(ctx, cmd.Path, cmd.Args[1:]...)
cmd.Env = cmd.Env
fileConfig := ext.GetFileConfig()
if fileConfig == nil {
return "", fmt.Errorf("no file configuration found")
}
// Handle path from stdout case
if pathFromStdout, ok := fileConfig["path_from_stdout"].(bool); ok && pathFromStdout {
return e.handlePathFromStdout(cmd, ext)
}
// Handle fixed file case
workDir, _ := fileConfig["work_dir"].(string)
outputFile, _ := fileConfig["output_file"].(string)
if outputFile == "" {
return "", fmt.Errorf("no output file specified in configuration")
}
// Set working directory if specified
if workDir != "" {
cmd.Dir = workDir
}
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
if ctx.Err() == context.DeadlineExceeded {
return "", fmt.Errorf("execution timed out after %v", timeout)
}
return "", fmt.Errorf("execution failed: %w\nerr: %s", err, stderr.String())
}
// Construct full file path
outputPath := outputFile
if workDir != "" {
outputPath = filepath.Join(workDir, outputFile)
}
content, err := os.ReadFile(outputPath)
if err != nil {
return "", fmt.Errorf("failed to read output file: %w", err)
}
// Handle cleanup if enabled
if ext.IsCleanupEnabled() {
defer os.Remove(outputPath)
}
return string(content), nil
}
// Helper method to handle path from stdout case
func (e *ExtensionExecutor) handlePathFromStdout(cmd *exec.Cmd, ext *ExtensionDefinition) (string, error) {
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to get output path: %w\nerr: %s", err, stderr.String())
}
outputPath := strings.TrimSpace(stdout.String())
content, err := os.ReadFile(outputPath)
if err != nil {
return "", fmt.Errorf("failed to read output file: %w", err)
}
if ext.IsCleanupEnabled() {
defer os.Remove(outputPath)
}
return string(content), nil
}

View File

@@ -0,0 +1,360 @@
package template
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestExtensionExecutor(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "fabric-ext-executor-*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create test script that has both stdout and file output modes
testScript := filepath.Join(tmpDir, "test-script.sh")
scriptContent := `#!/bin/bash
case "$1" in
"stdout")
echo "Hello, $2!"
;;
"file")
echo "Hello, $2!" > "$3"
echo "$3" # Print the filename for path_from_stdout
;;
*)
echo "Unknown command" >&2
exit 1
;;
esac`
if err := os.WriteFile(testScript, []byte(scriptContent), 0755); err != nil {
t.Fatalf("Failed to create test script: %v", err)
}
// Create registry and register our test extensions
registry := NewExtensionRegistry(tmpDir)
executor := NewExtensionExecutor(registry)
// Test stdout-based extension
t.Run("StdoutExecution", func(t *testing.T) {
configPath := filepath.Join(tmpDir, "stdout-extension.yaml")
configContent := `name: stdout-test
executable: ` + testScript + `
type: executable
timeout: 30s
operations:
greet:
cmd_template: "{{executable}} stdout {{1}}"
config:
output:
method: stdout`
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
t.Fatalf("Failed to create config: %v", err)
}
if err := registry.Register(configPath); err != nil {
t.Fatalf("Failed to register extension: %v", err)
}
output, err := executor.Execute("stdout-test", "greet", "World")
if err != nil {
t.Errorf("Failed to execute: %v", err)
}
expected := "Hello, World!\n"
if output != expected {
t.Errorf("Expected output %q, got %q", expected, output)
}
})
// Test file-based extension
t.Run("FileExecution", func(t *testing.T) {
configPath := filepath.Join(tmpDir, "file-extension.yaml")
configContent := `name: file-test
executable: ` + testScript + `
type: executable
timeout: 30s
operations:
greet:
cmd_template: "{{executable}} file {{1}} {{2}}"
config:
output:
method: file
file_config:
cleanup: true
path_from_stdout: true`
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
t.Fatalf("Failed to create config: %v", err)
}
if err := registry.Register(configPath); err != nil {
t.Fatalf("Failed to register extension: %v", err)
}
output, err := executor.Execute("file-test", "greet", "World|/tmp/test.txt")
if err != nil {
t.Errorf("Failed to execute: %v", err)
}
expected := "Hello, World!\n"
if output != expected {
t.Errorf("Expected output %q, got %q", expected, output)
}
})
// Test execution errors
t.Run("ExecutionErrors", func(t *testing.T) {
// Test with non-existent extension
_, err := executor.Execute("nonexistent", "test", "value")
if err == nil {
t.Error("Expected error executing non-existent extension, got nil")
}
// Test with invalid command that should exit non-zero
configPath := filepath.Join(tmpDir, "error-extension.yaml")
configContent := `name: error-test
executable: ` + testScript + `
type: executable
timeout: 30s
operations:
invalid:
cmd_template: "{{executable}} invalid {{1}}"
config:
output:
method: stdout`
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
t.Fatalf("Failed to create config: %v", err)
}
if err := registry.Register(configPath); err != nil {
t.Fatalf("Failed to register extension: %v", err)
}
_, err = executor.Execute("error-test", "invalid", "test")
if err == nil {
t.Error("Expected error from invalid command, got nil")
}
if !strings.Contains(err.Error(), "Unknown command") {
t.Errorf("Expected 'Unknown command' in error, got: %v", err)
}
})
}
func TestFixedFileExtensionExecutor(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "fabric-ext-executor-fixed-*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create test script
testScript := filepath.Join(tmpDir, "test-script.sh")
scriptContent := `#!/bin/bash
case "$1" in
"write")
echo "Hello, $2!" > "$3"
;;
"append")
echo "Hello, $2!" >> "$3"
;;
"large")
for i in {1..1000}; do
echo "Line $i" >> "$3"
done
;;
"error")
echo "Error message" >&2
exit 1
;;
*)
echo "Unknown command" >&2
exit 1
;;
esac`
if err := os.WriteFile(testScript, []byte(scriptContent), 0755); err != nil {
t.Fatalf("Failed to create test script: %v", err)
}
registry := NewExtensionRegistry(tmpDir)
executor := NewExtensionExecutor(registry)
// Helper function to create and register extension
createExtension := func(name, opName, cmdTemplate string, config map[string]interface{}) error {
configPath := filepath.Join(tmpDir, name+".yaml")
configContent := `name: ` + name + `
executable: ` + testScript + `
type: executable
timeout: 30s
operations:
` + opName + `:
cmd_template: "` + cmdTemplate + `"
config:
output:
method: file
file_config:`
// Add config options
for k, v := range config {
configContent += "\n " + k + ": " + strings.TrimSpace(v.(string))
}
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
return err
}
return registry.Register(configPath)
}
// Test basic fixed file output
t.Run("BasicFixedFile", func(t *testing.T) {
outputFile := filepath.Join(tmpDir, "output.txt")
config := map[string]interface{}{
"output_file": `"output.txt"`,
"work_dir": `"` + tmpDir + `"`,
"cleanup": "true",
}
err := createExtension("basic-test", "write",
"{{executable}} write {{1}} "+outputFile, config)
if err != nil {
t.Fatalf("Failed to create extension: %v", err)
}
output, err := executor.Execute("basic-test", "write", "World")
if err != nil {
t.Errorf("Failed to execute: %v", err)
}
expected := "Hello, World!\n"
if output != expected {
t.Errorf("Expected output %q, got %q", expected, output)
}
})
// Test no work_dir specified
t.Run("NoWorkDir", func(t *testing.T) {
config := map[string]interface{}{
"output_file": `"direct-output.txt"`,
"cleanup": "true",
}
err := createExtension("no-workdir-test", "write",
"{{executable}} write {{1}} direct-output.txt", config)
if err != nil {
t.Fatalf("Failed to create extension: %v", err)
}
_, err = executor.Execute("no-workdir-test", "write", "World")
if err != nil {
t.Errorf("Failed to execute: %v", err)
}
})
// Test cleanup behavior
t.Run("CleanupBehavior", func(t *testing.T) {
outputFile := filepath.Join(tmpDir, "cleanup-test.txt")
// Test with cleanup enabled
config := map[string]interface{}{
"output_file": `"cleanup-test.txt"`,
"work_dir": `"` + tmpDir + `"`,
"cleanup": "true",
}
err := createExtension("cleanup-test", "write",
"{{executable}} write {{1}} "+outputFile, config)
if err != nil {
t.Fatalf("Failed to create extension: %v", err)
}
_, err = executor.Execute("cleanup-test", "write", "World")
if err != nil {
t.Errorf("Failed to execute: %v", err)
}
// File should be deleted after execution
if _, err := os.Stat(outputFile); !os.IsNotExist(err) {
t.Error("Expected output file to be cleaned up")
}
// Test with cleanup disabled
config["cleanup"] = "false"
err = createExtension("no-cleanup-test", "write",
"{{executable}} write {{1}} "+outputFile, config)
if err != nil {
t.Fatalf("Failed to create extension: %v", err)
}
_, err = executor.Execute("no-cleanup-test", "write", "World")
if err != nil {
t.Errorf("Failed to execute: %v", err)
}
// File should remain after execution
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
t.Error("Expected output file to remain")
}
})
// Test error cases
t.Run("ErrorCases", func(t *testing.T) {
outputFile := filepath.Join(tmpDir, "error-test.txt")
config := map[string]interface{}{
"output_file": `"error-test.txt"`,
"work_dir": `"` + tmpDir + `"`,
"cleanup": "true",
}
// Test command error
err := createExtension("error-test", "error",
"{{executable}} error {{1}} "+outputFile, config)
if err != nil {
t.Fatalf("Failed to create extension: %v", err)
}
_, err = executor.Execute("error-test", "error", "World")
if err == nil {
t.Error("Expected error from failing command, got nil")
}
// Test invalid work_dir
config["work_dir"] = `"/nonexistent/directory"`
err = createExtension("invalid-dir-test", "write",
"{{executable}} write {{1}} output.txt", config)
if err != nil {
t.Fatalf("Failed to create extension: %v", err)
}
_, err = executor.Execute("invalid-dir-test", "write", "World")
if err == nil {
t.Error("Expected error from invalid work_dir, got nil")
}
})
// Test with missing output_file
t.Run("MissingOutputFile", func(t *testing.T) {
config := map[string]interface{}{
"work_dir": `"` + tmpDir + `"`,
"cleanup": "true",
}
err := createExtension("missing-output-test", "write",
"{{executable}} write {{1}} output.txt", config)
if err != nil {
t.Fatalf("Failed to create extension: %v", err)
}
_, err = executor.Execute("missing-output-test", "write", "World")
if err == nil {
t.Error("Expected error from missing output_file, got nil")
}
})
}

View File

@@ -0,0 +1,135 @@
package template
import (
"fmt"
"os"
"path/filepath"
"time"
"gopkg.in/yaml.v3"
)
// ExtensionManager handles the high-level operations of the extension system
type ExtensionManager struct {
registry *ExtensionRegistry
executor *ExtensionExecutor
configDir string
}
// NewExtensionManager creates a new extension manager instance
func NewExtensionManager(configDir string) *ExtensionManager {
registry := NewExtensionRegistry(configDir)
return &ExtensionManager{
registry: registry,
executor: NewExtensionExecutor(registry),
configDir: configDir,
}
}
// ListExtensions handles the listextensions flag action
func (em *ExtensionManager) ListExtensions() error {
if em.registry == nil || em.registry.registry.Extensions == nil {
return fmt.Errorf("extension registry not initialized")
}
for name, entry := range em.registry.registry.Extensions {
fmt.Printf("Extension: %s\n", name)
// Try to load extension details
ext, err := em.registry.GetExtension(name)
if err != nil {
fmt.Printf(" Status: DISABLED - Hash verification failed: %v\n", err)
fmt.Printf(" Config Path: %s\n\n", entry.ConfigPath)
continue
}
// Print extension details if verification succeeded
fmt.Printf(" Status: ENABLED\n")
fmt.Printf(" Executable: %s\n", ext.Executable)
fmt.Printf(" Type: %s\n", ext.Type)
fmt.Printf(" Timeout: %s\n", ext.Timeout)
fmt.Printf(" Description: %s\n", ext.Description)
fmt.Printf(" Version: %s\n", ext.Version)
fmt.Printf(" Operations:\n")
for opName, opConfig := range ext.Operations {
fmt.Printf(" %s:\n", opName)
fmt.Printf(" Command Template: %s\n", opConfig.CmdTemplate)
}
if fileConfig := ext.GetFileConfig(); fileConfig != nil {
fmt.Printf(" File Configuration:\n")
for k, v := range fileConfig {
fmt.Printf(" %s: %v\n", k, v)
}
}
fmt.Printf("\n")
}
return nil
}
// RegisterExtension handles the addextension flag action
func (em *ExtensionManager) RegisterExtension(configPath string) error {
absPath, err := filepath.Abs(configPath)
if err != nil {
return fmt.Errorf("invalid config path: %w", err)
}
// Get extension name before registration for status message
data, err := os.ReadFile(absPath)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
var ext ExtensionDefinition
if err := yaml.Unmarshal(data, &ext); err != nil {
return fmt.Errorf("failed to parse config file: %w", err)
}
if err := em.registry.Register(absPath); err != nil {
return fmt.Errorf("failed to register extension: %w", err)
}
if _, err := time.ParseDuration(ext.Timeout); err != nil {
return fmt.Errorf("invalid timeout value '%s': must be a duration like '30s' or '1m': %w", ext.Timeout, err)
}
// Print success message with extension details
fmt.Printf("Successfully registered extension:\n")
fmt.Printf("Name: %s\n", ext.Name)
fmt.Printf(" Executable: %s\n", ext.Executable)
fmt.Printf(" Type: %s\n", ext.Type)
fmt.Printf(" Timeout: %s\n", ext.Timeout)
fmt.Printf(" Description: %s\n", ext.Description)
fmt.Printf(" Version: %s\n", ext.Version)
fmt.Printf(" Operations:\n")
for opName, opConfig := range ext.Operations {
fmt.Printf(" %s:\n", opName)
fmt.Printf(" Command Template: %s\n", opConfig.CmdTemplate)
}
if fileConfig := ext.GetFileConfig(); fileConfig != nil {
fmt.Printf(" File Configuration:\n")
for k, v := range fileConfig {
fmt.Printf(" %s: %v\n", k, v)
}
}
return nil
}
// RemoveExtension handles the rmextension flag action
func (em *ExtensionManager) RemoveExtension(name string) error {
if err := em.registry.Remove(name); err != nil {
return fmt.Errorf("failed to remove extension: %w", err)
}
return nil
}
// ProcessExtension handles template processing for extension directives
func (em *ExtensionManager) ProcessExtension(name, operation, value string) (string, error) {
return em.executor.Execute(name, operation, value)
}

View File

@@ -0,0 +1,184 @@
package template
import (
"os"
"path/filepath"
"testing"
)
// TestExtensionManager is the main test suite for ExtensionManager
func TestExtensionManager(t *testing.T) {
// Create temporary directory for tests
tmpDir, err := os.MkdirTemp("", "fabric-ext-test-*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create test extension config
testConfig := filepath.Join(tmpDir, "test-extension.yaml")
testScript := filepath.Join(tmpDir, "test-script.sh")
// Create test script
scriptContent := `#!/bin/bash
if [ "$1" = "echo" ]; then
echo "Hello, $2!"
fi`
err = os.WriteFile(testScript, []byte(scriptContent), 0755)
if err != nil {
t.Fatalf("Failed to create test script: %v", err)
}
// Create test config
configContent := `name: test-extension
executable: ` + testScript + `
type: executable
timeout: 30s
description: "Test extension"
version: "1.0.0"
operations:
echo:
cmd_template: "{{executable}} echo {{1}}"
`
err = os.WriteFile(testConfig, []byte(configContent), 0644)
if err != nil {
t.Fatalf("Failed to create test config: %v", err)
}
// Initialize manager
manager := NewExtensionManager(tmpDir)
// Test cases
t.Run("RegisterExtension", func(t *testing.T) {
err := manager.RegisterExtension(testConfig)
if err != nil {
t.Errorf("Failed to register extension: %v", err)
}
})
t.Run("ListExtensions", func(t *testing.T) {
err := manager.ListExtensions()
if err != nil {
t.Errorf("Failed to list extensions: %v", err)
}
// Note: Output validation would require capturing stdout
})
t.Run("ProcessExtension", func(t *testing.T) {
output, err := manager.ProcessExtension("test-extension", "echo", "World")
if err != nil {
t.Errorf("Failed to process extension: %v", err)
}
expected := "Hello, World!\n"
if output != expected {
t.Errorf("Expected output %q, got %q", expected, output)
}
})
t.Run("RemoveExtension", func(t *testing.T) {
err := manager.RemoveExtension("test-extension")
if err != nil {
t.Errorf("Failed to remove extension: %v", err)
}
// Verify extension is removed by trying to process it
_, err = manager.ProcessExtension("test-extension", "echo", "World")
if err == nil {
t.Error("Expected error processing removed extension, got nil")
}
})
}
// TestExtensionManagerErrors tests error cases
func TestExtensionManagerErrors(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "fabric-ext-test-errors-*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tmpDir)
manager := NewExtensionManager(tmpDir)
t.Run("RegisterNonexistentConfig", func(t *testing.T) {
err := manager.RegisterExtension("/nonexistent/config.yaml")
if err == nil {
t.Error("Expected error registering nonexistent config, got nil")
}
})
t.Run("ProcessNonexistentExtension", func(t *testing.T) {
_, err := manager.ProcessExtension("nonexistent", "echo", "test")
if err == nil {
t.Error("Expected error processing nonexistent extension, got nil")
}
})
t.Run("RemoveNonexistentExtension", func(t *testing.T) {
err := manager.RemoveExtension("nonexistent")
if err == nil {
t.Error("Expected error removing nonexistent extension, got nil")
}
})
}
// TestExtensionManagerWithInvalidConfig tests handling of invalid configurations
func TestExtensionManagerWithInvalidConfig(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "fabric-ext-test-invalid-*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tmpDir)
invalidConfig := filepath.Join(tmpDir, "invalid-extension.yaml")
// Test cases with different invalid configurations
testCases := []struct {
name string
config string
wantErr bool
}{
{
name: "MissingExecutable",
config: `name: invalid-extension
type: executable
timeout: 30s`,
wantErr: true,
},
{
name: "InvalidTimeout",
config: `name: invalid-extension
executable: /bin/echo
type: executable
timeout: invalid`,
wantErr: true,
},
{
name: "EmptyName",
config: `name: ""
executable: /bin/echo
type: executable
timeout: 30s`,
wantErr: true,
},
}
manager := NewExtensionManager(tmpDir)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := os.WriteFile(invalidConfig, []byte(tc.config), 0644)
if err != nil {
t.Fatalf("Failed to create invalid config file: %v", err)
}
err = manager.RegisterExtension(invalidConfig)
if tc.wantErr && err == nil {
t.Error("Expected error registering invalid config, got nil")
} else if !tc.wantErr && err != nil {
t.Errorf("Unexpected error registering config: %v", err)
}
})
}
}

View File

@@ -0,0 +1,329 @@
package template
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"gopkg.in/yaml.v3"
// Add this import
)
// ExtensionDefinition represents a single extension configuration
type ExtensionDefinition struct {
// Global properties
Name string `yaml:"name"`
Executable string `yaml:"executable"`
Type string `yaml:"type"`
Timeout string `yaml:"timeout"`
Description string `yaml:"description"`
Version string `yaml:"version"`
Env []string `yaml:"env"`
// Operation-specific commands
Operations map[string]OperationConfig `yaml:"operations"`
// Additional config
Config map[string]interface{} `yaml:"config"`
}
type OperationConfig struct {
CmdTemplate string `yaml:"cmd_template"`
}
// RegistryEntry represents a registered extension
type RegistryEntry struct {
ConfigPath string `yaml:"config_path"`
ConfigHash string `yaml:"config_hash"`
ExecutableHash string `yaml:"executable_hash"`
}
type ExtensionRegistry struct {
configDir string
registry struct {
Extensions map[string]*RegistryEntry `yaml:"extensions"`
}
}
// Helper methods for Config access
func (e *ExtensionDefinition) GetOutputMethod() string {
if output, ok := e.Config["output"].(map[string]interface{}); ok {
if method, ok := output["method"].(string); ok {
return method
}
}
return "stdout" // default to stdout if not specified
}
func (e *ExtensionDefinition) GetFileConfig() map[string]interface{} {
if output, ok := e.Config["output"].(map[string]interface{}); ok {
if fileConfig, ok := output["file_config"].(map[string]interface{}); ok {
return fileConfig
}
}
return nil
}
func (e *ExtensionDefinition) IsCleanupEnabled() bool {
if fc := e.GetFileConfig(); fc != nil {
if cleanup, ok := fc["cleanup"].(bool); ok {
return cleanup
}
}
return false // default to no cleanup
}
func NewExtensionRegistry(configDir string) *ExtensionRegistry {
r := &ExtensionRegistry{
configDir: configDir,
}
r.registry.Extensions = make(map[string]*RegistryEntry)
r.ensureConfigDir()
if err := r.loadRegistry(); err != nil {
if Debug {
fmt.Printf("Warning: could not load extension registry: %v\n", err)
}
}
return r
}
func (r *ExtensionRegistry) ensureConfigDir() error {
extDir := filepath.Join(r.configDir, "extensions")
return os.MkdirAll(extDir, 0755)
}
// Update the Register method in extension_registry.go
func (r *ExtensionRegistry) Register(configPath string) error {
// Read and parse the extension definition to verify it
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
var ext ExtensionDefinition
if err := yaml.Unmarshal(data, &ext); err != nil {
return fmt.Errorf("failed to parse config file: %w", err)
}
// Validate extension name
if ext.Name == "" {
return fmt.Errorf("extension name cannot be empty")
}
if strings.Contains(ext.Name, " ") {
return fmt.Errorf("extension name '%s' contains spaces - names must not contain spaces", ext.Name)
}
// Verify executable exists
if _, err := os.Stat(ext.Executable); err != nil {
return fmt.Errorf("executable not found: %w", err)
}
// Get absolute path to config
absPath, err := filepath.Abs(configPath)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
// Calculate hashes
configHash := ComputeStringHash(string(data))
executableHash, err := ComputeHash(ext.Executable)
if err != nil {
return fmt.Errorf("failed to hash executable: %w", err)
}
// Store entry
r.registry.Extensions[ext.Name] = &RegistryEntry{
ConfigPath: absPath,
ConfigHash: configHash,
ExecutableHash: executableHash,
}
return r.saveRegistry()
}
func (r *ExtensionRegistry) validateExtensionDefinition(ext *ExtensionDefinition) error {
// Validate required fields
if ext.Name == "" {
return fmt.Errorf("extension name is required")
}
if ext.Executable == "" {
return fmt.Errorf("executable path is required")
}
if ext.Type == "" {
return fmt.Errorf("extension type is required")
}
// Validate timeout format
if ext.Timeout != "" {
if _, err := time.ParseDuration(ext.Timeout); err != nil {
return fmt.Errorf("invalid timeout format: %w", err)
}
}
// Validate operations
if len(ext.Operations) == 0 {
return fmt.Errorf("at least one operation must be defined")
}
for name, op := range ext.Operations {
if op.CmdTemplate == "" {
return fmt.Errorf("command template is required for operation %s", name)
}
}
return nil
}
func (r *ExtensionRegistry) Remove(name string) error {
if _, exists := r.registry.Extensions[name]; !exists {
return fmt.Errorf("extension %s not found", name)
}
delete(r.registry.Extensions, name)
return r.saveRegistry()
}
func (r *ExtensionRegistry) Verify(name string) error {
// Get the registry entry
entry, exists := r.registry.Extensions[name]
if !exists {
return fmt.Errorf("extension %s not found", name)
}
// Load and parse the config file
data, err := os.ReadFile(entry.ConfigPath)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
// Verify config hash
currentConfigHash := ComputeStringHash(string(data))
if currentConfigHash != entry.ConfigHash {
return fmt.Errorf("config file hash mismatch for %s", name)
}
// Parse to get executable path
var ext ExtensionDefinition
if err := yaml.Unmarshal(data, &ext); err != nil {
return fmt.Errorf("failed to parse config file: %w", err)
}
// Verify executable hash
currentExecutableHash, err := ComputeHash(ext.Executable)
if err != nil {
return fmt.Errorf("failed to verify executable: %w", err)
}
if currentExecutableHash != entry.ExecutableHash {
return fmt.Errorf("executable hash mismatch for %s", name)
}
return nil
}
func (r *ExtensionRegistry) GetExtension(name string) (*ExtensionDefinition, error) {
entry, exists := r.registry.Extensions[name]
if !exists {
return nil, fmt.Errorf("extension %s not found", name)
}
// Read current config file
data, err := os.ReadFile(entry.ConfigPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
// Verify config hash
currentHash := ComputeStringHash(string(data))
if currentHash != entry.ConfigHash {
return nil, fmt.Errorf("config file hash mismatch for %s", name)
}
// Parse config
var ext ExtensionDefinition
if err := yaml.Unmarshal(data, &ext); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
// Verify executable hash
currentExecHash, err := ComputeHash(ext.Executable)
if err != nil {
return nil, fmt.Errorf("failed to verify executable: %w", err)
}
if currentExecHash != entry.ExecutableHash {
return nil, fmt.Errorf("executable hash mismatch for %s", name)
}
return &ext, nil
}
func (r *ExtensionRegistry) ListExtensions() ([]*ExtensionDefinition, error) {
var exts []*ExtensionDefinition
for name := range r.registry.Extensions {
ext, err := r.GetExtension(name)
if err != nil {
// Instead of failing, we'll return nil for this extension
// The manager will handle displaying the error
exts = append(exts, nil)
continue
}
exts = append(exts, ext)
}
return exts, nil
}
func (r *ExtensionRegistry) calculateFileHash(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func (r *ExtensionRegistry) saveRegistry() error {
data, err := yaml.Marshal(r.registry)
if err != nil {
return fmt.Errorf("failed to marshal extension registry: %w", err)
}
registryPath := filepath.Join(r.configDir, "extensions", "extensions.yaml")
return os.WriteFile(registryPath, data, 0644)
}
func (r *ExtensionRegistry) loadRegistry() error {
registryPath := filepath.Join(r.configDir, "extensions", "extensions.yaml")
data, err := os.ReadFile(registryPath)
if err != nil {
if os.IsNotExist(err) {
return nil // New registry
}
return fmt.Errorf("failed to read extension registry: %w", err)
}
// Need to unmarshal the data into our registry
if err := yaml.Unmarshal(data, &r.registry); err != nil {
return fmt.Errorf("failed to parse extension registry: %w", err)
}
return nil
}

View File

@@ -0,0 +1,75 @@
package template
import (
"os"
"path/filepath"
"testing"
)
func TestRegistryPersistence(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "fabric-ext-registry-persist-*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create test executable
execPath := filepath.Join(tmpDir, "test-exec.sh")
execContent := []byte("#!/bin/bash\necho \"test\"")
err = os.WriteFile(execPath, execContent, 0755)
if err != nil {
t.Fatalf("Failed to create test executable: %v", err)
}
// Create valid config
configContent := `name: test-extension
executable: ` + execPath + `
type: executable
timeout: 30s
operations:
test:
cmd_template: "{{executable}} {{operation}}"`
configPath := filepath.Join(tmpDir, "test-extension.yaml")
err = os.WriteFile(configPath, []byte(configContent), 0644)
if err != nil {
t.Fatalf("Failed to create test config: %v", err)
}
// Test registry persistence
t.Run("SaveAndReload", func(t *testing.T) {
// Create and populate first registry
registry1 := NewExtensionRegistry(tmpDir)
err := registry1.Register(configPath)
if err != nil {
t.Fatalf("Failed to register extension: %v", err)
}
// Create new registry instance and verify it loads the saved state
registry2 := NewExtensionRegistry(tmpDir)
ext, err := registry2.GetExtension("test-extension")
if err != nil {
t.Fatalf("Failed to get extension from reloaded registry: %v", err)
}
if ext.Name != "test-extension" {
t.Errorf("Expected extension name 'test-extension', got %q", ext.Name)
}
})
// Test hash verification
t.Run("HashVerification", func(t *testing.T) {
registry := NewExtensionRegistry(tmpDir)
// Modify executable after registration
modifiedExecContent := []byte("#!/bin/bash\necho \"modified\"")
err := os.WriteFile(execPath, modifiedExecContent, 0755)
if err != nil {
t.Fatalf("Failed to modify executable: %v", err)
}
_, err = registry.GetExtension("test-extension")
if err == nil {
t.Error("Expected error when executable modified, got nil")
}
})
}

33
plugins/template/hash.go Normal file
View File

@@ -0,0 +1,33 @@
package template
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
)
// ComputeHash computes SHA-256 hash of a file at given path.
// Returns the hex-encoded hash string or an error if the operation fails.
func ComputeHash(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("open file: %w", err)
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", fmt.Errorf("read file: %w", err)
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// ComputeStringHash returns hex-encoded SHA-256 hash of the given string
func ComputeStringHash(s string) string {
h := sha256.New()
h.Write([]byte(s))
return hex.EncodeToString(h.Sum(nil))
}

View File

@@ -0,0 +1,119 @@
// template/hash_test.go
package template
import (
"os"
"path/filepath"
"testing"
)
func TestComputeHash(t *testing.T) {
// Create a temporary test file
content := []byte("test content for hashing")
tmpfile, err := os.CreateTemp("", "hashtest")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write(content); err != nil {
t.Fatalf("failed to write to temp file: %v", err)
}
if err := tmpfile.Close(); err != nil {
t.Fatalf("failed to close temp file: %v", err)
}
tests := []struct {
name string
path string
want string // known hash for test content
wantErr bool
}{
{
name: "valid file",
path: tmpfile.Name(),
want: "e25dd806d495b413931f4eea50b677a7a5c02d00460924661283f211a37f7e7f", // pre-computed hash of "test content for hashing"
wantErr: false,
},
{
name: "nonexistent file",
path: filepath.Join(os.TempDir(), "nonexistent"),
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ComputeHash(tt.path)
if (err != nil) != tt.wantErr {
t.Errorf("ComputeHash() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want && !tt.wantErr {
t.Errorf("ComputeHash() = %v, want %v", got, tt.want)
}
})
}
}
func TestComputeStringHash(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "empty string",
input: "",
want: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
{
name: "simple string",
input: "test",
want: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
},
{
name: "longer string with spaces",
input: "this is a test string",
want: "f6774519d1c7a3389ef327e9c04766b999db8cdfb85d1346c471ee86d65885bc",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ComputeStringHash(tt.input); got != tt.want {
t.Errorf("ComputeStringHash() = %v, want %v", got, tt.want)
}
})
}
}
// TestHashConsistency ensures both hash functions produce same results for same content
func TestHashConsistency(t *testing.T) {
content := "test content for consistency check"
// Create a file with the test content
tmpfile, err := os.CreateTemp("", "hashconsistency")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
defer os.Remove(tmpfile.Name())
if err := os.WriteFile(tmpfile.Name(), []byte(content), 0644); err != nil {
t.Fatalf("failed to write to temp file: %v", err)
}
// Get hashes using both methods
fileHash, err := ComputeHash(tmpfile.Name())
if err != nil {
t.Fatalf("ComputeHash failed: %v", err)
}
stringHash := ComputeStringHash(content)
// Compare results
if fileHash != stringHash {
t.Errorf("Hash inconsistency: file hash %v != string hash %v", fileHash, stringHash)
}
}

View File

@@ -2,6 +2,8 @@ package template
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
@@ -15,7 +17,20 @@ var (
Debug = false // Debug flag
)
var extensionManager *ExtensionManager
func init() {
homedir, err := os.UserHomeDir()
if err != nil {
debugf("Warning: could not initialize extension manager: %v\n", err)
}
configDir := filepath.Join(homedir, ".config/fabric")
extensionManager = NewExtensionManager(configDir)
// Extensions will work if registry exists, otherwise they'll just fail gracefully
}
var pluginPattern = regexp.MustCompile(`\{\{plugin:([^:]+):([^:]+)(?::([^}]+))?\}\}`)
var extensionPattern = regexp.MustCompile(`\{\{ext:([^:]+):([^:]+)(?::([^}]+))?\}\}`)
func debugf(format string, a ...interface{}) {
if Debug {
@@ -24,9 +39,120 @@ func debugf(format string, a ...interface{}) {
}
func ApplyTemplate(content string, variables map[string]string, input string) (string, error) {
var missingVars []string
r := regexp.MustCompile(`\{\{([^{}]+)\}\}`)
debugf("Starting template processing\n")
for strings.Contains(content, "{{") {
matches := r.FindAllStringSubmatch(content, -1)
if len(matches) == 0 {
break
}
replaced := false
for _, match := range matches {
fullMatch := match[0]
varName := match[1]
// Check if this is a plugin call
if strings.HasPrefix(varName, "plugin:") {
pluginMatches := pluginPattern.FindStringSubmatch(fullMatch)
if len(pluginMatches) >= 3 {
namespace := pluginMatches[1]
operation := pluginMatches[2]
value := ""
if len(pluginMatches) == 4 {
value = pluginMatches[3]
}
debugf("\nPlugin call:\n")
debugf(" Namespace: %s\n", namespace)
debugf(" Operation: %s\n", operation)
debugf(" Value: %s\n", value)
var result string
var err error
switch namespace {
case "text":
debugf("Executing text plugin\n")
result, err = textPlugin.Apply(operation, value)
case "datetime":
debugf("Executing datetime plugin\n")
result, err = datetimePlugin.Apply(operation, value)
case "file":
debugf("Executing file plugin\n")
result, err = filePlugin.Apply(operation, value)
debugf("File plugin result: %#v\n", result)
case "fetch":
debugf("Executing fetch plugin\n")
result, err = fetchPlugin.Apply(operation, value)
case "sys":
debugf("Executing sys plugin\n")
result, err = sysPlugin.Apply(operation, value)
default:
return "", fmt.Errorf("unknown plugin namespace: %s", namespace)
}
if err != nil {
debugf("Plugin error: %v\n", err)
return "", fmt.Errorf("plugin %s error: %v", namespace, err)
}
debugf("Plugin result: %s\n", result)
content = strings.ReplaceAll(content, fullMatch, result)
debugf("Content after replacement: %s\n", content)
continue
}
}
if pluginMatches := extensionPattern.FindStringSubmatch(fullMatch); len(pluginMatches) >= 3 {
name := pluginMatches[1]
operation := pluginMatches[2]
value := ""
if len(pluginMatches) == 4 {
value = pluginMatches[3]
}
debugf("\nExtension call:\n")
debugf(" Name: %s\n", name)
debugf(" Operation: %s\n", operation)
debugf(" Value: %s\n", value)
result, err := extensionManager.ProcessExtension(name, operation, value)
if err != nil {
return "", fmt.Errorf("extension %s error: %v", name, err)
}
content = strings.ReplaceAll(content, fullMatch, result)
replaced = true
continue
}
// Handle regular variables and input
debugf("Processing variable: %s\n", varName)
if varName == "input" {
debugf("Replacing {{input}}\n")
replaced = true
content = strings.ReplaceAll(content, fullMatch, input)
} else {
if val, ok := variables[varName]; !ok {
debugf("Missing variable: %s\n", varName)
missingVars = append(missingVars, varName)
return "", fmt.Errorf("missing required variable: %s", varName)
} else {
debugf("Replacing variable %s with value: %s\n", varName, val)
content = strings.ReplaceAll(content, fullMatch, val)
replaced = true
}
}
if !replaced {
return "", fmt.Errorf("template processing stuck - potential infinite loop")
}
}
}
debugf("Starting template processing\n")
for strings.Contains(content, "{{") {
matches := r.FindAllStringSubmatch(content, -1)

41
plugins/template/utils.go Normal file
View File

@@ -0,0 +1,41 @@
// utils.go in template package for now
package template
import (
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
)
// ExpandPath expands the ~ to user's home directory and returns absolute path
// It also checks if the path exists
// Returns expanded absolute path or error if:
// - cannot determine user home directory
// - cannot convert to absolute path
// - path doesn't exist
func ExpandPath(path string) (string, error) {
// If path starts with ~
if strings.HasPrefix(path, "~/") {
usr, err := user.Current()
if err != nil {
return "", fmt.Errorf("failed to get user home directory: %w", err)
}
// Replace ~/ with actual home directory
path = filepath.Join(usr.HomeDir, path[2:])
}
// Convert to absolute path
absPath, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("failed to get absolute path: %w", err)
}
// Check if path exists
if _, err := os.Stat(absPath); err != nil {
return "", fmt.Errorf("path does not exist: %w", err)
}
return absPath, nil
}

View File

@@ -76,12 +76,19 @@ func main() {
}
// Move the output PDF to the current directory
err = os.Rename(pdfPath, outputFile)
err = copyFile(pdfPath, outputFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error moving output file: %v\n", err)
os.Exit(1)
}
// Remove the original file after copying
err = os.Remove(pdfPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error cleaning up temporary file: %v\n", err)
os.Exit(1)
}
// Clean up temporary files
cleanupTempFiles(tmpDir)
@@ -103,3 +110,25 @@ func cleanupTempFiles(dir string) {
}
}
}
// Copy a file from source src to destination dst
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
if err != nil {
return err
}
return destFile.Sync()
}

275
restapi/ollama.go Normal file
View File

@@ -0,0 +1,275 @@
package restapi
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/danielmiessler/fabric/core"
"github.com/gin-gonic/gin"
"io"
"log"
"net/http"
"strings"
"time"
)
type OllamaModel struct {
Models []Model `json:"models"`
}
type Model struct {
Details ModelDetails `json:"details"`
Digest string `json:"digest"`
Model string `json:"model"`
ModifiedAt string `json:"modified_at"`
Name string `json:"name"`
Size int64 `json:"size"`
}
type ModelDetails struct {
Families []string `json:"families"`
Family string `json:"family"`
Format string `json:"format"`
ParameterSize string `json:"parameter_size"`
ParentModel string `json:"parent_model"`
QuantizationLevel string `json:"quantization_level"`
}
type APIConvert struct {
registry *core.PluginRegistry
r *gin.Engine
addr *string
}
type OllamaRequestBody struct {
Messages []OllamaMessage `json:"messages"`
Model string `json:"model"`
Options struct {
} `json:"options"`
Stream bool `json:"stream"`
}
type OllamaMessage struct {
Content string `json:"content"`
Role string `json:"role"`
}
type OllamaResponse struct {
Model string `json:"model"`
CreatedAt string `json:"created_at"`
Message struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"message"`
DoneReason string `json:"done_reason,omitempty"`
Done bool `json:"done"`
TotalDuration int64 `json:"total_duration,omitempty"`
LoadDuration int `json:"load_duration,omitempty"`
PromptEvalCount int `json:"prompt_eval_count,omitempty"`
PromptEvalDuration int `json:"prompt_eval_duration,omitempty"`
EvalCount int `json:"eval_count,omitempty"`
EvalDuration int64 `json:"eval_duration,omitempty"`
}
type FabricResponseFormat struct {
Type string `json:"type"`
Format string `json:"format"`
Content string `json:"content"`
}
func ServeOllama(registry *core.PluginRegistry, address string, version string) (err error) {
r := gin.New()
// Middleware
r.Use(gin.Logger())
r.Use(gin.Recovery())
// Register routes
fabricDb := registry.Db
NewPatternsHandler(r, fabricDb.Patterns)
NewContextsHandler(r, fabricDb.Contexts)
NewSessionsHandler(r, fabricDb.Sessions)
NewChatHandler(r, registry, fabricDb)
NewConfigHandler(r, fabricDb)
NewModelsHandler(r, registry.VendorManager)
typeConversion := APIConvert{
registry: registry,
r: r,
addr: &address,
}
// Ollama Endpoints
r.GET("/api/tags", typeConversion.ollamaTags)
r.GET("/api/version", func(c *gin.Context) {
c.Data(200, "application/json", []byte(fmt.Sprintf("{\"%s\"}", version)))
return
})
r.POST("/api/chat", typeConversion.ollamaChat)
// Start server
err = r.Run(address)
if err != nil {
return err
}
return
}
func (f APIConvert) ollamaTags(c *gin.Context) {
patterns, err := f.registry.Db.Patterns.GetNames()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err})
return
}
var response OllamaModel
for _, pattern := range patterns {
today := time.Now().Format("2024-11-25T12:07:58.915991813-05:00")
details := ModelDetails{
Families: []string{"fabric"},
Family: "fabric",
Format: "custom",
ParameterSize: "42.0B",
ParentModel: "",
QuantizationLevel: "",
}
response.Models = append(response.Models, Model{
Details: details,
Digest: "365c0bd3c000a25d28ddbf732fe1c6add414de7275464c4e4d1c3b5fcb5d8ad1",
Model: fmt.Sprintf("%s:latest", pattern),
ModifiedAt: today,
Name: fmt.Sprintf("%s:latest", pattern),
Size: 0,
})
}
c.JSON(200, response)
}
func (f APIConvert) ollamaChat(c *gin.Context) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "testing endpoint"})
return
}
var prompt OllamaRequestBody
err = json.Unmarshal(body, &prompt)
if err != nil {
log.Printf("Error unmarshalling body: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "testing endpoint"})
return
}
now := time.Now()
var chat ChatRequest
if len(prompt.Messages) == 1 {
chat.Prompts = []PromptRequest{{
UserInput: prompt.Messages[0].Content,
Vendor: "",
Model: "",
ContextName: "",
PatternName: strings.Split(prompt.Model, ":")[0],
}}
} else if len(prompt.Messages) > 1 {
var content string
for _, msg := range prompt.Messages {
content = fmt.Sprintf("%s%s:%s\n", content, msg.Role, msg.Content)
}
chat.Prompts = []PromptRequest{{
UserInput: content,
Vendor: "",
Model: "",
ContextName: "",
PatternName: strings.Split(prompt.Model, ":")[0],
}}
}
fabricChatReq, err := json.Marshal(chat)
if err != nil {
log.Printf("Error marshalling body: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err})
return
}
ctx := context.Background()
var req *http.Request
if strings.Contains(*f.addr, "http") {
req, err = http.NewRequest("POST", fmt.Sprintf("%s/chat", *f.addr), bytes.NewBuffer(fabricChatReq))
} else {
req, err = http.NewRequest("POST", fmt.Sprintf("http://127.0.0.1%s/chat", *f.addr), bytes.NewBuffer(fabricChatReq))
}
if err != nil {
log.Fatal(err)
}
req = req.WithContext(ctx)
fabricRes, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("Error getting /chat body: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err})
return
}
body, err = io.ReadAll(fabricRes.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "testing endpoint"})
return
}
var forwardedResponse OllamaResponse
var forwardedResponses []OllamaResponse
var fabricResponse FabricResponseFormat
err = json.Unmarshal([]byte(strings.Split(strings.Split(string(body), "\n")[0], "data: ")[1]), &fabricResponse)
if err != nil {
log.Printf("Error unmarshalling body: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "testing endpoint"})
return
}
for _, word := range strings.Split(fabricResponse.Content, " ") {
forwardedResponse = OllamaResponse{
Model: "",
CreatedAt: "",
Message: struct {
Role string `json:"role"`
Content string `json:"content"`
}(struct {
Role string
Content string
}{Content: fmt.Sprintf("%s ", word), Role: "assistant"}),
Done: false,
}
forwardedResponses = append(forwardedResponses, forwardedResponse)
}
forwardedResponse.Model = prompt.Model
forwardedResponse.CreatedAt = time.Now().UTC().Format("2006-01-02T15:04:05.999999999Z")
forwardedResponse.Message.Role = "assistant"
forwardedResponse.Message.Content = ""
forwardedResponse.DoneReason = "stop"
forwardedResponse.Done = true
forwardedResponse.TotalDuration = time.Since(now).Nanoseconds()
forwardedResponse.LoadDuration = int(time.Since(now).Nanoseconds())
forwardedResponse.PromptEvalCount = 42
forwardedResponse.PromptEvalDuration = int(time.Since(now).Nanoseconds())
forwardedResponse.EvalCount = 420
forwardedResponse.EvalDuration = time.Since(now).Nanoseconds()
forwardedResponses = append(forwardedResponses, forwardedResponse)
var res []byte
for _, response := range forwardedResponses {
marshalled, err := json.Marshal(response)
if err != nil {
log.Printf("Error marshalling body: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err})
return
}
for _, bytein := range marshalled {
res = append(res, bytein)
}
for _, bytebreak := range []byte("\n") {
res = append(res, bytebreak)
}
}
c.Data(200, "application/json", res)
//c.JSON(200, forwardedResponse)
return
}

1737
streamlit.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
package main
var version = "v1.4.121"
var version = "v1.4.128"